Skip to content

Paper Lab foundation: trade journal + Windows dev fixes - #1

Open
Adkr1989 wants to merge 342 commits into
masterfrom
feat/paper-lab-foundation
Open

Paper Lab foundation: trade journal + Windows dev fixes#1
Adkr1989 wants to merge 342 commits into
masterfrom
feat/paper-lab-foundation

Conversation

@Adkr1989

@Adkr1989 Adkr1989 commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Summary

Foundation for using OpenAlice as a paper-first executive trading workflow, added as a thin extension layer with no src/ behavior changes beyond Windows-compat fixes.

  • Trade journal (extensions/journal/): a standalone watcher that polls the engine's file-driven trade record (data/trading/<acct>/commit.json) and appends each new commit — thesis, operations, fills, guard rejections, P&L — to an Obsidian markdown journal. Zero coupling to src/. 14 unit tests; live-verified writing real entries from real paper trades.
  • Guardian Windows fixes (scripts/guardian/shared.ts): pnpm dev did not work on win32 — tsx/pnpm spawned by bare name (ENOENT on .CMD shim), tsx watch worker hung before binding, and the restart-flag basename ignored backslashes. Fixed all three; engine now boots and the UTA restart-flag protocol fires on Windows.
  • Wired extensions/ into vitest.config.ts + tsconfig.json so the layer is test- and type-checked as first-class.
  • Docs: paper-autonomy decision record + post-fork test baseline.

Test plan

  • npx tsc --noEmit clean
  • pnpm test — 1722 passed; the only failures are 10 pre-existing Windows-path assertions in src/core/paths.spec.ts (documented in docs/decisions/baseline-test-state.md)
  • pnpm dev boots Alice + UTA on Windows
  • Live Alpaca paper: guard reject (TSLA blocked by whitelist) + allow (AAPL submitted) + journal entries written

Boundary touch

Touches the guardian dev supervisor (process spawning) and reads trade records. Does NOT modify trading/broker logic, auth, or migrations. No credentials in the diff (data/ gitignored).

luokerenx4 and others added 30 commits June 8, 2026 10:34
…amily

feat: alice CLI export family + codex headless reporting + headless management plane (0.40.0-beta.2)
ConPTY's CreateProcess only appends `.exe` when resolving a bare command
name from PATH — it never tries `.cmd`/`.bat`. claude/codex ship native
`.exe`s and resolved fine; opencode/pi install as npm shims (only a
`.cmd`/`.ps1` exists, no `.exe`), so spawning them by bare name ENOENTed
and the workspace never launched. Reported by a community user on Windows.

Add `resolveLaunchCommand` (src/workspaces/win-command.ts): on win32, do
the PATH × PATHEXT lookup ourselves — a native `.exe` is spawned directly,
a `.cmd`/`.bat` shim is wrapped through `cmd.exe /d /c`. No-op off Windows.
Wired into both node-pty spawn sites (interactive `persistent-session`,
agent `probe`), where args are flags + a uuid so the shell wrap is
injection-safe.

