From 74b24366a04b9dbb32b7de656569b594f16a2a94 Mon Sep 17 00:00:00 2001 From: aaron Date: Sun, 23 Aug 2026 07:31:03 -0400 Subject: [PATCH] feat(extension): research + dev gate-pack fixtures as schema'd TOML with mapping-completeness tests (#498) Gate packs as typed data, not prose: the research pack (5 phases, 8 gates, hypothesizer/experimenter/analyzer, experiment-note + ledger-delta closing, issue-seed handoff) extracted from the live director research protocol, and the dev pack (3 phases, 5 gates, worktree-bound no-merge implementer, landed-delta closing, hypothesis-seed handoff) extracted from the issue-DAG walk. The suite in test/gate_packs.test.ts is the spec's gate_pack_mapping_complete criterion: pack schema with kind vocabularies, forward + reverse mapping completeness, non-triviality floors, unique gate names, and pairwise-distinct procedures under normalized-text equality. Parses via smol-toml (already a devDependency). --- packages/extension/gate-packs/dev.toml | 49 +++ packages/extension/gate-packs/research.toml | 76 +++++ packages/extension/test/gate_packs.test.ts | 346 ++++++++++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 packages/extension/gate-packs/dev.toml create mode 100644 packages/extension/gate-packs/research.toml create mode 100644 packages/extension/test/gate_packs.test.ts diff --git a/packages/extension/gate-packs/dev.toml b/packages/extension/gate-packs/dev.toml new file mode 100644 index 00000000..54774438 --- /dev/null +++ b/packages/extension/gate-packs/dev.toml @@ -0,0 +1,49 @@ +# Dev gate pack — the contract of record for the dev mode's phase graph, +# gates, roles, and handoffs, extracted from the issue-DAG walk (decompose, +# per-slice implementation, integration) and its implementer role contract. + +closing_artifact = "landed-delta record (issues closed, PRs merged, advisories resolved)" + +[[phases]] +name = "decompose" + + [[phases.gates]] + name = "dev-gate" + kind = "mechanical" + owner = "director" + procedure = "Attach every unit of package work to an issue and a PR before any file is modified: the walk validates each issue exists and is open and establishes the branch-and-PR topology before dispatching the first slice." + + [[phases.gates]] + name = "blocked-by-clearance" + kind = "mechanical" + owner = "director" + procedure = "Read each slice's blocked-by dependencies before creating any branch; a slice with an open blocker is never started and sits blocked until the blocker's PR merges." + +[[phases]] +name = "implement" +roles = ["implementer"] + + [[phases.gates]] + name = "tdd-red-green" + kind = "mechanical" + owner = "implementer" + procedure = "Drive each acceptance criterion through a red-then-green tracer bullet on the caller-provided worktree branch; never delete, skip, or mark a test broken to force green, and report a red that will not go green as a failed return rather than a negotiation." + +[[phases]] +name = "integrate" + + [[phases.gates]] + name = "draft-pr-lifecycle" + kind = "derived" + owner = "director" + procedure = "Open the PR as a draft at the first commit — never before it — and mark it ready only when the full suite is green; in the orchestrated walk the implementer opens no PR and never merges, and the director merges green branches sequentially, never partial or non-green work." + + [[phases.gates]] + name = "review" + kind = "human" + owner = "human" + procedure = "A unit finishing under human-in-the-loop review has its ready PR approved by a reviewer who is never the implementer, and no merge happens before that approval." + +[[handoffs]] +kind = "hypothesis_seed" +target = "autoresearch" diff --git a/packages/extension/gate-packs/research.toml b/packages/extension/gate-packs/research.toml new file mode 100644 index 00000000..3d6bb44f --- /dev/null +++ b/packages/extension/gate-packs/research.toml @@ -0,0 +1,76 @@ +# Research gate pack — the contract of record for the research mode's phase +# graph, gates, roles, and handoffs, extracted from the live director +# research protocol (session-ledger discipline, the cast pattern, verdicts +# derived from commands, human-only promotion). + +closing_artifact = "experiment note + ledger delta" + +[[phases]] +name = "hypothesize" +roles = ["hypothesizer"] + + [[phases.gates]] + name = "ledger-currency" + kind = "derived" + owner = "director" + procedure = "Re-read the session ledger from disk before trusting the hypothesis queue; a ledger whose in-flight section is older than the last edit on disk is stale and must be refreshed before any hypothesis is picked." + +[[phases]] +name = "deliberate" + + [[phases.gates]] + name = "spec-review" + kind = "mechanical" + owner = "director" + procedure = "File the spec card and run the spec review command; resolve every blocking finding and re-run within a round budget of 3 — the unreviewed escape hatch is forbidden for launch-shaped work that spends compute." + +[[phases]] +name = "experiment" +roles = ["experimenter"] + + [[phases.gates]] + name = "probe-boundary" + kind = "mechanical" + owner = "director" + procedure = "Classify work by the mechanical boundary before it runs: read-only probes and bounded side-effect-free one-liners need no spec, while anything that writes a durable file, launches a solve, or runs a test suite is an experiment and requires a reviewed spec." + + [[phases.gates]] + name = "checkout-claim" + kind = "derived" + owner = "director" + procedure = "Claim the environment row in the fleet checkout registry before casting the experimenter, and dispatch parallel experimenters only where the registry says their files are disjoint." + +[[phases]] +name = "gate" + + [[phases.gates]] + name = "run-gates" + kind = "mechanical" + owner = "director" + procedure = "Run the gates yourself through the shell — test suites and compiled-plan status — and derive every verdict from command output; verdicts are never self-reported and no language model judges a fidelity claim." + +[[phases]] +name = "analyze" +roles = ["analyzer"] + + [[phases.gates]] + name = "advisory-closure" + kind = "derived" + owner = "director" + procedure = "Close every advisory from the spec review as fixed, waived-with-reason, or obsolete, and record each closure in the ledger before the loop ends." + + [[phases.gates]] + name = "ledger-commit" + kind = "derived" + owner = "director" + procedure = "Commit the ledger update at the loop boundary as sole writer — hypothesis-verdict row, loop-log row, in-flight state, and next queue — so the ledger never lies about what is in the air." + + [[phases.gates]] + name = "catalog-promotion" + kind = "human" + owner = "human" + procedure = "Promote any result to the pulse catalog or a status record only through a human decision; promotion is human-only, always, and never an agent action." + +[[handoffs]] +kind = "issue_seed" +target = "autodev" diff --git a/packages/extension/test/gate_packs.test.ts b/packages/extension/test/gate_packs.test.ts new file mode 100644 index 00000000..b8d3e701 --- /dev/null +++ b/packages/extension/test/gate_packs.test.ts @@ -0,0 +1,346 @@ +import { describe, it, expect } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { parse } from "smol-toml"; + +// Gate-pack fixtures of record: the research pack (extracted from the live +// director research protocol) and the dev pack (extracted from the issue-DAG +// walk). This suite IS the spec's gate_pack_mapping_complete criterion: +// pack schema, forward + reverse mapping completeness, non-triviality +// floors, distinctness, and faithfulness of the extraction. + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const EXT = path.resolve(HERE, ".."); +const PACK_DIR = path.join(EXT, "gate-packs"); + +const GATE_KINDS = ["mechanical", "human", "derived"] as const; +const HANDOFF_KINDS = ["issue_seed", "hypothesis_seed"] as const; +const HANDOFF_TARGETS = ["autoresearch", "autodev"] as const; + +interface Gate { + name: string; + kind: string; + owner: string; + procedure: string; +} + +interface Phase { + name: string; + gates?: Gate[]; + roles?: string[]; +} + +interface Handoff { + kind: string; + target: string; +} + +interface Pack { + phases?: Phase[]; + closing_artifact?: string; + handoffs?: Handoff[]; +} + +function loadPack(file: string): Pack { + return parse(fs.readFileSync(path.join(PACK_DIR, file), "utf8")) as unknown as Pack; +} + +const research = loadPack("research.toml"); +const dev = loadPack("dev.toml"); +const PACKS: Array<[string, Pack]> = [ + ["research", research], + ["dev", dev], +]; + +function isNonEmptyString(v: unknown): v is string { + return typeof v === "string" && v.trim().length > 0; +} + +function phases(pack: Pack): Phase[] { + return Array.isArray(pack.phases) ? (pack.phases as Phase[]) : []; +} + +function gates(pack: Pack): Array<{ phase: string; gate: Gate }> { + const out: Array<{ phase: string; gate: Gate }> = []; + for (const phase of phases(pack)) { + for (const gate of phase.gates ?? []) out.push({ phase: phase.name, gate }); + } + return out; +} + +function roles(pack: Pack): string[] { + return phases(pack).flatMap((p) => p.roles ?? []); +} + +// Normalized-text equality: case-insensitive, whitespace collapsed. +function normalize(text: string): string { + return text.toLowerCase().replace(/\s+/g, " ").trim(); +} + +function gateByName(pack: Pack, name: string): Gate { + const found = gates(pack).find((g) => g.gate.name === name); + expect(found, `gate ${name} missing`).toBeDefined(); + return found!.gate; +} + +describe("gate-pack fixtures of record", () => { + it("the pack directory holds exactly the research and dev fixtures", () => { + expect(fs.readdirSync(PACK_DIR).sort()).toEqual(["dev.toml", "research.toml"]); + }); +}); + +describe.each(PACKS)("%s pack: schema", (_name, pack) => { + it("has exactly the pack fields: phases, closing_artifact, handoffs", () => { + expect(Object.keys(pack).sort()).toEqual(["closing_artifact", "handoffs", "phases"]); + }); + + it("declares exactly one closing artifact, a non-empty string", () => { + // The strict pack-key set above guarantees there is no second artifact field. + expect(isNonEmptyString(pack.closing_artifact)).toBe(true); + }); + + it("gives every phase a name and a gates array, carrying only schema'd keys", () => { + for (const phase of phases(pack)) { + expect(isNonEmptyString(phase.name)).toBe(true); + expect(Array.isArray(phase.gates)).toBe(true); + const keys = Object.keys(phase).sort(); + const allowed = + JSON.stringify(keys) === JSON.stringify(["gates", "name"]) || + JSON.stringify(keys) === JSON.stringify(["gates", "name", "roles"]); + expect(allowed, `phase keys ${keys.join(",")}`).toBe(true); + } + }); + + it("gives every gate exactly name, kind, owner, procedure — all non-empty strings", () => { + for (const { gate } of gates(pack)) { + expect(Object.keys(gate).sort()).toEqual(["kind", "name", "owner", "procedure"]); + expect(isNonEmptyString(gate.name)).toBe(true); + expect(isNonEmptyString(gate.kind)).toBe(true); + expect(isNonEmptyString(gate.owner)).toBe(true); + expect(isNonEmptyString(gate.procedure)).toBe(true); + } + }); + + it("enforces the gate-kind vocabulary: mechanical | human | derived", () => { + for (const { gate } of gates(pack)) { + expect(GATE_KINDS, `gate ${gate.name} kind ${gate.kind}`).toContain(gate.kind); + } + }); + + it("gives every handoff exactly kind and target, each in vocabulary", () => { + for (const handoff of pack.handoffs ?? []) { + expect(Object.keys(handoff).sort()).toEqual(["kind", "target"]); + expect(HANDOFF_KINDS).toContain(handoff.kind); + expect(HANDOFF_TARGETS).toContain(handoff.target); + } + }); +}); + +describe.each(PACKS)("%s pack: forward mapping completeness", (_name, pack) => { + it("every phase carries at least one gate", () => { + for (const phase of phases(pack)) { + expect((phase.gates ?? []).length, `phase ${phase.name}`).toBeGreaterThanOrEqual(1); + } + }); + + it("names exactly one closing artifact", () => { + expect(isNonEmptyString(pack.closing_artifact)).toBe(true); + }); + + it("every gate names an owner and a kind", () => { + for (const { gate } of gates(pack)) { + expect(isNonEmptyString(gate.owner), `${gate.name}.owner`).toBe(true); + expect(isNonEmptyString(gate.kind), `${gate.name}.kind`).toBe(true); + } + }); + + it("every role appears in at least one phase", () => { + for (const role of new Set(roles(pack))) { + expect(phases(pack).some((p) => (p.roles ?? []).includes(role)), `role ${role}`).toBe(true); + } + }); +}); + +describe.each(PACKS)("%s pack: reverse mapping completeness", (_name, pack) => { + it("every gate belongs to a phase (no pack-level or orphan gates)", () => { + // Gates are reachable only through phases[].gates (strict pack and phase + // key sets), so no gate can exist outside a phase; count consistency + // proves the enumeration sees them all. + expect("gates" in pack).toBe(false); + const viaPhases = phases(pack).reduce((n, p) => n + (p.gates ?? []).length, 0); + expect(gates(pack).length).toBe(viaPhases); + for (const { gate } of gates(pack)) { + expect(isNonEmptyString(gate.name)).toBe(true); + } + }); + + it("every role belongs to the pack — a non-empty string declared inside a phase", () => { + for (const phase of phases(pack)) { + for (const role of phase.roles ?? []) { + expect(isNonEmptyString(role), `role in phase ${phase.name}`).toBe(true); + } + } + }); + + it("every handoff has a target kind (a named autonomous mode)", () => { + for (const handoff of pack.handoffs ?? []) { + expect(HANDOFF_TARGETS, `handoff target ${handoff.target}`).toContain(handoff.target); + } + }); +}); + +describe.each(PACKS)("%s pack: non-triviality floors", (_name, pack) => { + it("declares at least 3 phases", () => { + expect(phases(pack).length).toBeGreaterThanOrEqual(3); + }); + + it("carries at least 5 gates", () => { + expect(gates(pack).length).toBeGreaterThanOrEqual(5); + }); + + it("carries at least one gate of each kind: mechanical, human, derived", () => { + const kinds = new Set(gates(pack).map((g) => g.gate.kind)); + for (const kind of GATE_KINDS) { + expect(kinds.has(kind), `no ${kind} gate`).toBe(true); + } + }); + + it("every phase that declares roles declares at least one", () => { + for (const phase of phases(pack)) { + if (phase.roles !== undefined) { + expect(phase.roles.length, `phase ${phase.name}`).toBeGreaterThanOrEqual(1); + } + } + }); + + it("declares at least one handoff", () => { + expect((pack.handoffs ?? []).length).toBeGreaterThanOrEqual(1); + }); + + it("gate names are unique within the pack", () => { + const names = gates(pack).map((g) => g.gate.name); + expect(new Set(names).size, `duplicate gate names`).toBe(names.length); + }); + + it("gate procedures are pairwise distinct under normalized-text equality", () => { + const procedures = gates(pack).map((g) => normalize(g.gate.procedure)); + for (let i = 0; i < procedures.length; i++) { + for (let j = i + 1; j < procedures.length; j++) { + expect(procedures[i] === procedures[j], `procedures ${i} and ${j} collide`).toBe(false); + } + } + }); +}); + +describe("research pack extraction (faithful to the director research protocol)", () => { + it("runs the five phases hypothesize → deliberate → experiment → gate → analyze", () => { + expect(phases(research).map((p) => p.name)).toEqual([ + "hypothesize", + "deliberate", + "experiment", + "gate", + "analyze", + ]); + }); + + it("carries the spec-review gate: mechanical, round budget of 3, no unreviewed launch-shaped work", () => { + const gate = gateByName(research, "spec-review"); + expect(gate.kind).toBe("mechanical"); + expect(gate.procedure).toContain("round budget of 3"); + expect(gate.procedure).toContain("launch-shaped"); + }); + + it("carries run gates whose verdicts derive from commands, never self-reported", () => { + const gate = gateByName(research, "run-gates"); + expect(gate.kind).toBe("mechanical"); + expect(gate.procedure).toContain("self-reported"); + }); + + it("carries catalog promotion as human-only", () => { + const gate = gateByName(research, "catalog-promotion"); + expect(gate.kind).toBe("human"); + expect(gate.owner).toBe("human"); + expect(gate.procedure).toContain("human-only"); + }); + + it("carries the protocol's additional gates: advisory closure and ledger commit", () => { + expect(gateByName(research, "advisory-closure").procedure).toContain("waived"); + expect(gateByName(research, "ledger-commit").procedure).toContain("sole writer"); + }); + + it("casts hypothesizer, experimenter, analyzer — each bound to its phase", () => { + const byPhase = new Map(phases(research).map((p) => [p.name, p.roles ?? []])); + expect(byPhase.get("hypothesize")).toEqual(["hypothesizer"]); + expect(byPhase.get("experiment")).toEqual(["experimenter"]); + expect(byPhase.get("analyze")).toEqual(["analyzer"]); + expect(new Set(roles(research))).toEqual( + new Set(["hypothesizer", "experimenter", "analyzer"]), + ); + }); + + it("closes with the experiment note + ledger delta", () => { + expect(research.closing_artifact).toContain("experiment note"); + expect(research.closing_artifact).toContain("ledger delta"); + }); + + it("hands off issue seeds to the dev mode", () => { + expect((research.handoffs ?? []).length).toBeGreaterThanOrEqual(1); + for (const handoff of research.handoffs ?? []) { + expect(handoff.kind).toBe("issue_seed"); + expect(handoff.target).toBe("autodev"); + } + }); +}); + +describe("dev pack extraction (faithful to the issue-DAG walk)", () => { + it("runs the three phases decompose → implement → integrate", () => { + expect(phases(dev).map((p) => p.name)).toEqual(["decompose", "implement", "integrate"]); + }); + + it("carries the dev gate: issue + PR before any package work", () => { + const gate = gateByName(dev, "dev-gate"); + expect(gate.kind).toBe("mechanical"); + expect(gate.procedure).toContain("issue"); + expect(gate.procedure).toContain("PR"); + }); + + it("carries TDD red-green with the test-protection clause", () => { + const gate = gateByName(dev, "tdd-red-green"); + expect(gate.kind).toBe("mechanical"); + expect(gate.procedure).toContain("force green"); + }); + + it("carries the draft-PR lifecycle: draft at first commit, ready only when green, never merge non-green", () => { + const gate = gateByName(dev, "draft-pr-lifecycle"); + expect(gate.kind).toBe("derived"); + expect(gate.procedure).toContain("draft"); + expect(gate.procedure).toContain("non-green"); + }); + + it("carries review as a human gate where the reviewer is never the implementer", () => { + const gate = gateByName(dev, "review"); + expect(gate.kind).toBe("human"); + expect(gate.procedure).toContain("never the implementer"); + }); + + it("binds one role: the implementer, worktree-bound and merge-free", () => { + const implement = phases(dev).find((p) => p.name === "implement"); + expect(implement?.roles).toEqual(["implementer"]); + expect(new Set(roles(dev))).toEqual(new Set(["implementer"])); + expect(gateByName(dev, "tdd-red-green").procedure).toContain("worktree"); + expect(gateByName(dev, "draft-pr-lifecycle").procedure).toContain("never merges"); + }); + + it("closes with the landed-delta record", () => { + expect(dev.closing_artifact).toContain("landed-delta record"); + }); + + it("hands off hypothesis seeds to the research mode", () => { + expect((dev.handoffs ?? []).length).toBeGreaterThanOrEqual(1); + for (const handoff of dev.handoffs ?? []) { + expect(handoff.kind).toBe("hypothesis_seed"); + expect(handoff.target).toBe("autoresearch"); + } + }); +});