diff --git a/packages/extension/agents/.seed-provenance.json b/packages/extension/agents/.seed-provenance.json index 16c812c88..ebdd512df 100644 --- a/packages/extension/agents/.seed-provenance.json +++ b/packages/extension/agents/.seed-provenance.json @@ -9,30 +9,30 @@ { "role": "hypothesizer", "source_path": "/home/aaron/.amico/server/opencode-project-staging/opencode-project/.opencode/agents/hypothesizer.md", - "seeded_sha256": "sha256:096fa4ba69acfae0d18036ab80b4b58a1e1edb8d7a9a4c6ff01dc16e3ef57880", + "seeded_sha256": "sha256:253528e0eedc9bf46aa6b800c9ed94a07ba63c635e609f02bf90d9182747c672", "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:4497d4d5aa3188a3e28626b86c2e91498e7799928893e2560b880f847901bc8d", + "seeded_sha256": "sha256:46fd8f01386d674be2e64a290874a1dffb1bdd47f0b2f979eea8a535e3e47d7e", "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:c0f190aa8636bee4ab2a3211e4cd16fb24994a0ff0ce2d3eaddff2a84b3ff6f9", + "seeded_sha256": "sha256:8f64825ce71a910a3e537a10951d52f13858bbf23f6181a5f1f202af7ff40ccf", "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:6b489254c13f960fc75822046276c33fd25b6b7c8c689e85efe8140442097eb0", + "seeded_sha256": "sha256:027b40fbe5ac0ad590026e0b41d9b72120fac994a0b27be993a1475524b5d4a1", "seeded_at": "2026-08-20T18:24:56.000Z" } ], "amended": true, - "amended_at": "2026-11-02T00:00:00.000Z", - "amendment_signed_by": "aaron via harmoniqs/amicode#860 amendment 1 (S3 model routing); amendment 2 via harmoniqs/amicode#1133 (post-campaign linkage-audit slice — the cast cards learn their hooks, the audit's items 1-2, AFK walk); amendment 3 via harmoniqs/amicode#2093 (receipt return contract — system-v2 audit slice, orchestrated walk)", - "amendment_note": "Amendment 3 (receipt return contract, #2093, 2026-11-02): each of the four cast cards gains the identical '## Return contract (receipt)' section the critic and librarian cards also carry — the full work product goes to a file the caller names (or a reported scratch path; read-only roles hand it over as one fenced block after the receipt for the caller to file), and the final message is a receipt of at most ~1,500 tokens (verdict/status, key numbers, artifact paths, open items). The implementer's step-7 structured yaml is named as its receipt; the hypothesizer's return line routes the full spec-card draft to the work product. Amendment 2 (post-campaign linkage audit, 2026-09-14): the four cast cards learn the landed skill-integrity hooks — analyzer gains duty #6 (the skill-friction check: instructions-vs-reality mismatches → findings-ledger notes quoting both sources; repeated un-skilled idioms → proposal-type findings, provenance-tagged), implementer's structured-return enumeration gains skill_findings (the front-line rule: findings, never silent workarounds), hypothesizer's briefing gains the campaign findings ledger (amicode/skills-integrity/findings/, personal vault) as a prior, experimenter's checkout-registry path moves to the Research-Project location /ledger/campaigns/CHECKOUTS.md. Amendment 1 (S3 model routing, #860): the model_routing slots of the four seeded role cards were filled with CLASS-level prefer-lists (a '## Model routing' section appending one 'Model routing, suggested:' line naming workhorse / strongest-reasoner / fast-cheap — the public suggestion vocabulary, never tuned assignments). seeded_sha256 entries updated to the amended bytes per this record's amendment protocol." + "amended_at": "2026-11-04T00:00:00.000Z", + "amendment_signed_by": "aaron via harmoniqs/amicode#860 amendment 1 (S3 model routing); amendment 2 via harmoniqs/amicode#1133 (post-campaign linkage-audit slice — the cast cards learn their hooks, the audit's items 1-2, AFK walk); amendment 3 via harmoniqs/amicode#2093 (receipt return contract — system-v2 audit slice, orchestrated walk); amendment 4 via harmoniqs/amicode#2118 (Method sections and overlay dispatch — Amicissimo release-audit slice, orchestrated walk)", + "amendment_note": "Amendment 4 (Method sections and overlay dispatch, #2118, 2026-11-04): each of the four cast cards gains 'dispatch: -tuning' in its frontmatter and a '## Method' section carrying a complete default for every method-class dimension, modelled on the librarian card — a numbered 'Default procedure' (the card's existing job text moved into it; the Briefing, Rules/Hard rules and Return paragraphs stay in place above it), the card's existing 'Model routing, suggested:' content as 'Model routing, default:' (the separate '## Model routing' section is folded in and removed), an 'Iteration budget, default:' derived from the card's steps cap and role, and an 'Example brief' in a text fence — so an entitled overlay always merges as a delta on a working default instead of throwing a missing-anchor error. The permission frontmatter, mode, temperature, steps, and the Return contract (receipt) section are byte-unchanged; no rule was weakened. Amendment 3 (receipt return contract, #2093, 2026-11-02): each of the four cast cards gains the identical '## Return contract (receipt)' section the critic and librarian cards also carry — the full work product goes to a file the caller names (or a reported scratch path; read-only roles hand it over as one fenced block after the receipt for the caller to file), and the final message is a receipt of at most ~1,500 tokens (verdict/status, key numbers, artifact paths, open items). The implementer's step-7 structured yaml is named as its receipt; the hypothesizer's return line routes the full spec-card draft to the work product. Amendment 2 (post-campaign linkage audit, 2026-09-14): the four cast cards learn the landed skill-integrity hooks — analyzer gains duty #6 (the skill-friction check: instructions-vs-reality mismatches → findings-ledger notes quoting both sources; repeated un-skilled idioms → proposal-type findings, provenance-tagged), implementer's structured-return enumeration gains skill_findings (the front-line rule: findings, never silent workarounds), hypothesizer's briefing gains the campaign findings ledger (amicode/skills-integrity/findings/, personal vault) as a prior, experimenter's checkout-registry path moves to the Research-Project location /ledger/campaigns/CHECKOUTS.md. Amendment 1 (S3 model routing, #860): the model_routing slots of the four seeded role cards were filled with CLASS-level prefer-lists (a '## Model routing' section appending one 'Model routing, suggested:' line naming workhorse / strongest-reasoner / fast-cheap — the public suggestion vocabulary, never tuned assignments). seeded_sha256 entries updated to the amended bytes per this record's amendment protocol." } diff --git a/packages/extension/agents/analyzer.md b/packages/extension/agents/analyzer.md index b0a21d61c..32ca002bf 100644 --- a/packages/extension/agents/analyzer.md +++ b/packages/extension/agents/analyzer.md @@ -6,6 +6,7 @@ steps: 40 permission: edit: deny bash: deny +dispatch: analyzer-tuning --- You are the ANALYZER in an Amicode autoresearch loop. Fresh context, independent eyes. You @@ -15,7 +16,20 @@ 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:** +**Your job:** run the default procedure under `## Method` below, within the rules here. + +**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. + +## Method + +Default procedure — the complete default; a tuning overlay sharpens this method, +never replaces it: 1. Read the ledger first — hypothesis table, methodology rules, gotchas. 2. Analyze the RAW artifacts. **The experimenter's note is context, NOT evidence**: every @@ -37,13 +51,24 @@ result files); the session ledger path; the gate verdicts the parent already rec sessions/artifacts). You are read-only: surface them in your return — the parent files them in the ledger. -**Rules:** +Model routing, default: strongest-reasoner → workhorse — independent analysis and +verdict proposals are the loop's judgment passes; the strongest reasoner available +reads the raw artifacts. The workhorse class suffices when the brief is a mechanical +re-extraction with no interpretive surface. -- 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. +Iteration budget, default: one analysis per cast, inside the card's 40-step cap. Read +the ledger and the raw artifacts first, extract and propose in a single pass, and +return the receipt rather than looping on a second reading; whatever the cap leaves +unanalyzed goes back as an open item for the parent, not a half-finished verdict. + +Example brief (the shape of the input, not the cast grammar): + +```text +Analyze experiment E7 for H3. Ledger: sessions/session-.md. Experiment note: +experiments/-e7.md (context only, not evidence). Raw artifacts: +/results.csv and /solve.log. Gate verdicts the parent recorded: +see the ledger's H3 row. Write the work product to /analysis-e7.md. +``` ## Return contract (receipt) @@ -60,10 +85,3 @@ receipt forward. The final message is a receipt of at most ~1,500 tokens: A structured result your card already defines (a verdict block, a debrief, filing receipts, a structured yaml) IS the receipt when it fits the bound — keep its shape. - -## Model routing - -Model routing, suggested: strongest-reasoner → workhorse — independent analysis and -verdict proposals are the loop's judgment passes; the strongest reasoner available -reads the raw artifacts. The workhorse class suffices when the brief is a mechanical -re-extraction with no interpretive surface. diff --git a/packages/extension/agents/experimenter.md b/packages/extension/agents/experimenter.md index 644d3d26b..3b1a75978 100644 --- a/packages/extension/agents/experimenter.md +++ b/packages/extension/agents/experimenter.md @@ -5,6 +5,7 @@ steps: 150 permission: edit: allow bash: allow +dispatch: experimenter-tuning --- You are the EXPERIMENTER in an Amicode autoresearch loop. Fresh context: you know only the @@ -16,19 +17,7 @@ the env assignment — Julia project path, worktree, /tmp env, per (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. +**Your job:** run the default procedure under `## Method` below, within the rules here. **Hard rules:** @@ -46,6 +35,42 @@ path to write when done. - If the env assignment conflicts with reality (path missing, branch moved), STOP and report the conflict — never silently switch checkouts. +## Method + +Default procedure — the complete default; a tuning overlay sharpens this method, +never replaces it: + +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. + +Model routing, default: workhorse — brief-driven execution over a reviewed spec is +disciplined engineering, not open-ended reasoning; the reliable workhorse class runs +it. Escalate to strongest-reasoner only when the brief's analysis step demands +interpretation the gates do not mechanically check. + +Iteration budget, default: one experiment per cast, inside the card's 150-step cap. +Smoke-run first, then the full run; a failed run is reported as it ran, not retried +until it looks better. When the cap is near, stop and return `EXHAUSTED:` with what +is done and what remains rather than truncating the run silently. + +Example brief (the shape of the input, not the cast grammar): + +```text +Run experiment E7 from the reviewed spec at specs/.md (acceptance, budget, +invariants). Env: the E7 row of /ledger/campaigns/CHECKOUTS.md — use exactly +that checkout. Write the experiment note to experiments/-e7.md; raw artifacts +next to the script. Anti-gaming contract: the spec's invariants section. +``` + ## Return contract (receipt) The full work product goes to a file, never into the final message alone: the path the @@ -61,10 +86,3 @@ receipt forward. The final message is a receipt of at most ~1,500 tokens: A structured result your card already defines (a verdict block, a debrief, filing receipts, a structured yaml) IS the receipt when it fits the bound — keep its shape. - -## Model routing - -Model routing, suggested: workhorse — brief-driven execution over a reviewed spec is -disciplined engineering, not open-ended reasoning; the reliable workhorse class runs -it. Escalate to strongest-reasoner only when the brief's analysis step demands -interpretation the gates do not mechanically check. diff --git a/packages/extension/agents/hypothesizer.md b/packages/extension/agents/hypothesizer.md index 399521eb5..01d1d6658 100644 --- a/packages/extension/agents/hypothesizer.md +++ b/packages/extension/agents/hypothesizer.md @@ -6,6 +6,7 @@ steps: 40 permission: edit: deny bash: deny +dispatch: hypothesizer-tuning --- You are the HYPOTHESIZER in an Amicode autoresearch loop. You see a fresh context: you know @@ -18,20 +19,7 @@ findings ledger (`amicode/skills-integrity/findings/`, personal vault) — read prior: known-bad recipes don't become proposed approaches; 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` +**Your job:** run the default procedure under `## Method` below, within the rules here. **Rules:** @@ -49,6 +37,42 @@ experiment notes and the insight corpus where relevant. top one, per the receipt contract below — the ranked list rides the receipt; the full spec-card draft is the work product. +## Method + +Default procedure — the complete default; a tuning overlay sharpens this method, +never replaces it: + +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` + +Model routing, default: strongest-reasoner — ranking hypotheses by testability × +impact and drafting falsifiable spec cards is judgment work; the strongest reasoner +available should hold the pen. + +Iteration budget, default: one ranking and one spec-card draft per cast, inside the +card's 40-step cap. Read the ledger and the notes it links first, rank once, draft the +single top spec card, and return the receipt; a second candidate gets its own cast. + +Example brief (the shape of the input, not the cast grammar): + +```text +Objective and standing directives: see the ledger's header. Ledger: +sessions/session-.md (read first: hypothesis table, gotchas, blocked list). +Findings ledger: amicode/skills-integrity/findings/. Prior notes: experiments/-e6.md +and insights/-.md. Write the ranked list and the spec-card draft to +/hypotheses.md. +``` + ## Return contract (receipt) The full work product goes to a file, never into the final message alone: the path the @@ -64,9 +88,3 @@ receipt forward. The final message is a receipt of at most ~1,500 tokens: A structured result your card already defines (a verdict block, a debrief, filing receipts, a structured yaml) IS the receipt when it fits the bound — keep its shape. - -## Model routing - -Model routing, suggested: strongest-reasoner — ranking hypotheses by testability × -impact and drafting falsifiable spec cards is judgment work; the strongest reasoner -available should hold the pen. diff --git a/packages/extension/agents/implementer.md b/packages/extension/agents/implementer.md index d33b909fd..7668aa2b9 100644 --- a/packages/extension/agents/implementer.md +++ b/packages/extension/agents/implementer.md @@ -5,6 +5,7 @@ steps: 200 permission: edit: allow bash: allow +dispatch: implementer-tuning --- You are the IMPLEMENTER — the per-slice Engineer of a dev-orchestration walk (the @@ -16,9 +17,8 @@ You are the hands of the walk, never its judge. 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. +**Your job:** run the default procedure under `## Method` below — the `implement-issue` +skill (with `--orchestrated`) owns it — within the hard rules here. **Hard rules:** @@ -47,6 +47,44 @@ rule), notes. That structured yaml IS your receipt under the return contract below: keep its shape, and put long material (full test output, logs) in the work-product file, not in `notes`. +## Method + +Default procedure — the complete default; a tuning overlay sharpens this method, +never replaces it: + +1. Verify the checkout: the caller-provided worktree branch is checked out and the + issue is open. If either conflicts with reality, stop and report — the hard rules + above bind. +2. 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. +3. Read every file the briefing points at before acting on it; never guess at content + you were pointed to. +4. Drive each acceptance criterion RED→GREEN through the tdd loop on the worktree + branch only; never merge partial or non-green work. +5. Return the implement-issue step-7 structured yaml as the receipt: a failed RED, an + infeasible Key Decision, or an untestable Acceptance Criterion is an escalation in + that return, never a bounce into design. + +Model routing, default: workhorse — TDD red-green over a TDD-ready issue is +disciplined engineering with mechanical verification; the reliable workhorse class +runs it. Escalate to strongest-reasoner only when the slice's Key Decisions demand +design judgment the issue did not settle. + +Iteration budget, default: one issue slice per cast, inside the card's 200-step cap. +Each RED gets bounded retry cycles to reach green; one that will not is a `failed` +return, not a negotiation. When the cap is near, stop and return `EXHAUSTED:` with what +is done and what remains. + +Example brief (the shape of the input, not the cast grammar): + +```text +Implement /# via implement-issue --orchestrated. Worktree: the +branch already checked out at . Parent decision surface: the parent +issue named in the sub-issue's header. Test command: the package's own test script, +run from the package directory. No PR, no merge, no push. +``` + ## Return contract (receipt) The full work product goes to a file, never into the final message alone: the path the @@ -62,10 +100,3 @@ receipt forward. The final message is a receipt of at most ~1,500 tokens: A structured result your card already defines (a verdict block, a debrief, filing receipts, a structured yaml) IS the receipt when it fits the bound — keep its shape. - -## Model routing - -Model routing, suggested: workhorse — TDD red-green over a TDD-ready issue is -disciplined engineering with mechanical verification; the reliable workhorse class -runs it. Escalate to strongest-reasoner only when the slice's Key Decisions demand -design judgment the issue did not settle. diff --git a/packages/extension/test/card_overlay_merge.test.ts b/packages/extension/test/card_overlay_merge.test.ts new file mode 100644 index 000000000..479f36c6a --- /dev/null +++ b/packages/extension/test/card_overlay_merge.test.ts @@ -0,0 +1,146 @@ +// card_overlay_merge.test.ts — #2118: every dispatching worker card (the four +// cast cards plus the librarian) carries a `## Method` section with a COMPLETE +// default for each method-class dimension, so a premium overlay is always a +// delta on a working default (ADR-0003, amicissimo) and never throws +// `base card lacks the anchor`. +// +// What is pinned per card: +// - `dispatch: -tuning` (and the critic carries none — new, no overlay) +// - a synthetic overlay with ALL FOUR method fields merges with no throw, +// and everything outside the Method slice (frontmatter incl. permission +// block, the Return contract, and the Output contract where the card has +// one) is byte-identical afterwards +// - interface-class and unclassified overlay fields are rejected +// - the unmerged base card is a complete default: non-empty text after each +// of the four anchors +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { + cardDispatch, + validateDispatchTarget, + mergeOverlayIntoCard, + METHOD_CLASS_FIELDS, +} from "../src/mode_cards"; + +const AGENTS_DIR = join(__dirname, "..", "agents"); +const DISPATCHING = ["analyzer", "experimenter", "hypothesizer", "implementer", "librarian"] as const; + +const card = (role: string) => readFileSync(join(AGENTS_DIR, `${role}.md`), "utf8"); + +/** Bytes of a `## ` section: heading to the next `## ` heading (or EOF). */ +const section = (text: string, heading: string) => { + const m = new RegExp(`^${heading}[^\\n]*$`, "m").exec(text); + if (!m) return null; + const rest = text.slice(m.index + m[0].length); + const next = /^## /m.exec(rest); + return text.slice(m.index, m.index + m[0].length + (next ? next.index : rest.length)); +}; +const frontmatterOf = (text: string) => text.slice(0, text.indexOf("\n---\n", 4) + 5); +const permissionBlock = (text: string) => /^permission:\n(?: {2}.+\n)+/m.exec(frontmatterOf(text))?.[0]; + +/** The card with its Method section cut out — everything an overlay must never reach. */ +const outsideMethod = (text: string) => { + const method = section(text, "## Method")!; + return text.replace(method, ""); +}; + +const FULL_OVERLAY = { + id: "synthetic-tuning", + fields: { + prompt_body: + "Default procedure — TUNED-PROCEDURE-MARKER:\n\n1. Tuned step one.\n2. Tuned step two.", + model_routing: "TUNED-ROUTING-MARKER strongest-reasoner for the hard casts", + iteration_budget: "TUNED-BUDGET-MARKER two passes per cast", + example_brief: "TUNED-BRIEF-MARKER read , then ", + }, +}; + +describe("dispatching worker cards accept a full method overlay (#2118)", () => { + for (const role of DISPATCHING) { + describe(role, () => { + const base = card(role); + + it(`declares dispatch: ${role}-tuning, a well-formed slug`, () => { + expect(cardDispatch(base)).toBe(`${role}-tuning`); + expect(() => validateDispatchTarget(`${role}.md`, cardDispatch(base)!)).not.toThrow(); + }); + + it("merges an overlay carrying all four method fields without throwing, outside-Method bytes untouched", () => { + expect(Object.keys(FULL_OVERLAY.fields).sort()).toEqual([...METHOD_CLASS_FIELDS].sort()); + const merged = mergeOverlayIntoCard(base, FULL_OVERLAY, `${role}.md`); + expect(merged.mergedFields).toEqual([...METHOD_CLASS_FIELDS]); + for (const marker of ["TUNED-PROCEDURE-MARKER", "TUNED-ROUTING-MARKER", "TUNED-BUDGET-MARKER", "TUNED-BRIEF-MARKER"]) { + expect(merged.text, `${role}: ${marker} landed`).toContain(marker); + } + // frontmatter (the permission block byte for byte) and the frozen sections + expect(frontmatterOf(merged.text)).toBe(frontmatterOf(base)); + expect(permissionBlock(base), `${role} has a permission block`).toBeDefined(); + expect(merged.text).toContain(permissionBlock(base)!); + const receipt = section(base, "## Return contract \\(receipt\\)"); + expect(receipt, `${role} carries the receipt contract`).not.toBeNull(); + expect(merged.text).toContain(receipt!); + const outputContract = section(base, "## Output contract"); + if (outputContract !== null) expect(merged.text).toContain(outputContract); + // and structurally: nothing outside the Method slice moved at all + expect(outsideMethod(merged.text)).toBe(outsideMethod(base)); + // the tuned defaults replaced the base defaults, one routing line remains + expect(merged.text).not.toContain("Model routing, default:"); + expect(merged.text).not.toContain("Iteration budget, default:"); + }); + + it("rejects an interface-class field and an unclassified field", () => { + expect(() => + mergeOverlayIntoCard(base, { id: "x", fields: { tool_permissions: "edit: allow" } }, `${role}.md`), + ).toThrow(/interface-class/); + expect(() => + mergeOverlayIntoCard(base, { id: "x", fields: { temperature_hack: "9" } }, `${role}.md`), + ).toThrow(/unclassified-class/); + // a method field riding along does not launder the offender + expect(() => + mergeOverlayIntoCard( + base, + { id: "x", fields: { model_routing: "workhorse", output_schema: "{}" } }, + `${role}.md`, + ), + ).toThrow(/interface-class/); + }); + + it("the empty overlay leaves the card byte-unchanged", () => { + const merged = mergeOverlayIntoCard(base, { id: "x", fields: {} }, `${role}.md`); + expect(merged.text).toBe(base); + expect(merged.mergedFields).toEqual([]); + }); + + it("the unmerged base is a complete default: non-empty text after each of the four anchors", () => { + const method = section(base, "## Method")!; + expect(method, `${role} carries ## Method`).not.toBeNull(); + // anchors in the pinned order, exactly one routing paragraph, one brief caption + const order = [/^Default procedure/m, /^Model routing, default:/m, /^Iteration budget, default:/m, /^Example brief/m]; + const at = order.map((re) => method.search(re)); + at.forEach((p, k) => expect(p, `${role}: anchor ${k} present`).toBeGreaterThan(-1)); + expect([...at].sort((a, b) => a - b)).toEqual(at); + expect((method.match(/^Model routing, /gm) ?? []).length).toBe(1); + expect((method.match(/^Iteration budget, /gm) ?? []).length).toBe(1); + expect((method.match(/^Example brief/gm) ?? []).length).toBe(1); + // content after each anchor (to the next anchor / paragraph) is real, not a stub + const afterDefault = method.slice(at[0]!, at[1]!); + expect(afterDefault, `${role}: numbered procedure`).toMatch(/^1\. \S/m); + expect(afterDefault.split("\n").slice(1).join("\n").trim().length).toBeGreaterThan(80); + expect(method.slice(at[1]!, at[2]!).replace(/^Model routing, default:/, "").trim().length).toBeGreaterThan(40); + expect(method.slice(at[2]!, at[3]!).replace(/^Iteration budget, default:/, "").trim().length).toBeGreaterThan(40); + const brief = /```text\n([\s\S]*?)```/.exec(method.slice(at[3]!)); + expect(brief, `${role}: example brief in a text fence`).not.toBeNull(); + expect(brief![1]!.trim().length).toBeGreaterThan(40); + // a model CLASS, never a model id + expect(method.slice(at[1]!, at[2]!)).toMatch(/workhorse|strongest-reasoner|fast-cheap/); + // no other `## ` heading falls inside the Method slice + expect(method.slice(method.indexOf("\n") + 1)).not.toMatch(/^## /m); + }); + }); + } + + it("the critic carries no dispatch target (new card, no overlay yet)", () => { + expect(cardDispatch(card("critic"))).toBeUndefined(); + }); +}); diff --git a/packages/extension/test/mode_cards_staging.test.ts b/packages/extension/test/mode_cards_staging.test.ts index 75432b444..73b71e1a7 100644 --- a/packages/extension/test/mode_cards_staging.test.ts +++ b/packages/extension/test/mode_cards_staging.test.ts @@ -294,9 +294,9 @@ describe("stageModCards — overlay merge (entitlement + overlays present)", () it("cards with NO dispatch target stage alone, no errors — role cards, directors, and an absent-target synthetic", () => { const { destDir, receipt, result } = stageEntitled(); - // D3 (#806): the four seeded role cards carry no dispatch target — - // bundle-owned registry artifacts, never overlay-tuned; the critic - // (#2093) is likewise a plain, untuned card + // #2118: the four seeded role cards now dispatch -tuning, but no + // overlay for them exists in this fixture registry, so they stage alone; + // the critic (#2093) is a plain, untuned card with no dispatch target for (const card of ["hypothesizer.md", "experimenter.md", "analyzer.md", "implementer.md", "critic.md", "develop.md", "research.md"]) { expect(readFileSync(join(destDir, card), "utf8")).toBe( readFileSync(join(AGENTS_SRC, card), "utf8"), @@ -435,29 +435,33 @@ describe("stageModCards — freeze violations stage the base alone, rejection re }); // ── #761: the dispatch-target validator ─────────────────────────────────── +/** #2118: the cards that carry a Method section + `dispatch: -tuning`. */ +const DISPATCHING_CARDS = ["analyzer.md", "experimenter.md", "hypothesizer.md", "implementer.md", "librarian.md"]; describe("dispatch-target validator", () => { 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 === "librarian.md") { - expect(target, "librarian — the surviving worker card — declares a dispatch target").toBeDefined(); + if (DISPATCHING_CARDS.includes(f)) { + expect(target, `${f} declares its -tuning dispatch target`).toBe(`${f.replace(/\.md$/, "")}-tuning`); expect(() => validateDispatchTarget(f, target!)).not.toThrow(); } else { - // directors + the four D3-seeded role cards dispatch no overlay + // directors and the critic (#2093, no overlay yet) dispatch no overlay expect(target, `${f} dispatches no overlay`).toBeUndefined(); } } }); - it("after D3 the only shipped dispatch target is librarian-tuning, on librarian alone", () => { + it("the shipped dispatch targets are exactly -tuning on the five worker cards (#2118)", () => { 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.entries()]).toEqual([["librarian-tuning", ["librarian.md"]]]); + expect([...targets.entries()].sort()).toEqual( + DISPATCHING_CARDS.map((f) => [`${f.replace(/\.md$/, "")}-tuning`, [f]] as [string, string[]]).sort(), + ); }); 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 index dde05cc18..e53103732 100644 --- a/packages/extension/test/role_cards.test.ts +++ b/packages/extension/test/role_cards.test.ts @@ -198,7 +198,7 @@ describe("staging — the seeded role cards ride the bundle, digest-verified (H2 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`, () => { + it(`${role}: frontmatter — description, subagent mode, permission scoping, and the -tuning overlay dispatch`, () => { const text = cardText(role); expect(text.startsWith("---\n")).toBe(true); const end = text.indexOf("\n---\n", 4); @@ -207,9 +207,10 @@ describe("role-card structural floor (pre-signature: shape, never flagged conten 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); + // #2118: the cast cards carry a Method section with a complete default + // and dispatch to -tuning, so an entitled overlay can merge + // (the merge itself is pinned in card_overlay_merge.test.ts) + expect(fm, `${role} dispatches ${role}-tuning`).toMatch(new RegExp(`^dispatch:[ \\t]*${role}-tuning$`, "m")); }); it(`${role}: zero blocklisted proprietary strings / banned names`, () => {