Headless dispatch gets the same resolution for native-exe agents
(claude/codex now work headless on win32 too), but `.cmd`-shim agents stay
headless-unsupported on Windows: the task prompt is the trailing arg and
routing it through cmd.exe would re-parse shell metacharacters (a real
injection surface). That now fails with a clear, recorded reason instead
of a silent ENOENT — interactive launch of those agents works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(workspace): launch opencode/pi from the frontend on Windows
The preset catalog hardcodes selectable models per provider; several had
drifted behind the providers' current releases. Most visibly, OpenAI Codex
(Subscription) only offered GPT 5.4 / 5.4 Mini, so users couldn't pick
GPT 5.5 (now Codex's default). Closes TraderAlice#284.

Verified each against the provider's official model docs:
- Codex (OAuth + API): add gpt-5.5, set as default
- Claude (OAuth + API): add claude-opus-4-8, set as default; drop opus-4-6
- Gemini: 2.5 line -> gemini-3.5-flash (default) / 3.1-flash-lite / 2.5-pro
- MiniMax: add MiniMax-M3, set as default
- GLM: glm-5.1 now served internationally (z.ai) — drop "China only";
  set as default; drop end-of-life glm-4.6
- Kimi (k2.6) / DeepSeek (v4) already current — unchanged

Also bumps the runtime fallback defaults that apply when a profile omits
a model (config.ts profile schemas, codex-provider, agent-sdk query) to
match. Legacy migration schema + migration bodies left frozen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…model-refresh

feat(ai-config): refresh provider model catalog to current lineups
…onal modal

First step of collapsing the AI-config story onto Workspace-native execution
(see plan): make Alice's existing central credential store
(`aiProviderSchema.credentials`) the single source for provider creds and let
it flow into workspaces, in both directions, without coupling to the legacy
GenerateRouter/profile machinery.

- Map Credential → WorkspaceAiCred: new `credential-injection.ts`
  (`credentialToWorkspaceAiCred`) — credential carries no model, so the caller
  supplies model + adapter knobs (claude authMode via `resolveAnthropicAuthMode`,
  codex wireApi).
- Template-driven injection: `TemplateMeta.agentCredentials`
  (`{ agentId: { credentialSlug, model?, authMode?, wireApi? } }`, from
  template.json). `injectWorkspaceCredentials` writes each declared agent's AI
  config at create time via the adapter's existing `writeAiConfig` — so a
  workspace boots ready-to-run, no manual UI step. Best-effort: a miss
  (agent not enabled / no adapter / slug absent) warns + skips, never fails the
  create.
- Security: injection runs POST initial-commit so the key never lands in the
  first commit, and `_common.sh setup_git_excludes` now also covers
  `opencode.json` + `.pi-agent/` (previously only claude/codex were excluded —
  a latent leak for the other two adapters).
- Bidirectional modal (`WorkspaceAIConfigModal`): keeps free-text entry; the
  picker now loads from saved *credentials* (not profiles); after a successful
  Save of a hand-entered key, offers "Save to Alice" so the cred is solidified
  into the central store and reusable everywhere. New
  `GET/POST /api/workspaces/credentials` + `addCredential` (dedupe + slug) +
  `inferCredentialVendor`.

World B (GenerateRouter / in-process providers / AgentWork) is untouched this
PR — it keeps running. Wiring automation onto headless Workspace dispatch and
deleting World B are the next two PRs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… AgentWork

Wire autonomous cron onto the headless Workspace dispatch primitive — the step
that makes the new automation actually run an agent. A cron job is now "run this
prompt in that workspace, headless"; the workspace agent runs it and reports
back via the Inbox (the run shows live in the Runs panel).

- CronJob + CronFirePayload gain `workspaceId` + `agent` (the explicit CLI:
  claude / codex / pi / opencode). Threaded through add/update/fire.
- Cron listener rewritten: on `cron.fire` it resolves the target workspace +
  adapter from a WorkspaceService ref and calls `dispatchHeadlessTask`, instead
  of emitting `agent.work.requested` into the legacy in-process path. Failures
  are LOUD (no target / unknown workspace / disabled agent / capacity / service
  not ready all log an error and skip) — never a silent orphan-fire.
- main.ts: the workspaceServiceRef box is created before the cron listener and
  injected; WebPlugin fills it on start (an early fire is a loud skip).
- Cron HTTP route + cronAdd/cronUpdate MCP tools accept the target; the MCP tool
  descriptions are updated off the stale "heartbeat tick" model. AutomationPage
  cron form gains workspace + agent selectors (agent options follow the picked
  workspace's enabled adapters); the card shows the target.
- Migration 0008 disables enabled cron jobs that have no workspaceId (legacy
  AgentWork-era jobs) so they stop firing into the retired path — visible for
  the user to re-target or delete.

Scope: cron only. Webhook ingest + heartbeat still ride AgentWork; they and the
whole in-process World B (GenerateRouter / providers / AgentWork) are removed in
PR3. After that, AgentWork has no callers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The AI Provider settings page was still profile-centric (backend / loginMethod /
model wizard, profiles grouped by credential, an SDK-adapters column, Test via
the in-process GenerateRouter) — all World-B concepts. Post-Workspace the only
thing it needs to manage is Alice's central api-key credentials, the set that
gets injected into workspaces (and pulled/pushed from the per-workspace modal).

- Page is now a credential list + add/edit modal. The preset catalog is reused
  purely as an "add credential" helper (endpoint + model suggestions + request
  shape); no profiles, no active-profile, no SDK column, no backend/loginMethod.
- Subscription logins (Claude Pro/Max, ChatGPT) are deliberately excluded — they
  live in the CLI's own auth (`claude login` / `codex login`), not in Alice. The
  modal only offers api-key presets.
- Test runs the lightweight HTTP probe (`agent-probe`), dispatched by request
  shape (anthropic vs openai) — NOT the provider router. This is the change that
  lets World B be deleted in the next PR: the page no longer touches it. A
  credential carries no model, so the modal picks a throwaway "test model" just
  to verify the key.
- New credential CRUD + probe-test routes under `/api/config/credentials`
  (reusing the core store fns); list redacts the raw key. Frontend client fns +
  demo handlers added.

The legacy `/api/config/profiles*` routes + GenerateRouter still exist (unused
by this page now); they're removed with the rest of World B in the next PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore the page's two-column layout: left is the credential vault, right is a
read-only "Agent runtimes" panel introducing the four CLI runtimes a workspace
can launch (Claude Code / Codex / opencode / Pi) — each with its auth model,
wire shape, and tool-access story. Replaces the old World-B "Available SDKs"
column; orients the user on which credential feeds which runtime. Editorial copy
grounded in the adapters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eserving)

Introduce a barId-keyed federation ("{sourceId}|{nativeSymbol}") over OHLCV
K-lines, unifying vendor (OpenTypeBB) and — Phase 1 — broker (UTA) sources
behind one interface. The AI analysis path routes through it and now reports
which source served the bars; the v1 calculateIndicator contract is unchanged.
OpenTypeBB is demoted from "the K-line provider" to one vendor source behind
the federation — extraction, not teardown.

- src/domain/market-data/bars/: BarService (searchBarSources + getBars),
  barId parse/format, window resolution (count/asOf/range) + MAX_BARS cap,
  vendor branch live + UTA branch via UTAAccountSDK.getHistorical.
- DataSourceMeta gains optional source/sourceId/barId/provider/barCapability;
  flows to calculator dataRange unchanged.
- uta-protocol: Bar/BarParams/BarInterval + optional capability-gated
  IBroker.getHistorical + AccountCapabilities.historicalBars;
  UTAAccountSDK.getHistorical (server route lands in Phase 1).
- tool/analysis.ts -> barService; main.ts wires it into EngineContext.
- Tests: 12 deterministic unit (bar-service.spec.ts) + 7 real-data e2e
  (bars.bbProvider.spec.ts, gated). Verified: tsc clean, pnpm test 1846/1846,
  4 asset classes x yfinance/fmp with cross-source AAPL close agreement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…x, shared

The credential-vault rewrite regressed the preset's enumeration value: it
rendered baseUrl as free text (dropping the China/International region options)
and didn't enumerate models where they're actually chosen. Restore it, and make
the pickers shared so both surfaces stay consistent.

- Shared `ui/src/lib/presetHelpers.ts`: presetModels / presetEndpoints /
  presetBaseUrlDefault / vendorPreset / VENDOR_BY_PRESET / baseUrlToVendor (+
  shape/wireApi/isApiKey). One vendor map, no drift between the two pages.
- New reusable controls (`components/credentials/PresetFields.tsx`):
  - EndpointField — region <select> from the preset's endpoints, with a
    "Custom…" escape that preserves a stored non-listed baseUrl (a bare select
    would silently overwrite a custom endpoint with endpoints[0] on save).
  - ModelCombobox — <input>+<datalist>: suggests known model ids (curbs typos
    like minimax-m3 vs MiniMax-M3) while still allowing a free-typed id.
- AIProviderPage credential modal uses EndpointField (region dropdown back) +
  ModelCombobox (test model). Runtime panel copy corrected (opencode/pi are
  multi-provider; tools reach every runtime via MCP or the alice CLI).
- WorkspaceAIConfigModal model field → ModelCombobox; suggestions inferred from
  the entered baseUrl's vendor (api.minimax.io → MiniMax-M3/M2.7), with a tab
  fallback (claude→anthropic, codex→openai). Vendor-axis, so it works on any tab
  pointed at any gateway; custom/local → free text.
- Storage: credentialSchema.baseUrl now trims + collapses '' → undefined so the
  ===-based dedup can't duplicate one logical credential (feedback_optional_
  empty_string class); same trim applied in extractCredentialFromProfile. Specs
  added.
