Skip to content

Latest commit

 

History

History
466 lines (402 loc) · 107 KB

File metadata and controls

466 lines (402 loc) · 107 KB

AMICODE patch-stack log

Local fork of sst/opencode @ v1.18.12 (merge 1f09d0dc57, 2026-08-04) on branch notturno/merge-upstream-2026-08-04, pending merge of PR #119 into local/amicode. Remote: upstream only (fetch = github.com/sst/opencode, push URL disabled to no_push_disabled). Never push.

Upstream sync 2026-08-04 (1.18.10 → 1.18.12)

Merged sst/opencode dev @ 7fe99387 (v1.18.12; v1.18.11 + v1.18.12 together, 139 files, +4342/−357) into local/amicode @ cf4f06b41. Zero conflicts, zero fork files touched — parity manifest clean on the merged tree. First pass produced by the notturno merge-sentinel machinery (hand-completed locally: the sentinel's hermetic clone lacked a git identity, so its in-CI merge died at commit time with a phantom "merge conflict (0 path(s))" — harmoniqs/amico#322, module fix in harmoniqs/amico#324).

  • Adopted upstream's, all bugfix-tier: MCP SSE reconnect-loop fix; provider configs with interleaved reasoning fields (reasoning_text / custom field names — same bucket as patch #24's provider/model port; verified landed in packages/llm/src/protocols/openai-responses.ts); Azure GPT-5.5+ reasoning fix; composer draft-store (large-image lag); project search across all known recent projects; stale-assistant-error clearing on resume; legacy-config skip for v2 servers; desktop fixes (external links, title bar, dir picker, file tree, gutter).
  • Kept fork's: everything — the merge touched no amicode-patched file; session-side-panel.tsx deletions (ADR 0001) stand.
  • Fork deviation (e2e): upstream's new project-picker-recent-search.spec.ts skipped — it drives upstream's home-projects surface ("Add project" on the home page), which the fork doesn't render (NewHome has no add-project entry; the legacy interface is compile-time retired, settings.tsx newLayoutDesigns = createMemo(() => true); home-projects-view.tsx is dead code here). The five-cap + search-all logic ships intact in dialog-select-directory.tsx; re-enable per #120.
  • Build + smoke-verify on the dev host before tagging v1.18.12-amicode.1 (release ritual unchanged; channel gate gotcha 2 still applies: OPENCODE_CHANNEL=dev).

Upstream sync 2026-08-01 (1.17.3 → 1.18.10, merge-base 2026-06-10)

