Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 32 additions & 86 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ Interview flows are data (`packages/extension/scores/`). Schemas: `packages/sche

1. `node >= 20`, `corepack enable` (repo pins `pnpm@9.15.9` — never install pnpm globally)
2. Julia ≥ 1.12 via juliaup: `curl -fsSL https://install.julialang.org | sh`
3. `gh auth status` succeeds AND `gh repo view harmoniqs/opencode` succeeds
(private fork mirror — the vendored binary downloads from its release; if 404, stop
and tell the human to request access from Aaron)
4. An LLM provider for the chat: `opencode auth login` after the binary is vendored
3. `gh auth status` succeeds (for PRs and issues)
4. bun (for compiling the engine binary): `curl -fsSL https://bun.sh/install | bash`
5. An LLM provider for the chat: `opencode auth login` after the binary is built
(or `ANTHROPIC_API_KEY` in the environment). Without one, the free anonymous tier is
used — functional but flaky; do not judge interview-quality bugs on the free tier.

Expand All @@ -30,11 +29,9 @@ Interview flows are data (`packages/extension/scores/`). Schemas: `packages/sche
git clone git@github.com:harmoniqs/amicode.git && cd amicode
corepack enable && pnpm install # check: exits 0, lockfile untouched
pnpm -r build # check: packages/extension/dist/extension.js exists
pnpm --filter amicode run fetch:opencode # check: vendor/opencode/<platform>/opencode exists
# Default lock source=release: downloads the pinned,
# features-ON binary from the harmoniqs/opencode release.
# No clone, no bun. Only changing the fork needs those —
# see "Changing opencode (the vendored fork)" below.
pnpm --filter amicode run build:binary # check: vendor/opencode/<platform>/opencode exists
# Compiles the engine from packages/app-bundle/overlay/.
# Requires bun. No external fork or download needed.
pnpm --filter amicode test # check: 200+ tests pass, 0 fail
bash packages/extension/scripts/install.sh # Julia project (~15 min first precompile) + VSIX + lab.toml
node packages/extension/scripts/healthcheck.mjs # check: 4/4 ✓ (julia, opencode, amico-run, creds)
Expand All @@ -46,9 +43,8 @@ Fleet + vendor + lock drift is how the last 3 fleet breaks hid (stale `main` beh

```bash
pnpm sync # check only: git fetch + gh auth + pnpm dry-run + fleet gate (no writes)
pnpm sync --fix # also writes: git pull --ff-only, pnpm install, fetch:opencode (pinned, --release), fleet install
pnpm sync --fork # fork helper: clone status + pnpm opencode:build / opencode:pin help
# or: bash scripts/repo-sync.sh --check / --fix / --fork
pnpm sync --fix # also writes: git pull --ff-only, pnpm install, build:binary, fleet install
# or: bash scripts/repo-sync.sh --check / --fix
# VS Code: Command Palette → Amicode: Repo Sync (runs --fix in a terminal)
```

Expand All @@ -75,16 +71,12 @@ macOS note: the vendored binary is unsigned — if Gatekeeper blocks it:

- **Dev host**: open this repo in VS Code, F5 ("Run Extension (amicode)"). The opencode
server runs on **fixed port 43117** (`amicode.opencodePort`); Remote-SSH users forward it once.
- **The vendored binary is a build artifact** — never edit it; it comes from
`harmoniqs/opencode` (thin fork, patch stack in its `AMICODE-PATCHES.md`). Rebrand/UI work
happens THERE, product logic lives HERE in config/plugin/scores (Layer 0). To change the
fork, see "Changing opencode (the vendored fork)" below.
- **`packages/app-bundle/overlay/` is a read-only tracking copy** — never edit the overlay
directly. It is copied FROM the opencode fork, never TO it, and is not a build input. To
change any app-layer file (components, pages, styles), edit the fork
(`~/harmoniqs/opencode` or `$AMICODE_OPENCODE_SRC`), rebuild with
`pnpm --filter amicode opencode:build`, then `sync:apply`. Edits to the overlay
silently vanish on the next sync.
- **The vendored binary is a build artifact** — never edit it; it is compiled from
`packages/app-bundle/overlay/` (the engine source tree). To change the engine,
see "Changing the engine (opencode patches)" below.
- **`packages/app-bundle/overlay/` IS the source of truth** — edit it directly.
This is the canonical engine source tree committed in this repo. Run
`pnpm --filter amicode run build:binary` after editing to compile a new binary.
- `packages/extension/opencode-plugin/` executes inside opencode's Bun runtime — it is NOT
part of the extension bundle; keep it dependency-free; exactly one export.
- `packages/extension/scores/` — interview flows as data. New user path = new `SCORE.md`
Expand All @@ -103,52 +95,24 @@ macOS note: the vendored binary is unsigned — if Gatekeeper blocks it:
AGENTS.md score section, visible to every agent.
- Never commit to `main`; branch + PR.

## Changing opencode (the vendored fork)

Default vendoring is `release` — `pnpm install` / `package` / F5 download the pinned,
features-ON binary; **no clone or bun needed**. You enter source mode only when you are
changing the fork, and you do it by running a command — **never** by editing the committed
`opencode.lock.json` `source` field (committing `local` forces a clone+bun build on everyone
and breaks fork-PR CI).

1. Clone the fork as a sibling and install bun:
`git clone git@github.com:harmoniqs/opencode.git ../opencode` (or set `AMICODE_OPENCODE_SRC`);
`curl -fsSL https://bun.sh/install | bash`.
2. Edit `../opencode`, then rebuild + re-vendor: **`pnpm --filter amicode opencode:build`**
(builds with `OPENCODE_CHANNEL=dev` → amicode UI gate ON; `--any-ref` so your in-progress
clone is accepted). Reload the Extension Dev Host (Cmd/Ctrl+R) to pick it up. This rebuilds
the **compiled binary** — the only path that shows web-app surfaces (`packages/app`: home
cards, v2 titlebar, draft flow), whose channel define is baked at build time (no `serve`-time
hot path for those).
3. Ship it: push the opencode branch, tag a release (its workflow builds all three binaries and
gate-checks them), then **`pnpm --filter amicode opencode:pin <tag>`** here — it downloads
and sha256-verifies every asset and rewrites the lock. Commit the lock bump + PR.
Note `opencode:pin` only re-stamps platforms ALREADY in `opencode.lock.json` — adding a new
platform means hand-adding its `asset` key (with any placeholder sha) before pinning.
## Changing the engine (opencode patches)