- Delete dead CredentialCard / SdkAdapterCard (leftovers of the profile system).

Verified: catalog was never the bug (endpoints/models intact); regression was
purely UI. Adversarial review PASS on all requirements + edit-mode custom-baseUrl
preservation + the vendor-axis suggestion path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…— Phase 1 (CCXT)

Make broker OHLCV bars fetchable end-to-end and discoverable, mirroring the
getQuote path. The federated bar service can now resolve a broker barId
(= aliceId) to a UTA read endpoint, and searchBarSources surfaces broker
candidates alongside vendor ones.

UTA side (the getQuote-mirror chain):
- UnifiedTradingAccount.getHistorical: loud-refuse (CONFIG) when the broker
  has no getHistorical, else _expandAliceIdIfNeeded + _callBroker.
- POST /api/trading/uta/:id/historical (revives Date params off the wire).
- CcxtBroker.getHistorical via exchange.fetchOHLCV + CCXT_TIMEFRAME map,
  validates against exchange.timeframes (loud-refuse unsupported interval);
  capability { supported, quality:'realtime' }. MockBroker: deterministic
  synthetic bars. CCXT bars are free/keyless/REST — the cheapest wedge.

Federated search + wire-shape fix:
- Add ContractSearchHit (flat { source, contract, derivativeSecTypes }) to
  uta-protocol — the shape /contracts/search actually returns. UTA's
  contract-search.ts now uses it; UTAManagerSDK.searchContracts is corrected
  from the mistyped grouped ContractSearchResult[] (no other caller).
- bar-service.searchBarSources unions vendor + UTA (allSettled, no dedup —
  redundancy is the feature); secType → assetClass mapping; barId = aliceId.

Tests: CcxtBroker.getHistorical (map + loud-refuse + resolve-fail), MockBroker,
federated searchBarSources (vendor∪uta + one-side-fail). Verified: Alice tsc
clean, services/uta tsc 0 errors, pnpm test 1852/1852, bbProvider e2e 21/21.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second real broker bar source, mirroring CCXT. Drains Alpaca market-data v2
`getBarsV2` (async generator) into string-typed bars; `adjustment:'all'` for
split/dividend-adjusted data. Free-tier accounts use the IEX feed (partial
tape) → capability quality 'iex' so the federation can surface the entitlement.

- AlpacaBroker.getHistorical + historicalBars capability.
- ALPACA_TIMEFRAME map + AlpacaBarRaw type.
- Tests: drain→bars mapping + resolve-fail. Verified: Alice tsc clean,
  services/uta tsc 0 errors, pnpm test 1854/1854.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…credential form

Model the API wire shape as a first-class, extensible registry instead of a
2-way boolean: anthropic-messages / openai-chat / openai-responses (room for
google etc.). A provider often exposes the SAME key behind multiple,
incompatible shapes at different endpoints — so the create form lets you pick
the shape and auto-fills the matching endpoint, rather than hard-coding one.

Backend:
- preset-catalog: `WireShape` + per-provider `wires` (shape × region endpoint
  table). GLM/MiniMax/Kimi/DeepSeek now carry BOTH their anthropic and
  openai-chat endpoints (verified URLs incl. /api/paas/v4, /v1, /anthropic);
  OpenAI carries responses + chat; Gemini the OpenAI-compat endpoint; custom all
  three. `WIRE_SHAPE_LABELS` shared.
- presets.ts serializes `wires` to the frontend.
- /api/config/credentials/test dispatches the prober by `wireShape` via a table
  (anthropic / openai-chat / openai-responses) — no if/else ladder.