Merged sst/opencode dev @ 19231fce4b (1,229 upstream commits) into the fork's 472. 78 conflicts resolved by policy: versions/lockfile mechanical; ~40 files hand-merged. Notables:

  • Adopted upstream's: session-ui package split (fork's message-part-groups/message-part-skill moved there with their consumers), review/diffs side panel (fork's vault tab grafted in; reviewOpen = tabsOpen — same persisted key), controller-driven composer/settings/home, AppNodeBuilder test harness (fork's promptAgnosticMatcher kept inside it), bounded SSE + heartbeat, native per-event-location contract (supersedes the fork's connection-pinned streams; the app reducer filters per-directory client-side).
  • Kept fork's: branding/fonts/accents, KaTeX macros (now threaded into upstream's hand-rolled renderKatexToken), AmicoSpinner sites, entity-rail card dispatch (new @opencode-ai/ui/amicode-* shims; packages/ui now depends on @opencode-ai/session-ui for ask-card's data context — module-level acyclic), titlebar inline tab strip (server-wide sync.session.get replaces the deleted dirBase64), prompt-agnostic cassette matcher.
  • Dropped: old message-timeline.tsx mounts (entity rail, AmicoSpinner, ask bridge) — upstream deleted the file; ports into pages/session/timeline/ are FOLLOW-UPS. debug-bar.tsx stays deleted. showSessionProgressBar row dropped (its consumer died with the timeline). Fork's markdown polish (~75 lines, heading hierarchy + inline-code chip) needs porting into session-ui's markdown.
  • prompt.ts silent-turn guard REFINED: fires only on reasoning-bearing turns with no error. Upstream treats mid-stream provider explosions as finish unknown (no error, no reasoning) and its run-process tests lock "end the run" for that shape — the unrefined guard re-invoked them.
  • i18n parity: upstream's strict parity test demands every en.ts key in every locale; the 72 amicode-era English keys were filled into all 17 app locales as ENGLISH fallbacks (verbatim, placeholders preserved). Non-English translations still deliberately unreviewed.
  • Test-suite gotcha (this machine): the Amicode session exports OPENCODE_CONFIG_CONTENT (skills.paths + permissions) — it leaks into any bun test spawned from an Amicode shell and breaks skill/permission fixtures. Run suites with env -u OPENCODE_CONFIG_CONTENT -u OPENCODE_SERVER_PASSWORD bun test.
  • Known reds, all pre-existing or upstream-flaky: pasqal connections (8) + amicode widgets (15) fail identically on origin/local/amicode (WIP features); httpapi-v2-pty "serves location-wrapped PTY routes" times out ~1-in-3 on clean upstream on this machine.
  • Build recipe below is unchanged (channel gate gotcha 2 still applies: OPENCODE_CHANNEL=dev).

Build recipe (v1.17.3)

Toolchain: bun 1.3.14 (~/.bun/bin/bun — must ALSO be on PATH, see gotcha 1). Node/pnpm not used by this repo (pure Bun workspace).

1. Install

cd ~/harmoniqs/opencode
PATH="$HOME/.bun/bin:$PATH" bun install

Gotcha 1: plain ~/.bun/bin/bun install FAILS — tree-sitter-powershell's node-gyp postinstall shim re-invokes bun by name (exit 127: node-gyp: 3: bun: not found). Prepending ~/.bun/bin to PATH fixes it; install then completes clean (2729 packages, root postinstall packages/core fix-node-pty + husky OK). No source patches needed.

2. How the release artifact is produced (recon)

  • CI (.github/workflows/publish.yml, job build-cli) runs ./packages/opencode/script/build.ts with OPENCODE_VERSION / OPENCODE_RELEASE set, then uploads packages/opencode/dist/opencode-linux*.
  • packages/opencode/script/build.ts is the whole recipe:
    1. imports script/generate.ts → fetches https://models.dev/api.json (network needed; override with MODELS_DEV_API_JSON=<file> or OPENCODE_MODELS_URL) → inlined as OPENCODE_MODELS_DEV define.
    2. Web UI embed: runs bun run --cwd packages/app build (vite build → packages/app/dist, SolidJS; Sentry plugin auto-disabled without SENTRY_* env), then generates an in-memory virtual module opencode-web-ui.gen.ts that imports every dist file with { type: "file" } and exports a { "relative/path": embedded-file-path } map. Skip with --skip-embed-web-ui.
    3. Bun.build({ compile: { target: "bun-linux-x64", outfile: "dist/opencode-linux-x64/bin/opencode" }, ... }) — i.e. bun build --compile, minified ESM, entrypoints src/index.ts + opentui parser.worker.js + src/cli/tui/worker.ts + the generated web-ui module.
    4. --single limits targets to current platform (native, non-baseline, non-musl); --skip-install skips the cross-platform bun install --os=* --cpu=* prebuild pulls (only needed for cross-compiling; keeps the tree clean).
    5. Runs its own smoke test (<binary> --version).
    • Version/channel come from packages/script/src/index.ts (Script): without env it fetches npm + uses the git branch as channel → set OPENCODE_VERSION=1.17.3 (forces channel latest, keeps Script.release false so no gh release upload runs).
    • Gotcha 2 (channel gate — bit releases amicode.1/.2): channel latest compiles the embedded web UI with VITE_OPENCODE_CHANNEL="prod" (packages/app/vite.js), which defaults settings.general.newLayoutDesigns OFF (packages/app/src/context/settings.tsx) — hiding EVERY amicode surface (home cards, institution card, v2 titlebar) at runtime, though the code is compiled in. ALWAYS also set OPENCODE_CHANNEL=dev (any non-latest channel maps to dev for the UI). Verify a build: grep -aoh 'newLayoutDesigns:.\{1,45\}' <binary> → find the default var, its assignment must be =!0. Fixed builds: v1.17.3-amicode.3 (2026-07-07, same source as amicode.2).

3. Exact local build command (worked)

cd ~/harmoniqs/opencode/packages/opencode
PATH="$HOME/.bun/bin:$PATH" OPENCODE_VERSION=1.17.3 OPENCODE_CHANNEL=dev bun run script/build.ts --single --skip-install
# artifact: packages/opencode/dist/opencode-linux-x64/bin/opencode
# convenience copy: ~/harmoniqs/opencode/dist/opencode-local
# OPENCODE_CHANNEL=dev is REQUIRED — see gotcha 2 (channel gate) above.
# darwin cross-compile (release asset): drop --single --skip-install, set
#   OPENCODE_BUILD_TARGETS="opencode-darwin-arm64"

3b. Smoke test (2026-07-03, local build)

./dist/opencode-local serve --port 14096
  • startup log: opencode server listening on http://127.0.0.1:14096 (+ warning: OPENCODE_SERVER_PASSWORD not set → unsecured)
  • GET / → 200, SolidJS app HTML shell (<title>OpenCode</title>, <div id="root">, script /assets/index-BfWaaOZM.js = the exact vite-build hash → served from the binary embed, no separate app step needed)
  • GET /assets/index-BfWaaOZM.js → 200 text/javascript 1,694,088 B
  • GET /health → 200 but it's the SPA index.html fallback — no dedicated /health route at v1.17.3
  • POST /session {} → 200 {"id":"ses_…","slug":"shiny-cabin","projectID":"…","directory":…,"cost":0,"tokens":{…},"title":"New session - …","version":"1.17.3","time":{created,updated}}
  • GET /session → 200 array of sessions; GET /doc → 200 (OpenAPI)

4. How the SolidJS app is served

packages/opencode/src/server/shared/ui.ts:

  • Compiled binary: import("opencode-web-ui.gen.ts") resolves → serveEmbeddedUIEffect serves the embedded packages/app/dist files (SPA fallback to index.html), with CSP derived from the inline theme-preload script hash.
  • Run-from-source (bun run src/index.ts serve): the generated module doesn't exist → catch → reverse-proxies https://app.opencode.ai instead. So run-from-source does NOT serve local app assets; only the compiled binary embeds them. (Env kill-switch: disableEmbeddedWebUi → proxy mode.)

5. How amicode consumes this fork

amicode (harmoniqs/amicode, packages/extension) vendors a compiled binary of this fork; it never runs it from source (per § 4, run-from-source proxies the public app.opencode.ai, so the amicode-branded surfaces only exist in a compiled build).

  • Default = pinned release. opencode.lock.json source: release → a plain pnpm install downloads the pinned, gate-ON release asset. Teammates need no clone/bun. Changing this fork is an explicit command, never a lock edit.
  • Iterate: clone this fork beside amicode (../opencode), then from amicode run pnpm --filter amicode opencode:build → invokes script/build.ts with OPENCODE_CHANNEL=dev (gotcha 2) and re-vendors the binary. Reload the Extension Dev Host to pick it up.
  • Publish: push a branch here, tag a release (the release workflow builds both targets with dev and asserts the gate ON), then from amicode pnpm --filter amicode opencode:pin <tag> to rewrite amicode's lock (downloads + sha256-verifies both assets).
  • amicode's packages/extension/scripts/assert_ui_gate.sh reds CI/release if any vendored binary ships with the gate OFF — the automated form of gotcha 2's grep.

Branding map (v1.17.3)

User-visible brand sites in the served web app (packages/app + packages/ui), enumerated at tag v1.17.3. Format: file:line — string/asset — replacement. [desktop] = ships only in the Electron desktop app, not the served web app.

# site current replacement
1 packages/app/index.html:6 <title>OpenCode</title> <title>Amicode</title>
2 packages/app/index.html:8 svg favicon /favicon-v3.svg /amico.svg (new brand asset)
3 packages/ui/src/assets/favicon/site.webmanifest:2-3 (symlinked as packages/app/public/site.webmanifest) name/short_name "OpenCode" "Amicode"
4 packages/ui/src/assets/favicon/amico.svg (new; symlinked as packages/app/public/amico.svg, matching the repo's favicon symlink pattern) (new) favicon: brand-accent disc #FFF676 + amico face (from amicode packages/extension/media/amico.svg, on-accent #000)
5 packages/ui/src/components/favicon.tsx:10 apple-mobile-web-app-title "OpenCode" "Amicode" (+ svg icon Link /amico.svg)
6 packages/ui/src/components/logo.tsx Logo = OPENCODE letterform SVG; Mark/Splash = "O" mark Logo → AMICODE wordmark text; Mark/Splash → amico face (inlined from amico.svg). Rendered at: home.tsx:1151 (watermark), pages/error.tsx:281, session-side-panel.tsx, session-new-view.tsx, app.tsx (loading splash)
7 packages/ui/src/v2/components/wordmark-v2.tsx OPENCODE glyph SVG AMICODE wordmark text (rendered on session-new-design-view.tsx new-session screen)
8 packages/ui/src/theme/context.tsx:72 theme display label opencode: "OpenCode" "Amicode" (key opencode untouched — storage/config contract)
9 packages/app/src/i18n/en.ts 30 brand mentions ("…models in OpenCode", "OpenCode Desktop", update/error/settings copy) "Amicode" (29 sites; "OpenCode Zen" kept, see below)
10 packages/app/src/desktop-menu.ts:75 [desktop] menu label "OpenCode" "Amicode"
11 packages/app/src/wsl/settings-model.ts:17-18 [desktop, Windows/WSL] "Install OpenCode"/"Update OpenCode" "Install/Update Amicode"
13 packages/ui/src/components/logo.css [data-component="logo-mark"] aspect-ratio 4/5 (old 16x20 viewBox) aspect-ratio 1/1 (new square amico mark)
12 packages/ui/src/theme/themes/oc-2.json (default theme) accent tokens v2-{background,text,icon}-*accent* = blue-600/700 (light), blue-400/300 (dark) Harmoniqs accent #FFF676 (brand.css --color-accent): dark text/icon/bg-accent → #FFF676, hover #FFFA9E; light bg-accent → #FFF676, light text/icon-accent → derived dark shades #857A00 / hover #6B6200 (raw #FFF676 is unreadable on light bg)

Deliberately left stock (and why):

  • 25 non-English app locales + all ui locales brand strings — demo is English; en.ts is the default/fallback dictionary; bulk-editing translations is churn without review.
  • "OpenCode Zen" (app en.ts:139) and "OpenCode Go" (ui en.ts:58) — proper names of the external commercial model-gateway services the binary still connects to; renaming would misrepresent a third-party service.
  • Shiki syntax-theme name "OpenCode" (ui context/marked.tsx, ui pierre/worker.ts, ui pierre/index.ts) — cross-module string identifier, not user-visible; partial rename breaks code highlighting.
  • Font names "OpenCode Sans/Mono" — CSS font-family identifiers.
  • Binary icon assets (favicon-96x96-v3.png, favicon-v3.ico, apple-touch-icon-v3.png, web-app-manifest-*.png, social-share.png) — need image regeneration tooling; svg favicon takes precedence in modern browsers. Morning follow-up.
  • All internal identifiers: opencode-titlebar-* DOM ids, opencode-* localStorage keys, OPENCODE_* env vars, config keys, API/SDK strings, @opencode-ai/* imports, oc-theme-preload-script id (the server CSP hash regex in packages/opencode/src/server/shared/ui.ts matches this exact id).
  • Storybook/stories files — dev-only, not in the served app.
  • desktop-menu.ts:203 "OpenCode Documentation" [desktop] — the link target IS opencode.ai/docs; relabeling would misattribute upstream docs.

Branded binary (v1.17.3 + L1, built 2026-07-03)

Rebuilt with the exact T3 recipe (OPENCODE_VERSION=1.17.3 bun run script/build.ts --single --skip-install in packages/opencode, ~/.bun/bin on PATH).

  • Binary: ~/harmoniqs/opencode/dist/opencode-local (copy of packages/opencode/dist/opencode-linux-x64/bin/opencode)
  • sha256 (L1 branding build): 930fd0457d9ffb389027ca23d09cc0b1ab9d6aaa1a2022864dc8e1b748d3c5f4
  • sha256 (L1+L2 build, current): 8f4c6751ca5c112e25652de7adb9a3b6b710f64fa4de8fb53ceb8949cf3e1ecb — L2 verify: GET / → 200 <title>Amicode</title>; card code compiled into the lazy session chunk (/assets/session-U3zHIEN6.js → 200, amicode-card ×5, AMICODE ×1); POST /session {} → 200 session object (no regression)
  • sha256 (L1+L2+L3 build, current): 7f60c60de1a23c82cd6477f2b6e67bb3956982a23cdbbc57db731f56927c5eff — L3 verify: GET / → 200 <title>Amicode</title>; session chunk /assets/session-BQYupOEO.js → 200 (amicode-entity-rail ×1, amicode-card ×3); POST /session {} → 200 session object (no regression). Vendored copy refreshed to the same sha (cp over the live file per coordinator — running server keeps the old inode; Aaron picks it up on next debug restart).
  • Vendor swap refreshed (2026-07-03): L1+L2 binary copied over ~/harmoniqs/amicode/packages/extension/vendor/opencode/linux-x64/opencode (idle-checked first; --version → 1.17.3; backup remains at opencode.stock). NOTE: the .sha256 sidecar is left at the MANIFEST value on purpose — fetch:opencode skips download only when sidecar == manifest sha, so overwriting it with the local binary's hash would make the next fetch re-download stock and clobber the swap.
  • Verify (serve --port 14096): GET / → 200 with <title>Amicode</title> + href="/amico.svg"; GET /amico.svg → 200 image/svg+xml 441 B; main JS /assets/index-CeQonklQ.js: AMICODE ×1 (wordmark; WordmarkV2 lives in the lazy session chunk), Amicode ×31 (i18n/meta); POST /session {} → 200 session object, version 1.17.3 (no functional regression); /site.webmanifest → name/short_name Amicode. (/health is SPA fallback at this tag — not a health check.)

Patch stack

  1. 4e20def26e — amicode: build fixes for local v1.17.3 reproduction (AMICODE-PATCHES.md, bun.lock ghostty-web drift)
  2. c566a17db7 — amicode: L1 branding — AMICODE wordmark, logo, accent. Files: packages/app/index.html, packages/ui/src/assets/favicon/{amico.svg,site.webmanifest}, packages/app/public/amico.svg (symlink), packages/ui/src/components/{favicon.tsx,logo.tsx,logo.css}, packages/ui/src/v2/components/wordmark-v2.tsx, packages/ui/src/theme/context.tsx, packages/ui/src/theme/themes/oc-2.json, packages/app/src/i18n/en.ts, packages/app/src/desktop-menu.ts, packages/app/src/wsl/settings-model.ts. User-visible strings/assets ONLY — no identifier, config-key, env-var, or API renames.
  3. 1929d3db09 — amicode: L2 renderer slot — amicode_* tool cards.
    • Renderer location: the session timeline (packages/app/src/pages/session/message-timeline.tsx:1057) delegates every message part to Part in packages/ui/src/components/message-part.tsx; tool parts dispatch at PART_MAPPING["tool"] via ToolRegistry.render(part().tool) ?? GenericTool. (Errored tool parts branch to ToolErrorCard before this dispatch, so amicode errors keep stock error rendering.)
    • Sole stock-code touch: packages/ui/src/components/message-part.tsx — import at line 38, dispatch branch at lines 1392–1395: /^amicode_/.test(part().tool) ? AmicodeToolCard : (ToolRegistry.render(...) ?? GenericTool). Prefix regex (not per-name ToolRegistry registration) so future amicode_* tools (to_hardware, calibrate) auto-match.
    • New files (all presentation): packages/ui/src/amicode/card.tsx (AmicodeToolCard: "AMICODE · " header in accent var(--v2-text-text-accent), 3px accent left border var(--v2-icon-icon-accent), monospace body = tool output, falls back to pretty-printed input while running); packages/ui/src/amicode/stage.ts (pure stage mapping: pick_system→System, set_model→Model, formulate→Formulation, solve→Run, else de-underscored+capitalized); packages/ui/src/amicode/stage.test.ts (3 tests, bun test src/amicode → 3 pass).
    • Visual check of the card in a live session = morning (headless smoke can't exercise a tool-call part without a model turn).
  4. 9829bd1864 — amicode: L3 entity rail + collapsed tool chips. Live feedback (Aaron, 2026-07-03): full cards repeated per call cluttered the transcript, and the raw tool return is agent-directed text (for the MODEL, not the human).
    • AmicodeToolCard collapsed to a one-line chip "AMICODE · updated ✓" (or "running…"); monospace body REMOVED on purpose — do not resurrect it.
    • NEW packages/ui/src/amicode/entity-rail.tsx — AmicodeEntityRail: compact sticky row, chips System / Formulation / Run showing the LATEST state per stage, derived client-side by scanning the session's amicode** tool parts (messages ULID-sorted; later parts win). Renders nothing until the session has ≥1 amicode** part (non-amicode sessions stay stock); stages without parts show a dimmed "—". pick_system AND set_model both feed the System chip.
    • NEW packages/ui/src/components/amicode-entity-rail.tsx — one-line re-export shim so packages/app imports resolve through the EXISTING "./*": "./src/components/*.tsx" export wildcard (packages/ui/package.json untouched).
    • stage.ts extended with pure helpers railStage + chipTextFromSummary (tolerant summary→chip parser: first parenthetical, comma tokens, rewrites omega=→ω=, delta=→δ=, drive_max=→cap, "N levels"→"N lvl", =-=−; unknown tokens pass through raw; no/empty parenthetical → undefined). stage.test.ts: 9 tests / 20 expects, bun test src/amicode → 9 pass.
    • Stock-code touch #2 (rail mount): packages/app/src/pages/session/message-timeline.tsx — import at line 20, mount at lines 1583–1584 (bottom of the sticky data-session-title header inside the timeline ScrollView: once per session view, sticky at top, no prompt overlap, no scroll theft; rail hidden when showHeader() is false — acceptable v0). Total stock-file touches across L2+L3: message-part.tsx (dispatch) + message-timeline.tsx (mount).
  5. (final night batch) — amicode: ask buttons + start-screen wordmark + default locale en. a. ASK BUTTONS (amicode_ask, 7th tool in the pack). NEW packages/ui/src/amicode/{ask.ts, ask-bridge.ts, ask-card.tsx, ask.test.ts}.
    • parseAskInput (pure): reads question/options[] from the tool part's INPUT args; trims, drops non-string/empty options; malformed → undefined → AmicodeToolCard falls back to the collapsed chip.
    • latestAssistantMessageID (pure): ULID-max over role==="assistant" — staleness guard; buttons render disabled when the part is NOT in the last assistant message, after a click (local picked state), or when no bridge is registered (share page/read-only surfaces can never submit).
    • Send path: ask-bridge module signal. The app registers it through the EXISTING rail mount — AmicodeEntityRail gained an onAsk prop; message-timeline passes (text) => sdk.client.session.promptAsync({ sessionID, parts: [{type:"text", text}] }) — same endpoint the prompt input's submit uses (model/agent are OPTIONAL in the server PromptInput schema → session/agent defaults apply). Bridge unregisters on rail unmount.
    • Stock-code touches (L2 family): packages/ui/src/components/message-part.tsx — messageID={part().messageID} added to the tool <Dynamic> (1 line, needed for the staleness guard); packages/app/src/pages/session/message-timeline.tsx — the existing rail mount now passes onAsk (same insertion site as L3).
    • Tests: bun test src/amicode → 14 pass / 33 expects (stage 9 + ask 5). b. START-SCREEN WORDMARK (L1 family). packages/app/src/components/session/session-new-view.tsx — localized session.new.title headline ("Build anything" / "Créez ce que vous voulez") REPLACED by <Logo class="w-56 max-w-full" /> (AMICODE wordmark component, deliberately NOT localized — brand marks don't translate). Mark (amico face) above and localized repo-path/branch/subtitle lines kept as-is. (The new-design variant session-new-design-view.tsx already renders WordmarkV2 since L1.) c. DEFAULT LOCALE = en (L1 family). packages/app/src/context/language.tsx — both init sites (warm line ~196 and provider initial line ~203) now default readStoredLocale() ?? "en" instead of ?? detectLocale(); an explicit stored preference (opencode.global.dat:language) still wins. Rationale: only en.ts is branded tonight. REVERT CONDITION: restore ?? detectLocale() at both sites once the other locales are branded (detectLocale is left in place, currently unreferenced).
    • Batch build (L1+L2+L3+ask/wordmark/locale) sha256: a236b87fe92043eea6129a4a5b260ca2adc7fe51fc634e49405872973a7a60cb at both dist/opencode-local and the vendored path. Verify: GET / → 200 <title>Amicode</title>; main chunk /assets/index-Dwcv_zit.js → 200, AMICODE ×1 (start-screen wordmark now in the MAIN bundle); session chunk /assets/session-CGaG1HX5.js → 200, amicode-ask-card ×1 + amicode-entity-rail ×1; POST /session {} → 200 session object; bun test src/amicode → 14 pass.
    • ⚠️ Vendor swap while the binary is RUNNING: plain cp fails with ETXTBSY. Use write-temp + mv -f (rename) — the running process keeps the old inode, next restart picks up the new file. Done that way this batch.
  6. (getting-started) — amicode: start-screen getting-started block + starter chips.
    • NEW packages/ui/src/amicode/getting-started.tsx — AmicodeGettingStarted (data-component="amicode-getting-started"): en-only tagline "Pulse design, from conversation to calibrated waveform." (not localized, consistent with patch 5c), dimmed how-it-works row "① Describe your system / ② Watch the solve live / ③ Send to hardware & calibrate" (flex-wrap, nowrap per step → wraps up to 3 lines on narrow panes), three accent-bordered starter chips (ask-button family) exported as AMICODE_STARTERS. Static content — no unit tests needed.
    • NEW packages/ui/src/components/amicode-getting-started.tsx — re-export shim (same wildcard-export pattern as the rail).
    • Stock touch (L1 family): packages/app/src/components/session/session-new-view.tsx — mounts the block under the wordmark, above the path/branch lines (kept).
    • Submit wiring = DIRECT (not the prefill fallback): startPrompt sets the composer draft via usePrompt().set([{type:"text", content, start:0, end:len}], len) — the submit button's blank() gate reads the SAME prompt store (prompt-input.tsx:317) — then next frame clicks the composer's own [data-action="prompt-submit"]:not([disabled]) button so the REAL handleSubmit runs (worktree resolution, session.create, promote/handoff, navigation, optimistic UI, composer model/agent selection — none of it replicated). Degradation: if no enabled submit button exists, the text stays pre-filled and [data-component="prompt-input"] gets focus (user hits Enter).
  7. (live-session UX bundle, 2026-07-03) — four parts: a. ASK-CARD GUARD FIX (live bug: buttons rendered locked). Old criterion "part must be in the LAST assistant message" was too strict — the model streams text/messages after calling amicode_ask. New criterion: buttons stay active until a USER message exists later (ULID order) than the card's message (hasUserReplyAfter(messages, messageID) replaces latestAssistantMessageID in ask.ts, the AskBridge interface, the rail registration, and the card's active()); local picked lock and no-bridge disable unchanged. b. ASK OPTION DETAILS. parseAskInput accepts optional details: string[] (Track-A commit 778e1bb): validated against the RAW options length, all-strings; mismatch/non-strings → treated as absent (never rejects the card); alignment with options preserved through the invalid-option filter. ask-card renders each detail as a dim 11px second line inside its button (data-slot="amicode-ask-option-detail"). c. COMPOSER PLACEHOLDER. Rotation = i18n keys via EXAMPLES array (packages/app/src/components/prompt-input.tsx:99). en.ts prompt.example.1-5 values → the five pulse-design examples; EXAMPLES trimmed to those five keys (stock touch; keys 6-25 left dormant in every dictionary for easy revert). "Ask anything..." prefix (prompt.placeholder.normal) unchanged. d. AMICO RELABEL (three-layer vocabulary: Amico = persona, Amicode = product). In-chat display labels only — entity-rail header, collapsed chip, ask-card header: "AMICODE" → "AMICO". Start-screen wordmark, <title>, favicon/manifest, getting-started block and all other chrome stay AMICODE/Amicode. All data-component/data-slot amicode-* identifiers unchanged. No unit test asserted the display label.
    • Tests after bundle: bun test src/amicode → 20 pass / 41 expects.
    • Bundle build sha256: 1c6fede30dc1c8eac7a4d544e0001b54b8e6399268c56ab2ee29582b7a80ffe2 (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify: GET / → 200 <title>Amicode</title>; session chunk session-IeOwC1wP.js → ask-card ×1 + rail ×1 markers, display label AMICO ×3; composer-state chunk session-composer-state-VTIq3S7H.js → getting-started ×1; en dictionary chunk index-B2YGTpT0.js → "Warm-start from my last pulse" ×1; POST /session {} → 200.
  8. (brand mark v2) — amicode: "digi" Harmoniqs H-robot replaces the amico smile on the fork's brand-mark surfaces.
    • packages/ui/src/components/logo.tsx — Mark/Splash render the H-robot (inlined shapes; body path fill="currentColor" riding color: var(--icon-strong-base), display rect #0A0A0A, glyphs #FFF676); viewBox 0 0 64 56 (8:7, NOT square). Logo wordmark, data-component hooks, and prop pass-through unchanged — call sites untouched.
    • packages/ui/src/components/logo.css — [data-component="logo-mark"] aspect-ratio 1/1 → 8/7.
    • packages/ui/src/assets/favicon/amico.svg — replaced with the FAVICON variant: 72×72 yellow chip (rx=14, #FFF676) + mark in <g transform="translate(4,8)" color="#17181A">. Public symlink packages/app/public/amico.svg follows automatically; favicon.tsx/index.html links and site.webmanifest unchanged (verified resolving).
    • Canonical source of the mark also lives at amicode:packages/extension/media/amico.svg — kept in sync MANUALLY; if the mark changes again, update both.
    • Brand-v2 build sha256: 0de38685480961a70775c64d81f46ed3a4e8197b2af6ebe7074bc0fceaa4aab3 (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify: GET / → 200 <title>Amicode</title>; GET /amico.svg → 200 image/svg+xml 1324 B (yellow chip rx=14 + robot path); main chunk index-Drab4s7t.js → robot path M2 2h16v14h28V2h16v52 ×1 AND AMICODE wordmark ×1; POST /session {} → 200; bun test src/amicode → 20 pass.
  9. (native question form branding) — consolidation: opencode's NATIVE question tool (packages/opencode/src/tool/question.ts, rendered by the question dock) is now the ONE ask mechanism; amicode_ask is DEPRECATED (L0 prompts updated on the night branch).
    • packages/app/src/pages/session/composer/session-question-dock.tsx (~line 434, inside the DockPrompt header fragment): "AMICO · Question" brand line inserted INSIDE question-header-title, above the progress summary — same label family as amicode-ask-card (accent AMICO, faint ·, base Question); display-only, no behavior change (radio options, descriptions, multiple, custom answer row, progress header all untouched).
    • packages/ui/src/components/message-part.css (question block, [data-slot="question-body"]): accent left border 3px solid var(--v2-icon-icon-accent) — matches the rail/cards treatment.
    • LEGACY pending morning review: packages/ui/src/amicode/{ask-card.tsx, ask-bridge.ts, ask.ts hasUserReplyAfter/parseAskInput} + the message-part.tsx amicode_ask routing remain in place as the legacy renderer for deprecated amicode_ask calls — do NOT remove tonight.
    • Question-branding build sha256: 69070a33f8d5ebdff4f38f3d68b4be5625398b581accfb577945e1e9d522f44c (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify: GET / → 200 <title>Amicode</title>; AMICO display labels total 4 — session chunk session-BfBMMSAH.js ×3 (rail/chip/ask-card) + composer-state chunk session-composer-state-D8UQPTE4.js ×1 (question dock, amicode-question-brand slot ×1 — the dock compiles into the COMPOSER chunk, not the session chunk); css asset carries the accent border rule ×1; POST /session {} → 200; bun test src/amicode → 20 pass.
  10. (H-glyph working spinner) — amicode: the session timeline's working/thinking spinner is the H-robot glyph.
  • Design v2 per Aaron (binary-eye-frames concept DROPPED before commit): small monochrome H silhouette with the screen slit knocked out (single path, fill-rule=evenodd, currentColor — mount-site style={{color}} passes through like stock Spinner).
  • Animation choice: gentle opacity pulse reusing the EXISTING pulse-opacity keyframes (styles/animations.css, 0.4→1) at 1.2s — chosen over Claude-style rotation because the H is non-radial and tumbles/blurs at 16px, and pulse matches the stock spinner's own animation language. prefers-reduced-motion: static glyph, NO animation (matchMedia guard; the repo's CSS-file media-query pattern doesn't cover inline animations).
  • NEW packages/ui/src/amicode/spinner.tsx (AmicoSpinner, data-component="amico-spinner") + packages/ui/src/components/amico-spinner.tsx re-export shim. No pure frame logic remained after the design change → no new unit tests.
  • Stock touch: packages/app/src/pages/session/message-timeline.tsx — import swap + <AmicoSpinner class="size-4" .../> replacing <Spinner .../> at the sticky-header working indicator (~line 1359); unused Spinner import removed. Other spinner sites left STOCK (list for morning): session-header.tsx:362 (header circle), home.tsx:848, sidebar-items.tsx:66+124, sidebar-workspace.tsx:102, dialog-connect-provider.tsx.
  • Spinner build sha256: 83f5a9fd3b35155bf2014115b54a27ef3d9542e8b14c7a83925175a73c1f7ebc (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify: GET / → 200 <title>Amicode</title>; session chunk session-CMqmIgk2.jsamico-spinner ×1 + pulse-opacity 1.2s ×1; POST /session {} → 200; bun test src/amicode → 20 pass.
  • SWEEP (Aaron: "no spinner" — the sticky-header site isn't where he looks): AmicoSpinner now at ALL visible working indicators: session-header.tsx:362 (top-right circle, size-3.5 + tint), message-part.tsx task-tool running row (~1841; inline 16x14 style because basic-tool.css sizes only [data-component="spinner"]), session-retry.tsx:58 (retry card), home.tsx:848, sidebar-items.tsx:66+124, sidebar-workspace.tsx:102 — each with the site's exact class/tint. Stock Spinner imports removed from swapped files. Left stock on purpose: dialog-connect-provider.tsx (provider setup, rarely seen), wsl/dialog-add-server.tsx (Windows-only).
  • Sweep build sha256: 8aa718ad957607ff4d4da563d836a54451420dfb98d390a722ec09217bf2383c (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify: GET / → 200 <title>Amicode</title>; amico-spinner ×1 in served MAIN chunk index-A7nOnjqy.js — the component definition hoisted to the main bundle now that main-route sites (home/sidebar) import it; all call sites share that one definition, so marker count = 1 definition, not per-site; pulse-opacity 1.2s wired ×1; POST /session {} → 200; bun test src/amicode → 20 pass.
  1. (release prep) — amicode: darwin-arm64 cross-compile + release-asset packaging.
  • packages/opencode/script/build.ts: OPENCODE_BUILD_TARGETS env filter (comma-separated artifact names, e.g. opencode-linux-x64,opencode-darwin-arm64); unset → stock behavior. Needed because stock offers only --single (current platform) or ALL 12 targets.
  • Darwin cross-compile SUCCEEDED first attempt: run WITHOUT --skip-install (the cross-platform prebuild pulls of @opentui/core / @parcel/watcher / @ff-labs/fff-bun are required for non-native targets; they did NOT dirty the tree) and WITHOUT --single. Output: dist/opencode-darwin-arm64/bin/opencode (Mach-O 64-bit arm64, ~120 MB, not smoke-testable on linux).
  • Determinism note: the linux-x64 binary from this run is BYTE-IDENTICAL to the sweep build (sha 8aa718ad…) — Bun compile is reproducible given identical source + models.dev snapshot.
  • Release assets (upstream layout: flat opencode inside; from dist//bin: linux tar -czf, darwin zip -r), staged at dist/release/:
    • opencode-linux-x64.tar.gz sha256 268596e61475dd79d3076dd65d39bf97926d56c28477419250ab3c678bd55b10
    • opencode-darwin-arm64.zip sha256 f1d6291485246e03a3d33eefb40b4d264d68c8e5812b11b4ffdfec4444ab9455 (opencode.lock.json stores the ARCHIVE sha — fetch_opencode.mjs hashes the downloaded asset bytes. tar.gz embeds mtimes: upload THESE exact files, do not repack.)
  • Publication status: push/release NOT executed — the permission system denied adding a push remote (git remote add origin); per agent policy the coordinator's message is not user consent, and this fork's standing guardrail is no push remote. Everything is staged for a human-run publish (commands in the handoff).
  1. (vaults panel tab) — amicode: "Vaults" tab in the connections popover + GET /amicode/vaults endpoint + Manage-vaults prompt handoff. Spec/plan: personal vault spec-20260703-140141-amicode-vaults-panel-tab / plan-20260703-143356-amicode-vaults-panel-tab (rollout step 6, panel half, of the amico-vault CLI spec; data source = amico-vault status --json, harmoniqs/amico PR #184).
  • FIRST SERVER-SIDE STOCK TOUCH in the stack: packages/opencode/src/server/routes/instance/httpapi/server.ts — import + amicodeVaultsRoute const below docRoute (exact docRoute idiom: raw HttpRouter.use, authOnlyRouterLayer) + one Layer.mergeAll entry (before uiRoute). Endpoint logic in NEW packages/opencode/src/server/amicode/vaults.ts: CLI resolution ($AMICOVAULT_BIN → $AMICO_OPS/scripts/amico-vault → ~/.amico/ops/scripts/amico-vault → PATH via Bun.which), AbortSignal.timeout(8s) deadline (the process-util timeout opt is only SIGTERM→SIGKILL grace — it never arms a deadline without abort), 10 s response cache, verbatim JSON relay; every failure synthesized into the SAME plural shape {ok:false, mounts:[], error:"<code>: <detail>"} (codes cli_not_found/timeout/exit/bad_output) — status() never rejects. Tests: packages/opencode/test/server/amicode-vaults.test.ts (3).
  • NEW packages/ui/src/amicode/vaults.ts — tolerant single consumer of the wire shape (last_sync is git %cr prose or literal "unknown", rendered verbatim, "unknown"/absent → "—"; non-array/singular mounts → bad_shape; per-entry field tolerance) + vaults.test.ts (9). NEW packages/ui/src/amicode/vaults-tab.tsx (AmicodeVaultsTab, data-component="amicode-vaults-tab"; rows: warn/clean dot [inlined warnings.length check — conscious deviation from the spec's "tested helper", trivial enough that a helper would be ceremony], id, dim kind, rw/ro chip, right-aligned last_sync, dim first-warning second line "+N more"; error state carries a Retry link amicode-vaults-retry; skeleton when view undefined; exports AMICODE_MANAGE_VAULTS_PROMPT) + components/amicode-vaults-tab.tsx re-export shim.
  • Stock touches (app): status-popover-body.tsx — createResource fetch per-active-server (source = shown() && ServerConnection.key → refetch on open/server-switch; Basic auth via the EXISTING authTokenFromCredentials from utils/server.ts; rejected fetch → same error+Retry state), vaults Tabs.Trigger with count badge + Tabs.Content (list + "Manage vaults" secondary Button), props widened {shown, onClose?}; status-popover.tsx — onClose={() => setShown(false)} threaded at BOTH Body mounts (v1 line ~70, DirectoryStatusPopover body thunk); ServerStatusPopover/ServerBody untouched. en.ts — 5 keys after manageServers (tab.vaults, action.manageVaults, dialog.vaults.{empty,retry,fetchFailed}).
  • Manage-vaults mechanism: session-new-view.tsx's local startPrompt closure EXTRACTED to packages/app/src/utils/start-prompt.ts (takes the resolved prompt context — must not call usePrompt() itself); session-new-view refactored to a thin wrapper (behavior-preserving); popover onManage = onClose() + startPrompt(prompt, AMICODE_MANAGE_VAULTS_PROMPT). Prompt sentence doubles as the future vault-setup score's onset trigger.
  • Vaults build sha256: a94226125bc8a8fa9022be8bcdecb2cef2956c19e1955a3dd5031e70de2c6eb1 (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify (scratch ports 43197-43199): GET /amicode/vaults → 200 relaying live CLI JSON (3 mounts, last_sync "24 minutes ago"/"7 days ago" prose); CLI-absent env (fake HOME, stripped PATH) → {"ok":false,"mounts":[],"error":"cli_not_found: amico-vault not found — is amico installed?"}; auth parity with /doc under OPENCODE_SERVER_PASSWORD (noauth 401 / auth 200, both routes); GET / → 200 <title>Amicode</title>; POST /session {} → 200; chunk check: status-popover-body-WTKgcSgC.jsamicode-vaults-tab ×1 (served 200), "Manage vaults" i18n in index-DVjD35cF.js. Tests: bun test src/amicode → 29 pass; opencode amicode-vaults tests → 3 pass; typecheck green in ui/app/opencode. Interactive click-through (tab render, Manage handoff) NOT yet human-verified — deferred to dev-host acceptance on 43117.
  1. (problem UI — spec B) — amicode: problem-header rail + diff receipts + entity view + problem switcher + GET /amicode/problems|problem|run-status. Spec/plan: personal vault spec-20260703-182503-amicode-problem-ui / plan-20260703-200416-amicode-problem-ui (three-ring interaction model; depends on spec A's workspaces on amicode branch aaron/problem-workspaces). Commits fc67485ad2..e6eee73c4c (9).
  • SERVER (second stock touch to server.ts): NEW packages/opencode/src/server/amicode/problems.ts — reads ~/.amico/problems (env AMICODE_PROBLEMS_DIR) + ~/.amico/runs (env AMICODE_RUNS_DIR) directly, .json sidecars only; one-shape failure synthesis per route (codes no_problems_dir / not_found: / bad_json / bad_output); TTL caches 10 s (problems/problem) + 1 s (run-status). score_stages resolution chain: problem.score.id → workspace interview_state.json score_id (spec A never stamps problem.score — the fallback is what makes pending chips live today) → must equal root score_manifest.json id, else []. run-status rules: FINISHED+parseable result.toml → finished; FINISHED alone → failed; else solving with last AMICODE_ITER (raw objective f on the wire; client renders F=1−f only when 0≤f≤1). Routes registered via Effect.gen + yield* router.add (chained .add() does NOT compose — add returns an Effect; first attempt broke 161 server tests, caught pre-commit) under authOnlyRouterLayer; new URL(request.url, "http://localhost") (relative request.url, pty.ts idiom). Tests: test/server/amicode-problems.test.ts (11).
  • UI PURE: NEW packages/ui/src/amicode/receipt.ts — single consumer of the AMICODE_DIFF sentinel (strict last-line parse; any failure → undefined → legacy chip, old sessions keep rendering; elision key → bare ellipsis; stripSentinel exported/tested but NO current consumer — card never renders raw output, noted per spec's strip rule) + receipt.test.ts (6). NEW problem.ts — single consumer of the three endpoint wire shapes: tolerant parsers, per-kind chip renderers + generic fallback, pending merge (score_stages × present), runChipText, railState (loading/ready/stale-last-good/unavailable), entityRows/historyRows/editPromptText + problem.test.ts (14). NEW ui-bridge.ts (receipt click → rail-registered openEntity, ask-bridge idiom).
  • RAIL REWRITE: entity-rail.tsx — session gate unchanged (≥1 amicode_* part); createResource on the completed-part counter (the one implemented refetch trigger; "section open" N/A for an always-visible rail — dialogs fetch on open; server switch handled per-call by amicodeGet resolving the connection each invocation); data-driven chips + dimmed pending chips; run chip polls /amicode/run-status @2.5 s while unfinished, feeds ONLY the run chip, NEVER triggers a problem refetch (spec's #67 transposition), stops on all-terminal + unmount; failure states: stale-last-good renders normally, no-data → status unavailable · Retry. DELETED from stage.ts: chipTextFromSummary/TOKEN_REWRITES/railStage/RAIL_STAGES (prose-regex path gone; amicodeStage kept as card fallback label).
  • CARD → RECEIPTS: card.tsx parses the part's output (message-part.tsx already passed it — zero new stock touches) → one-line AMICO · System · levels 3→4 receipt, click → entity view via ui-bridge; no-sentinel fallback = the old status chip.
  • RING-2 DIALOGS: NEW entity-view.tsx (fields table + per-field ✎ → draftPrompt handoff; history newest-first with seq anchor [missing-from-window → unanchored, no error] incl. event source tool/stage; run tier chip, free = warning "free · unvetted"; KaTeX Hamiltonians for transmon/rydberg via the fork's existing katex) + problem-switcher.tsx (rows: active dot, status, N entities, recorded; Open/New = startPrompt SUBMIT handoffs — UI stays write-free, the agent's amicode_problem moves the active pointer) + re-export shims (also exporting the parsers, vaults-tab idiom).
  • APP WIRING: utils/start-prompt.ts gains draftPrompt (draft+focus, no submit — the fallback branch promoted); NEW utils/amicode-fetch.ts (per-active-server Basic-auth GET, connection resolved per call); message-timeline.tsx — open-gated createResources for the two dialogs (cleared in dialog.show onClose so reopen refetches), dialog.show wiring, rail props; session-new-view.tsx + getting-started.tsx — conditional Resume chip from /amicode/problems (most-recent non-archived; the STATIC "Resume my pulse design" starter REMOVED per no-empty-chrome); en.ts — 7 amicode.* keys.
  • AMICODE-REPO SIDE (branch aaron/problem-workspaces, commit d2e2536): AGENTS.md + SCORE.md announce the rail ONCE when the first entity records (score version kept at 3 — guidance prose, not a stage-contract change; a bump would reset in-flight interview state). #47/#48 problem ref field deferred via comment on amicode#47.
  • Tests: ui bun test src → 70 pass / 0 fail; opencode FULL bun test → 3000 pass / 0 fail / 2 skip / 1 todo (skips pre-existing, not amicode); typecheck green in ui + app + opencode.
  • Problem-UI build sha256: a73d858373931826ef0ba6a6c4bd741a065516ba8bb3bd4cf5ff559ee9729289 (dist/opencode-local + vendored path, write-temp + mv -f swap). ⚠️ build INCLUDES uncommitted branding WIP present in the tree (RacingSansOne wordmark: index.css @font-face + logo.tsx/wordmark-v2.tsx family swap + woff2 asset, mtimes 21:10-21:11 — NOT part of this patch, left unstaged for its author). Verify (scratch ports 14097/14098): all three endpoints 200 against the LIVE workspace untitled-2026-07-03 — problems lists it active, problem returns system entity + score_stages ["system","formulation","run","pulse","device_session"] resolved through interview_state.json + 3 events, run-status {ok:true,runs:[]}; auth parity with /doc under OPENCODE_SERVER_PASSWORD (noauth 401/401, auth 200×3); GET / → 200 <title>Amicode</title>; chunk check: session chunk session-Dgq0NnUP.js → amicode-rail-problem/amicode-card-receipt/AMICODE_DIFF/amicode-entity-view ×1 each + amicode.problems.title ×2, composer chunk session-composer-state-DvDmcuNl.js → amicode-problem-switcher + amicode-gs-resume ×1 each. Interactive click-through (rail bind, receipt click, switcher/entity dialogs, Resume chip) NOT yet human-verified — deferred to dev-host acceptance on the extension-launched server (43117), same as patch 12.
  1. (brand fonts) — amicode: AMICODE wordmark → "Racing Sans One"; default code/monospace → "JuliaMono" (Aaron's change, committed + built on his heads-up; it was the uncommitted WIP flagged in #13's build note). Commit c155bf499b.
  • index.css: @font-face for both (JuliaMono full glyph set — Julia Unicode; Racing Sans One latin subset, font-display swap). logo.tsx + wordmark-v2.tsx: font-family 'Racing Sans One' first, weight 700→400. settings.tsx: monoDefault/monoFallback lead with JuliaMono. theme.css: --font-family-mono leads with JuliaMono. Terminal font DELIBERATELY unchanged (JetBrainsMono Nerd Font Mono via separate terminalFallback).
  • New assets (git-added — build breaks without them): public/assets/RacingSansOne-Regular.woff2 (21 KB) + JuliaMono-Regular.woff2 (946 KB).
  • Font build sha256: 2a15da111be516516fb1fbd1a4fb5ae02ad9bddd6373ede08cdf7b28c19d163a (dist/opencode-local + vendored path, write-temp + mv -f swap; SUPERSEDES #13's a73d8583… — same code, fonts now committed). Verify (scratch port 14099): GET /assets/RacingSansOne-Regular.woff2 → 200 font/woff2 21804 B; GET /assets/JuliaMono-Regular.woff2 → 200 font/woff2 946516 B; "Racing Sans One" in built css + index/new-session chunks, "JuliaMono" in index-Dwtxigfs.css; GET /amicode/problems → 200; GET / → 200 <title>Amicode</title>; ui bun test src → 70 pass; typecheck green ui+app (no snapshots assert fonts, per Aaron — confirmed nothing went red). Bonus confirmation: KaTeX_* woff2 assets now in dist — the entity view's katex import (#13) pulls its font set into the embed.
  1. (home CTA fallback) — amicode(home): "Open chat" works on a fresh profile.

    • BUG: startWithPrompt (fork wiring for the Meet-Amico card, patch 5ef6b7e0e) dead-ended silently when the persisted client-side project list was empty (fresh browser profile against a bare opencode serve): the !project branch called openNewSession(), which needs the SAME empty newSessionProject() and hits if (!conn || !project) return. Primary home CTA did nothing, no error. Hit live 2026-07-08 (web UI on a scratch dir).
    • FIX (packages/app/src/pages/home.tsx, startWithPrompt only): when no project is tracked, fall back to the focused server's own working directory — focusedSync().data.path.directory (synced from GET /path; "" until loaded, so the falsy guard holds) — open+touch it as a project (self-heals the home page), then tabs.newDraft with the prompt preserved. Deliberately NOT sync.data.project: the server's "global" project record has worktree "/".
    • Regression spec: packages/app/e2e/regression/home-open-chat-empty-projects.spec.ts — fresh profile (NO localStorage seed), mocked server, click the CTA (exact: true — the whole card is also a button whose accessible name contains "Open chat"), expect navigation to /new-session?draftId= + the cwd persisted as a tracked project. Verified failing on the unfixed code, passing with the fix. Playwright note: config reuses any server on port 3000 (reuseExistingServer) — run with PLAYWRIGHT_PORT=<free> if something else (e.g. the harmoniqs website dev server) holds 3000.
    • Checks: tsgo -b clean; bun run test:unit 376 pass / 0 fail.
  2. (Aero wordmark, re-added) — amicode: AMICODE wordmark/logo font-family gains 'Aero' ahead of the existing fallback chain, both call sites: logo.tsx ('Aero', var(--font-family-sans, ...)) and wordmark-v2.tsx ('Aero', 'Racing Sans One', var(--font-family-mono, ...)). Additive only — no fallback removed, no sizing/weight/spacing touched. This had been iterated as uncommitted working-tree WIP before the branch fast-forwarded 57 commits to current origin/local/amicode (e9b695191); re-applied fresh against the current font stacks (logo.tsx's had independently moved to a sans-serif stack + weight 750/letter-spacing 4 in the interim — preserved as-is, only the family list changed).

    ⚠️ LICENSE-PENDING — release blocker. "Aero" by Nirmal Biswas (Picatype) is donationware, USD 25+ to nirmalbiswas@gmail.com. Bundled at packages/app/public/assets/Aero-Regular.ttf for PREVIEW ONLY; not locked as the official face. Before any release: pay the license and record proof here, or drop 'Aero' from both font-family chains + remove its @font-face (index.css) + the asset.

    • index.css: new @font-face for "Aero" (truetype, weight 400, font-display: swap), inserted immediately before the existing Racing Sans One face; carries the license-pending comment inline.
    • New asset (git-added): public/assets/Aero-Regular.ttf (44,464 B).
    • Verify: bun run typecheck green in both packages/ui and packages/app (tsgo, no errors). Visual/glyph confirmation is manual-only (see build note under #16, which builds and vendors this alongside the paste fix).
  3. (composer paste bridge) — amicode: wire the main chat composer's handlePaste (prompt-input/attachments.ts) to the extension-clipboard bridge that patch #14's PR already shipped for a different input (home-cards.tsx's readClipboardViaBridge/pasteFallback, commit 5a2f1dde44) — that bridge answers {source:"amicode", kind:"clipboard-request"} on the extension side (packages/extension/src/chat_panel.ts, already on main), but nothing in the composer's own paste path ever sent it. Root cause matches home-cards.tsx's own comment verbatim: the chat iframe is sandboxed + cross-origin, so navigator.clipboard is denied and the native paste event's clipboardData arrives empty — Cmd/Ctrl+V into the composer was a silent no-op.

    • platform.tsx: new optional capability readClipboardText?(): Promise<string | null>, alongside the existing readClipboardImage?.
    • entry.tsx: implements it — no-op (null) when not framed (window.parent === window, i.e. plain browser/opencode.ai/desktop — zero behavior change there); when framed, posts {source:"amicode", kind:"clipboard-request", nonce} to window.parent and resolves on the matching {kind:"clipboard", nonce} reply or a 1500 ms timeout — same nonce/timeout/listener shape as the proven readClipboardViaBridge, just exposed as a platform capability instead of a component-local closure (this file already hosts the analogous readClipboardImage).
    • attachments.ts: handlePaste no longer bails out when event.clipboardData is null (only when it's ALSO missing readClipboardText, i.e. non-amicode contexts are untouched); after the existing file/image-bridge attempts, falls back to readClipboardText() when there's still no plain text.
    • prompt-input.tsx: threads readClipboardText: platform.readClipboardText into createPromptAttachments, mirroring readClipboardImage.
    • No shared bridge module extracted — matches this codebase's existing precedent (home-cards.tsx's bridge is also component-local, not a shared util); two call sites isn't yet worth a shared abstraction.
    • Tests: app bun test (66 files) → 376 pass / 0 fail, incl. the untouched attachments.test.ts (pure-function coverage only — handlePaste itself has no unit coverage in this codebase, matching home-cards.tsx's own bridge having none either; verification here is typecheck + build + the manual live-paste check below). Typecheck green in packages/app.
    • Build sha256 (composer-only build, superseded by the generalization below): 564a4cd783524e0f50541b716d9db4048b2e02430ebc77786a11a4504431c085.
    • ADDENDUM (same session, live-test finding — Aaron): composer-only wiring wasn't enough. Confirmed live: pasting into a different editable (the generic "Connect <provider>" API-key dialog, dialog-connect-provider.tsx — one of many such generic settings/connect forms) was still a silent no-op, because that dialog is a plain <input> with no knowledge of the bridge at all. This is the exact gap patch #14's era of WIP had already found and fixed once before (composer-only interception "left every OTHER editable broken (provider API-key field, etc.)") — generalized properly this time instead of re-patching one dialog at a time:
      • entry.tsx: new installGlobalPasteFallback(), installed once at app mount (alongside readClipboardText, not a separate file — same feature area). A single document-level paste listener: activates only when the event target is a form field or contenteditable AND the native event.clipboardData gave no plain text (the sandboxed-iframe symptom) — everywhere clipboardData already works (plain browser tabs, desktop) it's a pure no-op, no framed-check needed at the call site. On activation: fetches text via the same readClipboardText bridge, then splices it into <input>/<textarea> via selectionStart/selectionEnd + dispatches a synthetic input event (Solid's onInput picks it up), or execCommand("insertText", …) for contenteditable.
      • Composer double-fire is a non-issue by construction: attachments.ts's handlePaste calls stopPropagation() on every paste it handles (unconditionally now that readClipboardText is always provided when framed), so the event never bubbles to this new document listener for the composer specifically.
      • Tests: same 376/376 pass (app suite), typecheck green — no regressions. No unit coverage added, same house-precedent reasoning as above (DOM/postMessage bridge code, build+manual verified here).
    • Final build sha256 (covers #15 + #16 + this addendum together): 115216e82a6db15843fca46d5b32b587ba9a1e542297501cf2b45c394f745ea9. Verify: smoke test --version1.17.3; string-literal checks confirm the underlying protocol compiled in (Aero ×5, clipboard-request ×2, readClipboardText ×3 — note: the new installGlobalPasteFallback/isFormField are local identifiers, not string literals, so they're minifier-renamed and don't grep-match; absence there is expected, not a failure signal); channel gate ON (newLayoutDesigns sentinel =!0). Interactive Cmd/Ctrl+V confirmation (composer AND the generalized fallback) is live-VS-Code-only, NOT yet human-verified past Aaron's one negative report on the pre-addendum build — same deferred-to-dev-host-acceptance caveat as patches #12/#13.
  4. (mark drift fix, synced to amicode PR #99 final) — amicode: consolidated the fork's brand mark to ONE geometry, matching amicode's redesigned mark. The "kept in sync manually" cross-repo promise from patch #8 had already silently failed.

  • Trigger: amicode's mark was redesigned (PR #99) without a corresponding update here — the fork still rendered the OLD "digi" pixel-accented H-robot everywhere. PR #99 went through several iterations before landing on its final geometry; this entry tracks that FINAL state (square viewBox 0 0 3600 3600), not the intermediate "hackathon mark" (viewBox 116 287 3377 3035) an earlier draft of this fork PR had copied — that intermediate geometry is now itself stale and was replaced here.
  • Two copies of near-identical geometry lived in THIS repo (logo.tsx's Robot used by Mark/Splash, and spinner.tsx's AmicoSpinner), plus a third in favicon/amico.svg. Consolidated to a single MARK_PATH exported from logo.tsx.
  • Geometry now mirrors amicode PR #99's two authored SVGs (amicode:packages/extension/media/amico{,_reduced}.svg), both square 0 0 3600 3600:
    • MARK_PATH = amico_reduced.svg's outer-bracket path (fill-rule evenodd screen knockout). Used by Mark, Splash, AmicoSpinner, and mirrored as a literal in favicon/amico.svg — every SMALL context, matching amicode's own "small → reduced" rule.
    • MarkDetailed = amico.svg's full detailed mark (bracket path + internal circuit-pattern rects/polygons). Used ONLY by the Meet Amico home card (w-12/48px), large enough for the detail to resolve.
    • Mark/Splash/MarkDetailed viewBox 0 0 64 56/116 287 3377 30350 0 3600 3600; logo.css aspect-ratio 8/71/1 (the mark is square now).
  • Still theme-adaptive via currentColor + var(--icon-strong-base) — this is a live webview DOM, so currentColor resolves (unlike amicode's native VS Code tab icon, which needs committed {light,dark} files; see amicode PR #99).
  • NOT a re-established cross-repo sync promise — MARK_PATH is the single source of truth WITHIN this repo; it happens to match amicode's current geometry, kept aligned by hand when the mark changes.
  • Tests: ui bun test src/amicode green; typecheck green ui + app.
  • NOT done this round: full native bun run script/build.ts compile + vendored-binary swap — this patch only touches the embedded web UI. Deferred to the next amicode.N release tag, same split as patches #8/#11.
  1. (rich chat output, 2026-07-10) — amicode: assistant text style parity with Claude Code. packages/opencode/src/session/prompt/anthropic.txt ONLY (the provider prompt every Claude-model session gets).
  • Trigger (Aaron, live comparison): amicode replies rendered as flat unstyled paragraphs — no headings, no bold, no lists — while Claude Code's chat output is structured markdown. The renderer was never the problem (packages/ui markdown.tsx/css fully style headings/bold/tables/LaTeX); the prompt was: "output will be displayed on a command line interface. Your responses should be short and concise."
  • Change: "# Tone and style" → "# Communicating with the user", modeled on Claude Code's guidance — output renders as rich GFM (use inline code/bold/lists/headings/tables deliberately), match structure to content (simple question → plain prose), lead with the outcome, readable-beats-brief (complete sentences, no fragments/arrow chains), write for a teammate catching up. Emoji rule, text-vs-tool-communication rule, and never-create-files rule kept verbatim. Line 3 "interactive CLI tool" → "interactive coding agent" (the primary surface is the extension chat panel, not a monospace TTY).
  • Scope note: agents with a custom prompt (e.g. the extension's pulse-designer) BYPASS this file entirely (src/session/llm/request.ts — agent.prompt replaces SystemPrompt.provider). Interview-session parity comes from the amicode repo's companion patch to packages/extension/AGENTS.md ("Style & formatting" section, injected as instructions for every session) — same guidance, so plain chat and interview sessions agree.
  • default.txt/gpt.txt/gemini.txt untouched — non-Claude models keep stock style.
  • Patch commit b81fb756f2. Build sha256 3c26d8d9261dcf55b878e678fe359791e5f22e6833f87f5d9ce12b457c18e061 at dist/opencode-local AND the amicode vendored path — built clean at the pinned ref via fetch_opencode.mjs (amicode opencode.lock.json ref bumped to b81fb756f2); byte-identical to the pre-commit --any-ref dirty build, so the bun compile is deterministic for identical source. Verify: grep -ac "Being readable matters more than being brief" <binary> → 1; channel gate NG=!0 ON; fetch smoke test --version → 1.17.3. Amicode-side: vsix repackaged (55.64 MB, 133 files) + installed on erlich; extension vitest 484/484 green.
  • ADDENDUM 3 (2026-07-11, live-test round 2): custom-prompt agents now get communicating.txt too. Aaron's "still looks the same" screenshot was a pulse-designer INTERVIEW session — exactly the custom-prompt bypass documented in addendum 2. A live API probe of the plain-chat path (default agent, deepseek pin, POST /session/:id/message) confirmed the shared prompt WORKS — response came back with bold, a table, LaTeX, structured prose — so the gap was only the bypass (plus old sessions' flat history anchoring the model; test in a fresh session). Fix: llm/request.ts — custom-prompt agents append PROMPT_COMMUNICATING unless agent.mode === "subagent" (subagent final text is model-directed data, NOT user-facing — must stay style-free). The pulse-designer agent (mode unset → user-facing) now gets the rules in the system prompt proper instead of relying on AGENTS.md instructions alone. Typecheck green.
  • ADDENDUM 2 (same day — "does this need to be model specific?"): consolidated to ONE model-agnostic prompt. NEW prompt/communicating.txt (the canonical "Communicating with the user" section + structure-matching examples + an explicit supersede clause overriding any per-model CLI/terseness/length rule). system.ts provider() now returns [providerBase(model), PROMPT_COMMUNICATING] — EVERY model family (beast/gpt/codex/gemini/anthropic/trinity/kimi/default) gets it appended; the inlined copies added earlier in anthropic.txt/default.txt/gemini.txt were slimmed back to near-stock (minus their CLI-concise lines) so there is one source of truth. gpt/beast/codex/kimi/trinity left untouched — the supersede clause outranks their style rules without diffing them (upstream-merge friendliness). Typecheck green (tsgo). REMAINING GAP by design: agents with a custom prompt (request.ts:60) still bypass provider() — deliberate, because Task-tool subagents return model-directed raw data and must NOT get user-facing style rules; the user-facing pulse-designer agent is covered via the amicode AGENTS.md instructions instead.
  • ADDENDUM (same day, live-test finding — Aaron: "doesn't seem any different"): anthropic.txt was the WRONG file for the live setup. The prompt file is selected per model id (system.ts provider()), and the extension's model pin resolved to opencode/deepseek-v4-flash-free (no anthropic key in auth.json — preferredModel() creds fallback), so live sessions load default.txt — whose style rules are far harsher ("You MUST answer concisely with fewer than 4 lines… One word answers are best", monospace-CLI framing, one-word-answer examples) and steamroll the AGENTS.md instructions. Fix: same "Communicating with the user" section ported to default.txt (tone section + verbosity examples replaced with structure-matching examples; the second stray "fewer than 4 lines" mandate near Tool-usage-policy replaced too; "interactive CLI tool" → "interactive coding agent") and gemini.txt (Google creds exist locally → reachable; "Minimal Output: fewer than 3 lines" / monospace bullets replaced, No-Chitchat kept). gpt/beast/codex/kimi/trinity untouched (no matching creds; port on demand). Root-cause note for the future: to get Claude-quality prose AND the anthropic.txt path, add Anthropic creds — the pin then resolves to anthropic/claude-sonnet-5.
  1. (rydberg Hamiltonian fix, 2026-07-11) — amicode: the System card's composite Hamiltonian showed a driven-cavity form for a Rydberg atom (Aaron, live: platform walked transmon→cavity→transmon-cavity→rydberg and the card rendered Ĥ/ℏ = -Δ n̂ + ε(t)(â+â†) for a 3-lvl atom — "just not right"). packages/ui/src/amicode/system-render.ts systemHamiltonianLatex composed role-keyed drifts but appended ONE hardcoded bosonic quadrature drive ε(t)(â+â†) for every system.

    • Fix: role-aware driveTerm()atomΩ(t)/2 (|r⟩⟨1| + h.c.) (3-level Rydberg convention: |0⟩ dark, |1⟩↔|r⟩ laser-driven); everything else keeps the quadrature drive. Deduped per distinct role like the drifts, so mixed atom+cavity systems show both flavors. Atom drift -Δ n̂-Δ |r⟩⟨r| (detuning belongs on the Rydberg level; n̂ read as a bosonic number operator next to the old â-drive).
    • Tests: system-render.test.ts +3 (bare rydberg atom → Rabi drive, NO ε(t)/â; two atoms + vdW → deduped single drive + C₆ term; atom+cavity → both drives). ui bun test src/amicode 115 pass; typecheck green both packages.
  2. (markdown heading hierarchy, 2026-07-11) — amicode: section titles in chat rendered indistinguishable from body prose (Aaron, live: "section titles aren't rendering properly"). Root cause was upstream DESIGN, not a bug: packages/ui/src/components/markdown.css had /* Headings: Same size, distinguished by color and spacing */ — all h1–h6 at 14px medium, the same size/weight as body, colored --text-strong which the whole markdown container ALREADY uses (zero distinction), with margin-top 0 + margin-bottom 24px (gap on the wrong side — title floats orphaned above its section). The prompt work (#18) made the model EMIT headings; this made them visible.

    • markdown.css: real hierarchy — h1 18px / h2 16px / h3 15px / h4-h6 14px, all weight 600 (no semibold token exists; 400/500 only, raw 600 consistent with the file's raw px), margin 24px above + 8px below (binds title to its section; container's > *:first-child rule still zeroes the top of a leading heading). strong/b medium(500) → 600 — against the 400 body, 500 was near-invisible as "bold".
    • CSS-only, no tests to update; cascade checked (message-part.css/index.css define no competing heading rules).
  3. (expandable skill chips, 2026-07-11) — amicode: skill loads rendered as a name-only chip with hideDetails (Aaron: "when skills are used do I get a visual notification like in Claude Code?" — the chip existed, but nothing showed WHAT got injected). packages/ui/src/components/message-part.tsx skill renderer + NEW message-part-skill.ts.

    • skillBody() (pure, tested — message-part-skill.ts): strips the <skill_content>/<skill_files> transport wrapper and the model-facing plumbing lines (base-directory, relative-path note, sampling note, the # Skill: name heading the chip already shows) from the skill tool's output, leaving the SKILL.md instructions.
    • Renderer: hideDetails dropped; body renders via the standard tool-output + Markdown idiom (same as list/glob), so the chip stays collapsed by default and expands to the injected instructions. No output yet (running) → no chevron, unchanged look.
    • Companion (amicode repo, PR #132 commit b135875): injected opencode config gains permission.skill: "allow" — the staged skill set is surface+entitlement curated, so the skill tool's default per-name "ask" was friction without a boundary and a headless-session hang risk.
    • Tests: message-part-skill.test.ts +5; ui bun test 158 pass; typecheck (tsgo) green.
  4. (Connections auth-path scaffold, 2026-07-21) — amicode#194: the Connections tab scaffolds every candidate Pasqal auth path behind the wire contract, so the UI is ready whichever mechanism the direction lands on (decision open; server routes are a separate slice). packages/ui/src/amicode/connections.ts + connections-tab.tsx, app labels.

    • Wire (all tolerant, absent today → cards render byte-identical, regression-tested): auth_methods (known: credentials/browser/device-code/token; unknowns dropped), three mid-flow states waiting-browser / waiting-code / choose-project, plus user_code/verification_url/code_expires_at (device flow) and projects (post-auth picker; name falls back to id).
    • Model: cardModel branches for the three states (pending tone, no form/actions; waiting row carries the cancel exit; choose-project with no wire projects keeps cancel open instead of an empty picker). Method model: connectionAuthMethods (advertised wins, else legacy per-id single method — chooser renders only at ≥2), methodEntryKind (browser/device-code → start button; token → pasqal token+project or legacy base_url+token; credentials → #169 form unchanged).
    • Gates: startAuthPayload only for wire-advertised interactive methods; chooseProjectPayload only for wire-offered project ids; pasqalTokenSubmitPayload trims + refuses empties. A stale UI can never start a flow the server no longer offers.
    • Tab: method chooser chips, start button, user-code block (selectable, mono), project radio list, cancel row — new callbacks (onStartAuth/onChooseProject/onCancelAuth) are OPTIONAL props; the app passes labels only, so nothing new is reachable in prod until the producer advertises methods.
    • i18n: en.ts keys only (locale-fallback precedent from #166). Stories: connections.stories.tsx — full state atlas matrix + interactive walkthrough, mock wire data.
    • Tests: connections-paths.test.ts +17 (parsing tolerance, model branches, gates, legacy regression); existing exhaustive state loops extended. ui bun test src/amicode 264 pass; typecheck (tsgo) green both packages.
  5. (Pasqal keychain silent re-auth — Jack's workaround, 2026-07-21) — amicode#194: the interim auth path from Jack's live-API session (pasqal-auth-bundle). Pasqal's API accepts ONLY password-grant + service-account tokens (refresh tokens 403 on the gty claim; browser/device grants client-disabled), and user tokens live ~24h — so the fork server keeps the password in the OS keychain and silently re-mints on expiry. Design of record: ADR 0001 addendum (amicode repo, branch kate/adr-0001-pasqal-keychain). New packages/opencode/src/server/amicode/pasqal-secret.ts + connections.ts wiring.

    • pasqal-secret.ts: the keychain seam. Mechanism is Jack's choice (Kate) — @napi-rs/keyring (macOS Keychain / Linux Secret Service / Windows Cred Manager), lazy require so a missing/unbuilt native addon degrades this ONE feature (never crashes opencode) to SESSION-MEMORY (ADR 0001's named fallback — password in process RAM, re-prompt after restart; never a plaintext file). Injectable via setPasqalSecretStore(inMemorySecretStore()) so tests never touch a real keychain.
    • connections.ts: submit stores {username,password} in the keychain on valid+token (the credential-file poison guard still makes a password in pasqal.json impossible — token stays token-only at rest). attemptPasqalSilentReauth re-runs the #164 validator with the stored secret when the token is expired (revalidate + background revalidation both call it): valid → fresh token rewritten + connected; invalid (password changed/revoked) → keychain+token wiped, needs-key; unreachable/config → claim STANDS (offline, never a verdict). Disconnect wipes the keychain slot. Password rides the child env only — never argv, never a status field, never a response.
    • Service account (client_credentials) is the documented exit condition (retires the password entirely, ADR-clean); request is with Pasqal (Baptiste Peyras). Browser/device/token/refresh paths are evidence-ruled-out (see #194); the scaffold's tolerant method model harmlessly still parses auth_methods if a future server ever advertises them.
    • CAVEAT (cannot be verified here): the real @napi-rs/keyring backend + its load inside the bun-compiled binary needs a real-machine smoke test before trusted, and the dep must be added to packages/opencode/package.json (node-pty/tree-sitter precedent). All LOGIC is tested behind the injected in-memory store.
    • Tests: pasqal-secret.test.ts +5; amicode-connections.test.ts +6 silent-reauth (store-on-submit / re-mint / reject-wipes / unreachable-stands / disconnect-wipes / secret-free-response) + hermetic secret-store isolation in setup; existing expiry test made explicit about the no-password fallback. opencode bun test test/server/amicode-connections.test.ts test/server/pasqal-secret.test.ts test/server/amicode-connections-routes.test.ts → 105 pass. typecheck (tsgo) green. (15 unrelated built-in widget sources failures are a pre-existing missing-build-artifact issue, not this diff.)
  6. (upstream provider/model port — Jack, 2026-07-27) — first deliberate partial upstream sync. Our base is sst/opencode v1.17.3 (2026-06-10); upstream was 1,150 commits ahead at v1.18.7. Rather than a full merge (54 fork-touched files overlap), only the provider/model bucket was ported, because Claude 5 was silently broken at our base.

    • THE BUG: anthropicOpus47OrLater() matched /opus-(\d+)[.-](\d+)/ — requires BOTH major and minor. claude-opus-5 has no minor and claude-sonnet-5 isn't "opus", so neither matched. Both fell through to the legacy thinking:{type:"enabled",budgetTokens:16000} path with only high/max tiers, instead of the 5-tier type:"adaptive" path. Sonnet 5 released 2026-06-29 and Opus 5 on 2026-07-24 — both AFTER our fork point. Upstream fixed this in #38757 (generalize Claude adaptive thinking): the version regex now tolerates a missing minor and defaults unversioned Claude IDs to modern.
    • NOT a "new models" problem: the catalog is fetched live from models.dev at BUILD time (script/generate.ts; amicode-release.yml pins no snapshot), so Opus 5 already appeared in the picker — just hobbled. QA-ing "can I select Opus 5?" gives a false negative on the need to sync.
    • WHAT WAS PORTED (deliberately minimal — the END STATE of files, not 112 commits of history):
      • packages/opencode/src/provider/transform.ts — replaced wholesale with upstream/dev. Fork had never touched it → 0 conflicts. Also carries #38424 (prompt cache keys by SDK), #36624/#36543 (derive variants from reasoning metadata), and the Grok/GLM/Bedrock-Mantle variant fixes.
      • packages/opencode/test/provider/transform.test.ts — replaced wholesale with upstream/dev (our copy asserted 6 now-superseded behaviors).
      • packages/core/src/models-dev.ts — ADDITIVE only: upstream's ReasoningOption union + optional reasoning_options field on Model. transform.ts reads it; without it the port is 4 type errors.
      • packages/opencode/src/session/llm/request.ts — 3-way merged (git merge-file ours/v1.17.3/upstream), 0 conflicts. Brings upstream #33392 (strict: false on tools for @ai-sdk/openai, @ai-sdk/azure, @ai-sdk/amazon-bedrock/mantle). amicode patch #18 (PROMPT_COMMUNICATING) is preserved — different region of the file.
    • WHAT WAS DELIBERATELY NOT PORTED: the Effect layer-node → app-node refactor and the @opencode-ai/schema extraction (upstream gutted core/src/model.ts 127→41 and core/src/provider.ts 85→25 into a new package). Cherry-picking the bucket's 112 commits would drag both in. Take file end-states, not history.
    • MEASURED BLAST RADIUS: ran both transform versions over all 454 reasoning models in the live models.dev catalog — 418 identical, 36 changed, 0 errors. The 36: Claude 5 (opus/sonnet, across anthropic + all amazon-bedrock regions + opencode zen) goes high,maxlow,medium,high,xhigh,max with type:"adaptive"; xAI Grok 4.3/4.5/4.20/build and GLM-5.2 gain effort variants where they previously had NONE; claude-opus-4-5 keeps its low,medium,high keys but now also sends budgetTokens:16000 alongside effort. Claude 4.8/4.6/4.1 unchanged.
    • VERIFICATION: tsgo --noEmit green on packages/opencode AND packages/core. bun test test/provider/ → 503 pass / 0 fail. bun test test/session/ test/server/ → identical results before and after the port (3 pre-existing llm-native-recorded failures + a pre-existing Bun 1.3.14 teardown segfault, both reproduced byte-for-byte on the unported tree).
    • STILL UNVERIFIED (needs live API creds): whether the OLD legacy thinking:{type:"enabled",budgetTokens} form actually 400s against Opus 5 or merely degrades — i.e. whether this was "hobbled" or "unusable". Also unexercised by CI: the Grok/GLM effort variants and the opus-4-5 budgetTokens+effort combination.
    • FUTURE SYNC COST: this is a knowing trade — porting file end-states now makes a future clean upstream merge harder on these 4 files. Accepted. NOTE the bug class recurs every model generation (version-regex parsing of model IDs will break again at Opus 6), which argues for a standing narrow sync lane on transform.ts rather than one-off unfreezes.
    • SEPARATE RISK SPOTTED (not fixed here): the release build does a bare fetch(models.dev/api.json) with no fallback, so a models.dev outage hard-fails the build. Consider pinning MODELS_DEV_API_JSON for the hackathon build.
  7. (clickable chat links — file:// opens in the editor, 2026-08-01) — amicode: markdown links in chat were dead wholesale (Aaron, live: vault-note links authored per the amico-vault skill's new file:// convention — "clicking does nothing"). Diagnosis, three layers, all confirmed in source: (1) the marked link renderer emits <a href class="external-link" target="_blank"> intact — no sanitization (packages/ui/src/context/marked.tsx:572); (2) session-ui's markdown component had NO anchor click handler at all — its only listener was the code-copy button, so anchors fell to default target="_blank" navigation, which the sandboxed cross-origin chat iframe blocks — every scheme dead, not just file://; (3) even past (2), two downstream filters would have dropped file://: the app platform openExternal (http/https/mailto only, packages/app/src/entry.tsx:87) and the extension bridge (https-only regex, amicode packages/extension/src/chat_bridge.ts).

    • FORK: packages/session-ui/src/components/markdown.tsx — NEW setupExternalLinks(root): delegated click handler on a.external-link, framed contexts only (window.parent !== window; a plain browser tab keeps native behavior). preventDefault + postMessage to parent: https/mailto → {source:"amicode", kind:"open-external", url}, file:// → NEW bridge kind {kind:"open-file", url}; any other scheme untouched (native fallback, dead-in-iframe as before). Wired next to setupCodeCopy with the same mount-once-in-effect + onCleanup pattern.
    • EXTENSION (amicode repo): chat_bridge.ts — new open-file branch: kind + string + ^file:// gate in one condition (non-file → false, not consumed — open-external idiom); then decodeURIComponent(new URL(url).pathname), must be path.isAbsolute + ≤4096 + fs.existsSyncvscode.commands.executeCommand("vscode.open", vscode.Uri.file(fsPath)); unparseable/unreachable → consumed-silent (save-file idiom). Posix path semantics (fleet is mac/linux). Relay allowlists (chat_panel.ts lane-1, deck/shell.ts) gain "open-file".
    • Skill companion (already landed, becomes live with this build): amico-vault SKILL.md "Reporting writes in chat" — vault writes are reported as absolute file:// markdown links, one per file.
    • Tests: extension chat_bridge.test.ts +2 (opens an existing file incl. a %-decoded space path; rejects non-file schemes as not-consumed, missing files/non-absolute as consumed-silent) → file 9/9; full extension suite 843/846 — the 3 failures are PRE-EXISTING and environmental (server_auth.test.ts spawn-env resolves the machine's provisioned ~/.amico/amicode/venvs/pasqal-connector interpreter; logic committed at HEAD in extension.ts/pasqal_python.ts, untouched by this diff). session-ui bun test src/components → 69 pass / 0 fail. Extension tsc --noEmit clean.
    • Build sha256: df1dbe0de082a65c181e6aba7546acd4694842e76cc035c69b95352d08c5dac1 (packages/opencode/dist/opencode-darwin-arm64/bin/opencode + convenience copy dist/opencode-local; vendored into amicode packages/extension/vendor/opencode/darwin-arm64/opencode AND swapped into the installed extension ~/.vscode/extensions/harmoniqs.amicode-0.1.2/vendor/... — plain cp, ETXTBSY not hit). Verify: binary contains open-file ×8; channel-gate sentinel newLayoutDesigns !0 present; smoke --version → 1.17.3. Extension dist (extension.js open-file ×2, deck_shell.js ×1) copied into the installed extension's dist/.
    • Interactive click → vscode.open in the live webview NOT yet human-verified — needs a window reload (extension host + spawned server both restart from the new artifacts); same deferred-acceptance caveat as #12/#13/#16.
    • Divergence note: the vendored binary is now AHEAD of amicode opencode.lock.json's pinned fork ref until the fork commits (session-ui markdown.tsx) and the lock bumps — normal amicode.N tag flow, not done here.
  8. (harmonic wave indicator — de-shimmer the tool-status title, 2026-07-28) — amicode: the chat had two "working" indicators that read as identical — the fork's thinking line and the STOCK tool-group header (packages/ui/src/components/tool-status-title.tsx, e.g. "Working in shell" → "Worked in shell") — because both were shimmering text with no glyph. Earlier steps of this run gave the thinking line a standing-wave glyph (AmicoWave, packages/ui/src/amicode/amico-wave.tsx) and deleted the amc-text-shimmer CSS; this patch removes the shimmer from the last surface still asking for it.

    • tool-status-title.tsx: all five <TextShimmer text={...} active={...} offset={...} /> render positions (swap-mode active/done, suffix-mode prefix/active/done) replaced with plain <span>{text}</span>. TextShimmer import removed. The now-orphaned prefixLen memo (it existed only to feed the shimmer's offset phase-alignment prop) removed too — confirmed via oxlint before/after (498→499→498 warnings) that leaving it in place tripped no-unused-vars, so dropping it was the correct call rather than "keep everything."
    • Deliberately PRESERVED: the common() prefix-splitting logic, the width-morph animation (animate()/finish(), the requestAnimationFrame, the stored width, the data-ready flag), the data-component/data-active/data-mode attributes, the aria-label, and the active→done text swap itself. None of that is the shimmer — it's the genuinely good word-morph behavior.
    • text-shimmer.tsx untouched — it has many other live callers (basic-tool.tsx, message-part.tsx ×7, session-turn.tsx, v2/components/basic-tool-v2.tsx via text-shimmer-v2.tsx, plus stories), confirmed via rg -n 'TextShimmer' packages/. Not a candidate for deletion.
    • The dropped offset prop existed only so the shimmer's gradient phase stayed continuous across the prefix/tail split; with no shimmer there's nothing to phase-align, so it has no replacement. Verified live (Storybook, UI/AnimatedCountList stories) that the prefix and tail still read as one unbroken word with no seam: in the swap-mode Playground story, driving an active→done transition showed the mid-animation frame rendering both the active and done spans simultaneously under the animating width (data-ready="true", container style="width: 0px" mid-transition, then settling) — the word-morph is intact. The suffix/prefix-tail mode (data-mode="suffix") is currently unreachable from any real call site — both app usages (message-part.tsx context-tool-group and shell-group titles) and the only story pass split={false} — so it was verified by transiently flipping one story's split prop off (Done export, "Exploring"/"Explored"), confirming data-mode="suffix" renders "Explor" + "ed" as an unbroken "Explored" with no visible gap, then reverting that story edit before commit (git status shows only the two files below).
    • Tests: ui bun test src → 402 pass / 0 fail (unchanged). typecheck (tsgo) clean in packages/ui. oxlint packages/ui/src → 498 warnings / 0 errors both before and after (no new warnings once prefixLen was dropped).
  9. (text-card questions — free-form asks get a first-class card, 2026-08-02) — amicode#245 + ADR docs/adr/0002-free-form-questions-render-as-text-cards.md: the question schema grows an optional kind ("choice" default | "text") on the agent-facing AND server shapes (v1 packages/schema/src/v1/question.ts + v2 packages/schema/src/question.ts, both in base so Prompt and Info carry it; absent → choice, unknown kind fails decoding loudly). Three renderers branch a text card (header + bare text input + submit; NO option rows, NO "Type your own answer" pseudo-option; submit gated on non-empty trimmed text; answers ride the EXISTING typed-custom-answer path; dismissal unchanged): app dock (session-question-dock.tsx + new session-question-dock.helpers.ts — ALSO honors the custom flag for CHOICE cards, the TUI pattern it previously ignored), TUI (question.tsx), CLI (question.shared.ts + footer.question.tsx). Prose guard (session/prompt.ts) behavior UNCHANGED (still fires on prose questions in active interviews, once per assistant message, never after a question-tool call); predicate + nudge extracted to session/prose-guard.ts (turn-output.ts pattern); nudge text now bilingual (options-with-recommended-first for choice, kind: "text" for free-form). Tool descriptions teach the text kind (tool/question.txt + core tool/question.ts). Legacy SDK + client codegen regenerated (QuestionInfo/QuestionV2Info gain kind?: "choice" | "text"). Upstream-sync watch: the dock's question component is already a high-conflict file — keep the text-card branch and the customRow() gate honest on syncs.

    • Tests: schema test/question.test.ts (decode ×4 shapes: absent/choice/text/unknown), opencode test/cli/run/question.shared.test.ts (+5: text card shape/submit/empty-blocked, choice regression, custom:false), tui test/routes/session/question.test.tsx (+5: render coverage incl. typed-answer submit through a stub server), app session-question-dock.helpers.test.ts (+6), opencode test/session/prose-guard.test.ts (+10: predicate truth table + bilingual nudge), tool tests (+2 description, +1 kind passthrough).

Feature-branch recovery 2026-08-01 (after the upstream sync)

The upstream sync covered only local/amicode; live work sat on unmerged feature branches. Merged into amicode/merge-upstream-dev on top of the sync:

  • ann/thinking-words-updated (Quantizing/Obsidianing), feat/amicode-system-card-model, fix/amicode-receipt-currency — clean merges.
  • pr/amico-working-indicator — the harmonic wave indicator (AmicoWave in the thinking line, H-mark static; tool-status-title de-shimmered) + collapsed receipt runs + AmicoSkillChip. Conflicts: message-part count×useV2Actions threading unioned; shims amicode-receipt(-runs).
  • kate/context-tree-vault-zoom — framed external links route over the open-external bridge (kept upstream's openExternal; branch's global link interceptor dropped — patch #25 covers it).
  • feat/warrant-receipts-captured (contains approval-surfaces) — capability warrants: rail warrant chip, approval card (deliberately NOT onAsk), /amicode/warrants + /amicode/approve.
  • feat/harmonic-wave-indicator — recorded-Hamiltonian rendering, system-card physics rows, KaTeX dedupe (spine/aside-lane cancel against their own reverts).

DELIBERATELY NOT MERGED: amico/issue-56-living-chat (34 commits — glass sweep, Latent Constellation, bottom-dock composer, thought camera). It redesigns exactly the surfaces upstream replaced (old message-timeline, session-new-design-view, prompt-input, markdown — all deleted/moved upstream), 23 conflicted files. Merging would hybridize two design directions into neither. It stays on its branch; reviving the glass recipe is a deliberate port project onto the NEW ui, not a merge.

  1. (thinking block — two-row restructure, de-glitched, 2026-08-02) — amicode: the working indicator shattered beside a long activity heading (Aaron, live screenshot: "Superposing… 12s · ↑" on line one, "8.7k" and "tokens" orphaned on lines two and three, H-mark and wave drifting out of row). Root causes, all three mechanical: .amc-thinking-meta had no white-space: nowrap; .amc-thinking carried min-width: 0 next to a TextReveal heading that claims an explicit measured pixel width, so the heading won the flex shrink fight; align-items: baseline then scattered the wrapped block's pieces. Fix = restructure, not patches:
    • packages/ui/src/amicode/thinking-line.tsx — now a two-row GRID block that OWNS the H-mark (mark prop, default on): row 1 = mark + AmicoWave + cycling gerund, row 2 = the meta (elapsed · ↑ tokens · esc), always rendered from mount so block height never pops when tokens first arrive. DOM order is word-first so the grid's first baseline is the VERB's — the sibling heading (session-turn-thinking, now align-items: baseline) aligns with the verb, not the mark's bottom edge. a11y: ticker is aria-hidden with a single sr-only role=status — the old aria-live="polite" wrapper re-announced every word rotation every 2s.
    • packages/ui/src/amicode/amicode.css.amc-thinking* rewritten: inline-grid auto auto 1fr, meta at grid-column: 2 / -1 (starts under the wave), verb min-width: 16ch (covers the longest gerund + "…" so word rotation stops shifting the layout), meta nowrap + overflow: hidden + ellipsis, inline children (> * + * margins) so the whole meta truncates as one line.
    • packages/app/src/pages/session/timeline/message-timeline.tsxTimelineThinkingRow drops its <AmicoMark/> + the inline-styled .amc-thinking-row wrapper span (import removed); heading TextReveal gains truncate so it ellipsizes instead of squeezing the block.
    • packages/session-ui/src/components/message-part.tsx — the .amc-lane working mount drops amc-lane-head + <AmicoMark/> (import trimmed; block owns the mark). packages/session-ui/src/components/session-turn.css[data-slot="session-turn-thinking"] gains > [data-slot="amc-thinking"] { flex-shrink: 0 } + the baseline alignment.
    • Stories updated to the new DOM: thinking-line.stories.tsx (frame no longer double-marks), amico-wave.stories.tsx ThinkingRow mimic (+ stale "not mounted anywhere" notes corrected here and in amico-wave.tsx), amico-presence.stories.tsx (lane-head only for stepping_in/settling, ThinkingLine brings its own mark when on).
    • Pure logic (thinking.ts words/formatting/turnTokens) untouched. Tests: ui bun test src/amicode → 424 pass / 0 fail; typecheck (tsgo) clean in ui + session-ui + app.
    • Build (working tree, uncommitted — local 5b1a880fc…+dirty): vendored darwin-arm64 sha256 60204bec44b50b678d2c1500bf5147d1a0af54ed8bfa3441b8092ed7b4951c3e, smoke --version → 1.18.10, assert_ui_gate.sh → ON (shorthand memo). Verify (scratch port 14231): GET / → 200 <title>Amicode</title>; css asset carries .amc-thinking{display:inline-grid;grid-template-columns:auto auto 1fr} + 16ch + .amc-thinking-meta{…nowrap…ellipsis;grid-area:2/2/auto/-1} + amc-thinking-meta>*+*{margin-left:6px} + flex-shrink:0; main chunk carries Amico is thinking ×1 and amc-thinking-word/-meta/-elapsed ×1 each; POST /session {} → 200. Interactive eyeball (narrow/wide panes, live turn) deferred to dev-host reload — the served-asset checks are the pre-flight.
  2. (report-a-bug composer button, 2026-08-03) — amicode/opencode#116 (part of amicode#249): a bug-glyph icon button in the v2 composer's bottom control row — right-anchored, immediately left of send — gated on the amicode_bug_report=1 boot param (same adopt-once convention as amicode_hide_project: entry.tsx adopts from location.search at boot; standalone opencode never sets it, so the row is byte-identical there). Click posts amicode.reportBug over the app↔extension bridge ({source:"amicode",kind:"command",command} — chat_panel allowlist contract); with a bug dock OPEN it instead reveals/re-expands and posts nothing.
    • Boot-param gate: NEW packages/app/src/utils/amicode-bug-report.ts (adoptBugReportFlag(search) — deterministic set so tests drive both directions — + bugReportEnabled()); adopted in entry.tsx beside adoptHiddenProject. The gate is consulted in PromptInputV2Composer, NOT inside the button: gated-off ⇒ trailingControl={undefined} ⇒ the row's layout never shifts (an always-mounted-but-empty slot wrapper would still eat its 4px margin).
    • Bridge helpers extracted: NEW packages/app/src/utils/amicode-bridge.ts (inAmicode + postAmicode, moved verbatim out of use-amicode-commands.tsx, which re-exports both — the existing envelope contract test and the four importers untouched).
    • Dock seam (the #117 contract): NEW packages/app/src/pages/session/composer/bug-dock.ts — module-scope singleton (vault-panel pattern) exposing isOpen(), open(), close(), reveal(), revealNonce(). reveal() re-sets open AND bumps a generation nonce so an already-open (collapsed) dock still gets an observable re-expand signal. The button side only reads isOpen() and calls reveal(); #117 builds the real dock UI against this API. NEW report-bug.ts beside it holds REPORT_BUG_COMMAND + reportBug(dock = bugDock) (dock open → reveal, zero posts; else one bridge post) — component-free so the contract stays unit-testable (the app has NO component-render test surface: bun test resolves solid-js to its server build and cannot run Solid's compile-time JSX transform; visuals live in stories).
    • The button: NEW packages/app/src/components/report-bug-button.tsx (+ .css) — IconButtonV2 ghost-muted/large (28×28, matching the row) with the new bug glyph, aria-label="Report a bug" + TooltipV2 (inactive while disabled). State contract in the css, keyed on [data-action="report-bug"] (IconButtonV2 hard-pins data-component): color ONLY var(--v2-state-fg-danger) (red-800 light / red-500 dark — yellow stays the sole accent), muted opacity: 0.55 at rest → full on hover (the provider-icon pattern), pressed/focus full with the base 2px --v2-border-border-focus ring, disabled 0.3 + not-allowed (clearly deader than rest), border-radius: var(--radius-md) (control token), 0.16s opacity transition + reduced-motion cut. Forced [data-state] hooks honored so the story can render the matrix.
    • Glyph: bug added to the v2 icon family (packages/ui/src/v2/components/icon.tsx) — stroke-family beetle, currentColor, 16 viewBox; never emoji, no literal hex.
    • Slot: PromptInputV2 (session-ui) gains trailingControl?: JSX.Element — the modelControl optional-control pattern — rendered in a mr-1 flex items-center wrapper (4px grid) immediately left of PromptInputV2SubmitButton. IconButtonV2's state union widened with "focus" | "disabled" (its css already handled those [data-state]s; the type just blocked them).
    • Story: packages/app/src/components/report-bug-button.stories.tsx — InComposerRow (faithful row mirror with the REAL send button), States (all five), Schemes (light+dark panes via the amico-wave resolveThemeVariantV2-inline-tokens pattern — the data-color-scheme wrapper is a storybook no-op). Verified headless via storybook build + Playwright screenshots of all three.
    • Tests: NEW report-bug.test.ts (4: exact envelope + "*" origin when dock closed; reveal + nonce-bump + zero posts when open; posts again after dismiss; injected-seam reveal) and amicode-bug-report.test.ts (2: gate off without param / on with =1; only exact 1 enables). App bun run test:unit 783/783; ui 441/441; session-ui 94/94; tsgo clean in all three; oxlint 0 new findings (the one error in the touched session-ui file is the pre-existing \200B octal escape, on base).
  3. (bug-report dock + sentinel watcher, 2026-08-03) — amicode/opencode#117 (part of amicode#249; lifecycle: amicode ADR 0004): the bug session's in-flow home — a new member of the composer's dock family (todo/question/permission/revert), gated on the same amicode_bug_report=1 boot param as the #116 button. Opens on the bridge open-bug-report {sessionID} down-message; body is an iframe pinned to the bug session's route (the deck's URL-pinned chat-iframe idiom); the sentinel watcher matches ^AMICODE_BUG_FILED[ \t]+(\S+) (multiline, line-start anchored; horizontal-whitespace separator so a bare sentinel can't capture the next line's first word) in the hosted session's streamed text parts, posts bug-filed {sessionID, url} exactly once, and switches to the terminal end-state (issue link) until the extension closes the dock. Collapse (chevron) never posts and never kills; the close control posts bug-report-closed {sessionID} exactly once and dismisses locally (the extension aborts + hard-deletes).
    • Controller: NEW packages/app/src/pages/session/composer/bug-dock-controller.ts — component-free singleton (createBugDockController(deps) with injectable dock seam / post / enabled for tests; bugDockController the module instance). State machine: closed → chat → filed; owns the #116 seam's open()/close() (the button side only reads isOpen() + reveal()). Idempotent opens: duplicate same-id open-bug-report = reveal (re-expand; the filed end-state survives), different-id while open dropped (the extension's single-open guard owns which session is live), malformed dropped. Extension-initiated close-bug-report {sessionID} (the DOWN contract this slice chose — the PRD specced only open-bug-report down; #250's "archive + tell the app to close the dock" rides it) closes silently, posting nothing back. bugDockFrameSrc(...) pins the frame URL: app origin + /server/<key>/session/<id> + colorScheme/auth_token/amicode_hide_project params + amicode_pane=bug-dock (namespaced pane instance — never fights the main window's persisted UI state).
    • Component: NEW packages/app/src/pages/session/composer/session-bug-dock.tsxSessionBugDock (wired: revealNonce re-expand effect, the sentinel watcher scoped to the hosted session's sync-store parts, src pinned per session via on(sessionID) + untrack so a theme flip never reloads mid-report) over BugDockView (presentational, exported for stories). Family idiom: spring-animated max-height between measured content and the 42px header, header row toggles on click/Enter/Space, body visibility-hidden while collapsed (the iframe STAYS mounted — collapse keeps the session alive). Header: danger-token bug glyph + "Bug report" + faint collapsed status + chevron/close IconButtonV2s (aria-labels + tooltips). Filed end-state: success-token check + "Issue filed — this session is archived." + Open-issue ButtonV2 (platform.openExternal → the bridge in-webview; suppressed for the filed-via-browser token). Live theme flips forward into the frame via a data-color-scheme MutationObserver postMessage (the split-frame idiom).
    • Wiring: the session composer region renders <SessionBugDock/> gated on bugReportEnabled(), OUTSIDE the question/permission block gates (a blocked composer must never hide a live bug dock); the draft view (new-session-view.tsx) mounts it above its composer too — the #116 button renders there and a dead click is a bug. Down-messages are handled at APP level: AmicodeThemeBridge's existing window-message listener forwards open-bug-report/close-bug-report to the controller (self-gated on kind + the boot param), so an open can't be missed between pages. Boot param off ⇒ no render, no adoption — standalone opencode byte-identical.
    • Bridge shapes (the cross-repo contract with amicode#250): DOWN {source:"amicode",kind:"open-bug-report",sessionID} · DOWN {source:"amicode",kind:"close-bug-report",sessionID} · UP {source:"amicode",kind:"bug-filed",sessionID,url} (url may be the literal filed-via-browser) · UP {source:"amicode",kind:"bug-report-closed",sessionID} — the up-kinds join the chat_panel allowlist on the extension side.
    • Story: NEW session-bug-dock.stories.tsx — States (expanded chat / collapsed / filed+link / filed-via-browser) and Schemes (light+dark panes, the resolveThemeVariantV2 inline-token pattern). Verified via storybook build (both entries in the built index).
    • Tests: NEW bug-dock-controller.test.ts (28) — one TDD cycle per AC: open/adopt + frame-src params (AC1); chevron collapse/re-expand + reveal(), zero posts, dock seam untouched (AC2); close posts bug-report-closed exactly once + the real default-post envelope via spy + extension close silent + wrong-id ignored (AC3); sentinel matching filed / filed-via-browser / mid-line no-match / confirm-gate veto / bare-sentinel + parts scan + file() latch (exactly once) + end-state close paths (AC4); boot-param gate inert + duplicate/different-id/malformed opens idempotent + fresh adopt after full close (AC5). App bun run test:unit 811/811 (783 base + 28 new); tsgo clean; oxlint 0 errors (only the base-pattern as unknown as envelope-assertion warning, same as #116's test); storybook build green.