The engine source lives in `packages/app-bundle/overlay/packages/opencode/`.
Edit it in place — the overlay IS the source of truth (there is no external fork).

1. Edit files under `packages/app-bundle/overlay/packages/opencode/`
2. Run `pnpm --filter amicode run build:binary` to compile
3. Reload the Extension Dev Host (Cmd/Ctrl+R) to pick up the new binary

bun is required for compilation. The overlay is committed directly;
there is no fork to push to.

**Why `dev` matters:** every amicode surface is gated at runtime on
`settings.general.newLayoutDesigns`, whose default is `VITE_OPENCODE_CHANNEL !== "prod"`. A binary
built with `OPENCODE_CHANNEL=latest` (→ `"prod"`) compiles the features in but hides them.
`opencode:build` and the release workflow both force `dev`; `scripts/assert_ui_gate.sh` fails
The build script forces `dev`; `scripts/assert_ui_gate.sh` fails
CI and release if a binary ever ships with the gate off.

**Overlay sync:** `packages/app-bundle/overlay/` is a **tracking copy** of the fork's diff
against upstream — it is NOT a build input (the binary builds from the fork directly). The fork
is the source of truth for all app source files. **Never edit the overlay directly; never copy
overlay → fork** for files that exist in both; that overwrites newer fork code with stale
overlay snapshots.

After editing the fork:
```bash
pnpm --filter @amicode/app-bundle sync:check -- --source ../opencode --revision <sha>
pnpm --filter @amicode/app-bundle sync:apply -- --source ../opencode --revision <sha> --base <upstream-base>
```
`opencode:build` warns on drift but does not block. `sync:apply` is an explicit promotion,
not a rebuild side effect: it requires a clean `local/amicode` checkout, regenerates the
complete fork-vs-base file and deletion set, and updates manifest provenance atomically on
the current review branch. `sync:check` is read-only and proves a committed overlay against
one immutable fork revision.

## Releasing & publishing (amicode → Marketplace)

Two channels. **`vX.Y.Z-alpha.N`** is internal: it cuts a GitHub *prerelease* for direct install
Expand All @@ -158,12 +122,10 @@ and never reaches end users.
Version knobs that must agree: the **extension manifest** (`packages/extension/package.json`
version) must equal the **tag base** (`v0.0.3` and `v0.0.3-alpha.5` both → `0.0.3`) — enforced by
release.yml's version guard (a gate, not a bump; edit the manifest by hand when a cycle starts).
The **vendored fork** version is a separate knob, bumped via `opencode:pin` (above).

**`.github/workflows/release.yml`** — trigger: push any `v*` tag (or `workflow_dispatch` with
`tag`). Rebuilds the tag's committed release pin into **seven** vsixes. It never resolves a fork
branch or provisions a new binary, so an alpha and its promoted release carry the same payload.
Four are installable — `amicode.vsix` (universal, all binaries), `amicode-linux-x64.vsix`,
`tag`). Builds the engine binary from the overlay and packages **seven** vsixes. Four are
installable — `amicode.vsix` (universal, all binaries), `amicode-linux-x64.vsix`,
`amicode-linux-arm64.vsix`, `amicode-darwin-arm64.vsix` — and are the GitHub Release assets.
Three are **cover packages** carrying NO binary — `win32-x64`, `win32-arm64`, `darwin-x64` —
published to the Marketplace and nowhere else. A clean `vX.Y.Z` tag publishes all six
Expand Down Expand Up @@ -195,36 +157,20 @@ Needs the **`VSCE_PAT`** repo secret (an Azure DevOps PAT: org = all accessible,
Marketplace → Manage, <=1yr expiry, so rotate). Open VSX is deferred — issue #176 (needs
`OVSX_TOKEN`).

**`.github/workflows/prepare-release-candidate.yml`** — run this on the release-preparation branch
before cutting an alpha. It preflights the fine-grained `REPO_ACCESS_TOKEN`, builds and verifies a
BETA fork release, then records its tag, commit, and hashes in `opencode.lock.json` on that branch.
The alpha tag is cut only after that pin lands.

**`.github/workflows/promote.yml`** — the deliberate "this alpha is good enough" act.
`workflow_dispatch`, input `alpha_tag` (e.g. `v0.0.3-alpha.5`). Validates it (real pre-release
tag, its clean `vX.Y.Z` not yet taken, every check run green on that commit), cuts the clean tag
at the alpha's exact SHA, and dispatches release.yml. It does **not** touch the manifest; if the
base was already promoted it fails and tells you to bump + start a fresh alpha cycle.

**Candidate preparation provisions the fork binary.** The candidate workflow creates the next
fork tag from `local/amicode`, dispatches its BETA-channel build, and records the verified result
in the release-preparation branch before alpha. It requires **`REPO_ACCESS_TOKEN`** — a
fine-grained PAT scoped only to `harmoniqs/opencode`, with Actions and Contents read/write and SSO
authorization where required. Stable promotion has no cross-repository authority: it rebuilds only
from the alpha tag's committed pin. Retrying a release rebuilds the same tag and safely refreshes
its GitHub Release assets before retrying Marketplace targets.

Typical cycle: bump manifest -> prepare BETA fork candidate and commit its pin -> push
`v0.0.3-alpha.1..N` (internal prereleases) -> when one passes, **promote** it -> clean `v0.0.3`
-> Marketplace. The first promotion of a base needs no bump (alphas never hit the Marketplace, so
the version is still free); re-promoting an already-published base does.
Typical cycle: bump manifest → push `v0.0.3-alpha.1..N` (internal prereleases) → when one
passes, **promote** it → clean `v0.0.3` → Marketplace. The first promotion of a base needs no
bump (alphas never hit the Marketplace, so the version is still free); re-promoting an
already-published base does.