Frontend:
- Shared `useTestGate` hook: the standard "Test passes for the current form
  before Save" gate, keyed by the testable fields. The credential vault now
  enforces it (Save locked until a green Test), matching the workspace modal.
- Vault create/edit modal restructured: provider → API-mode selector (only when
  >1 shape) → region/endpoint (auto-filled, Custom escape) → key → test model →
  Test → Save. Cleaner header/footer, ✓-Tested state, stale-result re-test hint.
- presetHelpers: presetWires / wireEndpoints / defaultWireShape /
  wireShapeForBaseUrl; dropped the now-dead presetShape/presetWireApi/
  presetEndpoints/presetBaseUrlDefault.

wireShape is a build-time + probe-time concept this round (the endpoint implies
it; each adapter still speaks its own one shape). Making opencode/pi actually
emit multiple shapes is a follow-on adapter change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fold the workspace modal's bespoke per-tab test-before-save logic onto the same
useTestGate hook the credential vault uses — one abstraction, not two. Behavior
preserved exactly: one gate per tab (so switching tabs keeps each agent's
verdict), the result is bound to a `testKey` of the tested fields (agent-specific
wireApi/authMode), Save stays locked until the current form has a passing test,
and editing mid-flight re-locks. Removes the duplicate TestResult/formsMatch
machinery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unify "public-data source" into the UTA structure (not a parallel/dual layer)
via three account-level capability flags, then ship 3 built-in keyless data
UTAs so any user gets crypto K-lines out-of-box without an API key.

- UTAConfig gains keyless / readOnly / editable. keyless ⟹ readOnly.
- UnifiedTradingAccount: stores keyless/readOnly; _assertWritable() loud-refuses
  stage* (place/modify/close/cancel) on a read-only account — the security
  boundary made explicit + enforceable.
- CcxtBroker: keyless mode skips checkRequiredCredentials in init() (public
  endpoints only: loadMarkets / fetchTicker / fetchOHLCV). Threaded via the
  factory → brokerConfig.
- UTAManager: passes keyless/readOnly through; getAggregatedEquity excludes
  keyless UTAs (no account → no phantom $0 in the aggregate).
- Boot injects binance-readonly / okx-readonly / bybit-readonly (presetId
  ccxt-custom, keyless+readOnly+non-editable) — code-defined, NOT persisted to
  accounts.json, so they can't be edited/clobbered; a user's own exchange UTA
  uses a different id. They're redundant bar sources the federation surfaces
  for free (barId binance-readonly|BTC/USDT, etc.).

