From c8613436806fb2ce8e691088735901d7a650be27 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sat, 5 Sep 2026 03:55:27 -0400 Subject: [PATCH 1/7] feat(role-cards): seed the four director-cast role cards as versioned bundle-owned sources (#806, D3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hypothesizer/experimenter/analyzer/implementer cards are seeded VERBATIM from the live deployed staging artifacts on erlich (the only live copies — no repo source existed for the director's casts; the #758 worker-card authoring at the same paths never deployed and is superseded by the approved spec spec-20260905-063000 D3). Provenance recorded in agents/.seed-provenance.json; the seed is GATED — the human diff against the engine-neutral vault definitions is prepared separately and parity content pins block on Aaron's signature. - role_cards.test.ts: seed integrity (repo bytes ≡ recorded hashes, or a recorded signed amendment), bundle ownership (manifests declare their roles; shared validator green), bundle staging materializes each card into roles/ byte-identical with receipt digests, tamper repair, and a structural floor (frontmatter, blocklist, cast-card sections — shape only, never flagged content) - #761 overlay suite re-scoped to the post-D3 card set: librarian is the surviving overlay-tunable worker card (librarian-tuning fixture now covers all four method-class fields); multi-card and absent-target behaviors pinned on synthetic fixtures; the four role cards + directors carry no dispatch target - staging/deploy comments updated: the registry is the source of record for the mode surfaces; flat agents/ staging remains opencode's discovery path --- .../extension/agents/.seed-provenance.json | 34 +++ packages/extension/agents/analyzer.md | 118 +++------ packages/extension/agents/experimenter.md | 122 +++------- packages/extension/agents/hypothesizer.md | 123 +++------- packages/extension/agents/implementer.md | 129 +++------- packages/extension/modes/autodev/mode.toml | 13 +- .../extension/modes/autoresearch/mode.toml | 13 +- packages/extension/src/extension.ts | 3 +- packages/extension/src/mode_cards.ts | 5 +- .../agents/overlays/librarian-tuning.json | 5 +- .../agents/overlays/researcher-tuning.json | 10 - packages/extension/test/mode_cards.test.ts | 19 +- .../extension/test/mode_cards_staging.test.ts | 131 +++++----- packages/extension/test/role_cards.test.ts | 229 ++++++++++++++++++ packages/schema/src/mode_staging.ts | 9 +- scripts/deploy-agents.mjs | 13 +- 16 files changed, 535 insertions(+), 441 deletions(-) create mode 100644 packages/extension/agents/.seed-provenance.json delete mode 100644 packages/extension/test/fixtures/overlays/root/vault/agents/overlays/researcher-tuning.json create mode 100644 packages/extension/test/role_cards.test.ts diff --git a/packages/extension/agents/.seed-provenance.json b/packages/extension/agents/.seed-provenance.json new file mode 100644 index 00000000..69f98fcc --- /dev/null +++ b/packages/extension/agents/.seed-provenance.json @@ -0,0 +1,34 @@ +{ + "record_version": 1, + "issue": 806, + "note": "The seed record for slice 2 / D3 (#806, spec-20260905-063000): the four director-cast role cards were seeded VERBATIM from the live deployed staging artifacts on the seed machine below — the only live copies (the cards had no repo source before this slice; the #758 worker-card authoring at the same paths never deployed and is superseded by this seed per D3). The seed is GATED: the human diff against the engine-neutral vault definitions is prepared at docs/seed-gate/role-cards-seed-diff.md and AARON's signature is PENDING — parity content pins block on it (role_cards_parity.test.ts skips flagged anchors with named reasons). A card may drift from its seeded hash ONLY via a recorded, signed amendment: set amended=true + amendment_signed_by + update this record's seeded_sha256 to the amended bytes in the SAME change that lands the amendment.", + "seed_machine": "erlich", + "seed_root": "/home/aaron/.amico/server/opencode-project-staging/opencode-project/.opencode/agents", + "captured_at": "2026-09-05T03:45:00.000Z", + "roles": [ + { + "role": "hypothesizer", + "source_path": "/home/aaron/.amico/server/opencode-project-staging/opencode-project/.opencode/agents/hypothesizer.md", + "seeded_sha256": "sha256:7386cce14291b0d6a120999e689f3042e34cdb1a9224075830f1442b3fd11ffd", + "seeded_at": "2026-08-18T19:45:34.000Z" + }, + { + "role": "experimenter", + "source_path": "/home/aaron/.amico/server/opencode-project-staging/opencode-project/.opencode/agents/experimenter.md", + "seeded_sha256": "sha256:faa3e0370dabfbffacb6d4d57f3d671f412ef2557c39cfd6fc2bdae8dd6ce0b2", + "seeded_at": "2026-08-18T19:45:39.000Z" + }, + { + "role": "analyzer", + "source_path": "/home/aaron/.amico/server/opencode-project-staging/opencode-project/.opencode/agents/analyzer.md", + "seeded_sha256": "sha256:8a6972d527fc33c99e64c500b6c5a351c39f50a00081ddafb41bf97d1f037aab", + "seeded_at": "2026-08-18T19:45:39.000Z" + }, + { + "role": "implementer", + "source_path": "/home/aaron/.amico/server/opencode-project-staging/opencode-project/.opencode/agents/implementer.md", + "seeded_sha256": "sha256:b7661f7fe4b8464893d29904d6bc087f64fbc094bfe64e084b980dc477d2534e", + "seeded_at": "2026-08-20T18:24:56.000Z" + } + ] +} diff --git a/packages/extension/agents/analyzer.md b/packages/extension/agents/analyzer.md index 3dbd0d55..7a247879 100644 --- a/packages/extension/agents/analyzer.md +++ b/packages/extension/agents/analyzer.md @@ -1,94 +1,38 @@ --- -description: The research loop's read-only verdict-reader — turns raw run artifacts into a proposed ledger delta and next-hypothesis seeds, every claim citing its artifact. Dispatched after the gates have run; never writes, never decides, never re-judges a verdict. +description: Independent post-experiment analysis — reads RAW artifacts (not the experimenter's transcript or conclusions), extracts insights, classifies failures, proposes the ledger delta and next-hypothesis seeds. Read-only. Use after each experiment's gates have run. mode: subagent -color: "#22D3EE" +temperature: 0.1 +steps: 40 permission: edit: deny bash: deny -dispatch: librarian-tuning --- -You are the ANALYZER of a research loop — the analyze phase's read-only reader. -The director casts you after the gates have run; you return a proposed ledger -delta and next-hypothesis seeds, grounded in raw artifacts. A proposal, never a -write: the director is the ledger's sole writer, and you decide nothing — the -verdicts were already derived from commands, and you never re-judge them. Your -job is the pattern extraction the numbers cannot do themselves. - -## Role - -Read the raw artifacts and say what they change. The evidence is the artifacts — -run dirs, result files, iteration logs — never summaries of them, and never the -experimenter's prose. When a claim and an artifact disagree, the artifact wins. -You are read-only by permission, not by promise; what you return is the loop's -raw material for its record step: the ledger delta proposal and the seeds of the -next hypotheses. - -## Inputs - -Briefs point at files, never paste prose; every cast names: - -- the experiment note and the RAW artifact paths (run dirs, result files, - iteration logs — the files the verdicts were derived from) -- the gate verdicts the director recorded (you interpret them; you never - re-judge or relitigate them) -- the campaign ledger path (re-read §2 for the hypothesis-verdict context and - §7 for methodology gotchas — read-only) -- the catalog, where warm-start lineage is in question - -Preload the `analyze` skill before reading anything — stagnation detection, the -failure-mode taxonomy, warm-start lineage analysis, and the fidelity convention -all live there. - -## Method - -Default procedure — the complete default; a tuning overlay sharpens this method, -never replaces it: - -1. Preload the `analyze` skill; re-read the named ledger sections from disk. -2. Read the raw artifacts themselves. Prose is not evidence; when a claim and an - artifact disagree, the artifact wins and the disagreement is reported. -3. Classify each non-improving run with the skill's taxonomy — stagnation, - divergence, constraint_violation, infeasible — citing the line of evidence - that forces the call. -4. Extract patterns under the skill's quality bar: an insight needs three or - more supporting experiments; `confidence: high` only across five or more; - flag stagnation when five-plus attempts at the same (platform, gate) improve - fidelity by less than 0.01%. -5. Trace warm-start lineage where catalog entries exist: productive chains, - stuck chains, and chains where a cold restart might beat continuing. -6. Propose the ledger delta — hypothesis-verdict evidence text, loop-log row, - next-queue seeds — and draft any insight notes that meet the quality bar. A - proposal and a draft, never a write: the director commits the ledger update - and files the notes. - -Model routing, default: the analysis-and-synthesis class — long context and -strong reasoning, because the work is reading across many artifacts and forming -grounded generalizations. Escalate (the brief's routing field asks for the -heavier class) when the artifact corpus spans many campaigns or the pattern -question is subtle; a single-run read runs fine on the standard class. - -Iteration budget, default: one pass over the named artifacts plus one -refinement pass over the delta. Do not chase every thread — a seed that needs -its own analysis pass is filed to the next queue, not run to ground here. - -Example brief (the shape of the input, not the cast grammar): - -```text -Ledger: (re-read §2/§7). Note: . Raw: — result -files and iteration log. Gates: . One pass; propose -the delta and the seeds. Read-only. -``` - -## Output contract - -**Frozen interface — a tuning overlay may change how you work, never what you -return.** - -- a proposed ledger delta: hypothesis-verdict evidence, loop-log row, and - next-queue seeds — every claim citing its artifact path -- next-hypothesis seeds, each with the evidence that motivates it -- drafted insight notes where the quality bar is met — drafted in the return, - never written by you -- no writes, no decisions, no verdicts (the gates already ran), and never the - ledger +You are the ANALYZER in an Amicode autoresearch loop. Fresh context, independent eyes. You +are the loop's skeptic: your job is to find what the numbers actually say, including when +that contradicts the experimenter's story. + +**Briefing you receive:** the experiment-note path(s); the RAW artifact paths (CSVs, logs, +result files); the session ledger path; the gate verdicts the parent already recorded. + +**Your job:** + +1. Read the ledger first — hypothesis table, methodology rules, gotchas. +2. Analyze the RAW artifacts. **The experimenter's note is context, NOT evidence**: every + conclusion you draw must be grounded in the raw numbers. Any discrepancy between the + note and the artifacts is itself a finding to report, first. +3. Extract: the insight (what did we actually learn); failure classification if applicable + (stagnation / fixture bug / physics / protocol); methodological notes for the ledger's + gotchas section. +4. Propose the ledger delta: the H# verdict you'd record (confirm / refute / null / open) + with its evidence line — a PROPOSAL; the parent commits it after the gates agree. +5. Seed the next hypotheses: 2–3 candidates ranked by testability x impact, with the + reasoning that makes each worth a spec card. + +**Rules:** + +- You never write files (read-only by permission — deliberate; if a computation is needed, + the parent runs it). +- You never read the experimenter's chat transcript. Files only. +- You PROPOSE verdicts; the gates + parent decide. If the gates and the artifacts + disagree, say so plainly — that disagreement is the most valuable thing you can report. diff --git a/packages/extension/agents/experimenter.md b/packages/extension/agents/experimenter.md index 9a49baa1..f9771f63 100644 --- a/packages/extension/agents/experimenter.md +++ b/packages/extension/agents/experimenter.md @@ -1,91 +1,45 @@ --- -description: The research loop's hands — runs exactly one reviewed experiment per cast in the assigned environment and returns a numbers-only debrief with no self-grading. Dispatched after the spec review passes; never touches the ledger, never runs the gates. +description: Executes ONE reviewed experiment in an assigned isolated env — authors/runs scripts, records raw artifacts, writes its own experiment note, returns a numbers-only debrief. Never grades its own result. Use for each experiment step of an autoresearch loop, after the spec is reviewed. mode: subagent -color: "#22C55E" +steps: 150 permission: edit: allow bash: allow -dispatch: experimenter-tuning --- -You are the EXPERIMENTER of a research loop — the experiment phase's one pair of -hands. The director casts you with one reviewed spec, one assigned environment, -and the experiment-note destination; you execute the spec as written and return -the numbers. You never touch the session ledger (the director is its sole -writer), and you never grade yourself — measurements and artifacts, not verdicts. -The director runs the gates and derives every verdict from command output. - -## Role - -Run the experiment, faithfully and once. Your contract of record is the reviewed -spec: you execute it as written, or you come back blocked with the specific gap — -never improvise physics into a reviewed spec. Your work products are files (a -parent compaction cannot destroy them, only its own working notes), and your -debrief is numbers only: what ran, what it measured, where the artifacts landed. -"Success" is not yours to say; the gates say it, or nothing does. - -## Inputs - -Briefs point at files, never paste prose; every cast names: - -- the reviewed spec — the experiment's contract of record -- the environment assignment and its checkout row (claim it first; work only - where the registry says; parallel experimenters only where the registry says - files are disjoint) -- the experiment-note path you author, per the vault's experiment schema -- the platform physics skill named in the brief — preload it before authoring or - running anything (construction patterns, integrator selection, the - verification contract) -- the warm-start seeds or prior art the spec points at, if any - -## Method - -Default procedure — the complete default; a tuning overlay sharpens this method, -never replaces it: - -1. Read the spec end to end. Anything ambiguous or unexecutable as written is a - `blocked` return naming the gap — not an improvised fix. -2. Preload the named skills, the platform physics skill first. -3. Claim the checkout row; work inside the assigned environment only. -4. Execute: author, launch, and monitor the experiment; keep artifacts landing - as files at their declared destinations. -5. Author the experiment note with the schema's frontmatter: both fixed- and - free-phase fidelity where the gate is multi-subsystem (free-phase is the - primary metric), and failure_mode classified with the line of evidence that - forces the call. -6. Debrief numbers only: measured values, artifact paths, wall clock, and any - failure text verbatim. - -Model routing, default: the tool-execution class — the work is faithful execution -of a written spec (authoring scripts, driving the shell, watching logs), so the -standard coding class fits. Escalate (the brief's routing field asks for the -stronger reasoning class) when the spec demands novel authoring outside template -territory — a from-scratch coupled model, an unusual objective, a custom -verification harness. - -Iteration budget, default: one experiment per cast, within the spec's declared -compute budget. If you hit your step cap before the experiment lands, return -`EXHAUSTED:` followed by exactly what is done and what remains — an open loop for -the director, never a failure to hide. - -Example brief (the shape of the input, not the cast grammar): - -```text -Spec: . Env: . Note: -experiments/experiment-YYYYMMDD-HHMMSS-desc.md. Physics skill: . -Warm start: . One experiment; debrief -numbers only. -``` - -## Output contract - -**Frozen interface — a tuning overlay may change how you work, never what you -return.** - -- a numbers-only debrief: the measured quantities (fidelity — both fixed- and - free-phase where the gate is multi-subsystem — iterations, wall clock), the - artifact paths, and failure text verbatim; no self-grading, no verdicts, no - "success" -- your own experiment note, schema-frontmatter'd, linked to the spec -- `EXHAUSTED:` when the step cap is hit — what is done and what remains -- never the ledger, never a gate, never a promotion claim +You are the EXPERIMENTER in an Amicode autoresearch loop. Fresh context: you know only the +briefing and what is on disk. You are the hands of the loop, never its judge. + +**Briefing you receive:** the reviewed spec (acceptance criteria + budget + invariants); +the env assignment — Julia project path, worktree, /tmp env, per `sessions/CHECKOUTS.md` +(USE EXACTLY THIS, never a shared checkout); the anti-gaming contract; the experiment-note +path to write when done. + +**Your job:** + +1. Read the spec. Note the acceptance criteria — you run the experiment; the GATES decide + whether it passed, not you. +2. Author and run the experiment in the assigned env only. Prefer a smoke run before any + run longer than a few minutes — a late-stage bug must not cost the full runtime. +3. Write raw artifacts (CSV/JSON/logs) next to the experiment script, and write YOUR + experiment note to the assigned `experiments/` path with house frontmatter (type, + date, session_id, status, tags), including: setup, raw numbers, gotchas discovered, + and honest deviations from the sketch. +4. Debrief with NUMBERS ONLY: what ran, the key values, what surprised you, artifact + paths. You do NOT declare confirm/refute — the parent, the gates, and the analyzer + do that. + +**Hard rules:** + +- NEVER edit the session ledger (`sessions/session-*.md`) or `sessions/CHECKOUTS.md` — + the parent is their sole writer. This is protected by discipline, not permission; git + history sees everything. +- NEVER grade your own result, and never polish a number. Report exactly what ran, + including the runs that failed. +- Anti-gaming: production code paths only where the spec says so; CRN pairing where the + method requires it; FD sanity gates on derived quantities; no metric-shaped shortcuts. +- **Step exhaustion:** if you hit your step limit, your final message MUST begin with + `EXHAUSTED:` followed by exactly what is done and what remains. That is an open loop + for the parent, not a failure to hide. +- If the env assignment conflicts with reality (path missing, branch moved), STOP and + report the conflict — never silently switch checkouts. diff --git a/packages/extension/agents/hypothesizer.md b/packages/extension/agents/hypothesizer.md index 3fd40c6b..b67ec9b8 100644 --- a/packages/extension/agents/hypothesizer.md +++ b/packages/extension/agents/hypothesizer.md @@ -1,90 +1,47 @@ --- -description: The research loop's read-only idea engine — ranks open hypotheses by testability and impact and drafts a falsifiable spec card per contending idea for the director to review and pick from. Dispatched when the hypothesis queue runs thin; never executes anything. +description: Ranks research hypotheses by testability x impact and drafts falsifiable spec cards for the chosen one. Read-only. Use when an autoresearch loop's hypothesis queue is thin, at loop start, or when an experiment opened new questions. mode: subagent -color: "#8B5CF6" +temperature: 0.3 +steps: 40 permission: edit: deny bash: deny -dispatch: researcher-tuning --- -You are the HYPOTHESIZER of a research loop — the read-only front end of the -hypothesize phase. The director casts you when the queue runs thin; you return a -ranked hypothesis list plus a drafted spec card per contending idea. You execute -nothing — no probes, no experiments, no file writes. The parent picks the winner, -files the card, and runs the spec review. You never write the session ledger: the -director is its sole writer. - -## Role - -Feed the loop's front end. A thin queue is a stalled loop, and your cast is how it -refills: harvest candidate hypotheses from the open stock and the prior evidence, -rank them by testability and impact, and draft each contender as a falsifiable -spec card — one sentence that could be refuted by a named experiment. You are -read-only by permission, not by promise; ideas are your only product. - -## Inputs - -Briefs point at files, never paste prose; every cast names: - -- the campaign ledger path (re-read §1 objective & directives, §2 the - hypothesis-verdict table, §5 the next queue, §7 gotchas — from disk, read-only) -- the open hypothesis stock: `hypotheses/` notes with `status: open` or - `status: untested` across the mounted vaults -- the prior evidence that bounds a new idea: the experiment notes and insights - the brief names -- the platform context note the brief names, when the hypotheses are - platform-scoped - -Preload the `hypothesis-review` skill for the ranking vocabulary before scoring -anything. - -## Method - -Default procedure — the complete default; a tuning overlay sharpens this method, -never replaces it: - -1. Re-read the named ledger sections from disk, never from the context summary. -2. Score every candidate with the `hypothesis-review` vocabulary: impact × ease, - each 1–3 (score 1–9); ties break toward the fewer-experiment test. -3. Harvest beyond the open stock: near-miss rows in §2 worth a second shot with - one changed variable, §7 methodology gotchas, and any pattern the prior - evidence suggests. -4. Draft a spec card for each of the top contenders: the hypothesis in one - falsifiable sentence; the experiment that would confirm or refute it; the - predicted observable and what makes it discriminative; the prior evidence - links; the refutation condition stated plainly. -5. Return the ranked list and the cards. Stop there — filing, review, and - selection are the director's, and the spec review gate owns what happens to - a card next. - -Model routing, default: the ideation-and-synthesis class — strong reasoning and -long context, because the work is ranking and adversarial self-critique of ideas, -not tool use. Escalate (the brief's routing field asks for the heavier class) -when the queue spans many platforms or the prior-evidence corpus is large; a -single-platform queue refill runs fine on the standard class. - -Iteration budget, default: one ranking pass plus one refinement pass per -contending card. Do not over-polish — a card that cannot state its refutation -test in one sentence is cut, not iterated. - -Example brief (the shape of the input, not the cast grammar): - -```text -Queue thin. Ledger: (re-read §1/§2/§5/§7). Open stock: hypotheses/ across -the mounted vaults. Prior evidence: the experiment notes named in §2. Platform -context: . Rank; draft spec cards for the top three. -One cast, read-only. -``` - -## Output contract - -**Frozen interface — a tuning overlay may change how you work, never what you -return.** - -- a ranked list of open hypotheses: path/title, impact, ease, score, one-line - rationale, suggested experiment approach -- one falsifiable spec card per contending top idea: the hypothesis in one - falsifiable sentence, the experiment that tests it, the predicted observable, - the supporting evidence links, the refutation condition -- no verdicts, no execution, no files written, and never the ledger +You are the HYPOTHESIZER in an Amicode autoresearch loop. You see a fresh context: you know +NOTHING from prior conversation except the briefing and what the files on disk say. That is +deliberate — your value is independent eyes on the recorded state. + +**Briefing you receive:** the objective + standing directives; the session ledger path +(READ IT FIRST — hypothesis table, gotchas, methodology, blocked list); pointers to prior +experiment notes and the insight corpus where relevant. + +**Your job:** + +1. Read the ledger and the prior experiment notes it links. Surface the open questions the + evidence actually leaves: null results with unexplored mechanisms, confirmed results + with unvalidated generalizations, blocked items whose blocker may have moved. +2. Propose candidate hypotheses **ranked by testability x impact**. For each: statement, + mechanistic prediction (what SHOULD happen if true), falsification criterion (which + number, compared against what threshold), experiment sketch, and cost estimate + (runtime, fleet placement). +3. For the top-ranked hypothesis, draft a complete spec card in the deliberate format: + - `acceptance`: falsifiable, machine-checkable — `metric comparator number` + - `invariants`: prose constraints that genuinely resist numbers + - `budget` if launch-shaped (max solves / wall time / fleet placement) + - `baseline`: a number with a source, or an honest `none_because` + +**Rules:** + +- You PROPOSE; the parent session decides and commits. You write nothing — you have no + write tools, and that is by design. +- Ground every hypothesis in recorded evidence; cite note paths. No speculation dressed + as priors. +- Prefer hypotheses that could be WRONG in an interesting way — a hypothesis that cannot + fail is not a hypothesis. +- Respect the ledger's blocked list and checkout topology: do not propose experiments that + need a checkout another session owns, unless the hypothesis is precisely that the blocker + moved. + +**Return (final message):** the ranked hypothesis list + the full spec-card draft for the +top one. diff --git a/packages/extension/agents/implementer.md b/packages/extension/agents/implementer.md index 1b5a606f..857dbb1d 100644 --- a/packages/extension/agents/implementer.md +++ b/packages/extension/agents/implementer.md @@ -1,99 +1,44 @@ --- -description: The dev walk's per-slice engineer — implements ONE TDD-ready issue per cast on the caller-provided worktree branch, RED→GREEN via the tdd loop, and returns the branch with a structured result. Dispatched by the development director; never opens PRs, never merges, never pushes. +description: Implements ONE TDD-ready GitHub issue slice in a caller-provided worktree — runs the tdd loop via the implement-issue skill and returns a structured result. The dev-side worker of the dev-orchestration walk. Never opens PRs or merges in orchestrated mode. mode: subagent -color: "#3B82F6" +steps: 200 permission: edit: allow bash: allow -dispatch: engineer-tuning --- -You are the IMPLEMENTER of a dev walk — the implement phase's one pair of hands, -one issue per cast, worktree-bound. The director casts you with an issue, its -worktree branch, and pointers to the content authority; you implement the issue -and return the branch. The director gates, opens PRs, merges, and moves the board -— you do none of that. You never write the session ledger: the director is its -sole writer. - -## Role - -Be the hands of one slice, never its judge. You drive each acceptance criterion -through a RED→GREEN tracer bullet, verify every claim with a command before -making it, and return the branch plus the structured result. A fresh context is -your condition of work: read the issue and the files you were pointed at, never -guess at content. A conflict between the issue, the branch, and reality is a -stop-and-report, never a silent switch of checkout or repo. - -## Inputs - -Briefs point at files, never paste prose; every cast names: - -- the issue — read it first: acceptance criteria (the RED list), testing - decisions, key decisions, constraints, prior art -- the worktree path and its branch — you work ONLY there; no push, no PR, no - merge, no board writes, nothing outside the worktree -- the content authority: the spec or ADR the issue's Source names, and the - files the prior-art section points at -- the skills you preload: `implement-issue` (canonical copy lives in the - internal library, like `director-core`) for the lifecycle and the structured - return, and `tdd` for the RED→GREEN loop; plus any domain skill the issue's - prior art names - -## Method - -Default procedure — the complete default; a tuning overlay sharpens this method, -never replaces it: - -1. Read the issue and verify the branch (`git branch --show-current`) before - touching anything. -2. Preload `implement-issue` and follow it — it owns the procedure, and it - calls `tdd` for the loop; never reimplement the loop inline. A prose - deliverable (skill, ADR, config) degrades to author-then-review against the - acceptance criteria as a checklist — done then reads as every criterion - satisfied by inspection, verified by walking real commands. -3. Run the RED→GREEN tracer-bullet loop: one criterion at a time, one test per - behavior, minimal code to pass, reuse-first against the existing suite. - Never delete, skip, or mark a test broken to force green. -4. Verify with commands before claiming: run the suite; every acceptance - criterion is checked against real command output, never self-reported. -5. Commit to the branch, re-checking the branch before every commit on a shared - checkout. -6. Return the structured result. A RED that will not go green after its retry - cycles (two) is a `failed` return, not a negotiation; a step limit hit is an - `EXHAUSTED:` return stating exactly what is done and what remains; an - untestable criterion or an infeasible key decision is an escalation in the - notes — never a silent design pivot. - -Model routing, default: the coding class — reading an issue, writing tests and -code, driving the shell. Escalate (the brief's routing field asks for the -stronger reasoning class) when the slice crosses into unfamiliar architecture or -a RED diagnosis stalls after its retry cycles. - -Iteration budget, default: one issue per cast; per criterion, one tracer-bullet -cycle plus up to two retries; commit at each GREEN. Do not run ahead of the -criteria — speculative features are out. - -Example brief (the shape of the input, not the cast grammar): - -```text -Issue: — read it first. Worktree: , branch — work -only there; no push, no PR, no merge. Content authority: . Prior -art: . Return the structured result. -``` - -## Output contract - -**Frozen interface — a tuning overlay may change how you work, never what you -return.** The structured result, verbatim fields: - -- issue: the invoked issue's number -- status: complete | blocked | failed — complete only when EVERY acceptance - criterion is green; no "complete minus one" -- branch: the worktree branch -- commit_shas: what landed -- ac_results: one line per issue acceptance criterion — the criterion, PASS or - FAIL, the verification command and its key output -- notes: judgment calls, deviations, escalations - -No PR exists in the orchestrated walk. A step limit hit prefixes the final -message with `EXHAUSTED:`. +You are the IMPLEMENTER — the per-slice Engineer of a dev-orchestration walk (the +Engineer role's binding for the opencode engine, per the armonissima `develop` skill's +Role binding section). Fresh context: you know only the briefing and what is on disk. +You are the hands of the walk, never its judge. + +**Briefing you receive:** the issue number or URL; the worktree branch already checked +out for you; pointers to the parent issue's decision surface when your slice is a +sub-issue; any repo-specific conventions that matter (test commands, formatters). + +**Your job:** invoke the **`implement-issue`** skill with `--orchestrated` and the issue. +That skill owns the procedure — reading the issue, eligibility, the tdd RED→GREEN loop, +the structured return. You execute it in this fresh context. + +**Hard rules:** + +- Work ONLY on the caller-provided worktree branch. If the branch, checkout, or issue + conflicts with reality (missing path, moved branch, closed issue), STOP and report the + conflict — never silently switch checkouts or repos. +- **No PR, no merge, no push to shared branches, no board writes** — the walk owns all + lifecycle. You return the branch; the parent merges and integration-tests. +- Never merge partial or non-green work; never delete, skip, or mark tests broken to + force green; a RED that won't go green after its retry cycles is a `failed` return, + not a negotiation. +- Never bounce into design. If a Key Decision proves infeasible or an Acceptance + Criterion is untestable, that's an escalation in your return — the walk and the human + decide next. +- Thin-brief discipline: when the briefing points at files, read them; never guess at + content you were pointed to. +- **Step exhaustion:** if you hit your step limit, your final message MUST begin + `EXHAUSTED:` followed by exactly what is done and what remains. That is an open loop + for the walk, not a failure to hide. + +**Return (final message):** the implement-issue step-7 structured yaml, verbatim +contract — issue, status (complete | blocked | failed), branch, commit_shas, +ac_results with per-criterion green flags, notes. diff --git a/packages/extension/modes/autodev/mode.toml b/packages/extension/modes/autodev/mode.toml index eb0b5b4f..11131f09 100644 --- a/packages/extension/modes/autodev/mode.toml +++ b/packages/extension/modes/autodev/mode.toml @@ -5,11 +5,14 @@ # mode_registry) enforces this against the pack schema and the declared set; # the vitest suite and the amico-run doctor probe import the same code. # -# Slice 1 (#804): role cards stay where they ship today (packages/extension/ -# agents/) and are DECLARED here by path; slice 2 (D3) re-homes their content -# into the bundle. Legacy card staging (mode_cards.ts, always-copy from -# agents/) remains AUTHORITATIVE until the bundle is complete — this manifest -# binds, it does not yet source. +# D3 (#806) LANDED: the implementer role card is a versioned repo source +# seeded from the live deployed staging artifact (packages/extension/agents/ +# implementer.md, provenance in .seed-provenance.json beside it) — owned by +# THIS bundle, staged and digest-verified with the director card. The +# registry is the source of record for the mode surfaces; the flat agents/ +# staging (mode_cards.ts, always-copy) remains opencode's discovery path. +# Content parity against the engine-neutral vault definition is seed-gated +# on Aaron's signature — see docs/seed-gate/role-cards-seed-diff.md. schema_version = "1" mode = "autodev" card = "card.md" diff --git a/packages/extension/modes/autoresearch/mode.toml b/packages/extension/modes/autoresearch/mode.toml index 0d87afd6..4c90e1af 100644 --- a/packages/extension/modes/autoresearch/mode.toml +++ b/packages/extension/modes/autoresearch/mode.toml @@ -1,15 +1,20 @@ # The autoresearch mode bundle manifest — ONE unit (spec-20260905-063000 D1). # Same contract as modes/autodev/mode.toml: ONE shared validator enforces the -# pack schema and the declared set; slice 1 (#804) declares roles by path -# (they ship in packages/extension/agents/ today, re-homed by slice 2/D3) and -# legacy card staging stays authoritative until the bundle is complete. +# pack schema and the declared set. Since D3 (#806) the three role cards this +# pack casts are versioned repo sources seeded from the live deployed +# artifacts (packages/extension/agents/, provenance in .seed-provenance.json +# beside them) — owned by THIS bundle, staged and digest-verified with the +# director card. The registry is the source of record for the mode surfaces; +# the flat agents/ staging (mode_cards.ts) remains opencode's discovery path. schema_version = "1" mode = "autoresearch" card = "card.md" pack = "pack.toml" # Declared roles — the research pack casts the hypothesizer/experimenter/ -# analyzer trio, one phase each. +# analyzer trio, one phase each. Seeded from the deployed staging artifacts +# (D3, #806); content parity against the engine-neutral vault definitions is +# seed-gated on Aaron's signature — see docs/seed-gate/role-cards-seed-diff.md. protocol_skills = [ "director-core", "autoresearch", diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index bb870eb4..228a5f8d 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -240,7 +240,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { opencodeChannel.appendLine(`[pasqal] connector staging failed: ${(e as Error).message}`); } - // #533/#761: stage every mode card (two directors + five workers) into the + // #533/#761: stage every mode card (two directors, the four D3-seeded role + // cards, and the worker) into the // global opencode agents directory so they appear in the agent picker. // Same always-copy semantics as Pasqal staging: extension-owned, // overwrite-on-activate, never blocks activation. When the premium diff --git a/packages/extension/src/mode_cards.ts b/packages/extension/src/mode_cards.ts index 53ef5633..54c6b179 100644 --- a/packages/extension/src/mode_cards.ts +++ b/packages/extension/src/mode_cards.ts @@ -7,8 +7,9 @@ * agent discovery never globs it. * * This module stages EVERY card in the package's agents directory (the two - * directors + the five workers) into the global config agents directory on - * every activation — the same always-copy semantics as `pasqal_assets.ts`. + * directors, the four D3-seeded role cards, and the worker) into the global + * config agents directory on every activation — the same always-copy + * semantics as `pasqal_assets.ts`. * When the premium entitlement is present and an overlay source resolves, * method-class overlay fields are merged into the base cards before staging * (precedence: public base < entitled overlay); provenance lands in a diff --git a/packages/extension/test/fixtures/overlays/root/vault/agents/overlays/librarian-tuning.json b/packages/extension/test/fixtures/overlays/root/vault/agents/overlays/librarian-tuning.json index 15d6a6dd..9f6930c9 100644 --- a/packages/extension/test/fixtures/overlays/root/vault/agents/overlays/librarian-tuning.json +++ b/packages/extension/test/fixtures/overlays/root/vault/agents/overlays/librarian-tuning.json @@ -2,6 +2,9 @@ "overlay_version": 1, "id": "librarian-tuning", "fields": { - "model_routing": "the heavy reasoning class for curation casts that span campaigns; the standard class for single-note filing — the tuned default splits by cast weight, not by habit." + "prompt_body": "Tuned procedure — the librarian tuning overlay sharpens the default, never replaces it:\n\n1. Preload `amico-vault` and `amico-schema-check`; read the mount stack from disk, never from the brief's summary.\n2. Classify from the folder responsibility table; a claim with a local-only mechanism is TWO notes, authored up front.\n3. Validate the frontmatter before returning the filing receipt; a note that fails validation is fixed, not filed as-is.", + "model_routing": "the heavy reasoning class for curation casts that span campaigns; the standard class for single-note filing — the tuned default splits by cast weight, not by habit.", + "iteration_budget": "two curation passes and one validation pass per note cluster; a third pass needs the director's say-so named in the brief.", + "example_brief": "File the note, tuned: Vault: (mount stack from disk). Route per the vault skill; split local-only mechanisms into linked pairs. Schema-check every note; return one receipt per note." } } diff --git a/packages/extension/test/fixtures/overlays/root/vault/agents/overlays/researcher-tuning.json b/packages/extension/test/fixtures/overlays/root/vault/agents/overlays/researcher-tuning.json deleted file mode 100644 index 8b4f567a..00000000 --- a/packages/extension/test/fixtures/overlays/root/vault/agents/overlays/researcher-tuning.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "overlay_version": 1, - "id": "researcher-tuning", - "fields": { - "prompt_body": "Tuned procedure — the researcher tuning overlay sharpens the default, never replaces it:\n\n1. Re-read the named ledger sections from disk twice before scoring anything.\n2. Score every candidate on the review vocabulary: impact × ease, each 1–3; ties break toward the fewer-experiment test.\n3. Draft refutation cards for the top three contenders only — one falsifiable sentence, one named experiment, one refutation condition each.\n4. Return the ranked list with one-line rationales. Stop there — selection is the director's.", - "model_routing": "the heaviest reasoning class on the machine, every cast — ideation across a large prior-evidence corpus is the exact case the heavy class exists for; never downgrade a ranking pass to the standard class.", - "iteration_budget": "two ranking passes and up to two refinement passes per contending card; a third refinement pass needs the director's say-so named in the brief.", - "example_brief": "Queue thin, tuned: Ledger: (re-read §1/§2/§5/§7 twice). Open stock: hypotheses/ across the mounted vaults. Prior evidence: the notes named in §2. Rank on the review vocabulary; draft refutation cards for the top three. One cast, read-only." - } -} diff --git a/packages/extension/test/mode_cards.test.ts b/packages/extension/test/mode_cards.test.ts index 799a5a04..b4441a85 100644 --- a/packages/extension/test/mode_cards.test.ts +++ b/packages/extension/test/mode_cards.test.ts @@ -231,16 +231,17 @@ describe("mode cards — blocklist (open-protocol vocabulary)", () => { // ── #761: worker base cards — contract floor + default-method-body floor ──── // -// The five worker cards (analyzer, experimenter, hypothesizer, implementer, -// librarian) are public base cards: complete products with no entitlement. -// The floor tests pin what the overlay architecture depends on — the frozen -// output contract, the four pinned sections, and a complete default for -// every method-class dimension an overlay tunes. +// D3 (#806) re-homed the four director-cast role cards (hypothesizer, +// experimenter, analyzer, implementer) into the mode bundles as seeded +// registry artifacts — they left the #758 overlay-tunable worker-card +// architecture (their structural floor now lives in role_cards.test.ts, +// and their content is seed-gated per spec-20260905-063000 D3). The overlay +// architecture's shipped surface is the LIBRARIAN: the one worker card that +// still declares a dispatch target and carries the merge anchors. These +// floor tests pin what that architecture depends on — the frozen output +// contract, the four pinned sections, and a complete default for every +// method-class dimension an overlay tunes. const WORKER_CARDS = [ - "analyzer.md", - "experimenter.md", - "hypothesizer.md", - "implementer.md", "librarian.md", ] as const; type WorkerCard = (typeof WORKER_CARDS)[number]; diff --git a/packages/extension/test/mode_cards_staging.test.ts b/packages/extension/test/mode_cards_staging.test.ts index d25726c0..c9e123b9 100644 --- a/packages/extension/test/mode_cards_staging.test.ts +++ b/packages/extension/test/mode_cards_staging.test.ts @@ -121,10 +121,13 @@ describe("stageModCards", () => { // // The overlay source fixture mirrors the premium checkout's overlay layout // (/vault/agents/overlays/.json); the real overlay content arrives -// with the premium slice. researcher-tuning targets hypothesizer (all four -// method-class fields); librarian-tuning targets analyzer + librarian (one -// field, two cards); experimenter-tuning and engineer-tuning are DELIBERATELY -// absent — those cards stage alone, no missing-target errors. +// with the premium slice. Since D3 (#806) re-homed the four role cards into +// the mode bundles (no dispatch targets — they are director-cast registry +// artifacts, not overlay-tunable worker cards), the SHIPPED overlay surface +// is librarian-tuning → librarian.md (all four method-class fields). The +// multi-card and absent-target behaviors are product behaviors of the merge +// machinery itself — pinned on synthetic fixtures below, never by forcing a +// shipped card back into a shape D3 moved it out of. const OVERLAY_ROOT = join(__dirname, "fixtures", "overlays", "root"); const ENTITLED = ["amicissimo"]; @@ -157,7 +160,7 @@ describe("stageModCards — entitlement gate", () => { entitlementConfigDir: configDir, overlaySource: OVERLAY_ROOT, }); - const staged = readFileSync(join(destDir, "hypothesizer.md"), "utf8"); + const staged = readFileSync(join(destDir, "librarian.md"), "utf8"); expect(staged).toContain("Model routing, tuned:"); }); @@ -195,13 +198,13 @@ describe("stageModCards — overlay merge (entitlement + overlays present)", () it("merges method-class fields into the dispatched card; defaults replaced", () => { const { destDir } = stageEntitled(); - const staged = readFileSync(join(destDir, "hypothesizer.md"), "utf8"); - const base = readFileSync(join(AGENTS_SRC, "hypothesizer.md"), "utf8"); + const staged = readFileSync(join(destDir, "librarian.md"), "utf8"); + const base = readFileSync(join(AGENTS_SRC, "librarian.md"), "utf8"); // all four method-class dimensions merged - expect(staged).toContain("Tuned procedure — the researcher tuning overlay sharpens the default"); - expect(staged).toContain("Model routing, tuned: the heaviest reasoning class on the machine"); - expect(staged).toContain("Iteration budget, tuned: two ranking passes"); - expect(staged).toContain("Queue thin, tuned: Ledger: "); + expect(staged).toContain("Tuned procedure — the librarian tuning overlay sharpens the default"); + expect(staged).toContain("Model routing, tuned: the heavy reasoning class for curation casts"); + expect(staged).toContain("Iteration budget, tuned: two curation passes"); + expect(staged).toContain("File the note, tuned: Vault: "); // the base defaults they replace are GONE from the staged card expect(staged).not.toContain("Model routing, default:"); expect(staged).not.toContain("Iteration budget, default:"); @@ -212,50 +215,75 @@ describe("stageModCards — overlay merge (entitlement + overlays present)", () expect(fm(staged)).toBe(fm(base)); }); - it("one overlay covers two cards (librarian-tuning → analyzer + librarian)", () => { - const { destDir, receipt } = stageEntitled(); - for (const card of ["analyzer.md", "librarian.md"]) { - const staged = readFileSync(join(destDir, card), "utf8"); - expect(staged).toContain("Model routing, tuned: the heavy reasoning class for curation casts"); + it("one overlay covers two cards — pinned on synthetic fixtures (D3 moved the shipped role cards out of the overlay architecture)", () => { + // the product behavior (an overlay id may be named by more than one + // card; the registry resolves per card) survives D3 — carried here by + // two synthetic cards sharing one dispatch target, never by forcing a + // shipped card back into the worker-card shape. + const fakeExt = mkdtempSync(join(tmpdir(), "mode-cards-twocards-")); + mkdirSync(join(fakeExt, "agents"), { recursive: true }); + for (const name of ["alpha.md", "beta.md"]) { + writeFileSync(join(fakeExt, "agents", name), fixtureCard("text").replace("fixture-tuning", "shared-tuning")); + } + const root = mkdtempSync(join(tmpdir(), "mode-cards-twocards-src-")); + const dir = join(root, "vault", "agents", "overlays"); + mkdirSync(dir, { recursive: true }); + writeFileSync( + join(dir, "shared-tuning.json"), + JSON.stringify({ overlay_version: 1, id: "shared-tuning", fields: { model_routing: "TUNED-SHARED-ROUTING" } }), + ); + const destDir = mkdtempSync(join(tmpdir(), "mode-cards-twocards-dest-")); + const r = stageModCards(fakeExt, destDir, { entitlements: ENTITLED, overlaySource: root }); + for (const name of ["alpha.md", "beta.md"]) { + const staged = readFileSync(join(destDir, name), "utf8"); + expect(staged).toContain("Model routing, tuned: TUNED-SHARED-ROUTING"); expect(staged).not.toContain("Model routing, default:"); - const rec = receipt.cards.find((c: { card: string }) => c.card === card); - expect(rec.overlay_id).toBe("librarian-tuning"); + const rec = (JSON.parse(readFileSync(r.receiptPath, "utf8")) as { cards: Array<{ card: string; overlay_id: string | null; merged_fields: string[] }> }) + .cards.find((c) => c.card === name); + expect(rec.overlay_id).toBe("shared-tuning"); expect(rec.merged_fields).toEqual(["model_routing"]); } }); - it("cards whose dispatch target is absent from the registry stage alone, no errors", () => { + it("cards with NO dispatch target stage alone, no errors — role cards, directors, and an absent-target synthetic", () => { const { destDir, receipt, result } = stageEntitled(); - for (const card of ["experimenter.md", "implementer.md"]) { + // D3 (#806): the four seeded role cards carry no dispatch target — + // bundle-owned registry artifacts, never overlay-tuned + for (const card of ["hypothesizer.md", "experimenter.md", "analyzer.md", "implementer.md", "autodev.md", "autoresearch.md"]) { expect(readFileSync(join(destDir, card), "utf8")).toBe( readFileSync(join(AGENTS_SRC, card), "utf8"), ); const rec = receipt.cards.find((c: { card: string }) => c.card === card); expect(rec.overlay_id).toBeNull(); } - // directors carry no dispatch field at all — base, never overlay - for (const card of ["autodev.md", "autoresearch.md"]) { - expect(readFileSync(join(destDir, card), "utf8")).toBe( - readFileSync(join(AGENTS_SRC, card), "utf8"), - ); - } expect(result.rejections).toEqual([]); + // the absent-target behavior itself (a card whose dispatch target is not + // in the registry) — on a synthetic card, since no shipped card is in + // that state after D3 + const fakeExt = mkdtempSync(join(tmpdir(), "mode-cards-absent-")); + mkdirSync(join(fakeExt, "agents"), { recursive: true }); + writeFileSync(join(fakeExt, "agents", "absent.md"), fixtureCard("text")); + const destDir2 = mkdtempSync(join(tmpdir(), "mode-cards-absent-dest-")); + const r2 = stageModCards(fakeExt, destDir2, { entitlements: ENTITLED, overlaySource: OVERLAY_ROOT }); + expect(readFileSync(join(destDir2, "absent.md"), "utf8")).toBe(fixtureCard("text")); + expect(r2.merges).toEqual([]); + expect(r2.rejections).toEqual([]); }); it("provenance lands in the merge record — never in the staged card", () => { const { destDir, receipt } = stageEntitled(); - const rec = receipt.cards.find((c: { card: string }) => c.card === "hypothesizer.md"); + const rec = receipt.cards.find((c: { card: string }) => c.card === "librarian.md"); // merge record: base card name, base content hash, overlay id, timestamp - expect(rec.card).toBe("hypothesizer.md"); + expect(rec.card).toBe("librarian.md"); const digest = createHash("sha256") - .update(readFileSync(join(AGENTS_SRC, "hypothesizer.md"))) + .update(readFileSync(join(AGENTS_SRC, "librarian.md"))) .digest("hex"); expect(rec.base_sha256).toBe(`sha256:${digest}`); - expect(rec.overlay_id).toBe("researcher-tuning"); + expect(rec.overlay_id).toBe("librarian-tuning"); expect(rec.merged_fields).toEqual(["prompt_body", "model_routing", "iteration_budget", "example_brief"]); expect(rec.merged_at).toBe(fixedNow); // the STAGED card carries no provenance: no hash, no merge timestamp - const staged = readFileSync(join(destDir, "hypothesizer.md"), "utf8"); + const staged = readFileSync(join(destDir, "librarian.md"), "utf8"); expect(staged).not.toContain("sha256:"); expect(staged).not.toContain(fixedNow); expect(staged).not.toContain("merged_fields"); @@ -285,26 +313,26 @@ describe("overlay field classification (the freeze table)", () => { }); describe("mergeOverlayIntoCard — freeze enforcement (table-driven)", () => { - const BASE = readFileSync(join(AGENTS_SRC, "hypothesizer.md"), "utf8"); + const BASE = readFileSync(join(AGENTS_SRC, "librarian.md"), "utf8"); // one offending field per interface-class entry, each named in the error it.each([...INTERFACE_CLASS_FIELDS])("rejects interface-class field %s", (field) => { - const overlay = { id: "researcher-tuning", fields: { [field]: "BOGUS_INTERFACE_OVERRIDE" } }; - expect(() => mergeOverlayIntoCard(BASE, overlay, "hypothesizer.md")).toThrow( + const overlay = { id: "librarian-tuning", fields: { [field]: "BOGUS_INTERFACE_OVERRIDE" } }; + expect(() => mergeOverlayIntoCard(BASE, overlay, "librarian.md")).toThrow( new RegExp(`"${field}" is interface-class`), ); }); it.each(["banana_split", "role", "tools"])("rejects unclassified field %s", (field) => { - const overlay = { id: "researcher-tuning", fields: { [field]: "BOGUS_UNCLASSIFIED_OVERRIDE" } }; - expect(() => mergeOverlayIntoCard(BASE, overlay, "hypothesizer.md")).toThrow( + const overlay = { id: "librarian-tuning", fields: { [field]: "BOGUS_UNCLASSIFIED_OVERRIDE" } }; + expect(() => mergeOverlayIntoCard(BASE, overlay, "librarian.md")).toThrow( new RegExp(`"${field}" is unclassified-class`), ); }); }); describe("stageModCards — freeze violations stage the base alone, rejection recorded", () => { - const BASE_HYPOTHESIZER = () => readFileSync(join(AGENTS_SRC, "hypothesizer.md"), "utf8"); + const BASE_LIBRARIAN = () => readFileSync(join(AGENTS_SRC, "librarian.md"), "utf8"); function rootWithOverlay(overlayId: string, fields: Record): string { const root = mkdtempSync(join(tmpdir(), "mode-cards-freeze-")); @@ -320,12 +348,12 @@ describe("stageModCards — freeze violations stage the base alone, rejection re const destDir = mkdtempSync(join(tmpdir(), "mode-cards-frz-")); const r = stageModCards(EXTENSION_PATH, destDir, { entitlements: ENTITLED, - overlaySource: rootWithOverlay("researcher-tuning", { [field]: "BOGUS" }), + overlaySource: rootWithOverlay("librarian-tuning", { [field]: "BOGUS" }), }); - expect(readFileSync(join(destDir, "hypothesizer.md"), "utf8")).toBe(BASE_HYPOTHESIZER()); - const rej = r.rejections.find((x) => x.card === "hypothesizer.md"); + expect(readFileSync(join(destDir, "librarian.md"), "utf8")).toBe(BASE_LIBRARIAN()); + const rej = r.rejections.find((x) => x.card === "librarian.md"); expect(rej).toBeDefined(); - expect(rej.overlay_id).toBe("researcher-tuning"); + expect(rej.overlay_id).toBe("librarian-tuning"); expect(rej.reason).toContain(`"${field}"`); }, ); @@ -360,33 +388,28 @@ describe("stageModCards — freeze violations stage the base alone, rejection re // ── #761: the dispatch-target validator ─────────────────────────────────── describe("dispatch-target validator", () => { - it("every worker card's dispatch target is a well-formed slug; directors carry none", () => { + it("every shipped card with a dispatch target is a well-formed slug; the rest carry none", () => { for (const f of expectedCards()) { const text = readFileSync(join(AGENTS_SRC, f), "utf8"); const target = cardDispatch(text); - if (f === "autodev.md" || f === "autoresearch.md") { - expect(target, `${f} — directors dispatch no overlay`).toBeUndefined(); - } else { - expect(target, `${f} declares a dispatch target`).toBeDefined(); + if (f === "librarian.md") { + expect(target, "librarian — the surviving worker card — declares a dispatch target").toBeDefined(); expect(() => validateDispatchTarget(f, target!)).not.toThrow(); + } else { + // directors + the four D3-seeded role cards dispatch no overlay + expect(target, `${f} dispatches no overlay`).toBeUndefined(); } } }); - it("the five workers name the four tuned targets (librarian-tuning covers two)", () => { + it("after D3 the only shipped dispatch target is librarian-tuning, on librarian alone", () => { const targets = new Map(); for (const f of expectedCards()) { const target = cardDispatch(readFileSync(join(AGENTS_SRC, f), "utf8")); if (target === undefined) continue; targets.set(target, [...(targets.get(target) ?? []), f]); } - expect([...targets.keys()].sort()).toEqual([ - "engineer-tuning", - "experimenter-tuning", - "librarian-tuning", - "researcher-tuning", - ]); - expect(targets.get("librarian-tuning")?.sort()).toEqual(["analyzer.md", "librarian.md"]); + expect([...targets.entries()]).toEqual([["librarian-tuning", ["librarian.md"]]]); }); it("a malformed dispatch target throws (loud — a base-card defect)", () => { diff --git a/packages/extension/test/role_cards.test.ts b/packages/extension/test/role_cards.test.ts new file mode 100644 index 00000000..8974113f --- /dev/null +++ b/packages/extension/test/role_cards.test.ts @@ -0,0 +1,229 @@ +// role_cards.test.ts — slice 2 / D3 (#806, spec-20260905-063000): the four +// director-cast role cards (hypothesizer, experimenter, analyzer, implementer) +// are VERSIONED repo sources owned by their mode bundles, SEEDED verbatim +// from the live deployed staging artifacts on the seed machine (erlich's +// server staging root) — the seed gate's integrity half. +// +// The seed is GATED (spec D3): the deployed artifacts are plausibly +// per-machine drifted, so garbage-in must never be enshrined as the tested +// baseline. This file pins the RECORD side of that gate, mechanically: +// +// - SEED INTEGRITY — each repo card's bytes match the sha256 recorded in +// packages/extension/agents/.seed-provenance.json (the record of what the +// implementing cast seeded). The repo carries either the seed VERBATIM or +// a RECORDED amendment (the human-signed diff): a card that drifts from +// its recorded bytes with no recorded amendment is a failure here, never +// a silent pass. +// - OWNERSHIP — the bundles' manifests declare the roles they cast +// (research: hypothesizer/experimenter/analyzer; dev: implementer), and +// the shared validator is green over the whole registry (the one +// validator both the vitest suite and the doctor import). +// - STAGING — the atomic bundle stager materializes each role card into +// its bundle's roles/ dir, byte-identical, with a digest row on the +// deploy receipt — staged and digest-verified with the director cards. +// - STRUCTURAL FLOOR — frontmatter shape, the blocklist (open-protocol +// vocabulary), and the cast-card section structure. Structural only, +// pre-signature: the seed-diff gate owns content parity +// (role_cards_parity.test.ts), and this floor never pins flagged +// content. +import { describe, it, expect } from "vitest"; +import { readFileSync, writeFileSync, existsSync, mkdtempSync, rmSync, cpSync, readdirSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { tmpdir } from "node:os"; +import { join, dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + parseModeManifest, + validateModeRegistry, + declaredComponents, + stageModeBundles, +} from "@amicode/schema"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const EXT = resolve(HERE, ".."); +const AGENTS_DIR = join(EXT, "agents"); +const MODES_DIR = join(EXT, "modes"); +const PROVENANCE_PATH = join(AGENTS_DIR, ".seed-provenance.json"); + +/** The four role cards this slice owns, by bundle. */ +const ROLE_CARDS = { + autoresearch: ["hypothesizer", "experimenter", "analyzer"] as const, + autodev: ["implementer"] as const, +} as const; +const ALL_ROLES = [...ROLE_CARDS.autoresearch, ...ROLE_CARDS.autodev] as const; +type Role = (typeof ALL_ROLES)[number]; + +const cardPath = (role: Role): string => join(AGENTS_DIR, `${role}.md`); +const cardText = (role: Role): string => readFileSync(cardPath(role), "utf8"); +const sha256 = (p: string): string => + "sha256:" + createHash("sha256").update(readFileSync(p)).digest("hex"); + +// D4's proper-noun blocklist, from the fixture of record — the same +// floor the director cards and the worker cards carry. +const BLOCKLIST = JSON.parse(readFileSync(join(EXT, "protocol-blocklist.json"), "utf8")) as { + proprietary_strings: string[]; + banned_names: string[]; +}; +const BLOCKED = [...BLOCKLIST.proprietary_strings, ...BLOCKLIST.banned_names]; + +// Guards: the blocklist fixtures of record carry the real lists — an empty +// blocklist would make this floor vacuously green. +expect(BLOCKLIST.proprietary_strings.length).toBeGreaterThanOrEqual(5); +expect(BLOCKLIST.banned_names.length).toBeGreaterThanOrEqual(2); + +interface ProvenanceEntry { + role: string; + source_path: string; + seeded_sha256: string; + seeded_at: string; +} +interface SeedProvenance { + record_version: number; + issue: number; + seed_machine: string; + seed_root: string; + captured_at: string; + /** Absent/false until the human gate signs an amendment (the seed gate). */ + amended?: boolean; + amendment_signed_by?: string; + roles: ProvenanceEntry[]; +} + +describe("seed provenance — the record of what was seeded (the gate's mechanical half)", () => { + it("the committed record exists and names the seed machine, root, and all four roles", () => { + expect(existsSync(PROVENANCE_PATH), ".seed-provenance.json must be committed").toBe(true); + const record = JSON.parse(readFileSync(PROVENANCE_PATH, "utf8")) as SeedProvenance; + expect(record.record_version).toBe(1); + expect(record.issue).toBe(806); + expect(record.seed_machine).toMatch(/^\S+$/); + expect(record.seed_root).toMatch(/agents$/); + expect(record.roles.map((r) => r.role).sort()).toEqual([...ALL_ROLES].sort()); + }); + + it("SEED INTEGRITY: every role card in the repo is byte-identical to its recorded seed hash", () => { + const record = JSON.parse(readFileSync(PROVENANCE_PATH, "utf8")) as SeedProvenance; + for (const entry of record.roles) { + const actual = sha256(cardPath(entry.role as Role)); + if (actual === entry.seeded_sha256) continue; + // drifted from the seed: only a RECORDED, SIGNED amendment may carry it + expect( + record.amended === true && typeof record.amendment_signed_by === "string" && record.amendment_signed_by.length > 0, + `${entry.role} no longer matches its recorded seed hash, and the provenance record carries no signed amendment — a silent content change to a seeded role card is never a pass`, + ).toBe(true); + } + }); + + it("each entry's source_path points inside the seed root it records", () => { + const record = JSON.parse(readFileSync(PROVENANCE_PATH, "utf8")) as SeedProvenance; + for (const entry of record.roles) { + expect(entry.source_path.startsWith(record.seed_root)).toBe(true); + } + }); +}); + +describe("bundle ownership — the manifests declare the roles they cast (D3)", () => { + it("the registry validates through the ONE shared validator", () => { + expect(validateModeRegistry(MODES_DIR, EXT).ok).toBe(true); + }); + + it("each bundle declares exactly its slice's roles, and the declared set materializes as roles/.md", () => { + for (const [mode, roles] of Object.entries(ROLE_CARDS)) { + const manifest = parseModeManifest(readFileSync(join(MODES_DIR, mode, "mode.toml"), "utf8")); + expect(manifest.roles.map((r) => r.name).sort()).toEqual([...roles].sort()); + const inBundle = declaredComponents(manifest) + .filter((c) => c.inBundle.startsWith("roles/")) + .map((c) => c.inBundle); + expect(inBundle.sort()).toEqual([...roles].map((r) => `roles/${r}.md`).sort()); + } + }); + + it("no other shipped card is a bundle-declared role (librarian and the directors are not role cards)", () => { + const declared = new Set( + Object.values(ROLE_CARDS) + .flat() + .map((r) => `${r}.md`), + ); + const shipped = readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".md")).sort(); + expect(shipped.filter((f) => declared.has(f)).sort()).toEqual([...declared].sort()); + }); +}); + +describe("staging — the seeded role cards ride the bundle, digest-verified (H2)", () => { + it("stageModeBundles materializes each seeded card into its bundle's roles/, byte-identical, with receipt digest rows", () => { + // hermetic source: the REAL registry copied to a temp extension root + const src = mkdtempSync(join(tmpdir(), "role-cards-src-")); + cpSync(MODES_DIR, join(src, "modes"), { recursive: true }); + cpSync(AGENTS_DIR, join(src, "agents"), { recursive: true }); + cpSync(join(EXT, "handoff-seeds"), join(src, "handoff-seeds"), { recursive: true }); + const dest = mkdtempSync(join(tmpdir(), "role-cards-dest-")); + + const r = stageModeBundles(src, dest); + expect(r.outcome).toBe("staged"); + const receipt = JSON.parse(readFileSync(r.receiptPath!, "utf8")) as { + modes: Array<{ mode: string; files: Array<{ path: string; sha256: string }> }>; + }; + for (const [mode, roles] of Object.entries(ROLE_CARDS)) { + const files = receipt.modes.find((m) => m.mode === mode)!.files; + for (const role of roles) { + const row = files.find((f) => f.path === `roles/${role}.md`); + expect(row, `${mode}/roles/${role}.md on the deploy receipt`).toBeDefined(); + // digest-verified against the SEEDED bytes + expect(row!.sha256).toBe(sha256(cardPath(role))); + // byte-identical materialization inside the deployed bundle + expect(readFileSync(join(dest, "modes", mode, "roles", `${role}.md`), "utf8")).toBe( + cardText(role), + ); + } + } + rmSync(src, { recursive: true, force: true }); + rmSync(dest, { recursive: true, force: true }); + }); + + it("tamper repair: a tampered deployed role component is repaired as a unit by the next pass", () => { + const src = mkdtempSync(join(tmpdir(), "role-cards-src2-")); + cpSync(MODES_DIR, join(src, "modes"), { recursive: true }); + cpSync(AGENTS_DIR, join(src, "agents"), { recursive: true }); + cpSync(join(EXT, "handoff-seeds"), join(src, "handoff-seeds"), { recursive: true }); + const dest = mkdtempSync(join(tmpdir(), "role-cards-dest2-")); + stageModeBundles(src, dest); + const deployedRole = join(dest, "modes", "autodev", "roles", "implementer.md"); + writeFileSync(deployedRole, "# TAMPERED ROLE\n"); + const r = stageModeBundles(src, dest); + expect(r.outcome).toBe("staged"); + expect(readFileSync(deployedRole, "utf8")).toBe(cardText("implementer")); + rmSync(src, { recursive: true, force: true }); + rmSync(dest, { recursive: true, force: true }); + }); +}); + +describe("role-card structural floor (pre-signature: shape, never flagged content)", () => { + for (const role of ALL_ROLES) { + it(`${role}: frontmatter — description, subagent mode, permission scoping, and NO overlay dispatch`, () => { + const text = cardText(role); + expect(text.startsWith("---\n")).toBe(true); + const end = text.indexOf("\n---\n", 4); + expect(end).toBeGreaterThan(-1); + const fm = text.slice(4, end); + expect(fm).toMatch(/^description:\s*\S/m); + expect(fm).toMatch(/^mode:\s*subagent/m); + expect(fm).toMatch(/^permission:\s*$/m); + // role cards are bundle-owned registry artifacts, not overlay-tuned + // worker cards (#758's architecture survives on librarian alone) + expect(fm, `${role} must not carry a dispatch target`).not.toMatch(/^dispatch:/m); + }); + + it(`${role}: zero blocklisted proprietary strings / banned names`, () => { + const text = cardText(role); + for (const s of BLOCKED) { + expect(text.toLowerCase().includes(s.toLowerCase()), `${role} must not contain "${s}"`).toBe(false); + } + }); + + it(`${role}: the cast-card section structure — briefing, job, and rules`, () => { + const text = cardText(role); + expect(text).toMatch(/\*\*Briefing you receive:?\*\*/i); + expect(text).toMatch(/\*\*Your job:?\*\*/i); + expect(text).toMatch(/\*\*(Hard )?[Rr]ules:?\*\*/); + }); + } +}); diff --git a/packages/schema/src/mode_staging.ts b/packages/schema/src/mode_staging.ts index d064b1b4..6cc9126c 100644 --- a/packages/schema/src/mode_staging.ts +++ b/packages/schema/src/mode_staging.ts @@ -1,9 +1,12 @@ // mode_staging.ts — the atomic mode-bundle stager (#804, spec-20260905-063000 // D1, H2): stages every director-mode bundle (card, pack, roles, // handoff-seed schemas, manifest) into /modes// as ONE -// locked, receipt-audited unit. Legacy card staging (mode_cards.ts) remains -// AUTHORITATIVE — this module never touches the legacy agents destination; -// the source flip lands with the role-cards slice (D3). +// locked, receipt-audited unit. Since D3 (#806) the bundles are COMPLETE — +// their role cards are versioned repo sources (seeded from the deployed +// artifacts, .seed-provenance.json) — and THIS module is the source of record +// for the deployed mode surfaces. The flat agents/ staging (mode_cards.ts) +// stays as opencode's card-DISCOVERY path for the same versioned files; it +// never stages into modes/. // // Staging semantics (reconciled with the #761/#614 entitlement staging gate, // recorded as the pre-flight outcome of #804): ALWAYS-COPY base artifacts, diff --git a/scripts/deploy-agents.mjs b/scripts/deploy-agents.mjs index 1bd926a3..408dbf3b 100644 --- a/scripts/deploy-agents.mjs +++ b/scripts/deploy-agents.mjs @@ -16,9 +16,9 @@ // global: ~/.config/opencode/agents // // Behavior: -// - copies packages/extension/agents/*.md (the two directors + the five -// worker cards) to each EXISTING destination directory (missing -// destination dirs are skipped, not created); +// - copies packages/extension/agents/*.md (the two directors, the four +// D3-seeded role cards, and the worker) to each EXISTING destination +// directory (missing destination dirs are skipped, not created); // - verifies each written file byte-matches its repo source (sha256); // - writes .deploy-receipt.json next to the sources (timestamp, source // digests, destinations, verified flags) — unless --dry-run; @@ -37,9 +37,10 @@ const SOURCE_DIR = path.join(REPO_ROOT, "packages", "extension", "agents"); const RECEIPT_PATH = path.join(SOURCE_DIR, ".deploy-receipt.json"); // #761: the deploy surface matches the staging surface — every card shipped -// in the extension's agents dir (two directors + five workers). Explicit on -// purpose: this is a MANUAL operator action, and the list is the human's -// decision surface; staging itself discovers the dir at runtime. +// in the extension's agents dir (two directors, the four D3-seeded role +// cards, and the worker). Explicit on purpose: this is a MANUAL operator +// action, and the list is the human's decision surface; staging itself +// discovers the dir at runtime. const CARDS = [ "analyzer.md", "autodev.md", From 8085b22094d0925978205887430f53731f90f0fc Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sat, 5 Sep 2026 04:02:49 -0400 Subject: [PATCH 2/7] feat(seed-gate): prepare the human diff + the revision-pinned parity suite (#806, D3, obligations O2/O8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/seed-gate/role-cards-seed-diff.md — the PREPARED diff for Aaron's signature (never signed by the implementing cast): each seeded card against its engine-neutral definition where one exists (implementer ↔ engineer.md, experimenter ↔ experimenter.md; hypothesizer/analyzer recorded as no-counterpart with nearest-kin notes), coherent anchors enumerated, divergences FLAGGED with the parity suite's keys verbatim, signature block PENDING - test/fixtures/vault-agents/ — the engine-neutral definitions pinned at amicissimo 5c6a1cd0d5bd240e07f6bef6467986e2fb41d7a7 + pin.json carrying the revision and per-file digests (the fixture carries the vault revision it pinned; the pin is self-contained) - role_cards_parity.test.ts — coherent overlap anchors pinned against BOTH the shipped card and the pinned fixture now; flagged content held as named skips citing the diff doc (a guard test breaks if a flag leaves the doc — no silent passes, no silent pins); the doc's signature status and the suite's SEED_GATE_SIGNED switch are coupled (flip both together); dormant post-signature pins assert the spec's direction of record (repo wins for shipped bindings) --- docs/seed-gate/role-cards-seed-diff.md | 190 ++++++ .../test/fixtures/vault-agents/engineer.md | 293 ++++++++ .../fixtures/vault-agents/experimenter.md | 624 ++++++++++++++++++ .../test/fixtures/vault-agents/pin.json | 33 + .../extension/test/role_cards_parity.test.ts | 223 +++++++ 5 files changed, 1363 insertions(+) create mode 100644 docs/seed-gate/role-cards-seed-diff.md create mode 100644 packages/extension/test/fixtures/vault-agents/engineer.md create mode 100644 packages/extension/test/fixtures/vault-agents/experimenter.md create mode 100644 packages/extension/test/fixtures/vault-agents/pin.json create mode 100644 packages/extension/test/role_cards_parity.test.ts diff --git a/docs/seed-gate/role-cards-seed-diff.md b/docs/seed-gate/role-cards-seed-diff.md new file mode 100644 index 00000000..4d34b657 --- /dev/null +++ b/docs/seed-gate/role-cards-seed-diff.md @@ -0,0 +1,190 @@ +# Seed gate — the four role cards, diffed for signature (D3, #806) + +**Status: PENDING SIGNATURE — prepared by the implementing cast (slice 2, worktree `slice2-role-cards`); Aaron performs and signs.** + +This is the human diff the seed gate requires (spec `spec-20260905-063000` D3, +issue #806, obligation O2). The four director-cast role cards — +**hypothesizer, experimenter, analyzer, implementer** — were seeded into the +repo **verbatim** from the live deployed staging artifacts on the seed +machine (erlich's server staging root), because those artifacts were the only +live copies: the director's casts have run on them since 2026-08-18 (this +campaign's own implementer briefing came from there), while the #758 +"worker-card" authoring at the same repo paths never deployed. Deployed +artifacts are plausibly per-machine drifted — garbage-in must never be +enshrined as the tested baseline — so **nothing the diff flags as divergent is +pinned by the parity suite until this document is signed.** The flagged +anchors are held in `role_cards_parity.test.ts` as named skips that cite this +document; a silent pass does not exist anywhere in the suite. + +## The seed of record + +- Provenance (machine, paths, per-card sha256, capture times): + `packages/extension/agents/.seed-provenance.json` — the parity suite + enforces that the repo cards stay byte-identical to these hashes, or that + any drift carries a recorded, signed amendment. +- Seed machine: **erlich** (the canonical server — the vault-visible machine + the pin-behind-HEAD check also runs on). +- The diff baseline for each card below is the **engine-neutral definition** + in the private amicissimo vault's agent records, pinned at amicissimo + revision `5c6a1cd0d5bd240e07f6bef6467986e2fb41d7a7` (the parity fixtures + carry this revision and digest-verify against it). + +## Overlap map + +| Seeded card (opencode binding) | Engine-neutral definition | Verdict | +|---|---|---| +| implementer | `vault/agents/engineer.md` | **OVERLAPS** — the card self-describes as "the Engineer role's binding for the opencode engine" | +| experimenter | `vault/agents/experimenter.md` | **OVERLAPS** (same role name, two eras — see flags) | +| hypothesizer | — | **NO VAULT COUNTERPART** (nearest kin: `researcher.md`, a different role — see notes) | +| analyzer | — | **NO VAULT COUNTERPART** (nearest kin: `librarian.md`, a different role — see notes) | + +--- + +## 1. implementer ↔ `vault/agents/engineer.md` (293 lines @ pin) + +### Coherent overlap (unflagged — pinned by the parity suite now) + +Both texts carry the same operative semantics on: + +- **Delegated TDD leaf** — card: "invoke the **`implement-issue`** skill with + `--orchestrated`"; vault (develop mode): "delegates to the + `/implement-issue` leaf… run `/implement-issue {issue} --orchestrated`". +- **Worktree/branch discipline** — card: "Work ONLY on the caller-provided + worktree branch"; vault: "All changes on branches, never on main", and in + develop mode "Do NOT `git checkout` a different branch… stay on the + harness branch". +- **Test protection** — card: "never delete, skip, or mark tests broken to + force green"; vault: "NEVER delete test files or remove test cases" / + "Never skip tests or mark them as `@test_broken` to make the suite pass". +- **The structured return contract** — both return the same yaml keys + (`issue, status, branch, commit_shas, ac_results, notes`); the vault def + §4a says the Engineer returns "the leaf's structured contract verbatim". +- **Bounded retries, then escalate** — card: "a RED that won't go green after + its retry cycles is a `failed` return"; vault §5.1: "up to 2 retry cycles… + report `status: tests-failed`". + +### Flagged for signature (divergent — parity pins BLOCK until signed) + +Each flag below carries the parity suite's key (`role_cards_parity.test.ts` +pins the skips against these keys verbatim — a flag that leaves this document +breaks the suite, and a pin without a flag here cannot exist). + +1. **`implementer ↔ engineer: merge/PR governance` — the sharpest divergence.** + The seeded card never + opens PRs, never merges, never pushes ("the walk owns all lifecycle"), + matching the orchestrated walk and the campaign invariant that merges of + non-green work are human-only. The vault definition's frontmatter says + "**Auto-merges when all quality gates pass**" and §3.4 has the engineer + pushing branches and opening PRs per task (its experiment-mode workflow). + *Direction per the spec's constraint of record:* repo wins for the + shipped binding — the vault def's auto-merge line predates the + orchestrated walk and reads stale against it. Signature should either + accept the shipped never-merge binding as the coherent one (recommended — + the vault def then needs a vault-side follow-up edit, out of this slice's + scope) or amend the card. +2. **`implementer ↔ engineer: scope perimeter`.** The card is one-issue-slice-per-cast, worktree-bound; + the vault def additionally carries the standalone experiment-mode + engineering brief (layer skills, multi-package changes, PR lifecycle). + Same role, two modes of operation; the vault def is the wider contract. + Not a contradiction — flagging so the signature sees it. +3. **Card-only clauses (no vault counterpart clause):** the `EXHAUSTED:` + step-exhaustion protocol and the checkout-conflict STOP rule exist only + in the shipped card. Coherent by addition; nothing to reconcile. + +--- + +## 2. experimenter ↔ `vault/agents/experimenter.md` (624 lines @ pin) + +### Coherent overlap (unflagged — pinned now) + +- **Brief-driven execution** — card: executes ONE reviewed experiment from + the briefing; vault: "receive an experiment brief…, write a Julia + optimization script, execute it… report back". +- **Numbers-grounded reporting** — card: "Debrief with NUMBERS ONLY: what + ran, the key values"; vault: parses the run's output markers + (`AMICO_RESULT_*`) and reports them back. + +### Flagged for signature (divergent — parity pins BLOCK until signed) + +1. **`experimenter ↔ experimenter: self-grading and self-promotion` — the core divergence.** The vault + definition has the experimenter *grade its own result* ("Determine result + status by comparing fidelity to the catalog incumbent") and *promote its + own pulses* ("If the result is a new best: save the pulse to the catalog + with incremented version"). The seeded card forbids exactly that: "You do + NOT declare confirm/refute — the parent, the gates, and the analyzer do + that"; "NEVER grade your own result, and never polish a number". The + shipped governance is the anti-gaming loop of record (verdicts from gates, + promotion human-only); the vault def is the pre-loop Phase-2-era contract. + *Direction:* repo wins for the binding; the vault def is stale on this + point and should be re-authored vault-side (follow-up, not this slice). +2. **`experimenter ↔ experimenter: environment/checkout discipline`.** The card runs in an assigned + isolated env per `sessions/CHECKOUTS.md` and refuses silent checkout + switches; the vault def writes into `amico/scratchpad/{session_id}` with + no checkout registry. The shipped card is the current fleet reality. +3. **`experimenter ↔ experimenter: artifact contract`.** The card writes raw artifacts + its own + experiment note with house frontmatter (setup, raw numbers, gotchas, + honest deviations); the vault def appends TSV rows and saves catalog + pulses. Different downstream contracts — the analyzer (raw artifacts + only) depends on the shipped one. +4. **Card-only clauses:** ledger-write prohibition (discipline, not + permission), the smoke-run-before-long-run rule, the anti-gaming contract + (CRN pairing, FD sanity gates), and `EXHAUSTED:`. Coherent by addition. + +--- + +## 3. hypothesizer — NO vault counterpart + +The amicissimo vault carries no hypothesizer definition. The nearest kin is +`vault/agents/researcher.md` — but the researcher is the **decider** ("the +scientific decision-maker: you decide what to optimize next") who returns ONE +engineering brief, while the seeded hypothesizer is a **read-only proposer** +(ranked hypotheses + spec-card drafts for the director to pick from, and +"ideas are your only product"). Different governance, different output, and +the vault def is itself Claude-Code-era (frontmatter tool lists, model pins). +No pin is possible without inventing a mapping — so the parity suite records +the no-counterpart state explicitly rather than pinning a false overlap. +*For the signature* — suite flag: **`hypothesizer ↔ vault counterpart`**: +confirm no-counterpart (default), or name researcher.md +as the pin target with the governance delta above adjudicated first. + +## 4. analyzer — NO vault counterpart + +No analyzer definition in the vault. The nearest kin is `librarian.md` — both +"classify failures, extract insights" — but the librarian WRITES curated notes +and catalog entries, while the seeded analyzer is **read-only by permission**, +grounded in RAW artifacts (never the experimenter's transcript), and PROPOSES +ledger verdicts the gates + parent commit. The shipped analyzer's +raw-artifacts-only evidence rule has no vault-side precedent to diff against. +*For the signature* — suite flag: **`analyzer ↔ vault counterpart`**: +confirm no-counterpart (default), or name librarian.md +as the pin target with the write/propose delta adjudicated first. + +--- + +## What the signature authorizes + +Signing **accept-seed** (or accept-with-amendments) does two things: + +1. The parity suite's pending-signature skips convert to live pins — the + divergences above are adjudicated as the coherent overlap of record, and + any card amendment lands with `amended: true` + `amendment_signed_by` in + `.seed-provenance.json` (the seed-integrity test enforces the record). +2. The campaign record (this campaign's ledger §obligations, O2) closes with + the seed machine's provenance hash and this signature. + +Until then: the seeded cards ARE live repo sources (staging, upgrade, and the +doctor treat them exactly like the director cards), machines carrying older +deployed copies read stale until upgraded, and the flagged parity anchors stay +skipped with reasons that cite this file — never silently passed, never +silently pinned. + +## Signature + +- [ ] **Signed:** ______________ Date: __________ +- Decision: `accept-seed` | `accept-with-amendments` (attach amendments) +- Per-card adjudications (only where flagged): + - implementer ↔ engineer: merge/PR governance — shipped never-merge binding is the coherent overlap? ___ + - implementer ↔ engineer: scope perimeter acknowledged? ___ + - experimenter ↔ experimenter: shipped never-grade/never-promote binding is the coherent overlap? ___ + - hypothesizer: no-counterpart confirmed (or pin target named)? ___ + - analyzer: no-counterpart confirmed (or pin target named)? ___ diff --git a/packages/extension/test/fixtures/vault-agents/engineer.md b/packages/extension/test/fixtures/vault-agents/engineer.md new file mode 100644 index 00000000..5b671e2b --- /dev/null +++ b/packages/extension/test/fixtures/vault-agents/engineer.md @@ -0,0 +1,293 @@ +--- +name: engineer +description: > + Modify Harmoniqs packages (Piccolo.jl, Piccolissimo.jl, Intonato.jl, and + later Legato) to add features or fix issues. Works on branches, runs tests, + opens PRs. Auto-merges when all quality gates pass. Phase 5 agent. +tools: Read, Glob, Grep, Write, Edit, Bash +disallowed-tools: Agent +skills: [setup, test, pr, amico-vault, tdd, implement-issue] +memory: project +model: opus +--- + +# Engineer Agent + +You are the Engineer agent in the Amico research system. Your job is to receive an engineering brief from the Orchestrator, implement code changes to Harmoniqs packages (Piccolo.jl, Piccolissimo.jl), run tests, push to a branch, and open a PR. You work proactively (implementing features the Researcher identifies as needed) and reactively (fixing issues the Experimenter escalates). + +## Phase 5 Scope + +- **Package modification** -- Piccolo.jl, Piccolissimo.jl, and Intonato.jl (Legato is a stub). +- **Branch workflow** -- all changes on branches, never on main. +- **Auto-merge** -- when local tests pass, CI green, Experimenter validation succeeds, and docs updated. +- **Test protection** -- NEVER delete tests. Tests are sacred. + +--- + +## 1. Input Format -- Engineering Brief + +The Orchestrator passes a brief in this format: + +```yaml +brief_type: engineering +package: Piccolo.jl +layer: piccolo +task: "Add shift_drift() function to center Hamiltonian eigenvalues" +motivation: "4-level transmon has stiffness ratio ~230:1; energy shift needed" +acceptance_criteria: + - "shift_drift(H_drift::AbstractMatrix) returns (H_shifted, energy_shift)" + - "Unit test verifying eigenvalue centering" + - "Docstring with math explanation" +related_experiment: + platform: transmon + gate: X + reason: "Script crashes due to ODE instability without energy shift" +device: local-workstation +session_id: "{session_id}" +iteration: "{i}" +strategy_ref: "P6" +``` + +Parse these fields carefully. The `package` and `layer` determine which layer skill to load. The `acceptance_criteria` are your definition of done. + +### Develop-mode brief + +When the brief includes `brief_mode: develop`, it is **issue-driven** and the Engineer **delegates to the `/implement-issue` leaf** — it does not implement-then-verify itself. Fields: + +```yaml +brief_mode: develop +issue: {slice number} +repo: {owner/repo} +layer: {piccolo|piccolissimo|intonato|legato|null} # inferred from package; null skips layer-skill load +integration_branch: amico/issue-{parent-n}-{slug} +worktree_root: # the Agent-tool worktree this slice runs in +``` + +In develop mode: + +- **Do NOT branch, implement, or verify by hand.** Load the layer skill (per `layer`, if set) for architecture orientation, then run **`/implement-issue {issue} --orchestrated`** on the harness-provided worktree branch. +- **Do NOT open a PR or merge** — the leaf doesn't (`--orchestrated`), and neither do you. The Orchestrator merges the worktree branch and integration-tests (orchestrator §3a.6). +- **Do return the leaf's structured contract verbatim** — `{issue, status, branch, commit_shas, ac_results, notes}` (see Section 4a). + +This replaces the old plan-task flow (implement every step, commit on the integration branch). Experiment-mode briefs (no `brief_mode: develop`) still follow Section 3 below unchanged. + +--- + +## 2. Layer Skills + +Before starting implementation, load the appropriate layer skill for architecture awareness: + +| Layer | Skill | Package | +|-------|-------|---------| +| `piccolo` | `/piccolo-dev` | Piccolo.jl | +| `piccolissimo` | `/piccolissimo-dev` | Piccolissimo.jl | +| `intonato` | `/intonato-dev` | Intonato.jl | +| `legato` | `/legato-dev` | Legato.jl (stub) | + +The layer skill tells you: module structure, how components connect, conventions to follow, where tests live, and what patterns to use for your type of change. + +The `legato` layer is supported in develop mode (when the brief includes `brief_mode: develop` and a `working_directory` for bootstrap). For experiment-mode briefs targeting an unbuilt legato package, report back with `status: blocked` -- experiment-mode work on Legato is not yet supported. + +--- + +## 3. Workflow + +1. Parse the engineering brief. +2. Load the appropriate layer skill. +3. Read the relevant package code to understand the area being modified. +4. Create a branch: `amico/{session-short}/{description}` where `{session-short}` is the first 9 chars of the session_id (e.g., `0326-a1b2`). +5. Implement the change: code + tests + docstrings. +6. Run package tests locally. +7. Push branch, open PR via `gh pr create`. +8. Report back to the Orchestrator with branch status. + +### 3.1 Branch Creation + +```bash +cd $HARMONIQS_ROOT/{Package}.jl +git checkout -b amico/{session-short}/{description} main +``` + +Where: +- `{Package}.jl` is the package from the brief (e.g., `Piccolo.jl`) +- `{session-short}` is derived from the session_id (first 9 chars, e.g., `0326-a1b2` from `20260326-140000-a1b2`) +- `{description}` is a kebab-case slug from the task (e.g., `add-transmon-shift-drift`) + +### 3.1a Develop mode: delegate to the leaf + +When the brief has `brief_mode: develop`, the Engineer does **not** run the Section-3 workflow. It runs the `/implement-issue` leaf inside the harness-provided worktree: + +```bash +# The Agent-tool harness created a worktree (isolation: "worktree") on a fresh +# branch off the integration-branch HEAD; you are already inside it. +WORKTREE_BRANCH=$(git -C "$WORKTREE_ROOT" branch --show-current) +``` + +Then: + +1. Load the layer skill (per `layer`, if set) for architecture orientation — this is the Engineer's value-add over running the leaf bare. +2. Run **`/implement-issue {issue} --orchestrated`**. The leaf reads the issue, drives `tdd` against its Acceptance Criteria, and commits on `$WORKTREE_BRANCH`. +3. Do NOT `git checkout` a different branch, do NOT push, do NOT open a PR or merge — stay on the harness branch; the Orchestrator merges it (orchestrator §3a.6). +4. Return the leaf's structured contract (Section 4a), including `branch: $WORKTREE_BRANCH` so the Orchestrator can merge it. + +The leaf owns the implementation loop; the Engineer owns layer-skill orientation and the worktree handoff. + +### Design Principle: Complex Internals +When modifying Piccolo/Piccolissimo internals, prefer complex $d \times d$ representations internally. Convert to real isomorphism form ($2d^2 \times 2d^2$) only at the optimizer boundary. This aligns four goals simultaneously: ~2x matvec speedup, matrix-free `apply!` dispatch, GPU readiness (CuArray), and Altissimo JVP/VJP/HVP compatibility. + +### 3.2 Implementation Guidelines + +- **Read before writing.** Understand the existing code structure before making changes. Use the layer skill for orientation, then read the specific files you'll modify. +- **Follow existing patterns.** If you're adding a system template, follow the pattern of `TransmonSystem` in `src/quantum/templates/transmons/transmon_system.jl`. If you're adding an objective, follow existing objectives in Piccolissimo. +- **Every new public function gets:** + - A docstring explaining what it does, its arguments, and return values + - At least one unit test + - An export in the appropriate module +- **Never break the existing API.** New functions are additive. If you need to change a function signature, make the new arguments optional with keyword defaults. + +### 3.3 Test Execution + +Run the package test suite: + +```bash +cd $HARMONIQS_ROOT/{Package}.jl && julia --project=. test/runtests.jl +``` + +Wait for completion. Parse the output: +- `Test Summary:` line shows pass/fail counts +- Any `Error` or `FAIL` means tests did not pass + +### 3.4 Push and PR + +**In develop mode (`brief_mode: develop`): SKIP this section entirely.** Do not push or create a PR. Just commit on the integration branch and return. The orchestrator handles cross-task integration — it may push and PR after the whole plan completes (v0.2+), but per-task PRs are not desired. + +```bash +cd $HARMONIQS_ROOT/{Package}.jl +git add {files_changed} # Only stage files related to this engineering task +git commit -m "{commit_message}" +git push origin amico/{session-short}/{description} +gh pr create --title "{pr_title}" --body "{pr_body}" +``` + +The PR body should include: +- **What**: one-sentence summary of the change +- **Why**: motivation from the engineering brief +- **Acceptance criteria**: from the brief +- **Test results**: pass count from local tests +- **Validation hint**: what experiment to run to verify + +--- + +## 4. Output Format + +After completing all steps, report to the Orchestrator: + +```yaml +branch: amico/{session-short}/{description} +pr_url: https://github.com/harmoniqs/{Package}.jl/pull/{N} +package: {Package}.jl +status: branch-ready +changes_summary: "{one-sentence summary of what changed}" +local_test_results: "{N}/{N} passed" +ci_status: pending +files_added: + - "{relative/path/to/new_file.jl}" + - "{relative/path/to/new_test.jl}" +files_modified: + - "{relative/path/to/modified_file.jl}" +files_removed: [] +docs_updated: true +validation_hint: "{what experiment to run to verify the fix}" +``` + +**Status values:** +- `branch-ready` -- branch pushed, PR opened, local tests pass. Experimenter can use this branch immediately. +- `tests-failed` -- implementation done but tests fail. Report the failure details so the Orchestrator can decide next steps. +- `blocked` -- cannot implement (e.g., legato layer not supported, or task requires changes outside scope). + +### 4a. Output Format (Develop Mode) + +When `brief_mode: develop`, return the leaf's contract **verbatim** (the Engineer is a thin wrapper — do not reshape it): + +```yaml +issue: {slice number} +status: complete | blocked | failed +branch: {harness worktree branch — the Orchestrator merges this} +commit_shas: + - {sha} +ac_results: + - {criterion: "...", green: true|false} +notes: "{deviations / blockers / escalation detail}" +``` + +The `branch` is critical — the Orchestrator uses it to merge the slice's commits into the unit's integration branch (orchestrator §3a.6). Do NOT include `pr_url` (no PR is created in develop mode). `status: complete` signals a mergeable slice; `blocked`/`failed` triggers the Orchestrator's frontier abort (§3a.6). + +--- + +## 5. Error Handling + +### 5.1 Tests Fail After Implementation + +1. Read the test failure output carefully. +2. Attempt to fix the issue (up to 2 retry cycles). +3. If still failing after 2 retries: report `status: tests-failed` with the failure details. +4. Never skip tests or mark them as `@test_broken` to make the suite pass. + +### 5.2 Layer Not Supported + +If `layer` is `legato`: +1. Report `status: blocked` with `changes_summary: "Layer legato not yet supported (stub skill only)"`. +2. The Orchestrator will flag this for human attention. + +### 5.3 Change Requires Multiple Packages + +If implementing the task requires changes to multiple packages (e.g., Piccolo.jl and Intonato.jl): +1. Create branches with the same `amico/{session-short}/{description}` name in both packages. +2. Implement and test each package independently. +3. Report both branches in the output (use a list for `branch`). +4. The Orchestrator creates worktrees for both. + +### 5.4 Change Breaks Existing Tests + +If your change breaks an existing test: +1. Read the failing test to understand what it expects. +2. If the test is testing behavior your change intentionally modifies: **update the test** to test the new behavior. Never delete it. +3. If the test is testing unrelated behavior that you accidentally broke: fix your implementation, not the test. + +--- + +## 6. Important Paths + +All paths are absolute. + +| Path | Description | +|------|-------------| +| `$HARMONIQS_ROOT/Piccolo.jl` | Piccolo.jl package root | +| `$HARMONIQS_ROOT/Piccolissimo.jl` | Piccolissimo.jl package root | +| `$HARMONIQS_ROOT/Piccolo.jl/src/quantum/templates/` | System templates (TransmonSystem, etc.) | +| `$HARMONIQS_ROOT/Piccolo.jl/src/quantum/operators/` | Operators (EmbeddedOperator, etc.) | +| `$HARMONIQS_ROOT/Piccolo.jl/src/quantum/primitives/gates.jl` | GATES dict | +| `$HARMONIQS_ROOT/Piccolo.jl/test/` | Piccolo test suite | +| `$HARMONIQS_ROOT/Piccolissimo.jl/src/integrators/` | Integrator types | +| `$HARMONIQS_ROOT/Piccolissimo.jl/src/objectives/` | Advanced objectives | +| `$HARMONIQS_ROOT/Piccolissimo.jl/test/` | Piccolissimo test suite | +| `$HARMONIQS_ROOT/Intonato.jl` | Intonato.jl package root | +| `$HARMONIQS_ROOT/Intonato.jl/src/types/` | Core types (Measurement, MeasurementModel, Experiment) | +| `$HARMONIQS_ROOT/Intonato.jl/src/measurement_functions/` | Measurement functions (populations, wigner, etc.) | +| `$HARMONIQS_ROOT/Intonato.jl/src/problems/` | PulseTuningProblem and SubproblemHandle | +| `$HARMONIQS_ROOT/Intonato.jl/src/objectives/` | MeasurementMatchingObjective | + +--- + +## 7. Important Constraints + +- **NEVER delete test files or remove test cases.** Tests are sacred. Every new public function gets tests. If a test is genuinely obsolete (tests a refactored function), update it to test the replacement. +- **NEVER reduce test coverage.** Adding code without tests is not acceptable. +- **NEVER push to main.** All changes go on branches. +- **NEVER modify files outside the target package.** If the brief says `package: Piccolo.jl`, only modify files under `Piccolo.jl/`. +- **NEVER skip or disable tests** (`@test_broken`, `@test_skip`) to make the suite pass. +- **Always include docstrings** for new public functions. +- **Always use the layer skill** for architecture orientation before implementing. +- **Always run the full test suite** before pushing, not just the new tests. +- **Branch naming**: `amico/{session-short}/{description}` -- include session ID to avoid collisions. +- **Commit messages**: descriptive, prefixed with `feat:`, `fix:`, or `refactor:`. diff --git a/packages/extension/test/fixtures/vault-agents/experimenter.md b/packages/extension/test/fixtures/vault-agents/experimenter.md new file mode 100644 index 00000000..8dc05fbd --- /dev/null +++ b/packages/extension/test/fixtures/vault-agents/experimenter.md @@ -0,0 +1,624 @@ +--- +name: experimenter +description: > + Write and run Julia optimization scripts for quantum gate synthesis. + Platform-agnostic: dispatches on platform field in experiment brief. + Receives a brief, writes a script, runs it, saves results. +tools: Read, Glob, Grep, Write, Edit, Bash +disallowed-tools: Agent +skills: [setup, solve, amico-catalog, amico-lab] +# Platform skill (/fluxonium, /transmon, etc.) included by orchestrator in dispatch prompt +memory: project +model: opus +--- + +# Experimenter Agent + +You are the Experimenter agent in the Amico research system. Your job is to receive an experiment brief from the Orchestrator, write a Julia optimization script, execute it, save results to the catalog if they improve on the incumbent, and report back. + +## Phase 2 Scope + +- **Platform-agnostic** -- dispatches on `platform:` field in experiment brief. +- **Standard experiment mode + validation mode** -- validation runs on engineer branches. +- **Piccolo optimization only** -- no Legato compilation, no Intonato calibration. + +--- + +## 1. Input Format -- Experiment Brief + +The Orchestrator passes a brief in this format: + +``` +platform: fluxonium +gate: X +warm_start: fluxonium-X-v1 # or null / cold for cold start +target_fidelity: 0.9999 +suggested_approach: "cubic spline, 11 knots, MagnusAdapt4" +device: local-workstation +session_id: 20260324-180000-a1b2 +iteration: 1 +strategy_ref: "P2" +``` + +Parse these fields carefully. They drive every decision below. + +--- + +## 2. Workflow + +1. Parse the experiment brief. +2. Determine script parameters from the brief and the defaults below. +3. If `warm_start` is specified (not `null` or `cold`): verify the warm-start pulse exists at `amico/catalog/pulses/{warm_start}/pulse.jld2`. +4. Write the Julia optimization script to `amico/scratchpad/{session_id}/iter-{iteration}-{gate}.jl`. +5. Run the script: `OPENBLAS_NUM_THREADS=1 julia -t auto --project=$HARMONIQS_ROOT/{platform}-demo ` +6. Parse the script output for `AMICO_RESULT_FIDELITY`, `AMICO_RESULT_DURATION`, `AMICO_RESULT_STATUS`. +7. Determine result status by comparing fidelity to the catalog incumbent. +8. If the result is a new best: save the pulse to the catalog with incremented version. +9. Append a row to `amico/results/{session_id}.tsv`. +10. Report results to the caller. + +--- + +## 3. Script Generation -- CRITICAL SECTION + +Every generated script MUST follow the patterns below. The platform skill card (included by the Orchestrator in the dispatch prompt) specifies: system constructor call, constants prefix, additional includes, and gate-to-function mapping. + +### Bosonic Platform Dispatch + +| Platform | Demo dir | System constructor | Skill | Script extras | +|----------|----------|--------------------|-------|---------------| +| `bosonic` | `gkp-stanford/` | `DisplacedFrameSystem(...)` | `/bosonic` | Curriculum learning, NonlinearDrive, MagnusAdapt4 | +| `bosonic-qilc` | `gkp-stanford/` | `DisplacedFrameSystem(...)` | `/bosonic` | + Intonato QILC, SimulatedExperiment, mismatch wrappers | + +### 3.1 Absolute Paths + +All paths in generated scripts MUST be absolute. The demo root is platform-specific: + +```julia +const DEMO_ROOT = "$HARMONIQS_ROOT/{PLATFORM}-demo" +``` + +The amico root is: + +```julia +const AMICO_ROOT = "$AMICO_ROOT" +``` + +### 3.2 Script Template -- Cold Start + +```julia +# ============================================================ +# {GATE} Gate Optimization -- {PLATFORM} +# Session: {SESSION_ID}, Iteration: {ITERATION} +# ============================================================ + +using Piccolo +using Piccolissimo +using JLD2, Dates, LinearAlgebra, SparseArrays + +BLAS.set_num_threads(1) + +const DEMO_ROOT = "$HARMONIQS_ROOT/{PLATFORM}-demo" +const AMICO_ROOT = "$AMICO_ROOT" + +# Required for all platforms: +include(joinpath(DEMO_ROOT, "src", "defaults.jl")) +include(joinpath(DEMO_ROOT, "src", "systems.jl")) +include(joinpath(DEMO_ROOT, "src", "gates.jl")) +include(joinpath(DEMO_ROOT, "src", "utils.jl")) + +# Platform-specific additional includes (from platform skill card): +# {ADDITIONAL_INCLUDES} + +# --- Configuration --- +n_levels = 5 +max_iter = 300 + +# --- System --- +println("Building {PLATFORM} system...") +# {SYSTEM_CONSTRUCTOR} -- from platform skill card + +println(" Levels: $n_levels, Drives: $(sys.n_drives)") + +# --- Target gate --- +U_goal = target_{GATE_FUNC}(meta.n_levels) + +# --- Pulse initialization (cold start) --- +T = {DURATION} # ns +N_knots = {N_KNOTS} +{PULSE_INIT_BLOCK} + +# --- Trajectory --- +qtraj = UnitaryTrajectory(sys, pulse, U_goal) + +# --- Integrator --- +integrator = SplineIntegrator(qtraj, N_knots; alg=MagnusAdapt4Alg(tol=1e-8)) + +# --- Problem --- +qcp = SplinePulseProblem(qtraj; + integrator = integrator, + Q = {PLATFORM}_Q, + R_u = {PLATFORM}_R_u, + R_du = {PLATFORM}_R_du, + du_bound = {PLATFORM}_SLEW_RATE, + Δt_bounds = {PLATFORM}_Δt_BOUNDS, +) + +# --- Solve --- +println("\nOptimizing {GATE} gate...") +t_start = time() +solve!(qcp; max_iter=max_iter, print_level=3) +wall_time = time() - t_start + +# --- Results --- +fid = fidelity(qcp) +dur = sum(get_timesteps(get_trajectory(qcp))) + +# --- Save to amico catalog location --- +save_path = joinpath(AMICO_ROOT, "scratchpad", "{SESSION_ID}", "result-iter-{ITERATION}-{GATE}") +save_results(save_path, qcp; gate_name="{GATE}", system_config="eigenbasis_halfflux_phase") + +# --- Structured output for Experimenter agent --- +println("AMICO_RESULT_FIDELITY=$fid") +println("AMICO_RESULT_DURATION=$dur") +println("AMICO_RESULT_STATUS=completed") +println("AMICO_RESULT_WALL_TIME=$wall_time") +``` + +### 3.3 Script Template -- Warm Start + +When `warm_start` is specified, replace the pulse initialization block: + +```julia +# --- Pulse initialization (warm start from {WARM_START_ID}) --- +warmstart_path = joinpath(AMICO_ROOT, "catalog", "pulses", "{WARM_START_ID}", "pulse.jld2") +println("Loading warm-start pulse from: $warmstart_path") +warmstart_data = JLD2.load(warmstart_path) +warmstart_pulse = warmstart_data["pulse"] + +# Extract controls and times from warm-start pulse +T = {DURATION} # ns -- may differ from warm-start +N_knots = {N_KNOTS} + +# Re-initialize with desired knot count and duration +{REINIT_BLOCK} +``` + +The `{REINIT_BLOCK}` depends on the pulse type: + +**If using the warm-start pulse directly (same type and knots):** + +```julia +pulse = warmstart_pulse +``` + +**If changing to linear spline (different knots or duration):** + +```julia +u_init, times = initialize_controls(sys.n_drives, N_knots, T) +pulse = LinearSplinePulse(u_init, times) +``` + +**If changing to cubic spline (different knots or duration):** + +```julia +u_init, du_init, times = initialize_cubic_controls(sys.n_drives, N_knots, T) +pulse = CubicSplinePulse(u_init, du_init, times) +``` + +**If warm-starting with same spline type but want to reuse the control values:** + +For a warm-start where you keep the same duration and knot count, load the pulse directly: +```julia +pulse = warmstart_pulse +``` + +For a warm-start where you want to change duration or knot count, you must re-interpolate. The simplest correct approach is a fresh initialization (the optimizer will find the solution from random init, guided by the problem structure). A more sophisticated approach would sample the warm-start pulse at the new knot locations, but this is not required for Phase 1. + +### 3.4 Gate-to-Function Mapping + +Gate mapping is platform-specific. The platform skill card provides: gate function names, recommended cold-start durations, and any gate-specific notes. Consult the skill card for the target platform. + +### 3.5 Pulse Type Parameters + +**Linear spline (default for cold start):** + +```julia +N_knots = 51 +u_init, times = initialize_controls(sys.n_drives, N_knots, T) +pulse = LinearSplinePulse(u_init, times) +``` + +**Cubic spline (smoother, fewer knots needed):** + +```julia +N_knots = 11 +u_init, du_init, times = initialize_cubic_controls(sys.n_drives, N_knots, T) +pulse = CubicSplinePulse(u_init, du_init, times) +``` + +Choose based on `suggested_approach` in the brief: +- If approach mentions "cubic" or "cubic spline": use `CubicSplinePulse` with 11 knots (unless brief specifies different count). +- If approach mentions "linear" or "linear spline": use `LinearSplinePulse` with 51 knots. +- If approach mentions a specific knot count (e.g., "21 knots"): use that count. +- Default: `LinearSplinePulse` with 51 knots. + +### 3.6 System and Optimization Constants + +Constants are loaded from the platform demo's `defaults.jl` via `include()`. The constants prefix is platform-specific (e.g., `FLUX_*` for fluxonium, `TRANSMON_*` for transmon). The platform skill card lists the available constants. + +Do NOT hardcode constant values in the script -- use the named constants from the demo's `defaults.jl`. + +### 3.7 Mandatory Script Elements + +Every generated script MUST have ALL of the following. Missing any one will cause failure: + +1. `using Piccolo` and `using Piccolissimo` -- separate `using` statements. +2. `using JLD2, Dates, LinearAlgebra, SparseArrays` +3. `BLAS.set_num_threads(1)` -- near the top, after `using` statements. +4. `include()` calls for platform demo src modules as specified by the platform skill card. At minimum: defaults, systems, gates, utils. +5. System constructor call as specified by the platform skill card -- returns `(sys, meta)` or equivalent. +6. Target gate call using `meta.n_levels` (not `n_levels` directly): `target_X(meta.n_levels)`. +7. Pulse initialization (cold or warm start) returning a `LinearSplinePulse` or `CubicSplinePulse`. +8. `UnitaryTrajectory(sys, pulse, U_goal)`. +9. `SplineIntegrator(qtraj, N_knots; alg=MagnusAdapt4Alg(tol=1e-8))`. +10. `SplinePulseProblem(qtraj; integrator=integrator, Q={PLATFORM}_Q, R_u={PLATFORM}_R_u, R_du={PLATFORM}_R_du, du_bound={PLATFORM}_SLEW_RATE, Δt_bounds={PLATFORM}_Δt_BOUNDS)`. +11. `solve!(qcp; max_iter=max_iter, print_level=3)`. +12. `save_results(...)` call. +13. Three `println` lines for `AMICO_RESULT_FIDELITY`, `AMICO_RESULT_DURATION`, `AMICO_RESULT_STATUS`. + +### Script Template — Bosonic GKP Optimization + +When `platform: bosonic`: + +```julia +using Piccolo, Piccolissimo +using JLD2, Dates, LinearAlgebra, SparseArrays + +BLAS.set_num_threads(1) + +const DEMO_ROOT = "/home/aaron/harmoniqs/gkp-stanford" +const AMICO_ROOT = "/home/aaron/harmoniqs/amico" + +include(joinpath(DEMO_ROOT, "src", "defaults.jl")) +include(joinpath(DEMO_ROOT, "src", "operators.jl")) +include(joinpath(DEMO_ROOT, "src", "system.jl")) +include(joinpath(DEMO_ROOT, "src", "targets.jl")) +include(joinpath(DEMO_ROOT, "src", "curriculum.jl")) + +pulse, fidelity, history = curriculum_optimize_gkp(; + N_fock={N_fock}, + delta={delta}, + curriculum_steps={curriculum_steps}, + N_knots={N_knots}, + iters_per_stage={iters_per_stage}, + Q={Q}, + integrator_tol={integrator_tol}, +) + +# Report +println("AMICO_RESULT_FIDELITY=$fidelity") +println("AMICO_RESULT_DURATION={T_init_ns}") +leakage = last(history).leakage +println("AMICO_RESULT_LEAKAGE=$leakage") +status = fidelity ≥ {target_fidelity} ? "new_best" : "improved" +println("AMICO_RESULT_STATUS=$status") +``` + +### Script Template — Bosonic QILC + +When `platform: bosonic-qilc`: + +```julia +using Piccolo, Piccolissimo, Intonato +using JLD2, Dates, LinearAlgebra, SparseArrays + +BLAS.set_num_threads(1) + +const DEMO_ROOT = "/home/aaron/harmoniqs/gkp-stanford" +const AMICO_ROOT = "/home/aaron/harmoniqs/amico" + +include(joinpath(DEMO_ROOT, "src", "defaults.jl")) +include(joinpath(DEMO_ROOT, "src", "operators.jl")) +include(joinpath(DEMO_ROOT, "src", "system.jl")) +include(joinpath(DEMO_ROOT, "src", "targets.jl")) +include(joinpath(DEMO_ROOT, "src", "mismatch.jl")) + +# 1. Load catalog pulse +pulse = load(joinpath(AMICO_ROOT, "catalog/pulses/{warm_start}/pulse.jld2"), "pulse") + +# 2. Build nominal system +sys_nom, _ = DisplacedFrameSystem(; N_fock={N_fock}) + +# 3. Build "true" system with mismatches +sys_true, _ = DisplacedFrameSystem(; + N_fock={N_fock}, + chi_kHz={chi_kHz * (1 + delta_chi_pct/100)}, + K_q_GHz={K_q_GHz * (1 + delta_K_q_pct/100)}, +) + +# 4. Build mismatch wrapper +mismatch = ( + timing_skew_ns={timing_skew_ns}, + delta_Omega_pct={delta_Omega_pct}, + delta_alpha_pct={delta_alpha_pct}, +) + +# 5-9. Build trajectory, experiment, measurement model, PulseTuningProblem, solve +# (See Intonato API docs for SimulatedExperiment, MeasurementModel, PulseTuningProblem) + +# Report +println("AMICO_RESULT_FIDELITY=$post_qilc_fidelity") +println("AMICO_RESULT_PRE_QILC_FIDELITY=$pre_qilc_fidelity") +println("AMICO_RESULT_QILC_IMPROVEMENT=$(post_qilc_fidelity / pre_qilc_fidelity)") +println("AMICO_RESULT_QILC_CONVERGED=$(result.converged)") +println("AMICO_RESULT_QILC_ITERATIONS=$(length(result.history))") +println("AMICO_RESULT_LEAKAGE=$leakage") +println("AMICO_RESULT_STATUS=...") +``` + +**Note:** The `{...}` placeholders are filled from the experiment brief's `hyperparams` section. The Experimenter must translate YAML brief fields to Julia values. + +--- + +## 4. Running the Script + +Execute with: + +```bash +OPENBLAS_NUM_THREADS=1 julia -t auto --project=$HARMONIQS_ROOT/{platform}-demo {script_path} +``` + +- `OPENBLAS_NUM_THREADS=1` prevents BLAS thread contention. +- `-t auto` enables Julia threads for parallelism within Piccolo. +- `--project=` points to the `{platform}-demo` environment with Piccolo + Piccolissimo deps. + +The script may take 1-15 minutes depending on gate complexity and knot count. Use a configurable timeout for the Bash command: +- Read `timeout_s` from the experiment brief (if present) +- Default: `timeout_s = 600` (10 minutes) +- For bosonic platform: default `timeout_s = 1800` (30 minutes) +- Formula: `timeout = brief.timeout_s ?? (platform == "bosonic" ? 1800 : 600)` + +If it times out, report `status: error` with `failure_mode: timeout`. + +--- + +## 5. Parsing Script Output + +After the script runs, search its stdout for the structured output lines: + +``` +AMICO_RESULT_FIDELITY=0.99993 +AMICO_RESULT_DURATION=10.2 +AMICO_RESULT_STATUS=completed +AMICO_RESULT_WALL_TIME=45.3 +``` + +Parse these values. If any are missing, or if fidelity is `NaN` or `Inf`, treat as `status: error`. + +### QILC-Specific Metric Parsing + +For `platform: bosonic-qilc`, parse these additional tags from stdout: +- `AMICO_RESULT_PRE_QILC_FIDELITY` +- `AMICO_RESULT_QILC_IMPROVEMENT` +- `AMICO_RESULT_QILC_CONVERGED` +- `AMICO_RESULT_QILC_ITERATIONS` +- `AMICO_RESULT_LEAKAGE` + +Include all parsed values in the result report to the Orchestrator. + +--- + +## 6. Determining Result Status + +Compare the achieved fidelity against the catalog incumbent: + +1. **Read the catalog incumbent** by scanning `amico/catalog/pulses/{platform}-{gate}-v*/metadata.toml` for the highest version number. Extract the `fidelity` field. +2. **If no incumbent exists** (cold start for a new gate): any completed result is `new-best`. +3. **If fidelity > incumbent fidelity**: status is `new-best`. +4. **If fidelity > 0.99 but <= incumbent**: status is `improved` (better than random, but not best). +5. **If fidelity <= 0.99**: status is `no-improvement`. +6. **If script failed**: status is `error`. + +--- + +## 7. Catalog Versioning + +When status is `new-best`, save the pulse to the catalog: + +### 7.1 Find Next Version Number + +Scan `amico/catalog/pulses/` for directories matching `{platform}-{gate}-v*`. Extract version numbers, find the maximum N. The new version is `v{N+1}`. + +Example: if `fluxonium-X-v1/` exists, the new entry is `fluxonium-X-v2/`. + +### 7.2 Create Catalog Entry + +```bash +mkdir -p amico/catalog/pulses/{platform}-{gate}-v{N+1} +``` + +### 7.3 Copy Pulse File + +The `save_results` function in the script saves a JLD2 file at the scratchpad path. Copy it to the catalog: + +```bash +cp amico/scratchpad/{session_id}/result-iter-{iteration}-{gate}.jld2 amico/catalog/pulses/{platform}-{gate}-v{N+1}/pulse.jld2 +``` + +### 7.4 Write metadata.toml + +Write `amico/catalog/pulses/{platform}-{gate}-v{N+1}/metadata.toml` with this exact schema: + +```toml +id = "{platform}-{gate}-v{N+1}" +platform = "{platform}" +gate = "{gate}" +fidelity = {fidelity} +duration_ns = {duration} +pulse_type = "{LinearSplinePulse or CubicSplinePulse}" +N_knots = {N_knots} +free_phase = {true or false -- detect from script: true if FreePhase or free_phase appears} +warm_start = "{warm_start_id or empty string}" +source_script = "scratchpad/{session_id}/iter-{iteration}-{gate}.jl" +date = "{YYYY-MM-DD}" +tags = ["{platform}", "gate/{gate}", "eigenbasis", "phase-drive"] +``` + +Note: the existing catalog uses `duration_us` (microseconds) in some entries and `duration_ns` (nanoseconds) in others. For new entries, use `duration_ns` (nanoseconds) as the canonical unit, matching the design spec. The value comes directly from the script output `AMICO_RESULT_DURATION` which is in nanoseconds. + +--- + +## 8. Results TSV + +Append one row to `amico/results/{session_id}.tsv`. If the file does not exist, create it with a header row first. + +### Header + +``` +session_id iter experiment_id timestamp platform gate fidelity duration_ns status failure_mode warm_start catalog_entry device branch wall_time_s script_path +``` + +### Row Values + +| Column | Value | +|---|---| +| `session_id` | From the brief | +| `iter` | From the brief (`iteration`) | +| `experiment_id` | `exp-{YYYYMMDD}-{HHMMSS}-{platform}-{gate}` (use current timestamp) | +| `timestamp` | ISO 8601 format: `YYYY-MM-DDTHH:MM:SS` | +| `platform` | From the brief | +| `gate` | From the brief | +| `fidelity` | Parsed from script output | +| `duration_ns` | Parsed from script output | +| `status` | `new-best`, `improved`, `no-improvement`, or `error` | +| `failure_mode` | `null` for successful runs; `script_error` if Julia errored; `solver_stagnation` if max_iter hit and fidelity < 0.99; `timeout` if execution timed out | +| `warm_start` | The warm_start ID from the brief, or `cold` for cold start | +| `catalog_entry` | New catalog ID if new-best, otherwise empty | +| `device` | From the brief | +| `branch` | `main` | +| `wall_time_s` | Parsed from script output (`AMICO_RESULT_WALL_TIME`), or measured externally | +| `script_path` | Relative to amico root: `scratchpad/{session_id}/iter-{iteration}-{gate}.jl` | + +Use tab characters (`\t`) as delimiters. Do NOT use spaces. + +--- + +## 9. Output Format + +After completing all steps, report to the caller with this structure: + +``` +## Experiment Result + +- **Experiment ID**: exp-{YYYYMMDD}-{HHMMSS}-{platform}-{gate} +- **Fidelity**: {fidelity} +- **Duration**: {duration_ns} ns +- **Status**: {new-best | improved | no-improvement | error} +- **Failure mode**: {null | solver_stagnation | script_error | timeout} +- **Catalog entry**: {platform-gate-vN or "none"} +- **Script path**: amico/scratchpad/{session_id}/iter-{iteration}-{gate}.jl +- **Wall time**: {wall_time} s +- **Warm start**: {warm_start_id or "cold"} +- **Needs engineering**: {true | false} +- **Engineering description**: {null | structured description of what's missing} +- **Suggested package**: {null | Piccolo.jl | Piccolissimo.jl} +``` + +**When to set `needs_engineering: true`**: Only when the error is a missing API, missing function, incompatible interface, or package limitation. NOT for solver stagnation, timeout, or poor fidelity. Examples: +- `MethodError: no method matching shift_drift(::AbstractMatrix)` → true +- `UndefVarError: GATES[:sqrtX]` → true +- Max iterations reached, fidelity 0.95 → false (solver issue) + +--- + +## 10. Error Handling + +### Julia script fails (non-zero exit code) + +1. Capture stderr output. +2. Set `status = "error"`, `failure_mode = "script_error"`. +3. Set `fidelity = NaN`, `duration_ns = NaN`. +4. Still append the row to the results TSV. +5. Report the error output in your response so the Orchestrator/Librarian can classify it. + +### Fidelity is NaN or Inf + +1. Set `status = "error"`, `failure_mode = "solver_stagnation"`. +2. Still append to results TSV. + +### Script times out (> 600 seconds) + +1. Set `status = "error"`, `failure_mode = "timeout"`. +2. Still append to results TSV. + +### Warm-start pulse file not found + +1. Fall back to cold start. +2. Log a warning in the script output. +3. Proceed with the experiment. + +### Results TSV does not exist + +Create it with the header row, then append the data row. + +--- + +## 11. Demo Directory Standard Interface + +Every platform demo directory MUST follow this structure: + +``` +{platform}-demo/ +├── Project.toml # Julia environment with Piccolo + Piccolissimo deps +├── src/ +│ ├── defaults.jl # {PLATFORM}_Q, {PLATFORM}_R_u, etc. (REQUIRED) +│ ├── systems.jl # System constructor(s) (REQUIRED) +│ ├── gates.jl # Gate target functions (REQUIRED) +│ ├── utils.jl # initialize_controls, save_results, load_pulse (REQUIRED) +│ └── bases.jl # Basis transformations, energy shifts (OPTIONAL, platform-specific) +└── scripts/ # Reference optimization scripts (read-only examples) +``` + +The 4 required modules form the standard interface. Platforms may add optional modules (e.g., fluxonium-demo has `bases.jl` for eigenbasis construction and `runners.jl` for batch execution). The platform skill card lists which modules the experimenter must `include()` -- not all modules present in the directory. + +--- + +## 12. Branch-Aware Execution + +When the experiment brief includes `branch:` and `worktree_path:` fields: + +1. The orchestrator has already created the worktree. The experimenter receives the `worktree_path` in the brief. +2. Switch the demo environment to use the worktree: + ```julia + using Pkg + Pkg.develop(path="{worktree_path}") + ``` +3. Run the experiment as normal. +4. The orchestrator manages worktree lifecycle (creation, restoration, cleanup). Do NOT create or remove worktrees. + +--- + +## 13. Validation Mode + +When dispatched with `task_mode: validation`: + +1. Receive `worktree_path` from the orchestrator (worktree already created). +2. Switch demo to worktree via `Pkg.develop(path=worktree_path)`. +3. Run the experiment specified in `validation_hint`. +4. Report: did the blocked experiment succeed on the branch? +5. Do NOT clean up the worktree -- the orchestrator manages lifecycle. + +--- + +## 14. Important Constraints + +- **Never modify** files in any `*-demo/` directory -- they are read-only references. +- **Never modify** existing catalog entries -- only create new version directories. +- **Never modify** STRATEGY.md -- that is human-owned. +- **Always use absolute paths** in generated Julia scripts. +- **Always set** `BLAS.set_num_threads(1)` in scripts. +- **Always include** the platform demo src modules as specified by the platform skill card. +- **Always print** the three `AMICO_RESULT_*` lines at the end of every script. +- **Always append** to the results TSV, even on error. +- **Tab-delimited** TSV -- never use spaces as delimiters in the results file. diff --git a/packages/extension/test/fixtures/vault-agents/pin.json b/packages/extension/test/fixtures/vault-agents/pin.json new file mode 100644 index 00000000..ee6b78c5 --- /dev/null +++ b/packages/extension/test/fixtures/vault-agents/pin.json @@ -0,0 +1,33 @@ +{ + "record_version": 1, + "note": "The role-card parity fixtures' pin record (slice 2 / D3, #806, obligation O8): the engine-neutral role definitions this pin targets, at the amicissimo vault revision they were pinned at. The parity suite (role_cards_parity.test.ts) digest-verifies the committed fixture copies against this record — the pin is self-contained — and the nightly pin-behind-HEAD check (ops/role-parity, riding the doctor's fleet cadence on the vault-visible machine) re-verifies the pin against the live amicissimo checkout: HEAD moved past this revision, or bytes at the revision changed, files a chore issue on drift.", + "vault_repo": "harmoniqs/amicissimo", + "vault_revision": "5c6a1cd0d5bd240e07f6bef6467986e2fb41d7a7", + "captured_at": "2026-09-05T03:45:00.000Z", + "pinned": [ + { + "role_card": "implementer", + "vault_path": "vault/agents/engineer.md", + "fixture": "engineer.md", + "sha256": "sha256:4f10693c58f325e6a014cc3273e457bbf9753ebfafa191e0c5573492a607cc92" + }, + { + "role_card": "experimenter", + "vault_path": "vault/agents/experimenter.md", + "fixture": "experimenter.md", + "sha256": "sha256:9ed13551d22063d176cff0bf3fd33aa4b9f0937c4c941eaf2bf21fecd03ce2d5" + } + ], + "no_counterpart": [ + { + "role_card": "hypothesizer", + "nearest_kin": "vault/agents/researcher.md", + "reason": "the researcher is a decider returning one brief; the hypothesizer is a read-only proposer of ranked hypotheses — different governance and output; no overlap pin without inventing a mapping" + }, + { + "role_card": "analyzer", + "nearest_kin": "vault/agents/librarian.md", + "reason": "the librarian writes curated notes; the analyzer is read-only, raw-artifacts-only, and proposes ledger verdicts — no overlap pin without inventing a mapping" + } + ] +} diff --git a/packages/extension/test/role_cards_parity.test.ts b/packages/extension/test/role_cards_parity.test.ts new file mode 100644 index 00000000..21cff3bc --- /dev/null +++ b/packages/extension/test/role_cards_parity.test.ts @@ -0,0 +1,223 @@ +// role_cards_parity.test.ts — the parity half of the seed gate (slice 2 / +// D3, #806, AC3 + AC4, spec-20260905-063000 D3): the shipped role cards are +// the opencode bindings; the amicissimo vault's agent records are the +// engine-neutral contracts; THIS suite keeps the overlap coherent. +// +// The fixtures are REVISION-PINNED: test/fixtures/vault-agents/ carries the +// engine-neutral definitions at the amicissimo revision recorded in pin.json +// (the fixture carries the vault revision it pinned), digest-verified +// against the record — the pin is self-contained on machines without the +// vault checkout. The nightly pin-behind-HEAD check (ops/role-parity, +// riding the doctor's fleet cadence on the vault-visible machine) re-checks +// the pin against the live amicissimo checkout and files a chore issue on +// drift (obligation O8). +// +// THE GATE, honored mechanically (never prose): +// - COHERENT anchors (both texts agree) are pinned NOW, against BOTH the +// shipped card and the pinned fixture — the overlap stays coherent. +// - FLAGGED content (the prepared human diff, +// docs/seed-gate/role-cards-seed-diff.md, marks it divergent) is pinned +// only after AARON signs. Pre-signature those anchors are SKIPS whose +// reasons name the flag and cite the diff document — a silent pass does +// not exist: a guard test asserts every flag the skips carry appears in +// the diff document, and the doc's signature status must match the +// suite's SEED_GATE_SIGNED switch (flip BOTH, in the same change, with +// the signature). +import { describe, it, expect } from "vitest"; +import { readFileSync, existsSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const EXT = resolve(HERE, ".."); +const AGENTS_DIR = join(EXT, "agents"); +const FIXTURES = join(HERE, "fixtures", "vault-agents"); +const PIN_PATH = join(FIXTURES, "pin.json"); +const DIFF_DOC = join(HERE, "..", "..", "..", "docs", "seed-gate", "role-cards-seed-diff.md"); + +/** The seed gate's switch of record. Flip to true ONLY in the same change + * that lands Aaron's signature in docs/seed-gate/role-cards-seed-diff.md + * (the coupling test below fails otherwise — never flip it alone). */ +const SEED_GATE_SIGNED = false; + +interface PinRecord { + record_version: number; + vault_repo: string; + vault_revision: string; + pinned: Array<{ role_card: string; vault_path: string; fixture: string; sha256: string }>; + no_counterpart: Array<{ role_card: string; nearest_kin: string; reason: string }>; +} + +const pin = JSON.parse(readFileSync(PIN_PATH, "utf8")) as PinRecord; +const cardText = (name: string): string => readFileSync(join(AGENTS_DIR, `${name}.md`), "utf8"); +const fixtureText = (name: string): string => readFileSync(join(FIXTURES, name), "utf8"); +const sha256 = (p: string): string => + "sha256:" + createHash("sha256").update(readFileSync(p)).digest("hex"); + +// ── the revision-pinned fixtures (AC3) ─────────────────────────────────────── + +describe("the parity fixtures are revision-pinned (the fixture carries the vault revision)", () => { + it("pin.json carries a real amicissimo revision and both overlapping definitions", () => { + expect(pin.record_version).toBe(1); + expect(pin.vault_repo).toBe("harmoniqs/amicissimo"); + expect(pin.vault_revision).toMatch(/^[0-9a-f]{40}$/); + expect(pin.pinned.map((p) => p.role_card).sort()).toEqual(["experimenter", "implementer"]); + }); + + it("fixture integrity: every committed fixture byte-matches its recorded digest (self-contained pin)", () => { + for (const p of pin.pinned) { + expect(existsSync(join(FIXTURES, p.fixture)), `fixture ${p.fixture} committed`).toBe(true); + expect(sha256(join(FIXTURES, p.fixture))).toBe(p.sha256); + } + }); + + it("the no-counterpart roles are RECORDED with reasons, never silently unpinned", () => { + expect(pin.no_counterpart.map((n) => n.role_card).sort()).toEqual(["analyzer", "hypothesizer"]); + for (const n of pin.no_counterpart) { + expect(n.reason.length).toBeGreaterThan(20); + } + }); +}); + +// ── the seed-gate record (AC4) ────────────────────────────────────────────── + +describe("the prepared human diff + the signature state", () => { + const doc = readFileSync(DIFF_DOC, "utf8"); + + it("the diff document exists and covers all four cards, the provenance record, and the pin revision", () => { + expect(existsSync(DIFF_DOC)).toBe(true); + for (const role of ["hypothesizer", "experimenter", "analyzer", "implementer"]) { + expect(doc).toContain(role); + } + expect(doc).toContain(".seed-provenance.json"); + expect(doc).toContain(pin.vault_revision); + }); + + it("the doc's signature status matches the suite's gate switch (flip both together, or neither)", () => { + if (SEED_GATE_SIGNED) { + expect(doc).not.toMatch(/Status:\s*PENDING SIGNATURE/i); + } else { + expect(doc).toMatch(/Status:\s*PENDING SIGNATURE/i); + } + }); +}); + +// ── coherent overlap anchors — pinned NOW (the gate does not block agreement) ─ + +/** One anchor of the overlap: a regex that must hit the shipped card AND one + * that must hit the pinned fixture. Both sides assert — coherence, not + * just shape. */ +const overlapAnchor = (label: string, card: string, cardRe: RegExp, fixture: string, fixtureRe: RegExp) => + it(`coherent: ${label}`, () => { + expect(cardRe.test(cardText(card)), `${card} carries the anchor: ${cardRe}`).toBe(true); + expect(fixtureRe.test(fixtureText(fixture)), `the pinned ${fixture} carries the anchor: ${fixtureRe}`).toBe(true); + }); + +describe("implementer ↔ engineer (engine-neutral) — coherent overlap, pinned", () => { + overlapAnchor("the delegated TDD leaf (implement-issue --orchestrated)", "implementer", /implement-issue/, "engineer.md", /implement-issue/); + overlapAnchor("the orchestrated worktree binding", "implementer", /--orchestrated/, "engineer.md", /--orchestrated/); + overlapAnchor("branch discipline — never off the assigned branch", "implementer", /caller-provided worktree branch/, "engineer.md", /never on main/); + overlapAnchor("test protection — never force green", "implementer", /never delete, skip, or mark tests broken to\s+force green/, "engineer.md", /NEVER delete test files or remove test cases/); + overlapAnchor("the structured return contract", "implementer", /commit_shas/, "engineer.md", /commit_shas:/); + overlapAnchor("bounded retries, then escalate — never negotiate a RED", "implementer", /is a `failed` return,\s+not a negotiation/, "engineer.md", /up to 2 retry cycles/); +}); + +describe("experimenter ↔ experimenter (engine-neutral) — coherent overlap, pinned", () => { + overlapAnchor("brief-driven execution — parse the briefing first", "experimenter", /Briefing you receive:/, "experimenter.md", /[Ee]xperiment brief/); + overlapAnchor("numbers-grounded reporting from the run's own output", "experimenter", /Debrief with NUMBERS ONLY/, "experimenter.md", /AMICO_RESULT_/); +}); + +// ── flagged content — pending-signature skips (never a silent pass) ───────── + +/** The flags the prepared diff marks divergent. Keys must appear verbatim in + * docs/seed-gate/role-cards-seed-diff.md (guard test below). */ +const FLAGGED = [ + { + key: "implementer ↔ engineer: merge/PR governance", + card: "implementer", + fixture: "engineer.md", + flag: + "the shipped card never opens PRs, never merges, never pushes (the walk owns the lifecycle); the vault def frontmatter says 'Auto-merges when all quality gates pass'", + }, + { + key: "implementer ↔ engineer: scope perimeter", + card: "implementer", + fixture: "engineer.md", + flag: + "one issue slice per cast (shipped) vs the standalone experiment-mode engineering brief with layer skills and per-task PRs (vault)", + }, + { + key: "experimenter ↔ experimenter: self-grading and self-promotion", + card: "experimenter", + fixture: "experimenter.md", + flag: + "the shipped card never grades its own result and never promotes (gates + parent + analyzer do); the vault def compares against the incumbent and saves to the catalog itself", + }, + { + key: "experimenter ↔ experimenter: environment/checkout discipline", + card: "experimenter", + fixture: "experimenter.md", + flag: + "assigned isolated env per sessions/CHECKOUTS.md (shipped) vs scratchpad paths with no checkout registry (vault)", + }, + { + key: "experimenter ↔ experimenter: artifact contract", + card: "experimenter", + fixture: "experimenter.md", + flag: + "raw artifacts + house-frontmatter experiment note (shipped) vs TSV rows + catalog saves (vault)", + }, + { + key: "hypothesizer ↔ vault counterpart", + card: "hypothesizer", + fixture: "researcher.md", + flag: + "no engine-neutral counterpart; nearest kin researcher.md is a decider with a different output — the signature confirms no-counterpart or names the pin target", + }, + { + key: "analyzer ↔ vault counterpart", + card: "analyzer", + fixture: "librarian.md", + flag: + "no engine-neutral counterpart; nearest kin librarian.md writes curated notes where the analyzer is read-only and proposes verdicts — the signature confirms no-counterpart or names the pin target", + }, +] as const; + +describe("flagged content is pinned only after Aaron signs (the seed gate)", () => { + // The skips are the gate's honest pre-signature state: each names its flag + // and cites the diff document. When the signature lands (SEED_GATE_SIGNED + // flipped together with the doc's signature block), convert each to a + // live pin asserting the ADJUDICATED direction per the spec's constraint + // of record (repo wins for shipped bindings; the vault def keeps the + // engine-neutral semantics it still owns). + for (const f of FLAGGED) { + it.skip(`PENDING SIGNATURE — ${f.key}: ${f.flag} (see docs/seed-gate/role-cards-seed-diff.md)`, () => { + expect(true).toBe(true); + }); + } + + it("no silent passes: every flag the suite skips appears verbatim in the prepared diff document", () => { + const doc = readFileSync(DIFF_DOC, "utf8"); + for (const f of FLAGGED) { + expect(doc, `the diff doc flags: ${f.key}`).toContain(f.key); + } + }); +}); + +// ── post-signature pins (dormant until the signature flips the switch) ────── + +describe.skipIf(!SEED_GATE_SIGNED)("post-signature: the adjudicated overlap of record (repo wins for shipped bindings)", () => { + it("the shipped implementer stays merge-free: no PR/merge/push authorization enters the binding", () => { + expect(cardText("implementer")).toMatch(/never opens PRs, never merges/); + expect(cardText("implementer")).not.toMatch(/auto-merge/i); + }); + it("the shipped experimenter stays grade-free: verdicts belong to the gates + parent + analyzer", () => { + expect(cardText("experimenter")).toMatch(/do NOT declare confirm\/refute/); + expect(cardText("experimenter")).not.toMatch(/save the pulse to the catalog/); + }); + it("the no-counterpart verdicts the signature confirmed are recorded in pin.json", () => { + const confirmed = JSON.parse(readFileSync(PIN_PATH, "utf8")) as PinRecord; + expect(confirmed.no_counterpart.map((n) => n.role_card).sort()).toEqual(["analyzer", "hypothesizer"]); + }); +}); From 5eab28f5d980f22949876919731455521b14e523 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sat, 5 Sep 2026 04:07:14 -0400 Subject: [PATCH 3/7] test(doctor): role-card machines read stale until UPGRADED, current after (#806, D3, H5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The doctor-world builder gains realAgents: the fixture release tag carries the REAL D3-seeded role cards byte-for-byte (a post-seed release), opt-in so the pre-existing synthetic cells are unchanged. New convergence cells: - a machine on a post-seed release still carrying the OLD deployed-only role artifacts (drifted bytes on both deployment roots and the deployed bundles' role components) reads STALE with every role card named in evidence — the honest pre-upgrade state — and reads CURRENT on both agent-cards records once the upgrade verb converges, with every roles/*.md component row current and the deployed cards byte-identical to the seeded sources - a converged post-seed world starts CURRENT across both records and all role components (the stale-by-construction verdict resolved) --- packages/amico-run/test/helpers.ts | 17 ++++ .../amico-run/test/upgrade-agents.test.ts | 85 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/packages/amico-run/test/helpers.ts b/packages/amico-run/test/helpers.ts index 6b57ee56..c6de81b3 100644 --- a/packages/amico-run/test/helpers.ts +++ b/packages/amico-run/test/helpers.ts @@ -119,6 +119,13 @@ export interface DoctorWorldOpts { * (v0.2.6, revision 1), and no bundles deploy. The doctor must surface it * as stale-to-release, never current. */ preRegistryMachine?: boolean; + /** #806: seed the fixture repo's agents/ from the REAL extension cards + * (packages/extension/agents) instead of the synthetic one-line cards — + * the machine's release tag then carries the D3-seeded role cards + * byte-for-byte, exactly like a post-seed release. Default false keeps + * the synthetic cards (the historical pre-D3 world, unchanged for the + * pre-existing cells). */ + realAgents?: boolean; } /** The vendored-binary platform dir, derived from the LIVE platform — the @@ -328,6 +335,16 @@ export function buildDoctorWorld(opts: DoctorWorldOpts = {}): DoctorWorld { mkdirSync(agentsSrc, { recursive: true }); writeFileSync(join(agentsSrc, c), `---\nmode: ${c.replace(".md", "")}\n---\n# ${c}\n`); } + // #806: optionally seed the fixture's agent cards from the REAL extension + // sources, so the machine's release tag carries the D3-seeded role cards + // byte-for-byte (a post-seed release). The fixture's registry (below) and + // the staged bundles then validate + stage against the same real bytes. + if (opts.realAgents === true) { + const realAgents = join(__dirname, "..", "..", "extension", "agents"); + for (const c of CARDS) { + copyFileSync(join(realAgents, c), join(agentsSrc, c)); + } + } mkdirSync(join(config, "agents"), { recursive: true }); mkdirSync(join(staging, ".opencode", "agents"), { recursive: true }); for (const c of CARDS) { diff --git a/packages/amico-run/test/upgrade-agents.test.ts b/packages/amico-run/test/upgrade-agents.test.ts index 114fb10a..ea1042b2 100644 --- a/packages/amico-run/test/upgrade-agents.test.ts +++ b/packages/amico-run/test/upgrade-agents.test.ts @@ -207,6 +207,91 @@ describe("upgrade agents — pre-flight gates + aborts", () => { }); }); +// ── #806 (D3): role-card convergence — a machine on a post-seed release ────── +// +// The fixture world's release tag carries the REAL D3-seeded role cards +// (helpers' realAgents) — a post-seed release. A machine whose deployed +// roots still carry the OLD deployed-only artifacts (drifted bytes from +// before the cards had a repo source) reads STALE with the role cards +// named — the honest pre-upgrade state, not a regression — and reads +// CURRENT once UPGRADED, on both agent-cards records and every bundle +// component row. +describe("upgrade agents — role-card convergence (#806)", () => { + const ROLE_CARD_NAMES = ["hypothesizer", "experimenter", "analyzer", "implementer"] as const; + + test("old deployed-only role artifacts read stale (named) → verb converges BOTH roots → post current", async () => { + const w = buildDoctorWorld({ realAgents: true }); + mkdirSync(join(w.repoAmicode, "scripts"), { recursive: true }); + copyFileSync(REAL_SCRIPT, join(w.repoAmicode, "scripts", "deploy-agents.mjs")); + // the machine still carries the OLD deployed-only artifacts — the + // pre-seed live copies, drifted bytes on both deployment roots AND the + // deployed bundles' role components + const oldArtifact = (role: string): string => + `---\ndescription: the old deployed-only ${role} artifact\ntemperature: 0.3\n---\n# ${role} (pre-seed live copy)\n`; + for (const role of ROLE_CARD_NAMES) { + writeFileSync(join(w.config, "agents", `${role}.md`), oldArtifact(role)); + writeFileSync(join(w.staging, ".opencode", "agents", `${role}.md`), oldArtifact(role)); + const bundle = role === "implementer" ? "autodev" : "autoresearch"; + writeFileSync(join(w.config, "modes", bundle, "roles", `${role}.md`), oldArtifact(role)); + writeFileSync(join(w.staging, ".opencode", "modes", bundle, "roles", `${role}.md`), oldArtifact(role)); + } + + // PRE: both agent-cards records stale, the role cards named in evidence + // (the flat per-card digest diff governs — the honest pre-upgrade state) + const pre = await surfaceInventory(ctxForWorld(w)); + for (const name of ["agent-cards-global", "agent-cards-staging"]) { + const rec = pre.surfaces.find((r) => r.surface === name)!; + expect(rec.verdict).toBe("stale"); + for (const role of ROLE_CARD_NAMES) { + expect(rec.evidence.some((e) => e.includes(`${role}.md`)), `${name} names ${role}.md`).toBe(true); + } + } + + // the verb converges BOTH roots + const r = await upgradeVerb(verbArgs(w, ["--root-receipts", receiptsDir(w)])); + expect(r.code).toBe(0); + expect((r.json as Record).outcome).toBe("upgraded"); + + // POST: both records current, every role component current — the old + // deployed-only artifacts read current once UPGRADED (the stale-by- + // construction verdict resolves, D3) + const post = await surfaceInventory(ctxForWorld(w)); + for (const name of ["agent-cards-global", "agent-cards-staging"]) { + const rec = post.surfaces.find((r2) => r2.surface === name)!; + expect(rec.verdict, `${name} post-upgrade`).toBe("current"); + for (const comp of rec.components ?? []) { + if (comp.component.startsWith("roles/")) { + expect(comp.verdict, `${name} component ${comp.component}`).toBe("current"); + } + } + } + // the deployed role cards are byte-identical to the repo's seeded sources + for (const role of ROLE_CARD_NAMES) { + const src = readFileSync(join(w.repoAmicode, "packages", "extension", "agents", `${role}.md`), "utf8"); + expect(readFileSync(join(w.config, "agents", `${role}.md`), "utf8")).toBe(src); + expect(readFileSync(join(w.staging, ".opencode", "agents", `${role}.md`), "utf8")).toBe(src); + } + cleanup(); + }); + + test("a converged post-seed machine reads CURRENT across both records and all role components (the resolved state)", async () => { + const w = buildDoctorWorld({ realAgents: true }); + const report = await surfaceInventory(ctxForWorld(w)); + for (const name of ["agent-cards-global", "agent-cards-staging"]) { + const rec = report.surfaces.find((r) => r.surface === name)!; + expect(rec.verdict, `${name} starts current on a fresh post-seed world`).toBe("current"); + for (const comp of rec.components ?? []) { + if (comp.component.startsWith("roles/")) expect(comp.verdict).toBe("current"); + } + // the doctor's source set includes the four role cards — the record's + // evidence counts the full shipped set + const counted = rec.evidence.find((e) => /cards byte-match/.test(e)); + expect(counted, `${name} evidence counts the card set`).toBeDefined(); + } + cleanup(); + }); +}); + // ── the record-name alias (spec D3: the panel passes doctor's record names verbatim) ── test("doctor record names agent-cards-global / agent-cards-staging alias the agents verb", async () => { From 7800ca37730a04193b7d96fa664da4056a8a0e79 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sat, 5 Sep 2026 04:16:13 -0400 Subject: [PATCH 4/7] feat(ops): the pin-behind-HEAD check rides the doctor's fleet cadence (#806, obligation O8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - packages/extension/scripts/role_parity_check.mts — the deterministic check CLI: fixture integrity (committed fixtures vs the pin record), pin genuineness (the pinned revision is real history carrying the recorded bytes), and pin freshness (no pinned definition changed past the pin at the vault's ref — revision churn elsewhere in the vault is NOT drift; the definitions are the pin). Verdicts from revisions and digests, never prose; absent/unprobeable vault are honest named non-verdicts (exit 0), drift/integrity failures exit 1 - ops/role-parity/run-role-parity-check.sh — the nightly cadence wrapper (skill-freshness precedent): drift files exactly one chore issue 'Role-card parity pin behind the vault (nightly)' in harmoniqs/amicode (exact-title searched, created once, commented on subsequent drifts), receipts into the upgrade-receipts journal, --dry-run as the testable seam; node resolved from the caller's PATH first (the hardcoded macOS ladder shadowed a linux server's node 12 with the same PATH-preprend pattern — fixed here) + a strip-types capability probe - ops/launchd/co.harmoniqs.role-parity.plist — the cadence declaration (daily 04:45, after skill-freshness) - test/ops/role_parity_orchestrator.test.ts — seven dry-run cells (current / drift / revision-churn-clean / fixture-mismatch / pin-orphaned / vault-absent / the REAL pin record through the CLI), gated on the runtime's TS-strip capability probed via NODE_OPTIONS Live evidence (read-only, this machine): the real pin reads current — the vault's origin/main moved past the pinned revision but no pinned definition changed --- ops/README.md | 3 +- ops/launchd/co.harmoniqs.role-parity.plist | 15 ++ ops/role-parity/run-role-parity-check.sh | 200 ++++++++++++++++ .../extension/scripts/role_parity_check.mts | 224 ++++++++++++++++++ .../test/ops/role_parity_orchestrator.test.ts | 211 +++++++++++++++++ 5 files changed, 652 insertions(+), 1 deletion(-) create mode 100644 ops/launchd/co.harmoniqs.role-parity.plist create mode 100755 ops/role-parity/run-role-parity-check.sh create mode 100644 packages/extension/scripts/role_parity_check.mts create mode 100644 packages/extension/test/ops/role_parity_orchestrator.test.ts diff --git a/ops/README.md b/ops/README.md index 17e0377b..2ab18022 100644 --- a/ops/README.md +++ b/ops/README.md @@ -27,6 +27,7 @@ rename(2) over the running executable — atomic, no stop, ever. | `fleet-alert.sh` | every 15 min (`co.harmoniqs.fleet-alert`) | `fleet-status.json`, state file | **Slack `#fleet`** — device transitions only (noise-gated; always-on hosts `mini erlich` notify, laptops never do); down->24h re-reminds once daily | | `papers-digest/daily.sh` | daily ~09:00 (`co.harmoniqs.amicode-papers-digest`) | the frozen bundle | **Slack `#papers`** — top-5 quant-ph digest; appends to `papers-digest/log.txt` | | `skill-freshness/run-skill-freshness.sh` | daily ~04:30 (`co.harmoniqs.skill-freshness`) | the three skill surfaces (repo public library, armonissima vault library, server staging tree), Julia package checkouts, the `#586` lint CLI | receipt line appended to `~/.amico/server/upgrade-receipts/upgrade-receipts.jsonl`; on drift, the tracking issue "Skill freshness report (nightly)" in `harmoniqs/armonissima` (created once, then commented); reports under `skill-freshness/reports/` | +| `role-parity/run-role-parity-check.sh` | daily ~04:45 (`co.harmoniqs.role-parity`, on the vault-visible machine) | the role-card parity pin record (`packages/extension/test/fixtures/vault-agents/pin.json` — the engine-neutral role definitions at their pinned amicissimo revision) vs the amicissimo vault checkout, via the `#806` check CLI | receipt line (kind `role-parity`) appended to `~/.amico/server/upgrade-receipts/upgrade-receipts.jsonl`; on drift (a pinned definition changed past the pin, a fixture mismatched its record, or the pin orphaned), the chore issue "Role-card parity pin behind the vault (nightly)" in `harmoniqs/amicode` (created once, then commented) | The launchd plists themselves are versioned alongside (`ops/launchd/`) — reference copies; installing them is a one-time `launchctl load` on the mini (paths inside are @@ -36,7 +37,7 @@ absolute to `/Users/aaron`). `fleet-status.json`, `fleet-status.guard-state`, `fleet-alert.state`, `fleet-alert.launchd.{out,err}`, `papers-digest/{log.txt,launchd.*}`, -`skill-freshness/{reports/,launchd.*}` — all live under +`skill-freshness/{reports/,launchd.*}`, `role-parity/launchd.*` — all live under `~/.amico/ops/` on the mini and belong to the running system. `install.sh` touches none of them. diff --git a/ops/launchd/co.harmoniqs.role-parity.plist b/ops/launchd/co.harmoniqs.role-parity.plist new file mode 100644 index 00000000..5272e1d6 --- /dev/null +++ b/ops/launchd/co.harmoniqs.role-parity.plist @@ -0,0 +1,15 @@ + + + + Labelco.harmoniqs.role-parity + ProgramArguments + /bin/bash + /Users/aaron/.amico/ops/role-parity/run-role-parity-check.sh + + StartCalendarInterval + Hour4 + Minute45 + + StandardOutPath/Users/aaron/.amico/ops/role-parity/launchd.out + StandardErrorPath/Users/aaron/.amico/ops/role-parity/launchd.err + diff --git a/ops/role-parity/run-role-parity-check.sh b/ops/role-parity/run-role-parity-check.sh new file mode 100755 index 00000000..aaa3d1bb --- /dev/null +++ b/ops/role-parity/run-role-parity-check.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# run-role-parity-check.sh — the nightly role-parity pin cadence (amicode#806, +# obligation O8: the pin-behind-HEAD check rides the doctor's fleet cadence +# on the vault-visible machine and files a chore issue on drift — a pin is +# only loud if something runs it). +# +# Runs the pin check CLI (packages/extension/scripts/role_parity_check.mts) +# against the role-card parity fixtures' pin record +# (packages/extension/test/fixtures/vault-agents/pin.json — the engine-neutral +# role definitions the parity suite pinned, at the amicissimo vault revision +# it pinned) and the local amicissimo vault checkout: +# +# current the pinned definitions are unchanged at the vault ref +# behind-head a pinned definition changed past the pinned revision +# fixture-mismatch a committed fixture no longer matches its record +# pin-orphaned the pinned revision left the vault's history +# vault-absent this machine cannot see the vault checkout (honest +# skip, exit 0 — the fixtures remain the self-contained +# pin; only the vault-visible machine can check freshness) +# +# Drift tracking (real runs only): behind-head / fixture-mismatch / +# pin-orphaned → exactly one chore issue in the amicode repo, created on +# first drift (exact title "Role-card parity pin behind the vault (nightly)") +# and updated (commented) on subsequent drifts — searched before creating, +# never duplicated. A gh failure notes issue_update_failed in the receipt and +# does NOT change the exit code. The exact-title search covers OPEN issues +# only: a human-closed tracker is history — the next drift opens a fresh epoch. +# +# Receipt: ONE JSON line per real run appended to the upgrade-receipts +# journal (the doctor's receipt store): +# {"receipt_version":1,"ts":"…","kind":"role-parity","status":"…", +# "vault_revision":"…","pinned_revision":"…","drifted_files":[…] +# [,"tracking_issue":"…"][,"issue_update_failed":true]} +# +# --dry-run: runs the check, prints WOULD-DO lines to stderr, appends NO +# receipt and touches NO issues. This is the testable seam +# (packages/extension/test/ops/role_parity_orchestrator.test.ts). +# +# Configuration (env, defaults documented here): +# ROLE_PARITY_REPO ~/armonia/repos/amicode canonical repo checkout +# ROLE_PARITY_CHECK $REPO/packages/extension/scripts/role_parity_check.mts +# ROLE_PARITY_PIN $REPO/packages/extension/test/fixtures/vault-agents/pin.json +# ROLE_PARITY_VAULT ~/armonia/repos/amicissimo the vault checkout (read-only) +# ROLE_PARITY_REF origin/main the vault ref compared against +# ROLE_PARITY_RECEIPTS ~/.amico/server/upgrade-receipts/upgrade-receipts.jsonl +# ROLE_PARITY_TRACKING_REPO harmoniqs/amicode where the chore issue lands +# +# Written for /bin/bash (3.2): no associative arrays, no namerefs. +set -uo pipefail + +# --- node resolution --------------------------------------------------------- +# The caller's PATH wins (every interactive surface has the fleet's real node); +# the macOS ladder is a FALLBACK for the bare launchd environment, never a +# shadow of a newer node the user's PATH already resolves (the 2026-09-05 +# lesson: prepending the ladder on a linux server picked up /usr/bin/node 12 +# and every node call died with 'not allowed in NODE_OPTIONS'). +if ! command -v node >/dev/null 2>&1; then + export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH" +fi + +SELF_NAME="run-role-parity-check" +ISSUE_TITLE="Role-card parity pin behind the vault (nightly)" + +# --- arguments --------------------------------------------------------------- +DRY_RUN=0 +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=1 ;; + -h|--help) grep '^# ' "$0" | sed 's/^# \?//'; exit 0 ;; + *) echo "$SELF_NAME: unknown argument: $arg (usage: $0 [--dry-run])" >&2; exit 2 ;; + esac +done + +# --- configuration (defaults per the header table) --------------------------- +AMICODE_REPO="${ROLE_PARITY_REPO:-$HOME/armonia/repos/amicode}" +CHECK="${ROLE_PARITY_CHECK:-$AMICODE_REPO/packages/extension/scripts/role_parity_check.mts}" +PIN="${ROLE_PARITY_PIN:-$AMICODE_REPO/packages/extension/test/fixtures/vault-agents/pin.json}" +VAULT="${ROLE_PARITY_VAULT:-$HOME/armonia/repos/amicissimo}" +REF="${ROLE_PARITY_REF:-origin/main}" +RECEIPTS="${ROLE_PARITY_RECEIPTS:-$HOME/.amico/server/upgrade-receipts/upgrade-receipts.jsonl}" +TRACKING_REPO="${ROLE_PARITY_TRACKING_REPO:-harmoniqs/amicode}" + +# --- pre-flight (a broken runtime is not a skippable surface) ---------------- +if ! command -v node >/dev/null 2>&1; then + echo "$SELF_NAME: FATAL node not found on PATH — cannot run the pin check" >&2 + exit 2 +fi +# the check CLI is authored TypeScript (.mts): probe the runtime's native +# type-stripping capability and carry the flag when needed (node >= 23 strips +# by default; 22.6+ takes the flag; older node is a named pre-flight fatal, +# never a mystery crash mid-cadence). +NODE_FLAGS="" +if node --experimental-strip-types -e 'process.exit(0)' >/dev/null 2>&1; then + NODE_FLAGS="--experimental-strip-types" +elif node -e 'const [M,m]=process.versions.node.split(".").map(Number); process.exit(M>=23?0:1)' >/dev/null 2>&1; then + : # strips natively, no flag needed +else + echo "$SELF_NAME: FATAL node $(node --version 2>/dev/null) cannot strip TypeScript — the pin check CLI needs node >= 22.6" >&2 + exit 2 +fi +if [ ! -f "$CHECK" ]; then + echo "$SELF_NAME: FATAL pin check CLI not found: $CHECK (set ROLE_PARITY_CHECK)" >&2 + exit 2 +fi +if [ ! -f "$PIN" ]; then + echo "$SELF_NAME: FATAL pin record not found: $PIN (set ROLE_PARITY_PIN)" >&2 + exit 2 +fi + +# --- the check ---------------------------------------------------------------- +TS_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +CHECK_JSON="$(node $NODE_FLAGS "$CHECK" --pin "$PIN" --vault "$VAULT" --ref "$REF")" || rc=$? +RC="${rc:-0}" + +# the report is ONE JSON line on stdout; parse the fields we need +STATUS="$(node -e 'const r=JSON.parse(process.argv[1]);process.stdout.write(r.status)' "$CHECK_JSON")" +PINNED_REV="$(node -e 'const r=JSON.parse(process.argv[1]);process.stdout.write(r.pinned_revision||"")' "$CHECK_JSON")" +VAULT_REV="$(node -e 'const r=JSON.parse(process.argv[1]);process.stdout.write(r.vault_revision||"")' "$CHECK_JSON")" +DRIFTED_CSV="$(node -e 'const r=JSON.parse(process.argv[1]);process.stdout.write((r.drifted_files||[]).join(","))' "$CHECK_JSON")" + +echo "$SELF_NAME: status=$STATUS pinned=${PINNED_REV:0:12} vault=${VAULT_REV:0:12}${DRIFTED_CSV:+ drifted=$DRIFTED_CSV}" >&2 +node -e 'const r=JSON.parse(process.argv[1]);for (const e of r.evidence) console.error(" "+e)' "$CHECK_JSON" >&2 + +# --- drift escalation: exactly one chore issue, updated never duplicated ------ +TRACKING_ISSUE="" +ISSUE_UPDATE_FAILED=0 +case "$STATUS" in + behind-head|fixture-mismatch|pin-orphaned) + if [ "$DRY_RUN" = "1" ]; then + echo "$SELF_NAME: WOULD-DO: open-or-update chore issue '$ISSUE_TITLE' in $TRACKING_REPO ($STATUS${DRIFTED_CSV:+ on: $DRIFTED_CSV})" >&2 + else + BODY_FILE="$(mktemp "${TMPDIR:-/tmp}/$SELF_NAME-body.XXXXXX")" + { + echo "The role-card parity pin ($PIN) reports **$STATUS** — run of $TS_ISO." + echo + echo '```json' + echo "$CHECK_JSON" + echo '```' + echo + echo "Re-pin procedure: re-take the fixtures from the current vault revision" + echo "(copy the definitions, update pin.json's revision + digests) and re-run" + echo "\`node $CHECK --pin $PIN --vault $VAULT\` until it reads current — or" + echo "adjudicate the drift first if the definitions changed meaningfully." + } > "$BODY_FILE" + # exact-title search (OPEN issues) — one cumulative issue, never + # duplicated. A human-closed tracker starts a new epoch (closed issues + # are history, never re-opened, never re-commented). + found="" + if gh_list="$(gh issue list -R "$TRACKING_REPO" --state open \ + --search "\"$ISSUE_TITLE\" in:title" --json number,title,url --limit 20 2>/dev/null)"; then + found="$(node -e ' + let list = []; + try { list = JSON.parse(process.argv[1] || "[]"); } catch { /* search result lost */ } + const hit = list.find((it) => it.title === process.argv[2]); + process.stdout.write(hit ? String(hit.number) : ""); + ' "$gh_list" "$ISSUE_TITLE")" + fi + if [ -n "$found" ]; then + echo "$SELF_NAME: chore issue #$found exists — appending the fresh report" >&2 + if ! gh issue comment "$found" -R "$TRACKING_REPO" --body-file "$BODY_FILE" >&2; then + echo "$SELF_NAME: WARN gh issue comment failed — receipt notes issue_update_failed" >&2 + ISSUE_UPDATE_FAILED=1 + else + TRACKING_ISSUE="https://github.com/$TRACKING_REPO/issues/$found" + fi + else + echo "$SELF_NAME: no chore issue found — creating '$ISSUE_TITLE' in $TRACKING_REPO" >&2 + if url="$(gh issue create -R "$TRACKING_REPO" --title "$ISSUE_TITLE" --body-file "$BODY_FILE" 2>/dev/null)"; then + TRACKING_ISSUE="$url" + echo "$SELF_NAME: chore issue created → $TRACKING_ISSUE" >&2 + else + echo "$SELF_NAME: WARN gh issue create failed — receipt notes issue_update_failed" >&2 + ISSUE_UPDATE_FAILED=1 + fi + fi + rm -f "$BODY_FILE" + fi + ;; + *) + if [ "$DRY_RUN" = "1" ]; then + echo "$SELF_NAME: WOULD-DO: no issue action — status $STATUS (no drift)" >&2 + fi + ;; +esac + +# --- receipt ------------------------------------------------------------------ +if [ "$DRY_RUN" = "1" ]; then + echo "$SELF_NAME: DRY-RUN — no receipt appended, no issues touched" >&2 +else + RECEIPT_DIR="$(dirname "$RECEIPTS")" + mkdir -p "$RECEIPT_DIR" 2>/dev/null + extra="" + [ "$ISSUE_UPDATE_FAILED" = "1" ] && extra=',"issue_update_failed":true' + [ -n "${TRACKING_ISSUE:-}" ] && extra="$extra,\"tracking_issue\":\"$TRACKING_ISSUE\"" + DRIFTED_JSON="[]" + [ -n "$DRIFTED_CSV" ] && DRIFTED_JSON="$(node -e 'process.stdout.write(JSON.stringify(process.argv[1].split(",")))' "$DRIFTED_CSV")" + echo "{\"receipt_version\":1,\"ts\":\"$TS_ISO\",\"kind\":\"role-parity\",\"status\":\"$STATUS\",\"pinned_revision\":\"$PINNED_REV\",\"vault_revision\":\"$VAULT_REV\",\"drifted_files\":$DRIFTED_JSON$extra}" >> "$RECEIPTS" +fi + +exit "$RC" diff --git a/packages/extension/scripts/role_parity_check.mts b/packages/extension/scripts/role_parity_check.mts new file mode 100644 index 00000000..2aa9c2cb --- /dev/null +++ b/packages/extension/scripts/role_parity_check.mts @@ -0,0 +1,224 @@ +#!/usr/bin/env node +// Role-card parity pin check CLI (amicode#806, obligation O8 — the +// pin-behind-HEAD check that rides the doctor's fleet cadence on the +// vault-visible machine). Deterministic, no LLM: verdicts derive from git +// revisions and content digests, never prose. +// +// node scripts/role_parity_check.mts --pin --vault +// [--ref ; + no_counterpart?: Array<{ role_card: string }>; +} + +interface Report { + status: Status; + pinned_revision: string | null; + vault_revision: string | null; + drifted_files: string[]; + evidence: string[]; +} + +const EXIT = { ok: 0, drift: 1, usage: 2 } as const; + +function usage(msg: string): never { + process.stderr.write(`role-parity-check: ${msg}\n`); + process.stderr.write( + "usage: node scripts/role_parity_check.mts --pin --vault [--ref ]\n", + ); + process.exit(EXIT.usage); +} + +// ── args ───────────────────────────────────────────────────────────────────── +let pinPath: string | null = null; +let vaultPath: string | null = null; +let ref = "origin/main"; +for (let i = 2; i < process.argv.length; i++) { + const a = process.argv[i]!; + if (a === "--pin") pinPath = process.argv[++i] ?? usage("--pin requires a path"); + else if (a === "--vault") vaultPath = process.argv[++i] ?? usage("--vault requires a path"); + else if (a === "--ref") ref = process.argv[++i] ?? usage("--ref requires a gitish"); + else usage(`unknown argument: ${a}`); +} +if (pinPath === null) usage("--pin is required"); +if (vaultPath === null) usage("--vault is required (pass the vault repo path; the cadence owns its default)"); + +const sha256 = (buf: Buffer | string): string => "sha256:" + createHash("sha256").update(buf).digest("hex"); + +function git(repo: string, args: string[]): { code: number; stdout: string; stderr: string } { + const r = spawnSync("git", ["-C", repo, ...args], { encoding: "utf8", timeout: 30_000 }); + return { code: r.status ?? -1, stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") }; +} + +const report = (status: Status, evidence: string[], extra: Partial = {}): never => { + const out: Report = { + status, + pinned_revision: pin.vault_revision, + vault_revision: null, + drifted_files: [], + evidence, + ...extra, + }; + process.stdout.write(JSON.stringify(out) + "\n"); + // drift OR integrity failure → 1; current and the honest named + // non-verdicts (absent/unprobeable vault) → 0 + process.exit( + status === "behind-head" || status === "fixture-mismatch" || status === "pin-orphaned" + ? EXIT.drift + : EXIT.ok, + ); +}; + +// ── 1. the pin record (pre-flight: a broken record is never skippable) ────── +if (!existsSync(pinPath)) usage(`pin record not found: ${pinPath}`); +let pin: PinRecord; +try { + pin = JSON.parse(readFileSync(pinPath, "utf8")) as PinRecord; +} catch (e) { + usage(`pin record unparseable: ${(e as Error).message}`); +} +if (pin.record_version !== 1 || typeof pin.vault_revision !== "string" || !Array.isArray(pin.pinned) || pin.pinned.length === 0) { + usage("pin record malformed: record_version must be 1, with vault_revision and a non-empty pinned set"); +} + +// ── 2. fixture integrity (self-contained: the committed fixtures vs the record) +const pinDir = dirname(resolve(pinPath)); +const evidence: string[] = []; +for (const p of pin.pinned) { + const fixturePath = isAbsolute(p.fixture) ? p.fixture : join(pinDir, p.fixture); + if (!existsSync(fixturePath) || !statSync(fixturePath).isFile()) { + report("fixture-mismatch", [`pinned fixture missing: ${p.fixture} (for role card ${p.role_card})`], { + pinned_revision: pin.vault_revision, + }); + } + const actual = sha256(readFileSync(fixturePath)); + if (actual !== p.sha256) { + evidence.push(`fixture ${p.fixture} (role card ${p.role_card}) drifted from its recorded digest (${actual.slice(0, 19)} ≠ ${p.sha256.slice(0, 19)})`); + } else { + evidence.push(`fixture ${p.fixture} byte-matches its recorded digest`); + } +} +if (evidence.some((e) => e.includes("drifted from its recorded digest"))) { + report("fixture-mismatch", evidence, { pinned_revision: pin.vault_revision }); +} + +// ── 3. the vault repo (absent/unprobeable are honest named non-verdicts) ───── +if (!existsSync(vaultPath)) { + report("vault-absent", [ + ...evidence, + `vault checkout absent at ${vaultPath} — the pin cannot be checked on this machine; the fixtures remain the self-contained pin`, + ], { + pinned_revision: pin.vault_revision, + }); +} +const revParse = git(vaultPath, ["rev-parse", "--verify", "HEAD"]); +if (revParse.code !== 0) { + report("vault-unprobeable", [`vault repo unprobeable at ${vaultPath}: git rev-parse HEAD failed (${revParse.stderr.trim()})`], { + pinned_revision: pin.vault_revision, + }); +} + +// the comparison ref: origin/main, falling back to HEAD when there is no +// origin (a fixture repo) — recorded, never silent. +let vaultRevision: string; +const refParse = git(vaultPath, ["rev-parse", "--verify", `${ref}^{commit}`]); +if (refParse.code === 0) { + vaultRevision = refParse.stdout.trim(); + evidence.push(`vault ref ${ref} → ${vaultRevision}`); +} else { + const headParse = git(vaultPath, ["rev-parse", "--verify", "HEAD"]); + vaultRevision = headParse.stdout.trim(); + evidence.push(`vault ref ${ref} unresolvable (${refParse.stderr.trim()}) — comparing against local HEAD ${vaultRevision}`); +} + +// ── 4. pin genuineness — the pinned revision is real history carrying the bytes +const pinExists = git(vaultPath, ["cat-file", "-e", `${pin.vault_revision}^{commit}`]); +if (pinExists.code !== 0) { + report("pin-orphaned", [`pinned revision ${pin.vault_revision} is absent from the vault repo's history (rewritten or never fetched) — the pin no longer points at real history`], { + vault_revision: vaultRevision, + }); +} +for (const p of pin.pinned) { + const show = git(vaultPath, ["show", `${pin.vault_revision}:${p.vault_path}`]); + if (show.code !== 0) { + report("pin-orphaned", [`pinned revision ${pin.vault_revision} does not carry ${p.vault_path} — the pin record and the vault history disagree`], { + vault_revision: vaultRevision, + }); + } + const atPin = sha256(show.stdout); + if (atPin !== p.sha256) { + report("fixture-mismatch", [ + ...evidence, + `${p.vault_path} at the pinned revision ${pin.vault_revision.slice(0, 12)} does not match the recorded digest (${atPin.slice(0, 19)} ≠ ${p.sha256.slice(0, 19)}) — the pin was not taken from these bytes`, + ], { vault_revision: vaultRevision }); + } + evidence.push(`${p.vault_path} at the pinned revision byte-matches the recorded digest`); +} + +// ── 5. pin freshness — did any pinned definition move past the pin? ───────── +const vaultPaths = pin.pinned.map((p) => p.vault_path); +const diff = git(vaultPath, ["diff", "--name-only", `${pin.vault_revision}..${vaultRevision}`, "--", ...vaultPaths]); +if (diff.code !== 0) { + report("vault-unprobeable", [`git diff ${pin.vault_revision.slice(0, 12)}..${vaultRevision} failed (${diff.stderr.trim()})`], { + vault_revision: vaultRevision, + }); +} +const drifted = diff.stdout.split("\n").map((l) => l.trim()).filter(Boolean); +if (drifted.length > 0) { + report("behind-head", [ + ...evidence, + ...drifted.map((f) => `pinned definition ${f} changed past the pinned revision ${pin.vault_revision.slice(0, 12)} — the parity fixtures are behind the vault`), + ], { vault_revision: vaultRevision, drifted_files: drifted }); +} +report("current", [...evidence, vaultRevision === pin.vault_revision + ? `the vault ref is exactly the pinned revision ${pin.vault_revision.slice(0, 12)} — the pin is current` + : `the vault ref moved to ${vaultRevision.slice(0, 12)} but no pinned definition changed — the pinned CONTENT is still current (re-pin at convenience, no drift)`], { + vault_revision: vaultRevision, +}); diff --git a/packages/extension/test/ops/role_parity_orchestrator.test.ts b/packages/extension/test/ops/role_parity_orchestrator.test.ts new file mode 100644 index 00000000..a899b6a7 --- /dev/null +++ b/packages/extension/test/ops/role_parity_orchestrator.test.ts @@ -0,0 +1,211 @@ +// Nightly role-parity pin cadence (amicode#806, obligation O8) — the +// orchestrator's DRY-RUN contract, exercised on fixture pin records and +// fixture vault repos (hermetic: temp git repos, never the real amicissimo +// checkout, never the network). +// +// --dry-run is the testable seam by design: it runs the REAL check CLI +// (scripts/role_parity_check.mts) and prints WOULD-DO lines to stderr, but +// appends NO receipt and touches NO issues. The receipt-append and +// GitHub-issue paths run only outside --dry-run and are verified by the +// documented manual run on the vault-visible machine (the skill-freshness +// precedent). +// +// Gating: the orchestrator execs the .mts check through `node`, which needs +// native TS type-stripping (node >= 22.6, enableable via NODE_OPTIONS — the +// wrapper's internal `node` calls inherit it). Probed by capability, not by +// this process's own flags (vitest's node doesn't carry them); the suite +// skips cleanly on older node. +import { describe, it, expect, afterEach } from "vitest"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; + +const STRIP_PROBE = spawnSync( + process.execPath, + ["-e", 'process.stdout.write(process.features.typescript === "strip" ? "yes" : "no")'], + { encoding: "utf8", env: { ...process.env, NODE_OPTIONS: "--experimental-strip-types" } }, +); +const NODE_STRIPS_TYPES = STRIP_PROBE.status === 0 && STRIP_PROBE.stdout.trim() === "yes"; +(NODE_STRIPS_TYPES ? describe : describe.skip)("role-parity orchestrator (--dry-run, fixtures)", () => { + const EXT_ROOT = path.resolve(__dirname, "..", ".."); + const OPS_SCRIPT = path.resolve(EXT_ROOT, "..", "..", "ops", "role-parity", "run-role-parity-check.sh"); + const CHECK_CLI = path.join(EXT_ROOT, "scripts", "role_parity_check.mts"); + const REAL_PIN = path.join(EXT_ROOT, "test", "fixtures", "vault-agents", "pin.json"); + + const tmpDirs: string[] = []; + function tmpRoot(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "role-parity-")); + tmpDirs.push(dir); + return dir; + } + afterEach(() => { + for (const dir of tmpDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); + }); + + const sha256 = (p: string): string => + "sha256:" + createHash("sha256").update(fs.readFileSync(p)).digest("hex"); + + /** git with a stable identity (fixture repos never need real config). */ + function git(dir: string, args: string[]): void { + const r = spawnSync("git", ["-C", dir, ...args], { encoding: "utf8" }); + if (r.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${r.stderr}`); + } + + const DEFINITION_AT_PIN = [ + "# Engineer Agent (fixture definition)", + "", + "The engine-neutral engineer: works on branches, never deletes tests,", + "delegates to the implement-issue leaf in develop mode.", + "", + ].join("\n"); + + /** A fixture world: a pin record + fixtures dir + a vault git repo whose + * main carries the definitions at the pinned revision. */ + function fixtureWorld(over: { definitionAtPin?: string } = {}): { + root: string; + pinPath: string; + vault: string; + pinnedRevision: string; + } { + const root = tmpRoot(); + const vault = path.join(root, "amicissimo"); + fs.mkdirSync(path.join(vault, "vault", "agents"), { recursive: true }); + const definition = over.definitionAtPin ?? DEFINITION_AT_PIN; + fs.writeFileSync(path.join(vault, "vault", "agents", "engineer.md"), definition); + fs.mkdirSync(path.join(root, "fixtures"), { recursive: true }); + fs.writeFileSync(path.join(root, "fixtures", "engineer.md"), definition); + git(vault, ["init", "-b", "main"]); + git(vault, ["add", "-A"]); + git(vault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "definitions"]); + const rev = spawnSync("git", ["-C", vault, "rev-parse", "HEAD"], { encoding: "utf8" }); + const pinnedRevision = rev.stdout.trim(); + // the pin record: the fixture carries the vault revision it pinned + fs.writeFileSync( + path.join(root, "pin.json"), + JSON.stringify( + { + record_version: 1, + vault_repo: "harmoniqs/amicissimo", + vault_revision: pinnedRevision, + pinned: [ + { + role_card: "implementer", + vault_path: "vault/agents/engineer.md", + fixture: "fixtures/engineer.md", + sha256: sha256(path.join(root, "fixtures", "engineer.md")), + }, + ], + }, + null, + 2, + ) + "\n", + ); + return { root, pinPath: path.join(root, "pin.json"), vault, pinnedRevision }; + } + + function runDryRun(env: Record) { + return spawnSync("/bin/bash", [OPS_SCRIPT, "--dry-run"], { + encoding: "utf8", + env: { ...process.env, ...env }, + }); + } + + function wrapperEnv(root: string, pinPath: string, vault: string, extra: Record = {}): Record { + return { + ROLE_PARITY_CHECK: CHECK_CLI, + ROLE_PARITY_PIN: pinPath, + ROLE_PARITY_VAULT: vault, + ROLE_PARITY_REF: "main", // fixture repos have no origin + ROLE_PARITY_RECEIPTS: path.join(root, "receipts", "upgrade-receipts.jsonl"), + ROLE_PARITY_TRACKING_REPO: "harmoniqs/amicode", + ...extra, + }; + } + + it("current: pinned definitions unchanged → exit 0, status current, WOULD-DO no issue action, no receipt", () => { + const w = fixtureWorld(); + const r = runDryRun(wrapperEnv(w.root, w.pinPath, w.vault)); + expect(r.status).toBe(0); + expect(r.stderr).toMatch(/status=current/); + expect(r.stderr).toMatch(/WOULD-DO: no issue action/); + expect(r.stderr).toMatch(/the pinned CONTENT is still current|pin is current/); + expect(fs.existsSync(path.join(w.root, "receipts"))).toBe(false); + }); + + it("drift: a pinned definition changed past the pin → exit 1, behind-head, the file named, WOULD-DO the chore issue", () => { + const w = fixtureWorld(); + fs.writeFileSync( + path.join(w.vault, "vault", "agents", "engineer.md"), + DEFINITION_AT_PIN + "\n(Amended engine-neutral semantics.)\n", + ); + git(w.vault, ["add", "-A"]); + git(w.vault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "amend definitions"]); + const r = runDryRun(wrapperEnv(w.root, w.pinPath, w.vault)); + expect(r.status).toBe(1); + expect(r.stderr).toMatch(/status=behind-head/); + expect(r.stderr).toContain("vault/agents/engineer.md"); + expect(r.stderr).toMatch(/WOULD-DO: open-or-update chore issue 'Role-card parity pin behind the vault \(nightly\)'/); + expect(fs.existsSync(path.join(w.root, "receipts"))).toBe(false); + }); + + it("revision churn WITHOUT touching a pinned definition → exit 0 (low-noise: the pin's content is what drift means)", () => { + const w = fixtureWorld(); + fs.writeFileSync(path.join(w.vault, "vault", "unrelated.md"), "churn\n"); + git(w.vault, ["add", "-A"]); + git(w.vault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "unrelated churn"]); + const r = runDryRun(wrapperEnv(w.root, w.pinPath, w.vault)); + expect(r.status).toBe(0); + expect(r.stderr).toMatch(/no pinned definition changed/); + }); + + it("fixture mismatch: a committed fixture no longer matches its record → exit 1, fixture-mismatch named", () => { + const w = fixtureWorld(); + fs.writeFileSync(path.join(w.root, "fixtures", "engineer.md"), DEFINITION_AT_PIN + "\n(drifted fixture bytes)\n"); + const r = runDryRun(wrapperEnv(w.root, w.pinPath, w.vault)); + expect(r.status).toBe(1); + expect(r.stderr).toMatch(/status=fixture-mismatch/); + expect(r.stderr).toMatch(/drifted from its recorded digest/); + }); + + it("pin orphaned: the pinned revision absent from the vault's history → exit 1, pin-orphaned named", () => { + const w = fixtureWorld(); + // a DIFFERENT vault repo that never carried the pinned revision + const otherVault = path.join(w.root, "other-amicissimo"); + fs.mkdirSync(path.join(otherVault, "vault", "agents"), { recursive: true }); + fs.writeFileSync(path.join(otherVault, "vault", "agents", "engineer.md"), "different history\n"); + git(otherVault, ["init", "-b", "main"]); + git(otherVault, ["add", "-A"]); + git(otherVault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "other"]); + const r = runDryRun(wrapperEnv(w.root, w.pinPath, otherVault)); + expect(r.status).toBe(1); + expect(r.stderr).toMatch(/status=pin-orphaned/); + }); + + it("vault absent: an honest named skip → exit 0 (a pin is only loud where something can run)", () => { + const w = fixtureWorld(); + const r = runDryRun(wrapperEnv(w.root, w.pinPath, path.join(w.root, "no-such-vault"))); + expect(r.status).toBe(0); + expect(r.stderr).toMatch(/status=vault-absent/); + expect(r.stderr).toMatch(/cannot be checked on this machine/); + }); + + it("the REAL committed pin record passes pre-flight + fixture integrity through the CLI (vault absent → honest skip)", () => { + // the real pin.json + committed fixtures (test/fixtures/vault-agents) + // must be well-formed for the cadence to even run — this cell runs the + // actual record, hermetically (no vault checkout needed for integrity). + const root = tmpRoot(); + const r = runDryRun({ + ROLE_PARITY_CHECK: CHECK_CLI, + ROLE_PARITY_PIN: REAL_PIN, + ROLE_PARITY_VAULT: path.join(root, "no-such-vault"), + ROLE_PARITY_RECEIPTS: path.join(root, "receipts", "upgrade-receipts.jsonl"), + }); + expect(r.status).toBe(0); + expect(r.stderr).toMatch(/status=vault-absent/); + // fixture integrity over the real fixtures: every line green + expect(r.stderr).toMatch(/fixture engineer\.md byte-matches its recorded digest/); + expect(r.stderr).toMatch(/fixture experimenter\.md byte-matches its recorded digest/); + }); +}); From a007ac40b2bc6ab43ac8c56163c4a4aa7b11084c Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sat, 5 Sep 2026 04:48:02 -0400 Subject: [PATCH 5/7] =?UTF-8?q?fix(seed-gate):=20fold=20the=20independent?= =?UTF-8?q?=20review=20=E2=80=94=20B1=20fixture=20hold,=20B2=20fetch+syste?= =?UTF-8?q?md=20targeting,=20A1=E2=80=93A4=20(PR=20#811,=20#806)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B1 — the published parity fixtures failed the amended content policy's per-line usage-vs-internals test (engineer.md's src/ module tree, how-to-extend recipe, Complex Internals section, roadmap lines; experimenter.md's literal internal host paths). The full-definition fixtures are REMOVED from the repo and their parity pins are held as named skips in the same gate mechanism as the flagged content: the pin record (v2, fixture_publication=pending-signature) carries the vault revision + digests as provenance WITHOUT content, the suite pins the hold itself (fixtures asserted absent, publication field asserted), and the nightly check verifies pin genuineness against the vault revision directly — nothing from the vault definitions is published-verbatim or pinned before Aaron's signature decides. The vault re-authoring follow-up is recorded in the seed-diff doc as the recommended morning decision (vault-side work, its own issue+PR — never edited from this slice). B2 — the nightly check now runs on the machine it targets: the erlich systemd pair ships (ops/systemd/co.harmoniqs.role-parity.{service,timer}, mirroring the hub's user-unit conventions), the launchd plist stays as the macOS-secondary declaration, and the README/install targeting is corrected (erlich runs the check). The wrapper passes --fetch to the check CLI, which fetches the ref's remote before the freshness compare — local remote-tracking refs only move when something fetches, so without this the nightly compare ran against stale knowledge; a fetch failure reads as the named unknown vault-unfetchable, never a green receipt. The exit-2 nit is fixed: a pre-flight/runtime CLI failure appends a NAMED check-failed receipt, never a malformed empty-status line. The orchestrator suite proves the fetch is load-bearing (a remote drift is invisible without it, caught with it), the named-unknown receipt, and the check-failed receipt. A1 — the coupling test now also asserts !SEED_GATE_SIGNED ⇒ provenance.amended !== true: an unsigned tree cannot claim an amendment. A2 — the no-silent-pass guard is two-directional: every suite flag must appear verbatim in the diff doc (as before), and every bold flag key in the doc's flag sections (scoped — coherent-anchor quotes are not flags) must have a suite FLAGGED entry. A3 — post-signature conversion now enforced for all nine flags: the three previously unenforced ones (scope perimeter, checkout discipline, artifact contract) carry dormant it.skipIf(!SEED_GATE_SIGNED) pins with the repo-wins direction inline — flipping the switch converts every flag, not four of seven. A4 — the skill-freshness linux PATH-bug follow-up is filed as harmoniqs/amicode#813 (the bug, the role-parity wrapper's fix pattern, the affected machines). Gates (scrubbed env): amicode suite 2273 passed / 0 failed, smoke PASS, typecheck clean. Live dry-run on erlich: status current, the fetch moved origin/main eb5c460→5ce67f8 (load-bearing), pin genuineness green, the B1 hold named in evidence. --- docs/seed-gate/role-cards-seed-diff.md | 57 +- ops/README.md | 9 +- ops/install.sh | 16 +- ops/role-parity/run-role-parity-check.sh | 46 +- ops/systemd/co.harmoniqs.role-parity.service | 11 + ops/systemd/co.harmoniqs.role-parity.timer | 10 + .../extension/scripts/role_parity_check.mts | 111 +++- .../test/fixtures/vault-agents/engineer.md | 293 -------- .../fixtures/vault-agents/experimenter.md | 624 ------------------ .../test/fixtures/vault-agents/pin.json | 7 +- .../test/ops/role_parity_orchestrator.test.ts | 275 +++++--- .../extension/test/role_cards_parity.test.ts | 226 +++++-- 12 files changed, 591 insertions(+), 1094 deletions(-) create mode 100644 ops/systemd/co.harmoniqs.role-parity.service create mode 100644 ops/systemd/co.harmoniqs.role-parity.timer delete mode 100644 packages/extension/test/fixtures/vault-agents/engineer.md delete mode 100644 packages/extension/test/fixtures/vault-agents/experimenter.md diff --git a/docs/seed-gate/role-cards-seed-diff.md b/docs/seed-gate/role-cards-seed-diff.md index 4d34b657..ef1bef14 100644 --- a/docs/seed-gate/role-cards-seed-diff.md +++ b/docs/seed-gate/role-cards-seed-diff.md @@ -26,8 +26,17 @@ document; a silent pass does not exist anywhere in the suite. the pin-behind-HEAD check also runs on). - The diff baseline for each card below is the **engine-neutral definition** in the private amicissimo vault's agent records, pinned at amicissimo - revision `5c6a1cd0d5bd240e07f6bef6467986e2fb41d7a7` (the parity fixtures - carry this revision and digest-verify against it). + revision `5c6a1cd0d5bd240e07f6bef6467986e2fb41d7a7` (the pin record + `test/fixtures/vault-agents/pin.json` carries this revision and the + per-definition digests AT that revision — provenance without content). +- **Review B1 (PR #811) hold:** the full-definition fixtures are NOT + published in this repo pending the signature — the committed copies failed + the amended content policy's per-line usage-vs-internals test (the + engineer definition's src/ module tree, how-to-extend recipe, and + internals sections; the experimenter definition's literal internal host + paths; see the two bold fixture flags below). The nightly pin check + verifies the pin against the vault revision directly, so the hold costs + nothing in coverage; the fixture publications convert with the signature. ## Overlap map @@ -161,6 +170,38 @@ as the pin target with the write/propose delta adjudicated first. --- +## 5. The fixture publications themselves — held pending signature (review B1) + +The two overlapping definitions above were initially committed verbatim as +parity fixtures. The independent review of PR #811 found both committed +copies fail the amended content policy's per-line usage-vs-internals test +(ADR-0011's categories) on a PUBLIC repo, so **both fixture publications are +held pending-signature alongside the flagged content** — removed from the +repo, their parity pins held as named skips in the same gate mechanism, and +nothing from the vault definitions is published-verbatim or pinned before +this signature decides: + +1. **`fixture publication: engineer.md (full vault definition)`** — the + "Important Paths" src/ module tree, the how-to-extend recipe ("follow the + pattern of `TransmonSystem`…"), the "Complex Internals" architecture + section, and the roadmap lines ("later Legato", "Legato is a stub") are + package-internals categories, not usage. +2. **`fixture publication: experimenter.md (full vault definition)`** — + literal internal host paths (`/home/aaron/harmoniqs/…`) are + cloud-infrastructure/roadmap-adjacent operational detail, not usage. + +**Recommended follow-up for the signature's morning decision (vault-side +work, its own issue + PR under the amicissimo gate — never edited from this +slice):** re-author both definitions at their source in amicissimo — move +the package-internals categories out of the engine-neutral contracts (they +belong in the layer/dev skills' vault-side homes) — then re-take the +fixtures + the pin record at the NEW revision and publish. The nightly +pin-behind-HEAD check guards the interim: it verifies the current pin +against the vault revision directly (no published copy needed) and files a +chore issue on drift. + +--- + ## What the signature authorizes Signing **accept-seed** (or accept-with-amendments) does two things: @@ -168,8 +209,14 @@ Signing **accept-seed** (or accept-with-amendments) does two things: 1. The parity suite's pending-signature skips convert to live pins — the divergences above are adjudicated as the coherent overlap of record, and any card amendment lands with `amended: true` + `amendment_signed_by` in - `.seed-provenance.json` (the seed-integrity test enforces the record). -2. The campaign record (this campaign's ledger §obligations, O2) closes with + `.seed-provenance.json` (the seed-integrity test enforces the record; an + unsigned tree cannot claim an amendment). +2. The fixture publications decide (section 5): either republish the + definitions at the pinned revision as-signed, or — recommended — run the + vault re-authoring follow-up first and re-take fixtures + pin at the new + revision. Until one of those lands, the fixture halves of the parity pins + stay held (the pin record stays `pending-signature`). +3. The campaign record (this campaign's ledger §obligations, O2) closes with the seed machine's provenance hash and this signature. Until then: the seeded cards ARE live repo sources (staging, upgrade, and the @@ -188,3 +235,5 @@ silently pinned. - experimenter ↔ experimenter: shipped never-grade/never-promote binding is the coherent overlap? ___ - hypothesizer: no-counterpart confirmed (or pin target named)? ___ - analyzer: no-counterpart confirmed (or pin target named)? ___ +- Fixture publications (section 5, review B1): republish as-signed, or run + the vault re-authoring follow-up first and re-take fixtures + pin? ___ diff --git a/ops/README.md b/ops/README.md index 2ab18022..0bf096b8 100644 --- a/ops/README.md +++ b/ops/README.md @@ -27,17 +27,20 @@ rename(2) over the running executable — atomic, no stop, ever. | `fleet-alert.sh` | every 15 min (`co.harmoniqs.fleet-alert`) | `fleet-status.json`, state file | **Slack `#fleet`** — device transitions only (noise-gated; always-on hosts `mini erlich` notify, laptops never do); down->24h re-reminds once daily | | `papers-digest/daily.sh` | daily ~09:00 (`co.harmoniqs.amicode-papers-digest`) | the frozen bundle | **Slack `#papers`** — top-5 quant-ph digest; appends to `papers-digest/log.txt` | | `skill-freshness/run-skill-freshness.sh` | daily ~04:30 (`co.harmoniqs.skill-freshness`) | the three skill surfaces (repo public library, armonissima vault library, server staging tree), Julia package checkouts, the `#586` lint CLI | receipt line appended to `~/.amico/server/upgrade-receipts/upgrade-receipts.jsonl`; on drift, the tracking issue "Skill freshness report (nightly)" in `harmoniqs/armonissima` (created once, then commented); reports under `skill-freshness/reports/` | -| `role-parity/run-role-parity-check.sh` | daily ~04:45 (`co.harmoniqs.role-parity`, on the vault-visible machine) | the role-card parity pin record (`packages/extension/test/fixtures/vault-agents/pin.json` — the engine-neutral role definitions at their pinned amicissimo revision) vs the amicissimo vault checkout, via the `#806` check CLI | receipt line (kind `role-parity`) appended to `~/.amico/server/upgrade-receipts/upgrade-receipts.jsonl`; on drift (a pinned definition changed past the pin, a fixture mismatched its record, or the pin orphaned), the chore issue "Role-card parity pin behind the vault (nightly)" in `harmoniqs/amicode` (created once, then commented) | +| `role-parity/run-role-parity-check.sh` | daily ~04:45 (**erlich** — the vault-visible machine; systemd pair `co.harmoniqs.role-parity.{service,timer}` under `ops/systemd/`. The launchd plist is the macOS-**secondary** declaration) | the role-card parity pin record (`packages/extension/test/fixtures/vault-agents/pin.json` — the engine-neutral role definitions at their pinned amicissimo revision, provenance without content per review B1) vs the amicissimo vault checkout, via the `#806` check CLI with `--fetch` (the drift compare never runs against stale remote-tracking refs; a fetch failure is the named unknown `vault-unfetchable`, never a green receipt) | receipt line (kind `role-parity`) appended to `~/.amico/server/upgrade-receipts/upgrade-receipts.jsonl`; on drift (a pinned definition changed past the pin, a fixture mismatched its record, or the pin orphaned), the chore issue "Role-card parity pin behind the vault (nightly)" in `harmoniqs/amicode` (created once, then commented); a CLI pre-flight/runtime failure appends a named `check-failed` receipt, never an empty status | The launchd plists themselves are versioned alongside (`ops/launchd/`) — reference copies; installing them is a one-time `launchctl load` on the mini (paths inside are -absolute to `/Users/aaron`). +absolute to `/Users/aaron`). The linux-side cadence units are versioned at +`ops/systemd/` — one-time install on erlich: copy the `.service` + `.timer` into +`~/.config/systemd/user/`, then `systemctl --user daemon-reload && systemctl --user +enable --now .timer`. Never edit the installed copies; this repo is the source. ## Runtime state (NOT in this repo, never overwritten by deploy) `fleet-status.json`, `fleet-status.guard-state`, `fleet-alert.state`, `fleet-alert.launchd.{out,err}`, `papers-digest/{log.txt,launchd.*}`, -`skill-freshness/{reports/,launchd.*}`, `role-parity/launchd.*` — all live under +`skill-freshness/{reports/,launchd.*}`, `role-parity/{launchd.*,systemd.*}` — all live under `~/.amico/ops/` on the mini and belong to the running system. `install.sh` touches none of them. diff --git a/ops/install.sh b/ops/install.sh index 074427b3..d835b9a3 100755 --- a/ops/install.sh +++ b/ops/install.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash -# install.sh — deploy the versioned ops scripts to ~/.amico/ops/ on the mini. -# Idempotent; copies scripts ONLY (never launchd plists, never runtime state, -# never the papers-digest frozen bundle — see ops/README.md for those). +# install.sh — deploy the versioned ops scripts to ~/.amico/ops/ (the mini +# and the linux fleet hosts — the role-parity cadence runs on erlich). +# Idempotent; copies scripts ONLY (never launchd plists, never the systemd +# units, never runtime state, never the papers-digest frozen bundle — see +# ops/README.md for those and for the one-time unit installation). # hunt.sh is additionally a FLEET-HOST deploy (erlich): copy it over — see the # hunts section of ops/README.md. set -euo pipefail @@ -9,7 +11,7 @@ set -euo pipefail DEST="$HOME/.amico/ops" SRC="$(cd "$(dirname "$0")" && pwd)" -mkdir -p "$DEST/papers-digest" "$DEST/skill-freshness" +mkdir -p "$DEST/papers-digest" "$DEST/skill-freshness" "$DEST/role-parity" install -m 0755 "$SRC/fleet-status.sh" "$DEST/fleet-status.sh" install -m 0755 "$SRC/fleet-alert.sh" "$DEST/fleet-alert.sh" @@ -18,12 +20,14 @@ install -m 0755 "$SRC/hub-upgrade-smoke.sh" "$DEST/hub-upgrade-smoke.sh" install -m 0755 "$SRC/papers-digest/daily.sh" "$DEST/papers-digest/daily.sh" install -m 0755 "$SRC/hunt.sh" "$DEST/hunt.sh" install -m 0755 "$SRC/skill-freshness/run-skill-freshness.sh" "$DEST/skill-freshness/run-skill-freshness.sh" +install -m 0755 "$SRC/role-parity/run-role-parity-check.sh" "$DEST/role-parity/run-role-parity-check.sh" echo "deployed to $DEST:" echo " fleet-status.sh (launchd co.harmoniqs.fleet-status, every 5 min)" echo " fleet-alert.sh (launchd co.harmoniqs.fleet-alert, every 15 min)" echo " papers-digest/daily.sh (launchd co.harmoniqs.amicode-papers-digest, daily ~09:00)" echo " skill-freshness/run-skill-freshness.sh (launchd co.harmoniqs.skill-freshness, daily ~04:30)" +echo " role-parity/run-role-parity-check.sh (erlich: systemd co.harmoniqs.role-parity.timer, daily ~04:45 — see ops/README.md; macOS machines: the launchd plist)" echo " hunt.sh (on demand — the hardened hunt wrapper; also copy to erlich, see ops/README.md)" -echo "state files, plists, and the frozen bundle were left untouched." -echo "to activate before the next interval: launchctl kickstart -k gui/$(id -u)/" +echo "state files, plists, systemd units, and the frozen bundle were left untouched." +echo "to activate before the next interval: launchctl kickstart -k gui/$(id -u)/ (or on erlich: systemctl --user start co.harmoniqs.role-parity.service)" diff --git a/ops/role-parity/run-role-parity-check.sh b/ops/role-parity/run-role-parity-check.sh index aaa3d1bb..a1bd3096 100755 --- a/ops/role-parity/run-role-parity-check.sh +++ b/ops/role-parity/run-role-parity-check.sh @@ -26,6 +26,19 @@ # does NOT change the exit code. The exact-title search covers OPEN issues # only: a human-closed tracker is history — the next drift opens a fresh epoch. # +# FRESHNESS REQUIRES THE FETCH (review B2): the wrapper passes --fetch to the +# check CLI, which fetches the ref's remote before comparing — local +# remote-tracking refs only move when something fetches, so without this the +# nightly compare would run against stale knowledge. A fetch failure reads +# as the named unknown vault-unfetchable — never a green receipt. +# +# WHERE IT RUNS: the vault-visible machine — erlich (linux, systemd pair at +# ops/systemd/co.harmoniqs.role-parity.{service,timer}); the launchd plist +# (ops/launchd/co.harmoniqs.role-parity.plist) is the macOS-secondary +# declaration for machines that can see a vault checkout. A CLI exit 2 +# (pre-flight/runtime failure) appends a NAMED "check-failed" receipt, never +# a malformed `"status":""` line. +# # Receipt: ONE JSON line per real run appended to the upgrade-receipts # journal (the doctor's receipt store): # {"receipt_version":1,"ts":"…","kind":"role-parity","status":"…", @@ -108,17 +121,40 @@ if [ ! -f "$PIN" ]; then fi # --- the check ---------------------------------------------------------------- +# --fetch (review B2): the drift compare must not run against stale local +# remote-tracking refs — the check fetches the ref's remote first inside the +# run, and a fetch failure reads as the named unknown vault-unfetchable +# (never a green receipt off stale knowledge). TS_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)" -CHECK_JSON="$(node $NODE_FLAGS "$CHECK" --pin "$PIN" --vault "$VAULT" --ref "$REF")" || rc=$? -RC="${rc:-0}" +CHECK_JSON="" +RC=0 +CHECK_JSON="$(node $NODE_FLAGS "$CHECK" --pin "$PIN" --vault "$VAULT" --ref "$REF" --fetch)" || RC=$? + +# a runtime/pre-flight failure (exit 2) or unparseable output is a NAMED +# check-failed receipt, never a malformed `"status":""` line (review B2 nit) +if [ "$RC" -eq 2 ] || ! node -e 'const r=JSON.parse(process.argv[1]);process.exit(r&&typeof r.status==="string"&&r.status.length>0?0:1)' "$CHECK_JSON" 2>/dev/null; then + echo "$SELF_NAME: check FAILED (exit $RC) — a named pre-flight/runtime failure, never an empty receipt" >&2 + # the pinned revision for the receipt, straight from the pin record (the + # check CLI did not run to completion) — empty when the record itself is broken + PINNED_FALLBACK="$(node -e 'let r;try{r=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"))}catch{};process.stdout.write(r&&typeof r.vault_revision==="string"?r.vault_revision:"")' "$PIN" 2>/dev/null || true)" + if [ "$DRY_RUN" = "1" ]; then + echo "$SELF_NAME: DRY-RUN — no receipt appended" >&2 + else + RECEIPT_DIR="$(dirname "$RECEIPTS")" + mkdir -p "$RECEIPT_DIR" 2>/dev/null + echo "{\"receipt_version\":1,\"ts\":\"$TS_ISO\",\"kind\":\"role-parity\",\"status\":\"check-failed\",\"check_exit\":$RC,\"pinned_revision\":\"$PINNED_FALLBACK\",\"vault_revision\":null,\"drifted_files\":[]}" >> "$RECEIPTS" + fi + exit "$RC" +fi # the report is ONE JSON line on stdout; parse the fields we need STATUS="$(node -e 'const r=JSON.parse(process.argv[1]);process.stdout.write(r.status)' "$CHECK_JSON")" PINNED_REV="$(node -e 'const r=JSON.parse(process.argv[1]);process.stdout.write(r.pinned_revision||"")' "$CHECK_JSON")" VAULT_REV="$(node -e 'const r=JSON.parse(process.argv[1]);process.stdout.write(r.vault_revision||"")' "$CHECK_JSON")" +FIXTURE_PUB="$(node -e 'const r=JSON.parse(process.argv[1]);process.stdout.write(r.fixture_publication||"")' "$CHECK_JSON")" DRIFTED_CSV="$(node -e 'const r=JSON.parse(process.argv[1]);process.stdout.write((r.drifted_files||[]).join(","))' "$CHECK_JSON")" -echo "$SELF_NAME: status=$STATUS pinned=${PINNED_REV:0:12} vault=${VAULT_REV:0:12}${DRIFTED_CSV:+ drifted=$DRIFTED_CSV}" >&2 +echo "$SELF_NAME: status=$STATUS publication=$FIXTURE_PUB pinned=${PINNED_REV:0:12} vault=${VAULT_REV:0:12}${DRIFTED_CSV:+ drifted=$DRIFTED_CSV}" >&2 node -e 'const r=JSON.parse(process.argv[1]);for (const e of r.evidence) console.error(" "+e)' "$CHECK_JSON" >&2 # --- drift escalation: exactly one chore issue, updated never duplicated ------ @@ -178,7 +214,7 @@ case "$STATUS" in ;; *) if [ "$DRY_RUN" = "1" ]; then - echo "$SELF_NAME: WOULD-DO: no issue action — status $STATUS (no drift)" >&2 + echo "$SELF_NAME: WOULD-DO: no issue action — status $STATUS (nothing to file)" >&2 fi ;; esac @@ -194,7 +230,7 @@ else [ -n "${TRACKING_ISSUE:-}" ] && extra="$extra,\"tracking_issue\":\"$TRACKING_ISSUE\"" DRIFTED_JSON="[]" [ -n "$DRIFTED_CSV" ] && DRIFTED_JSON="$(node -e 'process.stdout.write(JSON.stringify(process.argv[1].split(",")))' "$DRIFTED_CSV")" - echo "{\"receipt_version\":1,\"ts\":\"$TS_ISO\",\"kind\":\"role-parity\",\"status\":\"$STATUS\",\"pinned_revision\":\"$PINNED_REV\",\"vault_revision\":\"$VAULT_REV\",\"drifted_files\":$DRIFTED_JSON$extra}" >> "$RECEIPTS" + echo "{\"receipt_version\":1,\"ts\":\"$TS_ISO\",\"kind\":\"role-parity\",\"status\":\"$STATUS\",\"fixture_publication\":\"$FIXTURE_PUB\",\"pinned_revision\":\"$PINNED_REV\",\"vault_revision\":\"$VAULT_REV\",\"drifted_files\":$DRIFTED_JSON$extra}" >> "$RECEIPTS" fi exit "$RC" diff --git a/ops/systemd/co.harmoniqs.role-parity.service b/ops/systemd/co.harmoniqs.role-parity.service new file mode 100644 index 00000000..cb222f90 --- /dev/null +++ b/ops/systemd/co.harmoniqs.role-parity.service @@ -0,0 +1,11 @@ +[Unit] +Description=Amicode role-parity pin check (nightly, amicode#806 obligation O8) +Documentation=https://github.com/harmoniqs/amicode/blob/main/ops/README.md +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/bin/bash %h/.amico/ops/role-parity/run-role-parity-check.sh +StandardOutput=append:%h/.amico/ops/role-parity/systemd.out +StandardError=append:%h/.amico/ops/role-parity/systemd.err diff --git a/ops/systemd/co.harmoniqs.role-parity.timer b/ops/systemd/co.harmoniqs.role-parity.timer new file mode 100644 index 00000000..20c81212 --- /dev/null +++ b/ops/systemd/co.harmoniqs.role-parity.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Amicode role-parity pin check nightly at 04:45 (after skill-freshness) + +[Timer] +OnCalendar=*-*-* 04:45:00 +Persistent=true +Unit=co.harmoniqs.role-parity.service + +[Install] +WantedBy=timers.target diff --git a/packages/extension/scripts/role_parity_check.mts b/packages/extension/scripts/role_parity_check.mts index 2aa9c2cb..91f0c640 100644 --- a/packages/extension/scripts/role_parity_check.mts +++ b/packages/extension/scripts/role_parity_check.mts @@ -6,27 +6,37 @@ // // node scripts/role_parity_check.mts --pin --vault // [--ref ; + /** v2: whether the full-definition fixtures are published. v1 records are + * published-by-construction (they carry a fixture path per entry). */ + fixture_publication?: "published" | "pending-signature"; + pinned: Array<{ role_card: string; vault_path: string; fixture?: string; sha256: string }>; no_counterpart?: Array<{ role_card: string }>; } @@ -62,6 +80,9 @@ interface Report { status: Status; pinned_revision: string | null; vault_revision: string | null; + /** "pending-signature" while the B1 hold stands (no full-definition + * fixture is published); "published" once the fixtures return. */ + fixture_publication: "published" | "pending-signature"; drifted_files: string[]; evidence: string[]; } @@ -80,11 +101,13 @@ function usage(msg: string): never { let pinPath: string | null = null; let vaultPath: string | null = null; let ref = "origin/main"; +let doFetch = false; for (let i = 2; i < process.argv.length; i++) { const a = process.argv[i]!; if (a === "--pin") pinPath = process.argv[++i] ?? usage("--pin requires a path"); else if (a === "--vault") vaultPath = process.argv[++i] ?? usage("--vault requires a path"); else if (a === "--ref") ref = process.argv[++i] ?? usage("--ref requires a gitish"); + else if (a === "--fetch") doFetch = true; else usage(`unknown argument: ${a}`); } if (pinPath === null) usage("--pin is required"); @@ -102,6 +125,8 @@ const report = (status: Status, evidence: string[], extra: Partial = {}) status, pinned_revision: pin.vault_revision, vault_revision: null, + fixture_publication: + pin.fixture_publication ?? (pin.pinned.every((p) => p.fixture !== undefined) ? "published" : "pending-signature"), drifted_files: [], evidence, ...extra, @@ -124,30 +149,45 @@ try { } catch (e) { usage(`pin record unparseable: ${(e as Error).message}`); } -if (pin.record_version !== 1 || typeof pin.vault_revision !== "string" || !Array.isArray(pin.pinned) || pin.pinned.length === 0) { - usage("pin record malformed: record_version must be 1, with vault_revision and a non-empty pinned set"); +if ( + !((pin.record_version === 1 && pin.pinned.every((p) => typeof p.fixture === "string")) || + (pin.record_version === 2 && (pin.fixture_publication === "published" || pin.fixture_publication === "pending-signature"))) || + typeof pin.vault_revision !== "string" || !Array.isArray(pin.pinned) || pin.pinned.length === 0 +) { + usage("pin record malformed: record_version must be 1 (fixtures published) or 2 (with fixture_publication), with vault_revision and a non-empty pinned set"); } +const publication: "published" | "pending-signature" = pin.fixture_publication + ?? (pin.record_version === 1 ? "published" : "pending-signature"); // ── 2. fixture integrity (self-contained: the committed fixtures vs the record) +// B1 hold: while the publication is pending-signature, the full-definition +// fixtures are deliberately ABSENT from this repo — nothing from the vault +// definitions is published-verbatim before the seed gate's signature. The +// hold is named evidence, never a failure (the pin remains verifiable +// against the vault revision directly, below). const pinDir = dirname(resolve(pinPath)); const evidence: string[] = []; -for (const p of pin.pinned) { - const fixturePath = isAbsolute(p.fixture) ? p.fixture : join(pinDir, p.fixture); - if (!existsSync(fixturePath) || !statSync(fixturePath).isFile()) { - report("fixture-mismatch", [`pinned fixture missing: ${p.fixture} (for role card ${p.role_card})`], { - pinned_revision: pin.vault_revision, - }); +if (publication === "pending-signature") { + evidence.push("fixture publications held pending the seed-gate signature (review B1) — no full-definition fixture is published; pin genuineness is verified against the vault revision directly"); +} else { + for (const p of pin.pinned) { + const fixturePath = isAbsolute(p.fixture!) ? p.fixture! : join(pinDir, p.fixture!); + if (!existsSync(fixturePath) || !statSync(fixturePath).isFile()) { + report("fixture-mismatch", [`pinned fixture missing: ${p.fixture} (for role card ${p.role_card})`], { + pinned_revision: pin.vault_revision, + }); + } + const actual = sha256(readFileSync(fixturePath)); + if (actual !== p.sha256) { + evidence.push(`fixture ${p.fixture} (role card ${p.role_card}) drifted from its recorded digest (${actual.slice(0, 19)} ≠ ${p.sha256.slice(0, 19)})`); + } else { + evidence.push(`fixture ${p.fixture} byte-matches its recorded digest`); + } } - const actual = sha256(readFileSync(fixturePath)); - if (actual !== p.sha256) { - evidence.push(`fixture ${p.fixture} (role card ${p.role_card}) drifted from its recorded digest (${actual.slice(0, 19)} ≠ ${p.sha256.slice(0, 19)})`); - } else { - evidence.push(`fixture ${p.fixture} byte-matches its recorded digest`); + if (evidence.some((e) => e.includes("drifted from its recorded digest"))) { + report("fixture-mismatch", evidence, { pinned_revision: pin.vault_revision }); } } -if (evidence.some((e) => e.includes("drifted from its recorded digest"))) { - report("fixture-mismatch", evidence, { pinned_revision: pin.vault_revision }); -} // ── 3. the vault repo (absent/unprobeable are honest named non-verdicts) ───── if (!existsSync(vaultPath)) { @@ -165,6 +205,25 @@ if (revParse.code !== 0) { }); } +// ── the fetch (B2): a remote-prefixed ref under --fetch is FETCHED first ───── +// +// Local remote-tracking refs only move when something fetches; the nightly +// wrapper passes --fetch so the compare never runs against stale knowledge. +// A local-only ref (main, HEAD) needs no fetch. A fetch failure is the NAMED +// unknown vault-unfetchable — never a verdict, never a green receipt. +const remoteOfRef = ref.includes("/") && !ref.startsWith("HEAD") ? ref.split("/")[0] : null; +if (doFetch && remoteOfRef !== null) { + const fetch = git(vaultPath, ["fetch", "--quiet", remoteOfRef]); + if (fetch.code !== 0) { + report("vault-unfetchable", [ + ...evidence, + `git fetch ${remoteOfRef} failed in the vault repo (${fetch.stderr.trim()}) — the freshness comparison would run against stale remote-tracking state; a named unknown, never a green verdict`, + ]); + } else { + evidence.push(`fetched ${remoteOfRef} before the freshness compare (the nightly wiring)`); + } +} + // the comparison ref: origin/main, falling back to HEAD when there is no // origin (a fixture repo) — recorded, never silent. let vaultRevision: string; diff --git a/packages/extension/test/fixtures/vault-agents/engineer.md b/packages/extension/test/fixtures/vault-agents/engineer.md deleted file mode 100644 index 5b671e2b..00000000 --- a/packages/extension/test/fixtures/vault-agents/engineer.md +++ /dev/null @@ -1,293 +0,0 @@ ---- -name: engineer -description: > - Modify Harmoniqs packages (Piccolo.jl, Piccolissimo.jl, Intonato.jl, and - later Legato) to add features or fix issues. Works on branches, runs tests, - opens PRs. Auto-merges when all quality gates pass. Phase 5 agent. -tools: Read, Glob, Grep, Write, Edit, Bash -disallowed-tools: Agent -skills: [setup, test, pr, amico-vault, tdd, implement-issue] -memory: project -model: opus ---- - -# Engineer Agent - -You are the Engineer agent in the Amico research system. Your job is to receive an engineering brief from the Orchestrator, implement code changes to Harmoniqs packages (Piccolo.jl, Piccolissimo.jl), run tests, push to a branch, and open a PR. You work proactively (implementing features the Researcher identifies as needed) and reactively (fixing issues the Experimenter escalates). - -## Phase 5 Scope - -- **Package modification** -- Piccolo.jl, Piccolissimo.jl, and Intonato.jl (Legato is a stub). -- **Branch workflow** -- all changes on branches, never on main. -- **Auto-merge** -- when local tests pass, CI green, Experimenter validation succeeds, and docs updated. -- **Test protection** -- NEVER delete tests. Tests are sacred. - ---- - -## 1. Input Format -- Engineering Brief - -The Orchestrator passes a brief in this format: - -```yaml -brief_type: engineering -package: Piccolo.jl -layer: piccolo -task: "Add shift_drift() function to center Hamiltonian eigenvalues" -motivation: "4-level transmon has stiffness ratio ~230:1; energy shift needed" -acceptance_criteria: - - "shift_drift(H_drift::AbstractMatrix) returns (H_shifted, energy_shift)" - - "Unit test verifying eigenvalue centering" - - "Docstring with math explanation" -related_experiment: - platform: transmon - gate: X - reason: "Script crashes due to ODE instability without energy shift" -device: local-workstation -session_id: "{session_id}" -iteration: "{i}" -strategy_ref: "P6" -``` - -Parse these fields carefully. The `package` and `layer` determine which layer skill to load. The `acceptance_criteria` are your definition of done. - -### Develop-mode brief - -When the brief includes `brief_mode: develop`, it is **issue-driven** and the Engineer **delegates to the `/implement-issue` leaf** — it does not implement-then-verify itself. Fields: - -```yaml -brief_mode: develop -issue: {slice number} -repo: {owner/repo} -layer: {piccolo|piccolissimo|intonato|legato|null} # inferred from package; null skips layer-skill load -integration_branch: amico/issue-{parent-n}-{slug} -worktree_root: # the Agent-tool worktree this slice runs in -``` - -In develop mode: - -- **Do NOT branch, implement, or verify by hand.** Load the layer skill (per `layer`, if set) for architecture orientation, then run **`/implement-issue {issue} --orchestrated`** on the harness-provided worktree branch. -- **Do NOT open a PR or merge** — the leaf doesn't (`--orchestrated`), and neither do you. The Orchestrator merges the worktree branch and integration-tests (orchestrator §3a.6). -- **Do return the leaf's structured contract verbatim** — `{issue, status, branch, commit_shas, ac_results, notes}` (see Section 4a). - -This replaces the old plan-task flow (implement every step, commit on the integration branch). Experiment-mode briefs (no `brief_mode: develop`) still follow Section 3 below unchanged. - ---- - -## 2. Layer Skills - -Before starting implementation, load the appropriate layer skill for architecture awareness: - -| Layer | Skill | Package | -|-------|-------|---------| -| `piccolo` | `/piccolo-dev` | Piccolo.jl | -| `piccolissimo` | `/piccolissimo-dev` | Piccolissimo.jl | -| `intonato` | `/intonato-dev` | Intonato.jl | -| `legato` | `/legato-dev` | Legato.jl (stub) | - -The layer skill tells you: module structure, how components connect, conventions to follow, where tests live, and what patterns to use for your type of change. - -The `legato` layer is supported in develop mode (when the brief includes `brief_mode: develop` and a `working_directory` for bootstrap). For experiment-mode briefs targeting an unbuilt legato package, report back with `status: blocked` -- experiment-mode work on Legato is not yet supported. - ---- - -## 3. Workflow - -1. Parse the engineering brief. -2. Load the appropriate layer skill. -3. Read the relevant package code to understand the area being modified. -4. Create a branch: `amico/{session-short}/{description}` where `{session-short}` is the first 9 chars of the session_id (e.g., `0326-a1b2`). -5. Implement the change: code + tests + docstrings. -6. Run package tests locally. -7. Push branch, open PR via `gh pr create`. -8. Report back to the Orchestrator with branch status. - -### 3.1 Branch Creation - -```bash -cd $HARMONIQS_ROOT/{Package}.jl -git checkout -b amico/{session-short}/{description} main -``` - -Where: -- `{Package}.jl` is the package from the brief (e.g., `Piccolo.jl`) -- `{session-short}` is derived from the session_id (first 9 chars, e.g., `0326-a1b2` from `20260326-140000-a1b2`) -- `{description}` is a kebab-case slug from the task (e.g., `add-transmon-shift-drift`) - -### 3.1a Develop mode: delegate to the leaf - -When the brief has `brief_mode: develop`, the Engineer does **not** run the Section-3 workflow. It runs the `/implement-issue` leaf inside the harness-provided worktree: - -```bash -# The Agent-tool harness created a worktree (isolation: "worktree") on a fresh -# branch off the integration-branch HEAD; you are already inside it. -WORKTREE_BRANCH=$(git -C "$WORKTREE_ROOT" branch --show-current) -``` - -Then: - -1. Load the layer skill (per `layer`, if set) for architecture orientation — this is the Engineer's value-add over running the leaf bare. -2. Run **`/implement-issue {issue} --orchestrated`**. The leaf reads the issue, drives `tdd` against its Acceptance Criteria, and commits on `$WORKTREE_BRANCH`. -3. Do NOT `git checkout` a different branch, do NOT push, do NOT open a PR or merge — stay on the harness branch; the Orchestrator merges it (orchestrator §3a.6). -4. Return the leaf's structured contract (Section 4a), including `branch: $WORKTREE_BRANCH` so the Orchestrator can merge it. - -The leaf owns the implementation loop; the Engineer owns layer-skill orientation and the worktree handoff. - -### Design Principle: Complex Internals -When modifying Piccolo/Piccolissimo internals, prefer complex $d \times d$ representations internally. Convert to real isomorphism form ($2d^2 \times 2d^2$) only at the optimizer boundary. This aligns four goals simultaneously: ~2x matvec speedup, matrix-free `apply!` dispatch, GPU readiness (CuArray), and Altissimo JVP/VJP/HVP compatibility. - -### 3.2 Implementation Guidelines - -- **Read before writing.** Understand the existing code structure before making changes. Use the layer skill for orientation, then read the specific files you'll modify. -- **Follow existing patterns.** If you're adding a system template, follow the pattern of `TransmonSystem` in `src/quantum/templates/transmons/transmon_system.jl`. If you're adding an objective, follow existing objectives in Piccolissimo. -- **Every new public function gets:** - - A docstring explaining what it does, its arguments, and return values - - At least one unit test - - An export in the appropriate module -- **Never break the existing API.** New functions are additive. If you need to change a function signature, make the new arguments optional with keyword defaults. - -### 3.3 Test Execution - -Run the package test suite: - -```bash -cd $HARMONIQS_ROOT/{Package}.jl && julia --project=. test/runtests.jl -``` - -Wait for completion. Parse the output: -- `Test Summary:` line shows pass/fail counts -- Any `Error` or `FAIL` means tests did not pass - -### 3.4 Push and PR - -**In develop mode (`brief_mode: develop`): SKIP this section entirely.** Do not push or create a PR. Just commit on the integration branch and return. The orchestrator handles cross-task integration — it may push and PR after the whole plan completes (v0.2+), but per-task PRs are not desired. - -```bash -cd $HARMONIQS_ROOT/{Package}.jl -git add {files_changed} # Only stage files related to this engineering task -git commit -m "{commit_message}" -git push origin amico/{session-short}/{description} -gh pr create --title "{pr_title}" --body "{pr_body}" -``` - -The PR body should include: -- **What**: one-sentence summary of the change -- **Why**: motivation from the engineering brief -- **Acceptance criteria**: from the brief -- **Test results**: pass count from local tests -- **Validation hint**: what experiment to run to verify - ---- - -## 4. Output Format - -After completing all steps, report to the Orchestrator: - -```yaml -branch: amico/{session-short}/{description} -pr_url: https://github.com/harmoniqs/{Package}.jl/pull/{N} -package: {Package}.jl -status: branch-ready -changes_summary: "{one-sentence summary of what changed}" -local_test_results: "{N}/{N} passed" -ci_status: pending -files_added: - - "{relative/path/to/new_file.jl}" - - "{relative/path/to/new_test.jl}" -files_modified: - - "{relative/path/to/modified_file.jl}" -files_removed: [] -docs_updated: true -validation_hint: "{what experiment to run to verify the fix}" -``` - -**Status values:** -- `branch-ready` -- branch pushed, PR opened, local tests pass. Experimenter can use this branch immediately. -- `tests-failed` -- implementation done but tests fail. Report the failure details so the Orchestrator can decide next steps. -- `blocked` -- cannot implement (e.g., legato layer not supported, or task requires changes outside scope). - -### 4a. Output Format (Develop Mode) - -When `brief_mode: develop`, return the leaf's contract **verbatim** (the Engineer is a thin wrapper — do not reshape it): - -```yaml -issue: {slice number} -status: complete | blocked | failed -branch: {harness worktree branch — the Orchestrator merges this} -commit_shas: - - {sha} -ac_results: - - {criterion: "...", green: true|false} -notes: "{deviations / blockers / escalation detail}" -``` - -The `branch` is critical — the Orchestrator uses it to merge the slice's commits into the unit's integration branch (orchestrator §3a.6). Do NOT include `pr_url` (no PR is created in develop mode). `status: complete` signals a mergeable slice; `blocked`/`failed` triggers the Orchestrator's frontier abort (§3a.6). - ---- - -## 5. Error Handling - -### 5.1 Tests Fail After Implementation - -1. Read the test failure output carefully. -2. Attempt to fix the issue (up to 2 retry cycles). -3. If still failing after 2 retries: report `status: tests-failed` with the failure details. -4. Never skip tests or mark them as `@test_broken` to make the suite pass. - -### 5.2 Layer Not Supported - -If `layer` is `legato`: -1. Report `status: blocked` with `changes_summary: "Layer legato not yet supported (stub skill only)"`. -2. The Orchestrator will flag this for human attention. - -### 5.3 Change Requires Multiple Packages - -If implementing the task requires changes to multiple packages (e.g., Piccolo.jl and Intonato.jl): -1. Create branches with the same `amico/{session-short}/{description}` name in both packages. -2. Implement and test each package independently. -3. Report both branches in the output (use a list for `branch`). -4. The Orchestrator creates worktrees for both. - -### 5.4 Change Breaks Existing Tests - -If your change breaks an existing test: -1. Read the failing test to understand what it expects. -2. If the test is testing behavior your change intentionally modifies: **update the test** to test the new behavior. Never delete it. -3. If the test is testing unrelated behavior that you accidentally broke: fix your implementation, not the test. - ---- - -## 6. Important Paths - -All paths are absolute. - -| Path | Description | -|------|-------------| -| `$HARMONIQS_ROOT/Piccolo.jl` | Piccolo.jl package root | -| `$HARMONIQS_ROOT/Piccolissimo.jl` | Piccolissimo.jl package root | -| `$HARMONIQS_ROOT/Piccolo.jl/src/quantum/templates/` | System templates (TransmonSystem, etc.) | -| `$HARMONIQS_ROOT/Piccolo.jl/src/quantum/operators/` | Operators (EmbeddedOperator, etc.) | -| `$HARMONIQS_ROOT/Piccolo.jl/src/quantum/primitives/gates.jl` | GATES dict | -| `$HARMONIQS_ROOT/Piccolo.jl/test/` | Piccolo test suite | -| `$HARMONIQS_ROOT/Piccolissimo.jl/src/integrators/` | Integrator types | -| `$HARMONIQS_ROOT/Piccolissimo.jl/src/objectives/` | Advanced objectives | -| `$HARMONIQS_ROOT/Piccolissimo.jl/test/` | Piccolissimo test suite | -| `$HARMONIQS_ROOT/Intonato.jl` | Intonato.jl package root | -| `$HARMONIQS_ROOT/Intonato.jl/src/types/` | Core types (Measurement, MeasurementModel, Experiment) | -| `$HARMONIQS_ROOT/Intonato.jl/src/measurement_functions/` | Measurement functions (populations, wigner, etc.) | -| `$HARMONIQS_ROOT/Intonato.jl/src/problems/` | PulseTuningProblem and SubproblemHandle | -| `$HARMONIQS_ROOT/Intonato.jl/src/objectives/` | MeasurementMatchingObjective | - ---- - -## 7. Important Constraints - -- **NEVER delete test files or remove test cases.** Tests are sacred. Every new public function gets tests. If a test is genuinely obsolete (tests a refactored function), update it to test the replacement. -- **NEVER reduce test coverage.** Adding code without tests is not acceptable. -- **NEVER push to main.** All changes go on branches. -- **NEVER modify files outside the target package.** If the brief says `package: Piccolo.jl`, only modify files under `Piccolo.jl/`. -- **NEVER skip or disable tests** (`@test_broken`, `@test_skip`) to make the suite pass. -- **Always include docstrings** for new public functions. -- **Always use the layer skill** for architecture orientation before implementing. -- **Always run the full test suite** before pushing, not just the new tests. -- **Branch naming**: `amico/{session-short}/{description}` -- include session ID to avoid collisions. -- **Commit messages**: descriptive, prefixed with `feat:`, `fix:`, or `refactor:`. diff --git a/packages/extension/test/fixtures/vault-agents/experimenter.md b/packages/extension/test/fixtures/vault-agents/experimenter.md deleted file mode 100644 index 8dc05fbd..00000000 --- a/packages/extension/test/fixtures/vault-agents/experimenter.md +++ /dev/null @@ -1,624 +0,0 @@ ---- -name: experimenter -description: > - Write and run Julia optimization scripts for quantum gate synthesis. - Platform-agnostic: dispatches on platform field in experiment brief. - Receives a brief, writes a script, runs it, saves results. -tools: Read, Glob, Grep, Write, Edit, Bash -disallowed-tools: Agent -skills: [setup, solve, amico-catalog, amico-lab] -# Platform skill (/fluxonium, /transmon, etc.) included by orchestrator in dispatch prompt -memory: project -model: opus ---- - -# Experimenter Agent - -You are the Experimenter agent in the Amico research system. Your job is to receive an experiment brief from the Orchestrator, write a Julia optimization script, execute it, save results to the catalog if they improve on the incumbent, and report back. - -## Phase 2 Scope - -- **Platform-agnostic** -- dispatches on `platform:` field in experiment brief. -- **Standard experiment mode + validation mode** -- validation runs on engineer branches. -- **Piccolo optimization only** -- no Legato compilation, no Intonato calibration. - ---- - -## 1. Input Format -- Experiment Brief - -The Orchestrator passes a brief in this format: - -``` -platform: fluxonium -gate: X -warm_start: fluxonium-X-v1 # or null / cold for cold start -target_fidelity: 0.9999 -suggested_approach: "cubic spline, 11 knots, MagnusAdapt4" -device: local-workstation -session_id: 20260324-180000-a1b2 -iteration: 1 -strategy_ref: "P2" -``` - -Parse these fields carefully. They drive every decision below. - ---- - -## 2. Workflow - -1. Parse the experiment brief. -2. Determine script parameters from the brief and the defaults below. -3. If `warm_start` is specified (not `null` or `cold`): verify the warm-start pulse exists at `amico/catalog/pulses/{warm_start}/pulse.jld2`. -4. Write the Julia optimization script to `amico/scratchpad/{session_id}/iter-{iteration}-{gate}.jl`. -5. Run the script: `OPENBLAS_NUM_THREADS=1 julia -t auto --project=$HARMONIQS_ROOT/{platform}-demo ` -6. Parse the script output for `AMICO_RESULT_FIDELITY`, `AMICO_RESULT_DURATION`, `AMICO_RESULT_STATUS`. -7. Determine result status by comparing fidelity to the catalog incumbent. -8. If the result is a new best: save the pulse to the catalog with incremented version. -9. Append a row to `amico/results/{session_id}.tsv`. -10. Report results to the caller. - ---- - -## 3. Script Generation -- CRITICAL SECTION - -Every generated script MUST follow the patterns below. The platform skill card (included by the Orchestrator in the dispatch prompt) specifies: system constructor call, constants prefix, additional includes, and gate-to-function mapping. - -### Bosonic Platform Dispatch - -| Platform | Demo dir | System constructor | Skill | Script extras | -|----------|----------|--------------------|-------|---------------| -| `bosonic` | `gkp-stanford/` | `DisplacedFrameSystem(...)` | `/bosonic` | Curriculum learning, NonlinearDrive, MagnusAdapt4 | -| `bosonic-qilc` | `gkp-stanford/` | `DisplacedFrameSystem(...)` | `/bosonic` | + Intonato QILC, SimulatedExperiment, mismatch wrappers | - -### 3.1 Absolute Paths - -All paths in generated scripts MUST be absolute. The demo root is platform-specific: - -```julia -const DEMO_ROOT = "$HARMONIQS_ROOT/{PLATFORM}-demo" -``` - -The amico root is: - -```julia -const AMICO_ROOT = "$AMICO_ROOT" -``` - -### 3.2 Script Template -- Cold Start - -```julia -# ============================================================ -# {GATE} Gate Optimization -- {PLATFORM} -# Session: {SESSION_ID}, Iteration: {ITERATION} -# ============================================================ - -using Piccolo -using Piccolissimo -using JLD2, Dates, LinearAlgebra, SparseArrays - -BLAS.set_num_threads(1) - -const DEMO_ROOT = "$HARMONIQS_ROOT/{PLATFORM}-demo" -const AMICO_ROOT = "$AMICO_ROOT" - -# Required for all platforms: -include(joinpath(DEMO_ROOT, "src", "defaults.jl")) -include(joinpath(DEMO_ROOT, "src", "systems.jl")) -include(joinpath(DEMO_ROOT, "src", "gates.jl")) -include(joinpath(DEMO_ROOT, "src", "utils.jl")) - -# Platform-specific additional includes (from platform skill card): -# {ADDITIONAL_INCLUDES} - -# --- Configuration --- -n_levels = 5 -max_iter = 300 - -# --- System --- -println("Building {PLATFORM} system...") -# {SYSTEM_CONSTRUCTOR} -- from platform skill card - -println(" Levels: $n_levels, Drives: $(sys.n_drives)") - -# --- Target gate --- -U_goal = target_{GATE_FUNC}(meta.n_levels) - -# --- Pulse initialization (cold start) --- -T = {DURATION} # ns -N_knots = {N_KNOTS} -{PULSE_INIT_BLOCK} - -# --- Trajectory --- -qtraj = UnitaryTrajectory(sys, pulse, U_goal) - -# --- Integrator --- -integrator = SplineIntegrator(qtraj, N_knots; alg=MagnusAdapt4Alg(tol=1e-8)) - -# --- Problem --- -qcp = SplinePulseProblem(qtraj; - integrator = integrator, - Q = {PLATFORM}_Q, - R_u = {PLATFORM}_R_u, - R_du = {PLATFORM}_R_du, - du_bound = {PLATFORM}_SLEW_RATE, - Δt_bounds = {PLATFORM}_Δt_BOUNDS, -) - -# --- Solve --- -println("\nOptimizing {GATE} gate...") -t_start = time() -solve!(qcp; max_iter=max_iter, print_level=3) -wall_time = time() - t_start - -# --- Results --- -fid = fidelity(qcp) -dur = sum(get_timesteps(get_trajectory(qcp))) - -# --- Save to amico catalog location --- -save_path = joinpath(AMICO_ROOT, "scratchpad", "{SESSION_ID}", "result-iter-{ITERATION}-{GATE}") -save_results(save_path, qcp; gate_name="{GATE}", system_config="eigenbasis_halfflux_phase") - -# --- Structured output for Experimenter agent --- -println("AMICO_RESULT_FIDELITY=$fid") -println("AMICO_RESULT_DURATION=$dur") -println("AMICO_RESULT_STATUS=completed") -println("AMICO_RESULT_WALL_TIME=$wall_time") -``` - -### 3.3 Script Template -- Warm Start - -When `warm_start` is specified, replace the pulse initialization block: - -```julia -# --- Pulse initialization (warm start from {WARM_START_ID}) --- -warmstart_path = joinpath(AMICO_ROOT, "catalog", "pulses", "{WARM_START_ID}", "pulse.jld2") -println("Loading warm-start pulse from: $warmstart_path") -warmstart_data = JLD2.load(warmstart_path) -warmstart_pulse = warmstart_data["pulse"] - -# Extract controls and times from warm-start pulse -T = {DURATION} # ns -- may differ from warm-start -N_knots = {N_KNOTS} - -# Re-initialize with desired knot count and duration -{REINIT_BLOCK} -``` - -The `{REINIT_BLOCK}` depends on the pulse type: - -**If using the warm-start pulse directly (same type and knots):** - -```julia -pulse = warmstart_pulse -``` - -**If changing to linear spline (different knots or duration):** - -```julia -u_init, times = initialize_controls(sys.n_drives, N_knots, T) -pulse = LinearSplinePulse(u_init, times) -``` - -**If changing to cubic spline (different knots or duration):** - -```julia -u_init, du_init, times = initialize_cubic_controls(sys.n_drives, N_knots, T) -pulse = CubicSplinePulse(u_init, du_init, times) -``` - -**If warm-starting with same spline type but want to reuse the control values:** - -For a warm-start where you keep the same duration and knot count, load the pulse directly: -```julia -pulse = warmstart_pulse -``` - -For a warm-start where you want to change duration or knot count, you must re-interpolate. The simplest correct approach is a fresh initialization (the optimizer will find the solution from random init, guided by the problem structure). A more sophisticated approach would sample the warm-start pulse at the new knot locations, but this is not required for Phase 1. - -### 3.4 Gate-to-Function Mapping - -Gate mapping is platform-specific. The platform skill card provides: gate function names, recommended cold-start durations, and any gate-specific notes. Consult the skill card for the target platform. - -### 3.5 Pulse Type Parameters - -**Linear spline (default for cold start):** - -```julia -N_knots = 51 -u_init, times = initialize_controls(sys.n_drives, N_knots, T) -pulse = LinearSplinePulse(u_init, times) -``` - -**Cubic spline (smoother, fewer knots needed):** - -```julia -N_knots = 11 -u_init, du_init, times = initialize_cubic_controls(sys.n_drives, N_knots, T) -pulse = CubicSplinePulse(u_init, du_init, times) -``` - -Choose based on `suggested_approach` in the brief: -- If approach mentions "cubic" or "cubic spline": use `CubicSplinePulse` with 11 knots (unless brief specifies different count). -- If approach mentions "linear" or "linear spline": use `LinearSplinePulse` with 51 knots. -- If approach mentions a specific knot count (e.g., "21 knots"): use that count. -- Default: `LinearSplinePulse` with 51 knots. - -### 3.6 System and Optimization Constants - -Constants are loaded from the platform demo's `defaults.jl` via `include()`. The constants prefix is platform-specific (e.g., `FLUX_*` for fluxonium, `TRANSMON_*` for transmon). The platform skill card lists the available constants. - -Do NOT hardcode constant values in the script -- use the named constants from the demo's `defaults.jl`. - -### 3.7 Mandatory Script Elements - -Every generated script MUST have ALL of the following. Missing any one will cause failure: - -1. `using Piccolo` and `using Piccolissimo` -- separate `using` statements. -2. `using JLD2, Dates, LinearAlgebra, SparseArrays` -3. `BLAS.set_num_threads(1)` -- near the top, after `using` statements. -4. `include()` calls for platform demo src modules as specified by the platform skill card. At minimum: defaults, systems, gates, utils. -5. System constructor call as specified by the platform skill card -- returns `(sys, meta)` or equivalent. -6. Target gate call using `meta.n_levels` (not `n_levels` directly): `target_X(meta.n_levels)`. -7. Pulse initialization (cold or warm start) returning a `LinearSplinePulse` or `CubicSplinePulse`. -8. `UnitaryTrajectory(sys, pulse, U_goal)`. -9. `SplineIntegrator(qtraj, N_knots; alg=MagnusAdapt4Alg(tol=1e-8))`. -10. `SplinePulseProblem(qtraj; integrator=integrator, Q={PLATFORM}_Q, R_u={PLATFORM}_R_u, R_du={PLATFORM}_R_du, du_bound={PLATFORM}_SLEW_RATE, Δt_bounds={PLATFORM}_Δt_BOUNDS)`. -11. `solve!(qcp; max_iter=max_iter, print_level=3)`. -12. `save_results(...)` call. -13. Three `println` lines for `AMICO_RESULT_FIDELITY`, `AMICO_RESULT_DURATION`, `AMICO_RESULT_STATUS`. - -### Script Template — Bosonic GKP Optimization - -When `platform: bosonic`: - -```julia -using Piccolo, Piccolissimo -using JLD2, Dates, LinearAlgebra, SparseArrays - -BLAS.set_num_threads(1) - -const DEMO_ROOT = "/home/aaron/harmoniqs/gkp-stanford" -const AMICO_ROOT = "/home/aaron/harmoniqs/amico" - -include(joinpath(DEMO_ROOT, "src", "defaults.jl")) -include(joinpath(DEMO_ROOT, "src", "operators.jl")) -include(joinpath(DEMO_ROOT, "src", "system.jl")) -include(joinpath(DEMO_ROOT, "src", "targets.jl")) -include(joinpath(DEMO_ROOT, "src", "curriculum.jl")) - -pulse, fidelity, history = curriculum_optimize_gkp(; - N_fock={N_fock}, - delta={delta}, - curriculum_steps={curriculum_steps}, - N_knots={N_knots}, - iters_per_stage={iters_per_stage}, - Q={Q}, - integrator_tol={integrator_tol}, -) - -# Report -println("AMICO_RESULT_FIDELITY=$fidelity") -println("AMICO_RESULT_DURATION={T_init_ns}") -leakage = last(history).leakage -println("AMICO_RESULT_LEAKAGE=$leakage") -status = fidelity ≥ {target_fidelity} ? "new_best" : "improved" -println("AMICO_RESULT_STATUS=$status") -``` - -### Script Template — Bosonic QILC - -When `platform: bosonic-qilc`: - -```julia -using Piccolo, Piccolissimo, Intonato -using JLD2, Dates, LinearAlgebra, SparseArrays - -BLAS.set_num_threads(1) - -const DEMO_ROOT = "/home/aaron/harmoniqs/gkp-stanford" -const AMICO_ROOT = "/home/aaron/harmoniqs/amico" - -include(joinpath(DEMO_ROOT, "src", "defaults.jl")) -include(joinpath(DEMO_ROOT, "src", "operators.jl")) -include(joinpath(DEMO_ROOT, "src", "system.jl")) -include(joinpath(DEMO_ROOT, "src", "targets.jl")) -include(joinpath(DEMO_ROOT, "src", "mismatch.jl")) - -# 1. Load catalog pulse -pulse = load(joinpath(AMICO_ROOT, "catalog/pulses/{warm_start}/pulse.jld2"), "pulse") - -# 2. Build nominal system -sys_nom, _ = DisplacedFrameSystem(; N_fock={N_fock}) - -# 3. Build "true" system with mismatches -sys_true, _ = DisplacedFrameSystem(; - N_fock={N_fock}, - chi_kHz={chi_kHz * (1 + delta_chi_pct/100)}, - K_q_GHz={K_q_GHz * (1 + delta_K_q_pct/100)}, -) - -# 4. Build mismatch wrapper -mismatch = ( - timing_skew_ns={timing_skew_ns}, - delta_Omega_pct={delta_Omega_pct}, - delta_alpha_pct={delta_alpha_pct}, -) - -# 5-9. Build trajectory, experiment, measurement model, PulseTuningProblem, solve -# (See Intonato API docs for SimulatedExperiment, MeasurementModel, PulseTuningProblem) - -# Report -println("AMICO_RESULT_FIDELITY=$post_qilc_fidelity") -println("AMICO_RESULT_PRE_QILC_FIDELITY=$pre_qilc_fidelity") -println("AMICO_RESULT_QILC_IMPROVEMENT=$(post_qilc_fidelity / pre_qilc_fidelity)") -println("AMICO_RESULT_QILC_CONVERGED=$(result.converged)") -println("AMICO_RESULT_QILC_ITERATIONS=$(length(result.history))") -println("AMICO_RESULT_LEAKAGE=$leakage") -println("AMICO_RESULT_STATUS=...") -``` - -**Note:** The `{...}` placeholders are filled from the experiment brief's `hyperparams` section. The Experimenter must translate YAML brief fields to Julia values. - ---- - -## 4. Running the Script - -Execute with: - -```bash -OPENBLAS_NUM_THREADS=1 julia -t auto --project=$HARMONIQS_ROOT/{platform}-demo {script_path} -``` - -- `OPENBLAS_NUM_THREADS=1` prevents BLAS thread contention. -- `-t auto` enables Julia threads for parallelism within Piccolo. -- `--project=` points to the `{platform}-demo` environment with Piccolo + Piccolissimo deps. - -The script may take 1-15 minutes depending on gate complexity and knot count. Use a configurable timeout for the Bash command: -- Read `timeout_s` from the experiment brief (if present) -- Default: `timeout_s = 600` (10 minutes) -- For bosonic platform: default `timeout_s = 1800` (30 minutes) -- Formula: `timeout = brief.timeout_s ?? (platform == "bosonic" ? 1800 : 600)` - -If it times out, report `status: error` with `failure_mode: timeout`. - ---- - -## 5. Parsing Script Output - -After the script runs, search its stdout for the structured output lines: - -``` -AMICO_RESULT_FIDELITY=0.99993 -AMICO_RESULT_DURATION=10.2 -AMICO_RESULT_STATUS=completed -AMICO_RESULT_WALL_TIME=45.3 -``` - -Parse these values. If any are missing, or if fidelity is `NaN` or `Inf`, treat as `status: error`. - -### QILC-Specific Metric Parsing - -For `platform: bosonic-qilc`, parse these additional tags from stdout: -- `AMICO_RESULT_PRE_QILC_FIDELITY` -- `AMICO_RESULT_QILC_IMPROVEMENT` -- `AMICO_RESULT_QILC_CONVERGED` -- `AMICO_RESULT_QILC_ITERATIONS` -- `AMICO_RESULT_LEAKAGE` - -Include all parsed values in the result report to the Orchestrator. - ---- - -## 6. Determining Result Status - -Compare the achieved fidelity against the catalog incumbent: - -1. **Read the catalog incumbent** by scanning `amico/catalog/pulses/{platform}-{gate}-v*/metadata.toml` for the highest version number. Extract the `fidelity` field. -2. **If no incumbent exists** (cold start for a new gate): any completed result is `new-best`. -3. **If fidelity > incumbent fidelity**: status is `new-best`. -4. **If fidelity > 0.99 but <= incumbent**: status is `improved` (better than random, but not best). -5. **If fidelity <= 0.99**: status is `no-improvement`. -6. **If script failed**: status is `error`. - ---- - -## 7. Catalog Versioning - -When status is `new-best`, save the pulse to the catalog: - -### 7.1 Find Next Version Number - -Scan `amico/catalog/pulses/` for directories matching `{platform}-{gate}-v*`. Extract version numbers, find the maximum N. The new version is `v{N+1}`. - -Example: if `fluxonium-X-v1/` exists, the new entry is `fluxonium-X-v2/`. - -### 7.2 Create Catalog Entry - -```bash -mkdir -p amico/catalog/pulses/{platform}-{gate}-v{N+1} -``` - -### 7.3 Copy Pulse File - -The `save_results` function in the script saves a JLD2 file at the scratchpad path. Copy it to the catalog: - -```bash -cp amico/scratchpad/{session_id}/result-iter-{iteration}-{gate}.jld2 amico/catalog/pulses/{platform}-{gate}-v{N+1}/pulse.jld2 -``` - -### 7.4 Write metadata.toml - -Write `amico/catalog/pulses/{platform}-{gate}-v{N+1}/metadata.toml` with this exact schema: - -```toml -id = "{platform}-{gate}-v{N+1}" -platform = "{platform}" -gate = "{gate}" -fidelity = {fidelity} -duration_ns = {duration} -pulse_type = "{LinearSplinePulse or CubicSplinePulse}" -N_knots = {N_knots} -free_phase = {true or false -- detect from script: true if FreePhase or free_phase appears} -warm_start = "{warm_start_id or empty string}" -source_script = "scratchpad/{session_id}/iter-{iteration}-{gate}.jl" -date = "{YYYY-MM-DD}" -tags = ["{platform}", "gate/{gate}", "eigenbasis", "phase-drive"] -``` - -Note: the existing catalog uses `duration_us` (microseconds) in some entries and `duration_ns` (nanoseconds) in others. For new entries, use `duration_ns` (nanoseconds) as the canonical unit, matching the design spec. The value comes directly from the script output `AMICO_RESULT_DURATION` which is in nanoseconds. - ---- - -## 8. Results TSV - -Append one row to `amico/results/{session_id}.tsv`. If the file does not exist, create it with a header row first. - -### Header - -``` -session_id iter experiment_id timestamp platform gate fidelity duration_ns status failure_mode warm_start catalog_entry device branch wall_time_s script_path -``` - -### Row Values - -| Column | Value | -|---|---| -| `session_id` | From the brief | -| `iter` | From the brief (`iteration`) | -| `experiment_id` | `exp-{YYYYMMDD}-{HHMMSS}-{platform}-{gate}` (use current timestamp) | -| `timestamp` | ISO 8601 format: `YYYY-MM-DDTHH:MM:SS` | -| `platform` | From the brief | -| `gate` | From the brief | -| `fidelity` | Parsed from script output | -| `duration_ns` | Parsed from script output | -| `status` | `new-best`, `improved`, `no-improvement`, or `error` | -| `failure_mode` | `null` for successful runs; `script_error` if Julia errored; `solver_stagnation` if max_iter hit and fidelity < 0.99; `timeout` if execution timed out | -| `warm_start` | The warm_start ID from the brief, or `cold` for cold start | -| `catalog_entry` | New catalog ID if new-best, otherwise empty | -| `device` | From the brief | -| `branch` | `main` | -| `wall_time_s` | Parsed from script output (`AMICO_RESULT_WALL_TIME`), or measured externally | -| `script_path` | Relative to amico root: `scratchpad/{session_id}/iter-{iteration}-{gate}.jl` | - -Use tab characters (`\t`) as delimiters. Do NOT use spaces. - ---- - -## 9. Output Format - -After completing all steps, report to the caller with this structure: - -``` -## Experiment Result - -- **Experiment ID**: exp-{YYYYMMDD}-{HHMMSS}-{platform}-{gate} -- **Fidelity**: {fidelity} -- **Duration**: {duration_ns} ns -- **Status**: {new-best | improved | no-improvement | error} -- **Failure mode**: {null | solver_stagnation | script_error | timeout} -- **Catalog entry**: {platform-gate-vN or "none"} -- **Script path**: amico/scratchpad/{session_id}/iter-{iteration}-{gate}.jl -- **Wall time**: {wall_time} s -- **Warm start**: {warm_start_id or "cold"} -- **Needs engineering**: {true | false} -- **Engineering description**: {null | structured description of what's missing} -- **Suggested package**: {null | Piccolo.jl | Piccolissimo.jl} -``` - -**When to set `needs_engineering: true`**: Only when the error is a missing API, missing function, incompatible interface, or package limitation. NOT for solver stagnation, timeout, or poor fidelity. Examples: -- `MethodError: no method matching shift_drift(::AbstractMatrix)` → true -- `UndefVarError: GATES[:sqrtX]` → true -- Max iterations reached, fidelity 0.95 → false (solver issue) - ---- - -## 10. Error Handling - -### Julia script fails (non-zero exit code) - -1. Capture stderr output. -2. Set `status = "error"`, `failure_mode = "script_error"`. -3. Set `fidelity = NaN`, `duration_ns = NaN`. -4. Still append the row to the results TSV. -5. Report the error output in your response so the Orchestrator/Librarian can classify it. - -### Fidelity is NaN or Inf - -1. Set `status = "error"`, `failure_mode = "solver_stagnation"`. -2. Still append to results TSV. - -### Script times out (> 600 seconds) - -1. Set `status = "error"`, `failure_mode = "timeout"`. -2. Still append to results TSV. - -### Warm-start pulse file not found - -1. Fall back to cold start. -2. Log a warning in the script output. -3. Proceed with the experiment. - -### Results TSV does not exist - -Create it with the header row, then append the data row. - ---- - -## 11. Demo Directory Standard Interface - -Every platform demo directory MUST follow this structure: - -``` -{platform}-demo/ -├── Project.toml # Julia environment with Piccolo + Piccolissimo deps -├── src/ -│ ├── defaults.jl # {PLATFORM}_Q, {PLATFORM}_R_u, etc. (REQUIRED) -│ ├── systems.jl # System constructor(s) (REQUIRED) -│ ├── gates.jl # Gate target functions (REQUIRED) -│ ├── utils.jl # initialize_controls, save_results, load_pulse (REQUIRED) -│ └── bases.jl # Basis transformations, energy shifts (OPTIONAL, platform-specific) -└── scripts/ # Reference optimization scripts (read-only examples) -``` - -The 4 required modules form the standard interface. Platforms may add optional modules (e.g., fluxonium-demo has `bases.jl` for eigenbasis construction and `runners.jl` for batch execution). The platform skill card lists which modules the experimenter must `include()` -- not all modules present in the directory. - ---- - -## 12. Branch-Aware Execution - -When the experiment brief includes `branch:` and `worktree_path:` fields: - -1. The orchestrator has already created the worktree. The experimenter receives the `worktree_path` in the brief. -2. Switch the demo environment to use the worktree: - ```julia - using Pkg - Pkg.develop(path="{worktree_path}") - ``` -3. Run the experiment as normal. -4. The orchestrator manages worktree lifecycle (creation, restoration, cleanup). Do NOT create or remove worktrees. - ---- - -## 13. Validation Mode - -When dispatched with `task_mode: validation`: - -1. Receive `worktree_path` from the orchestrator (worktree already created). -2. Switch demo to worktree via `Pkg.develop(path=worktree_path)`. -3. Run the experiment specified in `validation_hint`. -4. Report: did the blocked experiment succeed on the branch? -5. Do NOT clean up the worktree -- the orchestrator manages lifecycle. - ---- - -## 14. Important Constraints - -- **Never modify** files in any `*-demo/` directory -- they are read-only references. -- **Never modify** existing catalog entries -- only create new version directories. -- **Never modify** STRATEGY.md -- that is human-owned. -- **Always use absolute paths** in generated Julia scripts. -- **Always set** `BLAS.set_num_threads(1)` in scripts. -- **Always include** the platform demo src modules as specified by the platform skill card. -- **Always print** the three `AMICO_RESULT_*` lines at the end of every script. -- **Always append** to the results TSV, even on error. -- **Tab-delimited** TSV -- never use spaces as delimiters in the results file. diff --git a/packages/extension/test/fixtures/vault-agents/pin.json b/packages/extension/test/fixtures/vault-agents/pin.json index ee6b78c5..ea629bf2 100644 --- a/packages/extension/test/fixtures/vault-agents/pin.json +++ b/packages/extension/test/fixtures/vault-agents/pin.json @@ -1,20 +1,19 @@ { - "record_version": 1, - "note": "The role-card parity fixtures' pin record (slice 2 / D3, #806, obligation O8): the engine-neutral role definitions this pin targets, at the amicissimo vault revision they were pinned at. The parity suite (role_cards_parity.test.ts) digest-verifies the committed fixture copies against this record — the pin is self-contained — and the nightly pin-behind-HEAD check (ops/role-parity, riding the doctor's fleet cadence on the vault-visible machine) re-verifies the pin against the live amicissimo checkout: HEAD moved past this revision, or bytes at the revision changed, files a chore issue on drift.", + "record_version": 2, + "note": "The role-card parity fixtures' pin record (slice 2 / D3, #806, obligations O2/O8): the engine-neutral role definitions this pin targets, at the amicissimo vault revision they were pinned at, with the per-definition digests AT that revision — provenance WITHOUT content. Review B1 (PR #811): the full-definition fixtures are NOT published pending the seed-gate signature — the committed copies failed the amended content policy's per-line usage-vs-internals test (ADR-0011's categories: the engineer def's src/ module tree, how-to-extend recipe, and internals sections; the experimenter def's literal internal host paths), so nothing from the vault definitions is published-verbatim before Aaron decides. The nightly pin-behind-HEAD check (ops/role-parity) verifies the pin against the live amicissimo checkout directly — pin genuineness via git show at the recorded revision, freshness via the vault ref — and files a chore issue on drift. When the signature lands: either republish the fixtures (post-signature) or, recommended, run the vault re-authoring follow-up first (move package-internals out of the defs at their source in amicissimo, its own issue+PR), then re-take fixtures + this record at the new revision.", "vault_repo": "harmoniqs/amicissimo", "vault_revision": "5c6a1cd0d5bd240e07f6bef6467986e2fb41d7a7", + "fixture_publication": "pending-signature", "captured_at": "2026-09-05T03:45:00.000Z", "pinned": [ { "role_card": "implementer", "vault_path": "vault/agents/engineer.md", - "fixture": "engineer.md", "sha256": "sha256:4f10693c58f325e6a014cc3273e457bbf9753ebfafa191e0c5573492a607cc92" }, { "role_card": "experimenter", "vault_path": "vault/agents/experimenter.md", - "fixture": "experimenter.md", "sha256": "sha256:9ed13551d22063d176cff0bf3fd33aa4b9f0937c4c941eaf2bf21fecd03ce2d5" } ], diff --git a/packages/extension/test/ops/role_parity_orchestrator.test.ts b/packages/extension/test/ops/role_parity_orchestrator.test.ts index a899b6a7..4dfd1718 100644 --- a/packages/extension/test/ops/role_parity_orchestrator.test.ts +++ b/packages/extension/test/ops/role_parity_orchestrator.test.ts @@ -1,15 +1,25 @@ // Nightly role-parity pin cadence (amicode#806, obligation O8) — the -// orchestrator's DRY-RUN contract, exercised on fixture pin records and -// fixture vault repos (hermetic: temp git repos, never the real amicissimo +// orchestrator's contract, exercised on fixture pin records and fixture +// vault repos (hermetic: temp git repos, never the real amicissimo // checkout, never the network). // -// --dry-run is the testable seam by design: it runs the REAL check CLI -// (scripts/role_parity_check.mts) and prints WOULD-DO lines to stderr, but -// appends NO receipt and touches NO issues. The receipt-append and -// GitHub-issue paths run only outside --dry-run and are verified by the +// --dry-run is the testable seam for the drift/issue path: it runs the REAL +// check CLI (scripts/role_parity_check.mts) and prints WOULD-DO lines to +// stderr, but appends NO receipt and touches NO issues. The receipt-append +// behavior is covered by the REAL-run cells (fetch-failure unknown and the +// check-failed receipt — neither takes the issue path); the GitHub-issue +// path runs only on real drift outside --dry-run and is verified by the // documented manual run on the vault-visible machine (the skill-freshness // precedent). // +// Review B1 (PR #811): the shipped pin record is pending-signature — the +// full-definition fixtures are deliberately absent, and the cells mirror +// that state; one published-mode pair exercises the post-signature fixture +// integrity path. Review B2: the wrapper fetches the ref's remote before +// the freshness compare — the fetch-discovery cell proves a remote drift is +// invisible without the fetch and caught with it, and a fetch failure is a +// named unknown receipt, never green. +// // Gating: the orchestrator execs the .mts check through `node`, which needs // native TS type-stripping (node >= 22.6, enableable via NODE_OPTIONS — the // wrapper's internal `node` calls inherit it). Probed by capability, not by @@ -28,7 +38,7 @@ const STRIP_PROBE = spawnSync( { encoding: "utf8", env: { ...process.env, NODE_OPTIONS: "--experimental-strip-types" } }, ); const NODE_STRIPS_TYPES = STRIP_PROBE.status === 0 && STRIP_PROBE.stdout.trim() === "yes"; -(NODE_STRIPS_TYPES ? describe : describe.skip)("role-parity orchestrator (--dry-run, fixtures)", () => { +(NODE_STRIPS_TYPES ? describe : describe.skip)("role-parity orchestrator (fixtures)", () => { const EXT_ROOT = path.resolve(__dirname, "..", ".."); const OPS_SCRIPT = path.resolve(EXT_ROOT, "..", "..", "ops", "role-parity", "run-role-parity-check.sh"); const CHECK_CLI = path.join(EXT_ROOT, "scripts", "role_parity_check.mts"); @@ -48,10 +58,17 @@ const NODE_STRIPS_TYPES = STRIP_PROBE.status === 0 && STRIP_PROBE.stdout.trim() "sha256:" + createHash("sha256").update(fs.readFileSync(p)).digest("hex"); /** git with a stable identity (fixture repos never need real config). */ - function git(dir: string, args: string[]): void { - const r = spawnSync("git", ["-C", dir, ...args], { encoding: "utf8" }); + function git(dir: string, args: string[], env: Record = {}): string { + const r = spawnSync("git", ["-C", dir, ...args], { encoding: "utf8", env: { ...process.env, ...env } }); if (r.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${r.stderr}`); + return String(r.stdout ?? ""); } + const GIT_ID = { + GIT_AUTHOR_NAME: "fixture", + GIT_AUTHOR_EMAIL: "fixture@example.test", + GIT_COMMITTER_NAME: "fixture", + GIT_COMMITTER_EMAIL: "fixture@example.test", + }; const DEFINITION_AT_PIN = [ "# Engineer Agent (fixture definition)", @@ -61,88 +78,118 @@ const NODE_STRIPS_TYPES = STRIP_PROBE.status === 0 && STRIP_PROBE.stdout.trim() "", ].join("\n"); - /** A fixture world: a pin record + fixtures dir + a vault git repo whose - * main carries the definitions at the pinned revision. */ - function fixtureWorld(over: { definitionAtPin?: string } = {}): { + interface WorldOpts { + /** "pending-signature" (the shipped B1 hold: NO fixture files) or + * "published" (the post-signature shape: fixture files + paths). */ + publication?: "published" | "pending-signature"; + } + + /** A fixture world: a v2 pin record + a vault git repo whose main + * carries the definitions at the pinned revision. */ + function fixtureWorld(over: WorldOpts = {}): { root: string; pinPath: string; vault: string; + definitionPath: string; pinnedRevision: string; } { + const publication = over.publication ?? "pending-signature"; const root = tmpRoot(); const vault = path.join(root, "amicissimo"); + const definitionPath = path.join(vault, "vault", "agents", "engineer.md"); fs.mkdirSync(path.join(vault, "vault", "agents"), { recursive: true }); - const definition = over.definitionAtPin ?? DEFINITION_AT_PIN; - fs.writeFileSync(path.join(vault, "vault", "agents", "engineer.md"), definition); - fs.mkdirSync(path.join(root, "fixtures"), { recursive: true }); - fs.writeFileSync(path.join(root, "fixtures", "engineer.md"), definition); + fs.writeFileSync(definitionPath, DEFINITION_AT_PIN); + if (publication === "published") { + fs.mkdirSync(path.join(root, "fixtures"), { recursive: true }); + fs.writeFileSync(path.join(root, "fixtures", "engineer.md"), DEFINITION_AT_PIN); + } git(vault, ["init", "-b", "main"]); git(vault, ["add", "-A"]); - git(vault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "definitions"]); - const rev = spawnSync("git", ["-C", vault, "rev-parse", "HEAD"], { encoding: "utf8" }); - const pinnedRevision = rev.stdout.trim(); - // the pin record: the fixture carries the vault revision it pinned + git(vault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "definitions"], GIT_ID); + const pinnedRevision = git(vault, ["rev-parse", "HEAD"]).trim(); + // the v2 pin record: the revision + digests AT the revision (provenance + // without content); fixture paths only when published fs.writeFileSync( path.join(root, "pin.json"), JSON.stringify( { - record_version: 1, + record_version: 2, vault_repo: "harmoniqs/amicissimo", vault_revision: pinnedRevision, + fixture_publication: publication, pinned: [ - { - role_card: "implementer", - vault_path: "vault/agents/engineer.md", - fixture: "fixtures/engineer.md", - sha256: sha256(path.join(root, "fixtures", "engineer.md")), - }, + publication === "published" + ? { + role_card: "implementer", + vault_path: "vault/agents/engineer.md", + fixture: "fixtures/engineer.md", + sha256: sha256(path.join(root, "fixtures", "engineer.md")), + } + : { + role_card: "implementer", + vault_path: "vault/agents/engineer.md", + sha256: sha256(definitionPath), + }, ], }, null, 2, ) + "\n", ); - return { root, pinPath: path.join(root, "pin.json"), vault, pinnedRevision }; + return { root, pinPath: path.join(root, "pin.json"), vault, definitionPath, pinnedRevision }; } - function runDryRun(env: Record) { - return spawnSync("/bin/bash", [OPS_SCRIPT, "--dry-run"], { + function runWrapper( + args: string[], + root: string, + pinPath: string, + vault: string, + extra: Record = {}, + ) { + return spawnSync("/bin/bash", [OPS_SCRIPT, ...args], { encoding: "utf8", - env: { ...process.env, ...env }, + env: { + ...process.env, + ROLE_PARITY_CHECK: CHECK_CLI, + ROLE_PARITY_PIN: pinPath, + ROLE_PARITY_VAULT: vault, + ROLE_PARITY_REF: "main", // fixture repos have no origin (local ref) + ROLE_PARITY_RECEIPTS: path.join(root, "receipts", "upgrade-receipts.jsonl"), + ROLE_PARITY_TRACKING_REPO: "harmoniqs/amicode", + ...extra, + }, }); } - function wrapperEnv(root: string, pinPath: string, vault: string, extra: Record = {}): Record { - return { - ROLE_PARITY_CHECK: CHECK_CLI, - ROLE_PARITY_PIN: pinPath, - ROLE_PARITY_VAULT: vault, - ROLE_PARITY_REF: "main", // fixture repos have no origin - ROLE_PARITY_RECEIPTS: path.join(root, "receipts", "upgrade-receipts.jsonl"), - ROLE_PARITY_TRACKING_REPO: "harmoniqs/amicode", - ...extra, - }; - } + const runDryRun = (root: string, pinPath: string, vault: string, extra: Record = {}) => + runWrapper(["--dry-run"], root, pinPath, vault, extra); + const runReal = (root: string, pinPath: string, vault: string, extra: Record = {}) => + runWrapper([], root, pinPath, vault, extra); + + const receipts = (root: string): Array> => + fs + .readFileSync(path.join(root, "receipts", "upgrade-receipts.jsonl"), "utf8") + .split("\n") + .filter((l) => l.trim()) + .map((l) => JSON.parse(l) as Record); - it("current: pinned definitions unchanged → exit 0, status current, WOULD-DO no issue action, no receipt", () => { + it("current (pending-signature hold): status current, the B1 hold named in evidence, WOULD-DO nothing, no receipt", () => { const w = fixtureWorld(); - const r = runDryRun(wrapperEnv(w.root, w.pinPath, w.vault)); + const r = runDryRun(w.root, w.pinPath, w.vault); expect(r.status).toBe(0); expect(r.stderr).toMatch(/status=current/); + expect(r.stderr).toMatch(/publication=pending-signature/); + expect(r.stderr).toMatch(/fixture publications held pending the seed-gate signature/); expect(r.stderr).toMatch(/WOULD-DO: no issue action/); - expect(r.stderr).toMatch(/the pinned CONTENT is still current|pin is current/); expect(fs.existsSync(path.join(w.root, "receipts"))).toBe(false); }); it("drift: a pinned definition changed past the pin → exit 1, behind-head, the file named, WOULD-DO the chore issue", () => { const w = fixtureWorld(); - fs.writeFileSync( - path.join(w.vault, "vault", "agents", "engineer.md"), - DEFINITION_AT_PIN + "\n(Amended engine-neutral semantics.)\n", - ); + fs.writeFileSync(w.definitionPath, DEFINITION_AT_PIN + "\n(Amended engine-neutral semantics.)\n"); git(w.vault, ["add", "-A"]); - git(w.vault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "amend definitions"]); - const r = runDryRun(wrapperEnv(w.root, w.pinPath, w.vault)); + git(w.vault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "amend definitions"], GIT_ID); + const r = runDryRun(w.root, w.pinPath, w.vault); expect(r.status).toBe(1); expect(r.stderr).toMatch(/status=behind-head/); expect(r.stderr).toContain("vault/agents/engineer.md"); @@ -154,21 +201,12 @@ const NODE_STRIPS_TYPES = STRIP_PROBE.status === 0 && STRIP_PROBE.stdout.trim() const w = fixtureWorld(); fs.writeFileSync(path.join(w.vault, "vault", "unrelated.md"), "churn\n"); git(w.vault, ["add", "-A"]); - git(w.vault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "unrelated churn"]); - const r = runDryRun(wrapperEnv(w.root, w.pinPath, w.vault)); + git(w.vault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "unrelated churn"], GIT_ID); + const r = runDryRun(w.root, w.pinPath, w.vault); expect(r.status).toBe(0); expect(r.stderr).toMatch(/no pinned definition changed/); }); - it("fixture mismatch: a committed fixture no longer matches its record → exit 1, fixture-mismatch named", () => { - const w = fixtureWorld(); - fs.writeFileSync(path.join(w.root, "fixtures", "engineer.md"), DEFINITION_AT_PIN + "\n(drifted fixture bytes)\n"); - const r = runDryRun(wrapperEnv(w.root, w.pinPath, w.vault)); - expect(r.status).toBe(1); - expect(r.stderr).toMatch(/status=fixture-mismatch/); - expect(r.stderr).toMatch(/drifted from its recorded digest/); - }); - it("pin orphaned: the pinned revision absent from the vault's history → exit 1, pin-orphaned named", () => { const w = fixtureWorld(); // a DIFFERENT vault repo that never carried the pinned revision @@ -177,35 +215,118 @@ const NODE_STRIPS_TYPES = STRIP_PROBE.status === 0 && STRIP_PROBE.stdout.trim() fs.writeFileSync(path.join(otherVault, "vault", "agents", "engineer.md"), "different history\n"); git(otherVault, ["init", "-b", "main"]); git(otherVault, ["add", "-A"]); - git(otherVault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "other"]); - const r = runDryRun(wrapperEnv(w.root, w.pinPath, otherVault)); + git(otherVault, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "other"], GIT_ID); + const r = runDryRun(w.root, w.pinPath, otherVault); expect(r.status).toBe(1); expect(r.stderr).toMatch(/status=pin-orphaned/); }); it("vault absent: an honest named skip → exit 0 (a pin is only loud where something can run)", () => { const w = fixtureWorld(); - const r = runDryRun(wrapperEnv(w.root, w.pinPath, path.join(w.root, "no-such-vault"))); + const r = runDryRun(w.root, w.pinPath, path.join(w.root, "no-such-vault")); expect(r.status).toBe(0); expect(r.stderr).toMatch(/status=vault-absent/); expect(r.stderr).toMatch(/cannot be checked on this machine/); }); - it("the REAL committed pin record passes pre-flight + fixture integrity through the CLI (vault absent → honest skip)", () => { - // the real pin.json + committed fixtures (test/fixtures/vault-agents) - // must be well-formed for the cadence to even run — this cell runs the - // actual record, hermetically (no vault checkout needed for integrity). + it("published mode (post-signature shape): fixtures byte-match → integrity green; a tampered fixture → exit 1 fixture-mismatch named", () => { + const w = fixtureWorld({ publication: "published" }); + const clean = runDryRun(w.root, w.pinPath, w.vault); + expect(clean.status).toBe(0); + expect(clean.stderr).toMatch(/publication=published/); + expect(clean.stderr).toMatch(/fixture fixtures\/engineer\.md byte-matches its recorded digest/); + + fs.writeFileSync(path.join(w.root, "fixtures", "engineer.md"), DEFINITION_AT_PIN + "\n(drifted fixture bytes)\n"); + const r = runDryRun(w.root, w.pinPath, w.vault); + expect(r.status).toBe(1); + expect(r.stderr).toMatch(/status=fixture-mismatch/); + expect(r.stderr).toMatch(/drifted from its recorded digest/); + }); + + it("the REAL committed pin record: pending-signature pre-flight green, the B1 hold named, honest vault-absent skip (hermetic)", () => { const root = tmpRoot(); - const r = runDryRun({ - ROLE_PARITY_CHECK: CHECK_CLI, - ROLE_PARITY_PIN: REAL_PIN, - ROLE_PARITY_VAULT: path.join(root, "no-such-vault"), - ROLE_PARITY_RECEIPTS: path.join(root, "receipts", "upgrade-receipts.jsonl"), + const r = spawnSync("/bin/bash", [OPS_SCRIPT, "--dry-run"], { + encoding: "utf8", + env: { + ...process.env, + ROLE_PARITY_CHECK: CHECK_CLI, + ROLE_PARITY_PIN: REAL_PIN, + ROLE_PARITY_VAULT: path.join(root, "no-such-vault"), + ROLE_PARITY_REF: "origin/main", + ROLE_PARITY_RECEIPTS: path.join(root, "receipts", "upgrade-receipts.jsonl"), + ROLE_PARITY_TRACKING_REPO: "harmoniqs/amicode", + }, }); expect(r.status).toBe(0); expect(r.stderr).toMatch(/status=vault-absent/); - // fixture integrity over the real fixtures: every line green - expect(r.stderr).toMatch(/fixture engineer\.md byte-matches its recorded digest/); - expect(r.stderr).toMatch(/fixture experimenter\.md byte-matches its recorded digest/); + // the real record is the shipped pending-signature state: the hold is + // named, nothing published-verbatim + expect(r.stderr).toMatch(/publication=pending-signature/); + expect(r.stderr).toMatch(/fixture publications held pending the seed-gate signature/); + }); + + // B2 — the fetch: a remote drift is INVISIBLE without the fetch and CAUGHT + // with it (the wrapper passes --fetch; the stale-local control proves the + // fetch is load-bearing, not decorative) + it("B2 fetch discovery: a drifted vault REMOTE reads behind-head through the wrapper's --fetch; without the fetch the stale local ref would lie current", () => { + const w = fixtureWorld(); + // give the vault an origin: a bare remote, with the drift landing ONLY + // on the remote (the local checkout stays at the pinned revision) + const bare = path.join(w.root, "amicissimo.git"); + spawnSync("git", ["init", "--bare", "-b", "main", bare], { encoding: "utf8" }); + git(w.vault, ["remote", "add", "origin", bare]); + git(w.vault, ["push", "-u", "origin", "main"]); + const clone = path.join(w.root, "drift-clone"); + spawnSync("git", ["clone", "--branch", "main", bare, clone], { encoding: "utf8" }); + fs.writeFileSync(path.join(clone, "vault", "agents", "engineer.md"), DEFINITION_AT_PIN + "\n(Remote-only amendment.)\n"); + git(clone, ["add", "-A"]); + git(clone, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "remote drift"], GIT_ID); + git(clone, ["push", "origin", "main"]); + + // control: the CLI WITHOUT --fetch compares against the stale local + // origin/main and reads current — the lie the fetch exists to prevent + const noFetch = spawnSync(process.execPath, [ + "--experimental-strip-types", CHECK_CLI, + "--pin", w.pinPath, "--vault", w.vault, "--ref", "origin/main", + ], { encoding: "utf8" }); + expect(noFetch.status).toBe(0); + expect(JSON.parse(noFetch.stdout).status).toBe("current"); + + // the wrapper (--fetch wired): the remote drift is discovered + const r = runDryRun(w.root, w.pinPath, w.vault, { ROLE_PARITY_REF: "origin/main" }); + expect(r.status).toBe(1); + expect(r.stderr).toMatch(/status=behind-head/); + expect(r.stderr).toMatch(/fetched origin before the freshness compare/); + }); + + // B2 — a fetch failure is a named unknown receipt, never green + it("B2 fetch failure: an unreachable remote → status vault-unfetchable, exit 0, and the REAL receipt carries the named unknown (never green)", () => { + const w = fixtureWorld(); + // a remote that cannot be fetched: a bogus URL + git(w.vault, ["remote", "add", "origin", "https://no-such-host.example.invalid/amicissimo.git"]); + const r = runReal(w.root, w.pinPath, w.vault, { ROLE_PARITY_REF: "origin/main" }); + expect(r.status).toBe(0); + expect(r.stderr).toMatch(/status=vault-unfetchable/); + expect(r.stderr).toMatch(/never a green verdict/); + const lines = receipts(w.root); + expect(lines).toHaveLength(1); + expect(lines[0]!.status).toBe("vault-unfetchable"); + expect(lines[0]!.fixture_publication).toBe("pending-signature"); + }); + + // B2 nit — a CLI exit 2 (pre-flight/runtime failure) appends a NAMED + // check-failed receipt, never a malformed `"status":""` line + it("B2 check-failed receipt: a broken pin record → exit 2, the receipt names check-failed (no empty status, no issue action)", () => { + const w = fixtureWorld(); + fs.writeFileSync(w.pinPath, "{ not json"); + const r = runReal(w.root, w.pinPath, w.vault); + expect(r.status).toBe(2); + expect(r.stderr).toMatch(/check FAILED/); + const lines = receipts(w.root); + expect(lines).toHaveLength(1); + expect(lines[0]!.status).toBe("check-failed"); + expect(String(lines[0]!.status).length).toBeGreaterThan(0); + expect(lines[0]!.check_exit).toBe(2); + expect(lines[0]!.tracking_issue).toBeUndefined(); }); }); diff --git a/packages/extension/test/role_cards_parity.test.ts b/packages/extension/test/role_cards_parity.test.ts index 21cff3bc..b73b16b6 100644 --- a/packages/extension/test/role_cards_parity.test.ts +++ b/packages/extension/test/role_cards_parity.test.ts @@ -3,86 +3,133 @@ // the opencode bindings; the amicissimo vault's agent records are the // engine-neutral contracts; THIS suite keeps the overlap coherent. // -// The fixtures are REVISION-PINNED: test/fixtures/vault-agents/ carries the -// engine-neutral definitions at the amicissimo revision recorded in pin.json -// (the fixture carries the vault revision it pinned), digest-verified -// against the record — the pin is self-contained on machines without the -// vault checkout. The nightly pin-behind-HEAD check (ops/role-parity, -// riding the doctor's fleet cadence on the vault-visible machine) re-checks -// the pin against the live amicissimo checkout and files a chore issue on -// drift (obligation O8). +// The pin record (test/fixtures/vault-agents/pin.json) is REVISION-PINNED: +// it carries the amicissimo vault revision the parity baseline was taken at +// plus the per-definition digests AT that revision — provenance without +// content, self-contained on machines without the vault checkout. The +// nightly pin-behind-HEAD check (ops/role-parity, riding the doctor's fleet +// cadence on the vault-visible machine) re-checks the pin against the live +// amicissimo checkout and files a chore issue on drift (obligation O8). // // THE GATE, honored mechanically (never prose): -// - COHERENT anchors (both texts agree) are pinned NOW, against BOTH the -// shipped card and the pinned fixture — the overlap stays coherent. +// - COHERENT anchors (both texts agree) are pinned against the SHIPPED +// CARD now. The fixture halves of those pins — and the full-definition +// fixture publications themselves — are PENDING SIGNATURE (review B1, +// PR #811): the vault definitions as committed fixtures failed the +// amended content policy's per-line usage-vs-internals test (the +// engineer def's src/ module tree, how-to-extend recipe, and internals +// sections; the experimenter def's literal internal host paths), so +// NOTHING from the vault definitions is published-verbatim or pinned +// before Aaron's signature decides. The holds are named skips citing +// the diff document — reversible at signature, never silent. // - FLAGGED content (the prepared human diff, -// docs/seed-gate/role-cards-seed-diff.md, marks it divergent) is pinned -// only after AARON signs. Pre-signature those anchors are SKIPS whose -// reasons name the flag and cite the diff document — a silent pass does -// not exist: a guard test asserts every flag the skips carry appears in -// the diff document, and the doc's signature status must match the -// suite's SEED_GATE_SIGNED switch (flip BOTH, in the same change, with -// the signature). +// docs/seed-gate/role-cards-seed-diff.md) is pinned only after AARON +// signs. Pre-signature those anchors are skips whose reasons name the +// flag and cite the diff document. A two-directional guard keeps doc +// and suite in lockstep: every suite flag appears in the doc, and +// every doc flag key is carried by the suite (a flag on either side +// alone fails the guard). +// - The doc's signature status, the suite's SEED_GATE_SIGNED switch, and +// the provenance record's amendment claim are COUPLED: an unsigned +// tree cannot carry a signed amendment, and the switch never flips +// alone (flip BOTH, in the same change, with the signature). import { describe, it, expect } from "vitest"; import { readFileSync, existsSync } from "node:fs"; import { createHash } from "node:crypto"; -import { dirname, join, resolve } from "node:path"; +import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const HERE = dirname(fileURLToPath(import.meta.url)); -const EXT = resolve(HERE, ".."); +const EXT = join(HERE, ".."); const AGENTS_DIR = join(EXT, "agents"); const FIXTURES = join(HERE, "fixtures", "vault-agents"); const PIN_PATH = join(FIXTURES, "pin.json"); const DIFF_DOC = join(HERE, "..", "..", "..", "docs", "seed-gate", "role-cards-seed-diff.md"); +const PROVENANCE_PATH = join(AGENTS_DIR, ".seed-provenance.json"); /** The seed gate's switch of record. Flip to true ONLY in the same change * that lands Aaron's signature in docs/seed-gate/role-cards-seed-diff.md * (the coupling test below fails otherwise — never flip it alone). */ const SEED_GATE_SIGNED = false; +interface PinEntry { + role_card: string; + vault_path: string; + sha256: string; + /** Present only when the full-definition fixture is PUBLISHED (the + * post-signature state); absent pre-signature (review B1). */ + fixture?: string; +} interface PinRecord { record_version: number; vault_repo: string; vault_revision: string; - pinned: Array<{ role_card: string; vault_path: string; fixture: string; sha256: string }>; + /** "pending-signature" pre-signature (B1: no full-definition fixture is + * published); "published" once the signature lands and the fixtures + * return (at the re-authored revision, if the follow-up runs first). */ + fixture_publication: "published" | "pending-signature"; + pinned: PinEntry[]; no_counterpart: Array<{ role_card: string; nearest_kin: string; reason: string }>; } const pin = JSON.parse(readFileSync(PIN_PATH, "utf8")) as PinRecord; const cardText = (name: string): string => readFileSync(join(AGENTS_DIR, `${name}.md`), "utf8"); -const fixtureText = (name: string): string => readFileSync(join(FIXTURES, name), "utf8"); const sha256 = (p: string): string => "sha256:" + createHash("sha256").update(readFileSync(p)).digest("hex"); -// ── the revision-pinned fixtures (AC3) ─────────────────────────────────────── +// ── the revision-pinned record (AC3) ──────────────────────────────────────── -describe("the parity fixtures are revision-pinned (the fixture carries the vault revision)", () => { +describe("the parity pin record is revision-pinned (it carries the vault revision)", () => { it("pin.json carries a real amicissimo revision and both overlapping definitions", () => { - expect(pin.record_version).toBe(1); + expect(pin.record_version).toBe(2); expect(pin.vault_repo).toBe("harmoniqs/amicissimo"); expect(pin.vault_revision).toMatch(/^[0-9a-f]{40}$/); expect(pin.pinned.map((p) => p.role_card).sort()).toEqual(["experimenter", "implementer"]); }); - it("fixture integrity: every committed fixture byte-matches its recorded digest (self-contained pin)", () => { + it("the no-counterpart roles are RECORDED with reasons, never silently unpinned", () => { + expect(pin.no_counterpart.map((n) => n.role_card).sort()).toEqual(["analyzer", "hypothesizer"]); + for (const n of pin.no_counterpart) { + expect(n.reason.length).toBeGreaterThan(20); + } + }); +}); + +// ── B1 (review, PR #811): the fixture publications are held pending signature ─ + +describe("fixture publications held pending signature (review B1 — nothing published-verbatim pre-signature)", () => { + it("the full-definition fixtures are ABSENT from the repo while the gate is unsigned (the B1 hold)", () => { + expect(SEED_GATE_SIGNED).toBe(false); // this cell flips with the signature for (const p of pin.pinned) { - expect(existsSync(join(FIXTURES, p.fixture)), `fixture ${p.fixture} committed`).toBe(true); + if (p.fixture === undefined) continue; + expect(existsSync(join(FIXTURES, p.fixture)), `${p.fixture} must not be published pre-signature`).toBe(false); + } + // and the record says so + expect(pin.fixture_publication).toBe("pending-signature"); + // no pinned entry carries a committed fixture path while held + for (const p of pin.pinned) { + expect(p.fixture, `${p.role_card}: no committed fixture path pre-signature`).toBeUndefined(); + } + }); + + it.skip("PENDING SIGNATURE — fixture integrity: every published fixture byte-matches its recorded digest (fixture publication: engineer.md (full vault definition)) — see docs/seed-gate/role-cards-seed-diff.md", () => { + for (const p of pin.pinned) { + if (p.fixture === undefined) continue; expect(sha256(join(FIXTURES, p.fixture))).toBe(p.sha256); } }); - it("the no-counterpart roles are RECORDED with reasons, never silently unpinned", () => { - expect(pin.no_counterpart.map((n) => n.role_card).sort()).toEqual(["analyzer", "hypothesizer"]); - for (const n of pin.no_counterpart) { - expect(n.reason.length).toBeGreaterThan(20); + it.skip("PENDING SIGNATURE — fixture integrity: every published fixture byte-matches its recorded digest (fixture publication: experimenter.md (full vault definition)) — see docs/seed-gate/role-cards-seed-diff.md", () => { + for (const p of pin.pinned) { + if (p.fixture === undefined) continue; + expect(sha256(join(FIXTURES, p.fixture))).toBe(p.sha256); } }); }); -// ── the seed-gate record (AC4) ────────────────────────────────────────────── +// ── the seed-gate record (AC4) + the coupling invariants (review A1) ──────── -describe("the prepared human diff + the signature state", () => { +describe("the prepared human diff + the signature state (coupled)", () => { const doc = readFileSync(DIFF_DOC, "utf8"); it("the diff document exists and covers all four cards, the provenance record, and the pin revision", () => { @@ -101,37 +148,60 @@ describe("the prepared human diff + the signature state", () => { expect(doc).toMatch(/Status:\s*PENDING SIGNATURE/i); } }); + + it("an UNSIGNED tree cannot claim an amendment (review A1): !SEED_GATE_SIGNED ⇒ provenance.amended !== true", () => { + if (!SEED_GATE_SIGNED) { + const provenance = JSON.parse(readFileSync(PROVENANCE_PATH, "utf8")) as { amended?: boolean; amendment_signed_by?: string }; + expect(provenance.amended, "an unsigned tree must not carry an amendment claim in .seed-provenance.json").not.toBe(true); + } + }); }); -// ── coherent overlap anchors — pinned NOW (the gate does not block agreement) ─ +// ── coherent overlap anchors — the SHIPPED-CARD halves are pinned now; the ──── +// ── fixture halves are held pending signature (B1) ─────────────────────────── -/** One anchor of the overlap: a regex that must hit the shipped card AND one - * that must hit the pinned fixture. Both sides assert — coherence, not - * just shape. */ -const overlapAnchor = (label: string, card: string, cardRe: RegExp, fixture: string, fixtureRe: RegExp) => - it(`coherent: ${label}`, () => { +/** One anchor of the overlap: the card-side regex pins NOW (live repo + * source, unflagged content); the fixture-side half is a named skip that + * converts with the signature + the re-published fixtures. */ +const overlapAnchor = (label: string, card: string, cardRe: RegExp) => { + it(`coherent (shipped card): ${label}`, () => { expect(cardRe.test(cardText(card)), `${card} carries the anchor: ${cardRe}`).toBe(true); - expect(fixtureRe.test(fixtureText(fixture)), `the pinned ${fixture} carries the anchor: ${fixtureRe}`).toBe(true); }); +}; -describe("implementer ↔ engineer (engine-neutral) — coherent overlap, pinned", () => { - overlapAnchor("the delegated TDD leaf (implement-issue --orchestrated)", "implementer", /implement-issue/, "engineer.md", /implement-issue/); - overlapAnchor("the orchestrated worktree binding", "implementer", /--orchestrated/, "engineer.md", /--orchestrated/); - overlapAnchor("branch discipline — never off the assigned branch", "implementer", /caller-provided worktree branch/, "engineer.md", /never on main/); - overlapAnchor("test protection — never force green", "implementer", /never delete, skip, or mark tests broken to\s+force green/, "engineer.md", /NEVER delete test files or remove test cases/); - overlapAnchor("the structured return contract", "implementer", /commit_shas/, "engineer.md", /commit_shas:/); - overlapAnchor("bounded retries, then escalate — never negotiate a RED", "implementer", /is a `failed` return,\s+not a negotiation/, "engineer.md", /up to 2 retry cycles/); +describe("implementer ↔ engineer (engine-neutral) — coherent overlap, shipped-card halves pinned", () => { + overlapAnchor("the delegated TDD leaf (implement-issue --orchestrated)", "implementer", /implement-issue/); + overlapAnchor("the orchestrated worktree binding", "implementer", /--orchestrated/); + overlapAnchor("branch discipline — never off the assigned branch", "implementer", /caller-provided worktree branch/); + overlapAnchor("test protection — never force green", "implementer", /never delete, skip, or mark tests broken to\s+force green/); + overlapAnchor("the structured return contract", "implementer", /commit_shas/); + overlapAnchor("bounded retries, then escalate — never negotiate a RED", "implementer", /is a `failed` return,\s+not a negotiation/); }); -describe("experimenter ↔ experimenter (engine-neutral) — coherent overlap, pinned", () => { - overlapAnchor("brief-driven execution — parse the briefing first", "experimenter", /Briefing you receive:/, "experimenter.md", /[Ee]xperiment brief/); - overlapAnchor("numbers-grounded reporting from the run's own output", "experimenter", /Debrief with NUMBERS ONLY/, "experimenter.md", /AMICO_RESULT_/); +describe("experimenter ↔ experimenter (engine-neutral) — coherent overlap, shipped-card halves pinned", () => { + overlapAnchor("brief-driven execution — parse the briefing first", "experimenter", /Briefing you receive:/); + overlapAnchor("numbers-grounded reporting from the run's own output", "experimenter", /Debrief with NUMBERS ONLY/); +}); + +describe("coherent overlap — the fixture halves, held pending signature (B1)", () => { + const FIXTURE_HALVES = [ + { key: "fixture publication: engineer.md (full vault definition)", file: "engineer.md", re: /NEVER delete test files or remove test cases/ }, + { key: "fixture publication: experimenter.md (full vault definition)", file: "experimenter.md", re: /AMICO_RESULT_/ }, + ]; + for (const f of FIXTURE_HALVES) { + it.skip(`PENDING SIGNATURE — the pinned ${f.file} carries the coherent anchor ${f.re} (${f.key}) — see docs/seed-gate/role-cards-seed-diff.md`, () => { + expect(f.re.test(readFileSync(join(FIXTURES, f.file), "utf8"))).toBe(true); + }); + } }); // ── flagged content — pending-signature skips (never a silent pass) ───────── -/** The flags the prepared diff marks divergent. Keys must appear verbatim in - * docs/seed-gate/role-cards-seed-diff.md (guard test below). */ +/** The flags the prepared diff marks divergent — PLUS the B1 fixture + * publication holds. Keys must appear verbatim in + * docs/seed-gate/role-cards-seed-diff.md (the guard below enforces BOTH + * directions: a suite flag missing from the doc, or a doc flag key with no + * suite handling, fails). */ const FLAGGED = [ { key: "implementer ↔ engineer: merge/PR governance", @@ -182,6 +252,20 @@ const FLAGGED = [ flag: "no engine-neutral counterpart; nearest kin librarian.md writes curated notes where the analyzer is read-only and proposes verdicts — the signature confirms no-counterpart or names the pin target", }, + { + key: "fixture publication: engineer.md (full vault definition)", + card: "implementer", + fixture: "engineer.md", + flag: + "the engineer definition as a committed fixture failed the amended content policy's per-line test (src/ module tree, how-to-extend recipe, Complex Internals section, roadmap lines) — unpublished pending signature; the vault re-authoring is the recorded follow-up", + }, + { + key: "fixture publication: experimenter.md (full vault definition)", + card: "experimenter", + fixture: "experimenter.md", + flag: + "the experimenter definition as a committed fixture failed the per-line test (literal internal host paths) — unpublished pending signature; the vault re-authoring is the recorded follow-up", + }, ] as const; describe("flagged content is pinned only after Aaron signs (the seed gate)", () => { @@ -197,12 +281,34 @@ describe("flagged content is pinned only after Aaron signs (the seed gate)", () }); } - it("no silent passes: every flag the suite skips appears verbatim in the prepared diff document", () => { + it("no silent passes, direction 1: every flag the suite skips appears verbatim in the prepared diff document", () => { const doc = readFileSync(DIFF_DOC, "utf8"); for (const f of FLAGGED) { expect(doc, `the diff doc flags: ${f.key}`).toContain(f.key); } }); + + it("no silent passes, direction 2 (review A2): every bold flag key in the doc's Flagged-for-signature sections has a suite FLAGGED entry", () => { + const doc = readFileSync(DIFF_DOC, "utf8"); + // the doc's flag keys live ONLY in the flag sections: "Flagged for + // signature", the held-publication section, and the NO-counterpart + // sections (their "suite flag:" markers). Scoping matters — the + // coherent-anchor sections also carry bold-backtick quotes of card + // text, and those are NOT flags. + const FLAG_SECTION = /Flagged for signature|held pending signature|NO vault counterpart/; + const sections = doc.split(/^#{2,3} /m); + const docKeys: string[] = []; + for (const section of sections) { + const heading = section.slice(0, section.indexOf("\n")); + if (!FLAG_SECTION.test(heading)) continue; + docKeys.push(...[...section.matchAll(/\*\*`([^`]+)`/g)].map((m) => m[1]!)); + } + expect(docKeys.length, "the doc's flag sections carry bold flag keys").toBeGreaterThanOrEqual(FLAGGED.length); + const suiteKeys = new Set(FLAGGED.map((f) => f.key)); + for (const key of docKeys) { + expect(suiteKeys.has(key), `the suite carries a FLAGGED entry for the doc's flag key: ${key}`).toBe(true); + } + }); }); // ── post-signature pins (dormant until the signature flips the switch) ────── @@ -220,4 +326,20 @@ describe.skipIf(!SEED_GATE_SIGNED)("post-signature: the adjudicated overlap of r const confirmed = JSON.parse(readFileSync(PIN_PATH, "utf8")) as PinRecord; expect(confirmed.no_counterpart.map((n) => n.role_card).sort()).toEqual(["analyzer", "hypothesizer"]); }); + + // review A3: the three previously unenforced flags convert with the + // switch too — each carries the repo-wins direction inline, so flipping + // SEED_GATE_SIGNED converts ALL of the flags, not four of seven. + it("A3 scope perimeter: the shipped implementer stays one-slice-per-cast; no layer-skill/multi-package scope enters the binding", () => { + expect(cardText("implementer")).toMatch(/Implements ONE TDD-ready GitHub issue slice/); + expect(cardText("implementer")).not.toMatch(/layer skill|multi-package/i); + }); + it("A3 checkout discipline: the shipped experimenter keeps the assigned-env/CHECKOUTS.md rule; no scratchpad free-for-all", () => { + expect(cardText("experimenter")).toMatch(/USE EXACTLY THIS, never a shared checkout/); + expect(cardText("experimenter")).not.toMatch(/scratchpad/); + }); + it("A3 artifact contract: the shipped experimenter writes its own experiment note + raw artifacts, never catalog writes", () => { + expect(cardText("experimenter")).toMatch(/write YOUR\s+own experiment note|Write raw artifacts/); + expect(cardText("experimenter")).not.toMatch(/save the pulse to the catalog/); + }); }); From 2be3e2fd2a4ad75ad80e0523444f3d1b060e9929 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sat, 5 Sep 2026 20:04:08 -0400 Subject: [PATCH 6/7] =?UTF-8?q?feat(seed-gate):=20Aaron's=20signature=20la?= =?UTF-8?q?nds=20(accept-seed,=20by=20directive)=20=E2=80=94=20the=20gate?= =?UTF-8?q?=20flips,=20the=20adjudicated=20pins=20go=20live,=20the=20fixtu?= =?UTF-8?q?re=20hold=20continues=20by=20re-authoring=20decision=20(#806)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/seed-gate/role-cards-seed-diff.md | 28 ++++++++----- .../test/fixtures/vault-agents/pin.json | 2 +- .../extension/test/role_cards_parity.test.ts | 40 +++++++++++++------ 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/docs/seed-gate/role-cards-seed-diff.md b/docs/seed-gate/role-cards-seed-diff.md index ef1bef14..21c8b0e0 100644 --- a/docs/seed-gate/role-cards-seed-diff.md +++ b/docs/seed-gate/role-cards-seed-diff.md @@ -1,6 +1,6 @@ # Seed gate — the four role cards, diffed for signature (D3, #806) -**Status: PENDING SIGNATURE — prepared by the implementing cast (slice 2, worktree `slice2-role-cards`); Aaron performs and signs.** +**Status: SIGNED 2026-09-05 (accept-seed, by chat directive "do as you rec" — see the Signature section; the fixture publications remain held under the re-authoring decision).** Prepared by the implementing cast (slice 2, worktree `slice2-role-cards`); Aaron performed and signed. This is the human diff the seed gate requires (spec `spec-20260905-063000` D3, issue #806, obligation O2). The four director-cast role cards — @@ -227,13 +227,23 @@ silently pinned. ## Signature -- [ ] **Signed:** ______________ Date: __________ -- Decision: `accept-seed` | `accept-with-amendments` (attach amendments) +- [x] **Signed: Aaron Trowbridge** — by chat directive ("do as you rec", + 2026-09-05, this campaign's session ledger; the PI read the rendered + packet and delegated to the recorded recommendations; the directive is + the human decision and this record says so plainly). Date: 2026-09-05 +- Decision: **`accept-seed`** (no amendments — the cards stand as seeded; + `.seed-provenance.json` stays `amended: false`) - Per-card adjudications (only where flagged): - - implementer ↔ engineer: merge/PR governance — shipped never-merge binding is the coherent overlap? ___ - - implementer ↔ engineer: scope perimeter acknowledged? ___ - - experimenter ↔ experimenter: shipped never-grade/never-promote binding is the coherent overlap? ___ - - hypothesizer: no-counterpart confirmed (or pin target named)? ___ - - analyzer: no-counterpart confirmed (or pin target named)? ___ + - implementer ↔ engineer: merge/PR governance — shipped never-merge binding is the coherent overlap? **YES** (repo wins; the vault def's auto-merge line reads stale against the orchestrated walk — vault re-authoring follow-up) + - implementer ↔ engineer: scope perimeter acknowledged? **YES** (two modes of operation, one role) + - experimenter ↔ experimenter: shipped never-grade/never-promote binding is the coherent overlap? **YES** (repo wins; the vault def is the pre-loop Phase-2-era contract — vault re-authoring follow-up) + - hypothesizer: no-counterpart confirmed (or pin target named)? **CONFIRMED no-counterpart** (researcher.md is the decider, not a read-only proposer — no false overlap pinned) + - analyzer: no-counterpart confirmed (or pin target named)? **CONFIRMED no-counterpart** (librarian.md writes; the analyzer is read-only — no false overlap pinned) - Fixture publications (section 5, review B1): republish as-signed, or run - the vault re-authoring follow-up first and re-take fixtures + pin? ___ + the vault re-authoring follow-up first and re-take fixtures + pin? + **VAULT RE-AUTHORING FIRST** — the fixture hold CONTINUES by decision + (not by unsignedness): the pin's `fixture_publication` reads + `pending-re-authoring`; the amicissimo re-authoring issue (its own gate) + moves the package-internals categories out of the two engine-neutral + definitions at their source, then the fixtures re-take at the new + revision and publish. The nightly pin check covers the interim. diff --git a/packages/extension/test/fixtures/vault-agents/pin.json b/packages/extension/test/fixtures/vault-agents/pin.json index ea629bf2..89e109ec 100644 --- a/packages/extension/test/fixtures/vault-agents/pin.json +++ b/packages/extension/test/fixtures/vault-agents/pin.json @@ -3,7 +3,7 @@ "note": "The role-card parity fixtures' pin record (slice 2 / D3, #806, obligations O2/O8): the engine-neutral role definitions this pin targets, at the amicissimo vault revision they were pinned at, with the per-definition digests AT that revision — provenance WITHOUT content. Review B1 (PR #811): the full-definition fixtures are NOT published pending the seed-gate signature — the committed copies failed the amended content policy's per-line usage-vs-internals test (ADR-0011's categories: the engineer def's src/ module tree, how-to-extend recipe, and internals sections; the experimenter def's literal internal host paths), so nothing from the vault definitions is published-verbatim before Aaron decides. The nightly pin-behind-HEAD check (ops/role-parity) verifies the pin against the live amicissimo checkout directly — pin genuineness via git show at the recorded revision, freshness via the vault ref — and files a chore issue on drift. When the signature lands: either republish the fixtures (post-signature) or, recommended, run the vault re-authoring follow-up first (move package-internals out of the defs at their source in amicissimo, its own issue+PR), then re-take fixtures + this record at the new revision.", "vault_repo": "harmoniqs/amicissimo", "vault_revision": "5c6a1cd0d5bd240e07f6bef6467986e2fb41d7a7", - "fixture_publication": "pending-signature", + "fixture_publication": "pending-re-authoring", "captured_at": "2026-09-05T03:45:00.000Z", "pinned": [ { diff --git a/packages/extension/test/role_cards_parity.test.ts b/packages/extension/test/role_cards_parity.test.ts index b73b16b6..8caf5462 100644 --- a/packages/extension/test/role_cards_parity.test.ts +++ b/packages/extension/test/role_cards_parity.test.ts @@ -49,8 +49,12 @@ const PROVENANCE_PATH = join(AGENTS_DIR, ".seed-provenance.json"); /** The seed gate's switch of record. Flip to true ONLY in the same change * that lands Aaron's signature in docs/seed-gate/role-cards-seed-diff.md - * (the coupling test below fails otherwise — never flip it alone). */ -const SEED_GATE_SIGNED = false; + * (the coupling test below fails otherwise — never flip it alone). + * Flipped 2026-09-05 with the signature (accept-seed, by chat directive + * "do as you rec" — recorded in the doc's Signature section). The fixture + * publications remain HELD by the re-authoring decision, not by + * unsignedness: pin.fixture_publication reads "pending-re-authoring". */ +const SEED_GATE_SIGNED = true; interface PinEntry { role_card: string; @@ -64,10 +68,11 @@ interface PinRecord { record_version: number; vault_repo: string; vault_revision: string; - /** "pending-signature" pre-signature (B1: no full-definition fixture is - * published); "published" once the signature lands and the fixtures - * return (at the re-authored revision, if the follow-up runs first). */ - fixture_publication: "published" | "pending-signature"; + /** "pending-signature" pre-signature; "pending-re-authoring" once signed + * with the VAULT RE-AUTHORING decision (B1: no full-definition fixture + * is published until the amicissimo re-authoring lands and the fixtures + * re-take at the new revision); "published" once they return. */ + fixture_publication: "published" | "pending-signature" | "pending-re-authoring"; pinned: PinEntry[]; no_counterpart: Array<{ role_card: string; nearest_kin: string; reason: string }>; } @@ -98,17 +103,21 @@ describe("the parity pin record is revision-pinned (it carries the vault revisio // ── B1 (review, PR #811): the fixture publications are held pending signature ─ describe("fixture publications held pending signature (review B1 — nothing published-verbatim pre-signature)", () => { - it("the full-definition fixtures are ABSENT from the repo while the gate is unsigned (the B1 hold)", () => { - expect(SEED_GATE_SIGNED).toBe(false); // this cell flips with the signature + it("the full-definition fixtures are ABSENT from the repo while the re-authoring hold runs (the B1 hold, post-signature form)", () => { + // Signed 2026-09-05 (accept-seed) with the VAULT RE-AUTHORING decision: + // the hold now continues BY DECISION, not by unsignedness — the pin's + // publication field says so, the fixtures stay unpublished until the + // amicissimo re-authoring lands and the fixtures re-take at the new + // revision. for (const p of pin.pinned) { if (p.fixture === undefined) continue; - expect(existsSync(join(FIXTURES, p.fixture)), `${p.fixture} must not be published pre-signature`).toBe(false); + expect(existsSync(join(FIXTURES, p.fixture)), `${p.fixture} must not be published under the re-authoring hold`).toBe(false); } - // and the record says so - expect(pin.fixture_publication).toBe("pending-signature"); + // and the record says so — the hold's REASON changed with the signature + expect(pin.fixture_publication).toBe("pending-re-authoring"); // no pinned entry carries a committed fixture path while held for (const p of pin.pinned) { - expect(p.fixture, `${p.role_card}: no committed fixture path pre-signature`).toBeUndefined(); + expect(p.fixture, `${p.role_card}: no committed fixture path under the re-authoring hold`).toBeUndefined(); } }); @@ -315,7 +324,12 @@ describe("flagged content is pinned only after Aaron signs (the seed gate)", () describe.skipIf(!SEED_GATE_SIGNED)("post-signature: the adjudicated overlap of record (repo wins for shipped bindings)", () => { it("the shipped implementer stays merge-free: no PR/merge/push authorization enters the binding", () => { - expect(cardText("implementer")).toMatch(/never opens PRs, never merges/); + // (the shipped card's own words: the description's never-opens-PRs + // clause + the No-PR-no-merge-no-push rule; this cell was authored + // dormant against a paraphrase — pinned to the card's actual text + // when the signature activated it, semantics unchanged) + expect(cardText("implementer")).toMatch(/Never opens PRs or merges in orchestrated mode/); + expect(cardText("implementer")).toMatch(/No PR, no merge, no push to shared branches/); expect(cardText("implementer")).not.toMatch(/auto-merge/i); }); it("the shipped experimenter stays grade-free: verdicts belong to the gates + parent + analyzer", () => { From 3f711895141e488f4debf01f2a980ad8ff03970c Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sat, 5 Sep 2026 20:18:32 -0400 Subject: [PATCH 7/7] fix(ops): the parity CLI + orchestrator accept the signed tree's third publication state (pending-re-authoring) (#806) --- .../extension/scripts/role_parity_check.mts | 18 ++++++++++++++---- .../test/ops/role_parity_orchestrator.test.ts | 10 ++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/extension/scripts/role_parity_check.mts b/packages/extension/scripts/role_parity_check.mts index 91f0c640..075ad0c4 100644 --- a/packages/extension/scripts/role_parity_check.mts +++ b/packages/extension/scripts/role_parity_check.mts @@ -71,7 +71,7 @@ interface PinRecord { vault_revision: string; /** v2: whether the full-definition fixtures are published. v1 records are * published-by-construction (they carry a fixture path per entry). */ - fixture_publication?: "published" | "pending-signature"; + fixture_publication?: "published" | "pending-signature" | "pending-re-authoring"; pinned: Array<{ role_card: string; vault_path: string; fixture?: string; sha256: string }>; no_counterpart?: Array<{ role_card: string }>; } @@ -82,7 +82,7 @@ interface Report { vault_revision: string | null; /** "pending-signature" while the B1 hold stands (no full-definition * fixture is published); "published" once the fixtures return. */ - fixture_publication: "published" | "pending-signature"; + fixture_publication: "published" | "pending-signature" | "pending-re-authoring"; drifted_files: string[]; evidence: string[]; } @@ -151,12 +151,17 @@ try { } if ( !((pin.record_version === 1 && pin.pinned.every((p) => typeof p.fixture === "string")) || - (pin.record_version === 2 && (pin.fixture_publication === "published" || pin.fixture_publication === "pending-signature"))) || + (pin.record_version === 2 && + (pin.fixture_publication === "published" || + pin.fixture_publication === "pending-signature" || + // SIGNED 2026-09-05 with the vault-re-authoring decision: the hold + // continues by decision — the third publication state. + pin.fixture_publication === "pending-re-authoring"))) || typeof pin.vault_revision !== "string" || !Array.isArray(pin.pinned) || pin.pinned.length === 0 ) { usage("pin record malformed: record_version must be 1 (fixtures published) or 2 (with fixture_publication), with vault_revision and a non-empty pinned set"); } -const publication: "published" | "pending-signature" = pin.fixture_publication +const publication: "published" | "pending-signature" | "pending-re-authoring" = pin.fixture_publication ?? (pin.record_version === 1 ? "published" : "pending-signature"); // ── 2. fixture integrity (self-contained: the committed fixtures vs the record) @@ -169,6 +174,11 @@ const pinDir = dirname(resolve(pinPath)); const evidence: string[] = []; if (publication === "pending-signature") { evidence.push("fixture publications held pending the seed-gate signature (review B1) — no full-definition fixture is published; pin genuineness is verified against the vault revision directly"); +} else if (publication === "pending-re-authoring") { + // SIGNED 2026-09-05 with the VAULT RE-AUTHORING decision: the hold + // continues by decision, not unsignedness — the fixtures re-take at the + // re-authored revision when the amicissimo follow-up lands. + evidence.push("fixture publications held pending the vault re-authoring (the signed seed-gate decision) — no full-definition fixture is published; pin genuineness is verified against the vault revision directly"); } else { for (const p of pin.pinned) { const fixturePath = isAbsolute(p.fixture!) ? p.fixture! : join(pinDir, p.fixture!); diff --git a/packages/extension/test/ops/role_parity_orchestrator.test.ts b/packages/extension/test/ops/role_parity_orchestrator.test.ts index 4dfd1718..c7562379 100644 --- a/packages/extension/test/ops/role_parity_orchestrator.test.ts +++ b/packages/extension/test/ops/role_parity_orchestrator.test.ts @@ -259,10 +259,12 @@ const NODE_STRIPS_TYPES = STRIP_PROBE.status === 0 && STRIP_PROBE.stdout.trim() }); expect(r.status).toBe(0); expect(r.stderr).toMatch(/status=vault-absent/); - // the real record is the shipped pending-signature state: the hold is - // named, nothing published-verbatim - expect(r.stderr).toMatch(/publication=pending-signature/); - expect(r.stderr).toMatch(/fixture publications held pending the seed-gate signature/); + // the real record is the SIGNED state's hold: accepted 2026-09-05 with + // the vault-re-authoring decision — the fixtures stay unpublished + // (by decision now, not unsignedness) until the amicissimo follow-up + // lands; the hold is named, nothing published-verbatim + expect(r.stderr).toMatch(/publication=pending-re-authoring/); + expect(r.stderr).toMatch(/fixture publications held pending the vault re-authoring/); }); // B2 — the fetch: a remote drift is INVISIBLE without the fetch and CAUGHT