## Known sharp edges

- `test:slow` without `AMICO_TEST_JULIA_PROJECT` silently skips the Julia gates.
- The vendor `.sha256` stamp is the ACTUAL binary hash; `.source` records provenance
(`local <ref>` or `release <repo>@<tag>`). Local-source installs always rebuild; a
release-mode run re-downloads whenever the stamp differs from the lock manifest.
- Free-tier live e2e tiers are non-deterministic; a single tier-C failure is sampling noise.
- Julia 1.12.x minor-version drift vs the pinned Manifest prints a warning and proceeds.
- A tag pushed by CI's `GITHUB_TOKEN` does not fire `release.yml`'s `push` trigger (Actions'
Expand Down
121 changes: 36 additions & 85 deletions packages/app-bundle/README.md
Original file line number Diff line number Diff line change
@@ -1,98 +1,49 @@
# @amicode/app-bundle — the fork-owned app overlay (M2, #451)
# @amicode/app-bundle — the engine source overlay

The M2 artifact: the Amicode app surface, carried as an **overlay** on a
pinned **canonical opencode** base — the mechanism that retires the fork at
cutover while keeping every Amicode surface.
The Amicode engine source tree. This overlay is applied on top of a pinned
**canonical opencode** base to produce the Amicode-branded binary. The overlay
IS the source of truth — edit it directly.

## Slice (b) — the complete app-graph delta — SHIPPED
## How it works

**Scope (corrected mid-slice)**: the complete fork-vs-base delta of the
app's build graph — `packages/{app,ui,session-ui,schema,core,sdk}` — 422
files (per-package: app 94A/105M/1D, ui 142A/22M, session-ui 8A/16M,
schema 2A/5M, core 3A/23M, sdk 0A/2M).
The overlay contains all Amicode-specific patches to the opencode engine.
At build time, the materializer fetches the canonical upstream tarball,
applies the overlay (adds/overwrites + manifest deletions), and the result
is compiled into the vendored binary.

The planned additive-only scope **does not typecheck** — the finding that
drove the correction: ~10 additive app files depend on symbol-level
additions in modified files (`settings.developer`, `tabs.openPath`,
`model.pin`, `QuestionInfo.kind` — the typed question cards spanning
schema/core/sdk), and the fork's debug-bar deletion forces `layout.tsx`
into the overlay (base layout imports the deleted module). The fork delta
is a cross-cutting FEATURE delta, not an app-layer delta.

**The M3 cutover port inventory is machine-derived** (manifest:
`server_coupled_port_inventory`, 35 files in schema/core/sdk): every app
feature whose types or runtime live in the fork's server-side packages.
At cutover the canonical server ships NONE of these — the bundle will hit
exactly these gaps — so each is a port-upstream / extension-service /
drop decision. Recorded, not resolved.

**The true overlays** (big M-files upstream still evolves: home.tsx,
session-header, message-part, timeline, …) ride wholesale — correct for
the one-push cutover. The compose-vs-fork decomposition is deliberate
POST-cutover maintenance (`manifest.true_overlays` is the worklist).

### Proofs (all green, 2026-08-21)

1. **Equivalence** — every overlay file byte-identical to the fork's at the
pin (round-trip verified at extraction; manifest hash-verified at
materialization). Symlink-aware (`app/public/amico.svg` → ui asset).
2. **Composition** — `bun install` (4,695 pkgs) → `schema` typecheck →
`session-ui` typecheck + `ui` tsc build → `app` `tsgo -b` typecheck →
`app` **vite production build** (14.3s, 1,587 assets, Amicode surfaces
present in the emitted bundle) → **103/103 session-ui unit tests**.

## Slice (a) — the complete `packages/ui` delta — SHIPPED

**Scope**: every file under `packages/ui` the fork changed vs the upstream
base — 164 files (142 added, 22 modified; machine-derived, see
`manifest.json`). Not a hand-picked list: `materialize(base, overlay)` is
byte-identical to the fork's `packages/ui` at the pin.

**The base correction**: the fork's true upstream base is `v1.18.12~1`
(`b0b114923`) — the 2026-08-04 merge landed upstream up to just-before the
v1.18.12 tag (whose final commit only bumps version strings). The overlay
therefore materializes onto the **v1.18.12 release tarball**; the only
base-vs-fork differences in files the overlay doesn't own are version-string
bumps in files the overlay DOES own (`package.json`). Earlier diffs taken
against v1.18.10 overcounted by folding in upstream's own 1.18.10→1.18.12
changes.
```sh
# compile the engine binary from the overlay
pnpm --filter amicode run build:binary

### The two proofs (both green, 2026-08-21)
# materialize a full source tree: canonical base + overlay (for inspection)
node scripts/materialize.mjs --out <dir> [--tag v1.18.12] [--repo anomalyco/opencode]
```

1. **Equivalence** — `materialize(upstream v1.18.12, overlay)` produces a
`packages/ui` byte-identical to the fork's at `v1.18.10-amicode.14`
(`diff -r`: zero lines).
2. **Composition** — the materialized tree installs (`bun install`, 4,695
packages) and builds (`tsc -p tsconfig.build.json`) cleanly, emitting 220
files including `dist/amicode/*`.
bun is required for compilation. The binary lands in
`packages/extension/vendor/opencode/<platform>/opencode`.

### Usage
## History

```sh
# verify that the committed overlay reproduces one immutable fork revision
pnpm --filter @amicode/app-bundle sync:check -- --source <fork> --revision <sha>
This overlay was extracted from the `harmoniqs/opencode` fork (the M2
fork-absorption milestone, #1091). The fork is now archived; all engine
development happens directly in this overlay.

# explicitly promote a clean local/amicode revision on an Amicode review branch
pnpm --filter @amicode/app-bundle sync:apply -- --source <fork> --revision <sha> --base <upstream-base>
### Extraction provenance

# materialize a full source tree: canonical base + overlay
node scripts/materialize.mjs --out <dir> [--tag v1.18.12] [--repo anomalyco/opencode]
```
**Slice (b) — the complete app-graph delta:**
Scope: the complete fork-vs-base delta of the app's build graph —
`packages/{app,ui,session-ui,schema,core,sdk}` — 422 files. Machine-derived
from the fork at `v1.18.10-amicode.14` against upstream `v1.18.12`.

Promotion reads files at the immutable revision, never from the working tree,
and records the complete file hashes and deletion set. It refuses dirty source
checkouts and refuses to write directly on Amicode `main`; a rebuild only runs
the read-only verification. The materializer fetches the canonical
tarball once per tag (`.cache/`, gitignored), applies the overlay
(adds/overwrites), applies manifest deletions, and verifies every overlay
file's hash in the output.
**Slice (a) — the complete `packages/ui` delta:**
Scope: every file under `packages/ui` the fork changed vs the upstream base —
164 files (142 added, 22 modified). `materialize(upstream v1.18.12, overlay)`
produces a `packages/ui` byte-identical to the fork's at the pin.

### Later slices (docs/m2-app-extraction-inventory.md)
### Proofs (all green at extraction, 2026-08-21)

- (b) `packages/app` + `packages/session-ui` additive files
- (c) the true overlays (home, session-header, message-part, timeline) —
decomposed from whole-file ownership into composable extensions where
upstream evolution demands it
- (d) i18n tables, the debug-bar deletion, e2e port
- the consumer flip: the amicode service serves the built bundle; deck panes
point at the service origin (CSP/`?auth_token=` wiring)
1. **Equivalence** — every overlay file byte-identical to the fork's at the
pin (round-trip verified at extraction; manifest hash-verified at
materialization).
2. **Composition** — `bun install` → typecheck chain → `app` vite production
build (14.3s, 1,587 assets) → **103/103 session-ui unit tests**.
Loading
Loading