Tests: write-guard (read-only/keyless refuse) + real keyless e2e
(CcxtBroker.e2e.spec.ts, gated CCXT_E2E=1) proving init-without-key +
getHistorical → real bars on binance/okx/bybit. vitest.e2e.config now includes
services/**. Verified: Alice tsc clean, services/uta tsc 0 errors, pnpm test
1857/1857, keyless e2e 3/3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ved endpoint URL

Two gaps from the screenshot review:

1. The endpoint dropdown showed only a region label ("International (minimax.io)")
   — the user couldn't see where requests actually go. EndpointField now renders
   the concrete resolved URL (read-only) under the region select; only "Custom…"
   makes it an editable free-text field.

2. wireShape wasn't stored on the credential — so a Test verified a shape the
   saved credential didn't record. Critically, OpenAI Chat Completions and
   Responses share ONE base URL (api.openai.com/v1), so baseUrl alone can't say
   which shape a credential is. wireShape is now a first-class credential field:
   - `credentialWireShapeEnum` + `credentialSchema.wireShape`; `addCredential`
     dedup includes it (chat vs responses are distinct creds).
   - GET/POST/PUT /credentials carry it; the vault form saves it and, on edit,
     restores it from the stored value (falling back to endpoint inference for
     pre-wireShape creds).
   - The credential list shows a wire-shape chip.

Runtime consumption (adapters emitting per the stored wireShape) is the remaining
follow-on; this makes the credential self-describing and the Test meaningful.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot tiny text

The resolved endpoint URL was small muted "→ …" text below the dropdown — too
small to register. Render it as a proper input box instead: read-only (but
selectable, so the URL is copyable) when a region is picked, and editable the
moment the user selects "Custom". Same visual weight as a real field, so it's
clear exactly where requests go.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Phase 2)

A fresh, bounded, side-effect-free expression language for technical analysis,
keyed by barId so the model can target a specific source (broker/vendor) or mix
sources in one script. v1 calculateIndicator is untouched. Built from scratch
(the Nov-2024 thinking parser was not reused).

- calc-v2/: lexer → recursive-descent parser → evaluator. Surface = Python/
  pandas subset: `s = bars("alpaca-paper|AAPL","1d",count=250)` let-bindings +
  a final result expression; series columns s.close/.high/.low/.open/.volume;
  functions sma/ema/stdev/max/min/sum/average/rsi/bbands/macd/atr/rvol/obv/mfi/
  vwap (reuse indicator math verbatim); index s.close[-1]; arithmetic + - * /.
  Indicators return the latest scalar (no [-1]); only raw columns are series.
- Compiler-grade diagnostics (structured CalcDiagnostic, first-class in the
  tool contract): syntax w/ position, unknown-function + did-you-mean,
  undeclared-name, arity, type, insufficient-bars, and pandas-reflex redirects
  (method chaining → use sma(...); indexing a scalar → drop the [-1]).
- bars() window: count + asOf (count anchor) OR start/end date range. Fixed the
  vendor branch to honor end_date (passed to OpenTypeBB + defensive post-filter).
- New calculateQuant MCP tool (description doubles as the language spec).

Tests: 9 parser + 12 evaluator unit + 6 real-yfinance e2e (SMA/RSI compute
correctly, start→end bounds the window, insufficient-bars is a structured error,
cross-source basis). Verified: Alice tsc clean, pnpm test 1898/1898.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
So the AI picks the right calculator and doesn't mix the two syntaxes.

- cli-commands: add `alice analysis quant` → calculateQuant (v2 now CLI-reachable
  alongside `analysis indicator` = v1).
- calculateIndicator (v1) description: lead with v1-vs-v2 — v1 = quick ticker /
  vendor-default; for a specific source (broker bars matching a held position) or
  mixing sources, use calculateQuant. Flags the syntax difference (UPPERCASE
  formula `SMA(CLOSE('AAPL','1d'),50)` vs lowercase pandas script `sma(s.close,50)`).
- openalice-cli SKILL.md: add a v2 quant example + an "indicator vs quant" note.
- cli-commands.spec: register the quant tool for the alias-map anti-rot guard.

Verified: Alice tsc clean, pnpm test 1878/1878.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed unsaved)

The key WAS saved — GET /credentials just redacted it, so reopening the edit
modal showed an empty key field and looked like the save was lost. Return the
apiKey from GET /credentials (same admin-gated exposure as the workspace
credentials + agent-profiles routes) and pre-fill it in the edit form, with a
Show/Hide toggle so the user can verify it. This also makes edit-mode testable
(the key is present to probe with).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r's default

In the workspace AI config modal, "Load from saved credential" flashed the base
URL + key but left the Model field untouched — so a stale model from a previous
provider (minimax-m3 on a GLM endpoint) stuck around and 404'd on Test. Loading
now sets the model to the credential vendor's first preset model (user can still
pick another from the combobox) and clears the prior test verdict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…it at runtime

The workspace Test dispatched by agent (opencode/pi always probed openai-chat),
ignoring the credential's wireShape — so "Test passed" could mean a shape the
runtime wouldn't actually use. Now both the vault and the workspace Test go
through ONE dispatcher and the runtime honors the same shape end-to-end.

- agent-probe.ts: `probeByWireShape(wireShape, …)` — single switch mapping shape
  → prober (anthropic / openai-chat / openai-responses), defaulting baseUrl per
  shape. Both /api/config/credentials/test and
  /api/workspaces/:id/agent-config/:agent/test call it.
- Adapters honor wireShape (WorkspaceAiCred.wireShape, threaded from the
  credential): opencode picks the @ai-sdk package, pi picks the `api` field,
  codex is Responses-only (always wire_api="responses" — it hard-rejects chat),
  claude is anthropic. readAiConfig surfaces wireShape; injection carries it.
- Workspace modal: FormState.wireShape (default per agent; set from the loaded
  credential's wireShape); Test + Save + dirty + testKey all include it. The
  workspace /credentials endpoint returns + POST stores wireShape.
- So: a GLM credential created as anthropic, loaded into Pi, tests anthropic AND
  the pi adapter writes api=anthropic-messages — test == runtime.

Specs: ai-config.spec updated for the wireShape round-trip + the codex
Responses-only change; added opencode/pi per-shape emission tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uthMode by baseUrl

Two coherence fixes from the adversarial review of the wireShape change:

- Codex is Responses-only, but loading a chat/anthropic credential into the
  codex tab set form.wireShape to that shape, so the Test probed it and "passed"
  while the codex adapter still writes wire_api="responses" — the exact
  test-lies-about-runtime bug. applyCredential now clamps the codex tab to
  openai-responses regardless of the loaded credential.
- The per-workspace Test resolved the anthropic auth header from the claude-only
  authMode field, defaulting others to x-api-key — so a MiniMax-international
  (api.minimax.io, Bearer-only) anthropic credential tested through an
  opencode/pi tab would 401. It now resolves by baseUrl via
  resolveAnthropicAuthMode, matching the vault test + the runtime injection.

Verified opencode's openai-responses → @ai-sdk/openai mapping against opencode's
own provider docs (@ai-sdk/openai for /v1/responses, @ai-sdk/openai-compatible
for chat) — correct, no change needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundation for "fill the key once": a credential can declare multiple wire
shapes (each with its endpoint) — its "wire capabilities" — instead of one
shape per credential. `credentialSchema.wires: { [shape]: baseUrl }`; legacy
`{baseUrl, wireShape}` kept and read transparently via the new `credentialWires()`
accessor (no migration — old creds upgrade on read).

addCredential now dedups by {vendor, authType, apiKey} (one key = one account)
and UPGRADES the matched record's wires in place rather than creating a
duplicate slug — which also fixes the dedup footgun the review flagged
(re-adding a pre-wireShape key no longer duplicates).

Next: preset region matrix + create form captures all of a region's wires +
injection matches a credential's wires to the agent's supported shapes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e it speaks

A provider exposes the same key behind several incompatible wire shapes that
differ only by endpoint (GLM: anthropic at /api/anthropic, openai-chat at
/api/paas/v4). Before, each shape was a separate credential — the user re-entered
the key per shape. Now a credential captures ALL of a region's wires at once
("fill the key once") and injection picks the shape the target agent speaks.

- preset-catalog: region-first matrix — `regions: [{ id, label, wires: {shape: url} }]`
  (was shape-first `wires[]`). presets.ts serializes regions; the model field is
  the only remaining schema-driven dropdown.
- Vault create form: pick provider → region → key. The "API mode" selector is
  gone; the form shows the region's wire capabilities (read-only) and Test probes
  the primary shape (one probe validates the shared key). Custom stays free-form
  (shape + URL).
- Injection: `credentialToWorkspaceAiCred` picks the agent's preferred wire from
  the credential's capabilities via `pickAgentWire`/`AGENT_WIRE_PREFERENCE`, and
  returns null (loud skip) when none is compatible — so a chat-only key can't be
  injected into codex (Responses-only).
- Workspace modal: the "Load from saved credential" picker lists only
  tab-compatible credentials and loads the matched wire; codex shows few/none,
  steering users to pi/opencode (the intended funnel).
- credentialSchema gains `wires` (partial map); legacy `{baseUrl,wireShape}` read
  transparently via `credentialWires()` — no migration. /api/config/credentials
  and /api/workspaces/credentials carry `wires`.

Specs: credential-injection.spec rewritten for the wires model + the
no-compatible-wire null/loud-skip; all green (1871).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mpty reply

The Test "(empty reply)" came from a reasoning model (glm-5.1) spending the tiny
32-token probe budget entirely on reasoning, leaving content empty — the test
connected, but the reply looked broken.

- agent-probe: bump the probe budget to 512 (one-off per Test) so a reasoning
  model can finish thinking AND emit a visible reply; fall back to
  reasoning_content if the final content is still empty.
- Both Test surfaces: when the reply is genuinely empty, show "provider
  reachable (returned no text)" instead of the awkward "(empty reply)".

Also delete the now-unused EndpointField (superseded by the region-based form).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…entials-template-injection

feat(workspace): central credentials — template injection + bidirectional config modal
…page-credential-vault

feat(ui): AI Provider page → credential vault; Test via probe (unblocks World-B deletion)
The model loop runs inside the native workspace CLIs now; the parallel
in-process stack had no live consumers left (cron → headless dispatch in PR2,
Test → probe in PR3a). Removed end to end (-6.1k LOC):

- AI backends: ai-provider-manager.ts (GenerateRouter), ai-providers/{agent-sdk,
  codex,vercel-ai-sdk,mock}/, sdk-adapters.ts, core/ai-config.ts.
- AgentWork: core/agent-work.ts + agent-work-listener.ts, and main.ts's
  provider-chain / runner / listener / task-source wiring + EngineContext.router.
- Heartbeat (OpenClaw remnant — output was already stubbed, drove only the
  deleted loop, redundant with PR2's cron→headless): task/heartbeat/, the
  /api/heartbeat route, and the Automation › Heartbeat UI surface (page, sidebar
  row, tab, URL redirect, i18n, demo mocks).
- Legacy profile surface: /api/config/profiles* + /sdk-adapters, the workspace
  /agent-profiles route + listAgentProfiles (dead since PR1's credential picker),
  and the matching demo mocks.

Kept: preset-catalog as a pure suggestion catalog (its sdkAdapters wiring
stripped); the credential vault + all of World A; the webhook /ingest +
agent.work.* event family (now producer-only / dormant — a future
webhook→headless listener can consume it; not removing a documented external
surface unilaterally).

Left dead-but-harmless for a focused follow-up: the core config `heartbeat`
section + `aiProviderSchema.profiles` schema/helpers (no consumers; removing
touches index-based config plumbing + exported helpers). Unused @ai-sdk/* deps
in package.json likewise deferred.

tsc (Alice + UI) clean; pnpm test 1732 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ame and others added 28 commits June 17, 2026 12:59
…e endpoint

CCXT's base enableDemoTrading sets urls.api = urls.demo; on an exchange with
no demo domain (e.g. okx, whose demo IS the sandbox x-simulated-trading
header) urls.api becomes undefined and the next request crashes deep in
sign() with a cryptic "Cannot read properties of undefined (reading 'rest')".
Detect the broken post-state and throw a clear CONFIG error instead, and wrap
setSandboxMode symmetrically. Real-ccxt construct-time regression spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Binance — the world's largest crypto exchange — gets a dedicated preset.
Modes: live + Demo Trading (the unified demo-*.binance.com simulator via
enableDemoTrading, spot + futures). Legacy Testnet is deliberately omitted:
futures testnet is deprecated (CCXT hard-throws) and spot-only testnet would
need a market-type-scoped account (tracked in ANG-111's sibling work).
Verified end-to-end on a real Binance demo account via the alice-uta CLI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(uta): CCXT-custom boolean create bug + demoTrading guard + Binance preset
Two-sidebar hierarchy:
- Three-tier depth: ActivityBar → bg-tertiary (recessed rail), Sidebar
  bg-secondary, main bg — the two left columns now read as distinct
  zones instead of one undifferentiated slab.
- Align the three top surfaces to a 40px header rhythm (branding h-10
  px-4, was px-5 py-4 ≈ 60px); slim rail 216→200; sidebar Panel → px
  min/max (200/420) + preserve-pixel-size for a stable fixed-width feel.
- Active nav item → bg-accent-dim (its old bg-tertiary collided with the
  new rail background); footer rhythm py-2→py-1.5.

Responsive:
- Kill the 768–1024 dead zone: add a middle tier (rail static ≥768,
  sidebar static ≥1024, drawer below) so the main pane keeps
  full-width-minus-rail and its md:-keyed content (Portfolio stat grid)
  no longer overflows/overlaps.
- Unify mobile drawer width 216→280 (matches the secondary drawer, no
  drill-in jump); body scroll lock while a drawer is open; z-index
  ladder — dialogs z-60 / toasts z-70 above the nav drawers z-50.
- Re-clicking the active rail item re-opens a hidden sidebar instead of
  toggling selection off (sidebarVisible).

Consistency / cheap fixes:
- SidebarRow: optional icon/title slots; migrate TrackedSidebar onto it.
- code-copy hover → --color-overlay-strong (was invisible in light);
  PageHeader → .text-title; responsive grid + dialog widths on News /
  Quote / AIProvider / Dialog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(ui): responsive shell + two-sidebar hierarchy pass
Trading workbench wants the most capable model, not fast/cheap tiers.
Bump + prune each preset's model list:

- GLM:    glm-5.1 -> glm-5.2 (sole flagship); drop 4.7-flash, 4.5-air
- Kimi:   default kimi-k2.7-code; keep k2.6; drop k2.5
- Claude: drop superseded Opus 4.7; drop Haiku 4.5 (cheap tier)
- Codex:  drop gpt-5.4-mini (cheap tier); gpt-5.5 stays current
- DeepSeek: drop v4-flash (cheap tier)
- MiniMax: unchanged (M3 already the current flagship)

Also refresh the stale Workspace AI-config modal placeholders
(claude-sonnet-4-6 -> claude-opus-4-8, gpt-4o -> gpt-5.5).

New CN model ids (glm-5.2, kimi-k2.7-code) verified against provider
docs; confirm via the credential Test path before relying on them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…datory-reasoning models

Kimi k2.7 (and similar) 400 with 'invalid thinking: only type=enabled is
allowed for this model' when the probe omits thinking — they cannot run it
disabled. The Anthropic probe now retries once with thinking enabled
(budget_tokens 1024, max_tokens bumped to 2048) only when the error is
thinking-related, leaving the common path (Claude/GLM/MiniMax/DeepSeek)
untouched. Verified end-to-end against the live Kimi endpoint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AI preset model refresh + thinking-probe fix + 0.51.0-beta.1
…date check

The packaged Electron shell only spawned Alice, which hard-requires
OPENALICE_UTA_URL at boot and threw without it — so the desktop app never
ran end to end. Port the prod.mjs supervision into apps/desktop: spawn UTA,
poll /__uta/health, spawn Alice with OPENALICE_UTA_URL injected, watch
restart-uta.flag to respawn UTA, and cascade-shutdown both children.

- Cross-platform tree-kill (taskkill /T /F on win32) so the two children and
  their PTY/CLI grandchildren don't leak on quit / UTA restart.
- Fix OPENALICE_APP_HOME: it must be app.getAppPath() (the dir that contains
  default/, ui/dist, src/workspaces, services/uta/dist), not dirname() — the
  old value pointed one level above the shipped files, breaking templates/UI.
- L1 update check: poll the GitHub releases list (prereleases included, since
  we ship -beta.N with no formal release soon), surface a non-blocking
  download dialog when a newer version exists. No auto-download, no signing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zero source imports — the in-process AI loop that used it was deleted in the
0.40 World B collapse; the model loop runs in native workspace CLIs now. The
package bundles a full copy of the Claude Code CLI (cli.js ~11M) + per-platform
ripgrep binaries (vendor/ ~40M) = 54M of dead weight in the desktop package.
@anthropic-ai/sdk (the key-probe), openai, and ai (the tool() primitive)
stay — those are live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
electron-builder config: include services/uta/dist + workspace templates,
dist/*.js (tsup code-splits into hashed chunks — dist/main.js alone misses
them), asar:false, mac dmg + win nsis (arch follows the runner). Unsigned —
no Developer ID / Windows cert yet.

release.yml: new build-desktop matrix (macos-14 arm64 / macos-13 x64 /
windows-latest) gated on needs.release.outputs.created so a full Electron
build (mac runners bill 10x) only fires when a new version is actually cut.
Each runner builds its own platform/arch (pnpm installs only the os/cpu
prebuilt — longbridge, node-pty), then softprops appends the artifact to the
release the release job already created (release.yml stays the tag owner).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First version to exercise the desktop build/publish pipeline — cutting this
release triggers build-desktop (mac arm64 + mac x64 + windows) to attach
installers to the GitHub Release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…port

feat(desktop): UTA-supervising Electron app + per-platform installer pipeline
GitHub's x64 mac runners queued 20+ min and never allocated on the
0.51.0-beta.2 release, hanging the whole run while arm64 + windows had long
since published. Intel Macs are a shrinking base (Apple stopped selling them
in 2023; arm64 covers every Mac since), not worth blocking every release.
Matrix is now macos-14 (arm64 dmg) + windows-latest (x64 nsis).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…build

ci(desktop): drop macos-13 (Intel) from the desktop build matrix
…tstrap errors

Two papercuts found dogfooding the packaged Windows build:

- The default Electron File/Edit/View/Window/Help menu renders *inside* the
  window on Windows/Linux (it never shows on macOS, where menus live in the
  system bar) — meaningless clutter for a single-window web-UI app. Set a
  minimal menu on macOS (keeps copy/paste accelerators), none elsewhere.

- Workspace bootstrap failures showed 'exited with code unknown' (a null
  exit code from a spawn failure) instead of the actual reason, which was
  already sitting in result.stderr (e.g. the 'bash not found, install Git
  for Windows' hint). Surface the stderr tail in the message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ood-fixes

fix(desktop): drop in-window menu bar + surface real bootstrap errors
…e) + port bash to Node

Workspace creation (the Harness: every workspace is a fresh git repo) had two
external deps that break on a bare machine: bash (the bootstrap scripts) and
system git. bash is absent on Windows; git is latent on Mac too (a fresh Mac's
/usr/bin/git is an Xcode-CLT stub — our dev Macs only have it because CLT was
installed long ago). So workspace creation failed on a bare Windows box, and
would on a bare Mac.

Now it needs neither:
- Bundle git via dugite (GitHub Desktop's package; postinstall fetches a
  per-platform standalone git — same per-platform model as longbridge, picked
  up by electron-builder's node_modules inclusion under asar:false).
- Port the bash bootstraps to Node: templates/_common.mjs (the sole dugite
  importer, exposes git()), chat/bootstrap.mjs, auto-quant/bootstrap.mjs. The
  launcher spawns them on the Electron-bundled Node (process.execPath +
  ELECTRON_RUN_AS_NODE) — no bash, no shebang reliance, plain ESM.
- Route ALL git through the bundled git: runGit (initial commit) and
  git-service (panel log/branch/status) → dugite's exec().
- template-registry prefers bootstrap.mjs, falls back to bootstrap.sh for
  third-party templates (which still need bash where they run).

dugite MUST stay in pnpm.onlyBuiltDependencies — its postinstall fetches the
git binary; drop it and node_modules/dugite/git/ is silently empty. Release CI
asserts the binary is present per-platform.

Verified: tsc clean, 2003 unit tests + e2e (incl a PATH-stripped case proving
no system git/bash) green; packaged .app carries dugite's git 2.53.0 + the
.mjs templates. Closes the bare-Windows / bare-Mac workspace-creation gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…trap

feat(workspaces): native cross-platform bootstrap — bundle git (dugite) + port bash to Node
…S kick war

ANG-120: opening two session tabs in one workspace (e.g. claude + opencode)
froze claude while opencode stayed alive.

Root cause: WorkspaceView's runningSlots mounted a <TerminalView> for EVERY
running session in the workspace — a leftover from the old single-shared-view
design where switching sessions was a CSS toggle within one view. The current
design is session-as-tab: each session has its own tab + its own WorkspaceView,
and TabHost keeps every tab mounted (display:none when inactive). So mounting
all running sessions in each view duplicated every session's terminal into
every open tab — a session open in N tabs got N WebSockets fighting over its
single-attach PTY, producing a kick/reconnect war (the cols 80↔146 churn +
pause/resume thrash + double resume-spawn seen in workspace-sessions.log).

Fix: mount only this tab's own pinned session (activeRecord if running). Each
session's terminal already persists across tab switches via TabHost's
display:none, so the multi-mount was pure redundancy on top of the bug. Net
effect: one terminal per session, one WS per session, no kick war.

Verified: tsc -b clean, 2003 tests pass. Unrelated to native-bootstrap (TraderAlice#363).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-120 defense-in-depth)

The frontend fix removes the cross-tab duplicate mounts that drove the resume
churn, but the resume route still had a check-then-spawn race: two POST
/resume within ms both passed the 'already running?' gate while the session
was paused, and each called pool.spawn() → two 'claude --resume <id>' racing
on one transcript.

Serialize resumes per ${wsId}::${recordId}: a later caller awaits the
in-flight resume, then an in-lock pool.get() re-check short-circuits it to
alreadyRunning instead of spawning a second agent. A failed/early-exit resume
leaves the slot free so a genuine retry still works.

Regression test: two simultaneous resumes spawn exactly once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cuts a release carrying the native cross-platform bootstrap (TraderAlice#363, bundled git
+ Node bootstrap) plus the ANG-120 multi-session fixes — the build to verify
bare-Windows workspace creation on the cloud box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nal-remount

fix(workspaces): stop multi-session terminal freeze (ANG-120) + cut 0.51.0-beta.3
…undation

# Conflicts:
#	package.json
#	scripts/guardian/shared.ts
#	vitest.config.ts
Adkr1989 pushed a commit that referenced this pull request Jun 21, 2026
…ive-volume movers

Shift volume analysis from absolute to relative. Two launcher-side layers:

- Indicator engine: add RVOL/OBV/MFI/VWAP pure functions (technical.ts) +
  calculator dispatch. RVOL is the key read — latest bar vs its own N-bar
  average — since raw volume isn't comparable across tickers. Document the
  scalar-operand foot-gun in the calculateIndicator tool description.
- Discovery: enrich gainers/losers/active rows with relative_volume (vol /
  3-month avg) and turnover (vol / shares-out), computed in
  getPredefinedScreener from averageDailyVolume3Month (added to the screener
  field whitelist — zero extra requests). equityDiscover gains
  sortBy="relative_volume" so "most active" surfaces genuine unusual volume
  instead of ever-active mega-caps.

Also fixes a pre-existing break in the same path: screener() never passed
validateResult:false, so yahoo's recently-expanded includeFields tripped
yahoo-finance2's strict ScreenerResult schema and threw — gainers/losers/
active were all dead against live yahoo. Mirrors the existing search() fix;
validateResult is screener()'s THIRD arg.

Verified live: absolute "most active" #1 was NVDA at RVOL 1.01 (normal
volume, no signal); relative-volume ranking surfaced RDW (+15.1%, 2.3x) and
AVGO (-12.6%, 3.3x) — the events the absolute list buries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants