diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 74f55483e..236e9b64a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -278,7 +278,7 @@ Enforcement is runtime code at the existing tool-mount point, not prompt wording #### Closed director fleet (`src/agent/directors/`) -Every shipped specialist is a **director package** — a prompt-first `DirectorPackage` (system prompt, tool envelope, spawn rights, nudge budget, report contract, `modelRole`, fleet authority `tier`) registered in a **closed** set of 10 ids. There is no catch-all worker: `spawn_agent` without `agent` or non-general `intent`, and `spawn_agent(intent="general")`, fail closed so the primary reclassifies. Nested directors with a spawn allowlist reject off-list children at `spawn_agent` dispatch time (not prompt-only). dispatch is the primary session identity: `spawn_agent(agent="dispatch")` is refused, and `directorProfiles()` omits it from the spawn catalog. +Every shipped specialist is a **director package** — a prompt-first `DirectorPackage` (system prompt, tool envelope, spawn rights, nudge budget, report contract, `modelRole`, fleet authority `tier`) registered in a **closed** set of 11 ids. There is no catch-all worker: `spawn_agent` without `agent` or non-general `intent`, and `spawn_agent(intent="general")`, fail closed so the primary reclassifies. Nested directors with a spawn allowlist reject off-list children at `spawn_agent` dispatch time (not prompt-only). dispatch is the primary session identity: `spawn_agent(agent="dispatch")` is refused, and `directorProfiles()` omits it from the spawn catalog. **Primary** @@ -288,17 +288,18 @@ Every shipped specialist is a **director package** — a prompt-first `DirectorP **Specialists** -| Director | Owns | Does not own | -| ----------- | -------------------------------------------------------------------- | ---------------------- | -| explorer | Read-only codebase mapping | Product edits | -| planner | PRD.md, SOLUTION_SCOPE.md, BUILD_PLAN.md authoring | Code | -| coder | Minimal safe diffs, root-cause fixes, tests | Pure docs, pure review | -| reviewer | Evidence-based defect and quality review with temp-test verification | Fixing product code | -| designer | DESIGN.md, design laws, tokens, interface polish | Non-UI product code | -| artist | SVGs, diagrams, generative graphic prompts | Product code | -| warden | Trust review of permission, provider-auth, and plugin-loader diffs | Fixes, general review | -| shakespeare | Docs maintenance (PRODUCT / ARCHITECTURE / IMPLEMENTATION) | Product code | -| prober | Measure-only latency and behavior probe per family/model | Fixes, product edits | +| Director | Owns | Does not own | +| ----------- | -------------------------------------------------------------------- | ---------------------------------- | +| explorer | Read-only codebase mapping | Product edits | +| planner | PRD.md, SOLUTION_SCOPE.md, BUILD_PLAN.md authoring | Code | +| coder | Minimal safe diffs, root-cause fixes, tests | Pure docs, pure review | +| reviewer | Evidence-based defect and quality review with temp-test verification | Fixing product code | +| designer | DESIGN.md, design laws, tokens, interface polish | Non-UI product code | +| artist | SVGs, diagrams, generative graphic prompts | Product code | +| warden | Trust review of permission, provider-auth, and plugin-loader diffs | Fixes, general review | +| shakespeare | Docs maintenance (PRODUCT / ARCHITECTURE / IMPLEMENTATION) | Product code | +| prober | Measure-only latency and behavior probe per family/model | Fixes, product edits | +| qa-lead | QA Lead: hands-on CLI, e2e, occupancy, capability proof | Unit tests, latency, product fixes | **Intent → director** (`spawn_agent(intent=…)` when `agent` is omitted). implement/review (and their default directors) fail closed without non-empty `success_criteria`. diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index f891eeba7..6e484a1be 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -155,12 +155,12 @@ Capabilities beyond the core toolset are opt-in plugins, enabled per workspace t ## Multi-agent (fleet agents) -The primary session is always **orchestrator** (single-agent mode is gone). Its director is **dispatch**: classify work, DIY tiny/single-file/one-route product edits, dispatch a **closed fleet of 10 directors** for substantial work, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary (CORE / `DISPATCH_TOOLS`) — path tools are the DIY surface; spawn remains the default for substantial, multi-file, parallel, or specialist work. Shell file-writes stay denied. MCP tools are not re-filtered by a product-write deny list (that list is gone). There is no static per-package write-path declaration (CL-6952 removed it — no shipped director ever set one). A concurrent dispatch landing on the same working directory as another still-running lane is recorded as a `conflict` intervention, not blocked. Operator slash recipes (`/implement`, `/plan`, `/refactor`, `/review`, `/pull-request`, `/issue`, `/docs`, `/interview`) tell dispatch which directors to spawn for substantial work; tiny/bounded edits may run on the primary. +The primary session is always **orchestrator** (single-agent mode is gone). Its director is **dispatch**: classify work, DIY tiny/single-file/one-route product edits, dispatch a **closed fleet of 11 directors** for substantial work, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary (CORE / `DISPATCH_TOOLS`) — path tools are the DIY surface; spawn remains the default for substantial, multi-file, parallel, or specialist work. Shell file-writes stay denied. MCP tools are not re-filtered by a product-write deny list (that list is gone). There is no static per-package write-path declaration (CL-6952 removed it — no shipped director ever set one). A concurrent dispatch landing on the same working directory as another still-running lane is recorded as a `conflict` intervention, not blocked. Operator slash recipes (`/implement`, `/plan`, `/refactor`, `/review`, `/pull-request`, `/issue`, `/docs`, `/interview`) tell dispatch which directors to spawn for substantial work; tiny/bounded edits may run on the primary. -| Role | Directors | -| ---------- | --------------------------------------------------------------------------------- | -| Primary | dispatch | -| Specialist | explorer, planner, coder, reviewer, designer, artist, warden, shakespeare, prober | +| Role | Directors | +| ---------- | ------------------------------------------------------------------------------------------ | +| Primary | dispatch | +| Specialist | explorer, planner, coder, reviewer, designer, artist, warden, shakespeare, prober, qa-lead | There is **no catch-all worker**. `spawn_agent` requires `agent=…` or a non-general `intent` (implement→coder, explore→explorer, plan→planner, review→reviewer); bare dispatch and `intent=general` are refused. Named `spawn_agent(agent=…)` selects a director package without requiring a plugin profile, except `dispatch` which is the primary session identity and is refused as a spawned worker. Nested spawn is runtime-enforced: only dispatch (full fleet allowlist) may spawn; all other workers, have no fleet tools. Primary omits an allowlist so plugin profiles remain reachable from the main session. diff --git a/scripts/ci-timings.json b/scripts/ci-timings.json index 057f0f998..c4991aae5 100644 --- a/scripts/ci-timings.json +++ b/scripts/ci-timings.json @@ -451,7 +451,7 @@ "src/agent/directors/prober/package.test.ts": 1, "src/agent/directors/rand/package.test.ts": 1, "src/agent/directors/shakespeare/package.test.ts": 1, - "src/agent/directors/tester/package.test.ts": 1, + "src/agent/directors/qa-lead/package.test.ts": 1, "src/agent/directors/warden/package.test.ts": 1, "src/agent/grok-residual.test.ts": 1, "src/agent/tool-classification.test.ts": 1, diff --git a/src/agent/directors/dispatch/package.ts b/src/agent/directors/dispatch/package.ts index f380b341a..0313df46d 100644 --- a/src/agent/directors/dispatch/package.ts +++ b/src/agent/directors/dispatch/package.ts @@ -16,6 +16,7 @@ You are Dispatch, the coordinator for Corbits Code. Specialists own substantive - Security auditing, trust boundaries, permissions, or secret guard review: spawn warden (permission and trust review). - Product, architecture, or implementation documentation: spawn shakespeare (PRODUCT, ARCHITECTURE, IMPLEMENTATION docs). - Model distribution benchmarks, latency probing, or prompt evaluation: spawn prober (latency and behavior distributions). +- Hands-on product exercise, e2e, occupancy, or actually using the CLI: spawn qa-lead (behavior proof, pass/fail with commands). Distinct from coder unit tests and prober measurement. # Delegation boundary - Delegate by default. File count does not determine complexity: a single-file bug fix or behavior change still belongs to coder. @@ -75,6 +76,7 @@ export const dispatchPackage: DirectorPackage = { "warden", "shakespeare", "prober", + "qa-lead", ], }, modelRole: "orchestrator", diff --git a/src/agent/directors/qa-lead/package.ts b/src/agent/directors/qa-lead/package.ts new file mode 100644 index 000000000..08597b21d --- /dev/null +++ b/src/agent/directors/qa-lead/package.ts @@ -0,0 +1,59 @@ +import type { DirectorPackage } from "../types.js"; +import { REVIEW_TOOLS } from "../tool-sets.js"; + +/** + * QA Lead worker (CL-9903). + * Hands-on product exercise — actually run the CLI / e2e / occupancy / + * capability path as behavior proof (pass/fail with commands); never author + * unit tests, never measure family/model latency, never fix product code. + */ +export const qaLeadPackage: DirectorPackage = { + id: "qa-lead", + primaryIntent: + "Hands-on product exercise: actually run CLI/e2e/occupancy/capability as proof (pass/fail with commands); never author unit tests, never measure latency, never fix product code", + outOfLane: [ + "authoring unit tests (coder)", + "measuring family/model latency (prober)", + "fixing product code", + "fleet orchestration or spawning", + ], + description: + "QA Lead — hands-on product exercise: CLI/e2e/occupancy/capability proof", + systemPrompt: `You are QA Lead (\`qa-lead\`), a specialist in Corbits Code. + +PRIMARY INTENT: hands-on product exercise. Actually run the product as an operator would and report behavior as proof — pass/fail with exact commands. Never author unit tests. Never measure family/model latency. Never fix product code. Never spawn. + +You are the hands-on lane — not Coder (unit tests with product diffs), not Prober (family/model latency matrices), not Reviewer, not an orchestrator, not a product fixer. Do not spawn specialists. Do not edit product code to make a failing exercise pass; a test that moves the target is not an exercise. + +BLINDERS ON: exercise what the brief's success_criteria ask for, on the product below. Do not wander into unit-test authorship, latency matrices, or product fixes. + +# What to run + +- The operator CLI: \`corbits\` and \`corbits exec\` against fixture copies or the worktree the brief names. +- Integration scenarios: \`bun test ./e2e\` — production agent loop, compaction occupancy, mailbox, permission, spawn lanes. +- Capability runs as behavior: \`bun run eval:capability\` (\`scripts/eval-capability.ts\` over \`evals/capability\`) — pass/fail of the product path, not a latency matrix. + +# How to judge + +- Each success_criteria item is pass, fail, or blocked, with the exact command, exit status, and relevant output. +- Compaction occupancy: run the e2e compaction suite (\`e2e/compaction-*.test.ts\` and related occupancy coverage) and report whether occupancy, mailbox mail, and compact-then-continue actually behave. +- Do not author \`src/**/*.test.ts\` unit tests — that is Coder's lane. Temporary repro files under \`tmp/\` are allowed only when the brief needs a scripted operator session, then clean them up. +- Do not slice by family/model latency (TTFT, tool-only streaks, salvage/nudge counts) — that is Prober's lane. A capability run here is a behavior verdict, not a distribution. + +# Findings route to follow-ups, never silent product edits + +- Report commands, exit codes, and failing output so Coder can reproduce. +- Product-looking conclusions go to Blockers as follow-up tickets — do not patch src/ here. + +# Report + +When done, stop tooling and reply with ONLY the Corbits report envelope — the shared scaffold owns its shape (Summary / Findings / Blockers / Paths, in that order), so this package does not re-specify it. Findings for this lane: pass/fail/blocked per success_criteria item, exact commands and exit statuses, occupancy/e2e/CLI evidence. + +DONE GATE: stop when the brief's exercise ask is answered with evidence OR explicitly blocked under Blockers. Do not expand into unit tests, latency matrices, product fixes, or orchestration. + +OUT OF LANE: authoring unit tests (route to coder), measuring family/model latency (route to prober), fixing product code, fleet orchestration, spawning.`, + tools: { allow: REVIEW_TOOLS }, + spawn: { maySpawn: false }, + tier: "leaf", + modelRole: "test", +}; diff --git a/src/agent/directors/registry.test.ts b/src/agent/directors/registry.test.ts index d5dd6a4cf..22a9d3480 100644 --- a/src/agent/directors/registry.test.ts +++ b/src/agent/directors/registry.test.ts @@ -13,9 +13,9 @@ import { } from "./registry.js"; describe("director registry", () => { - test("closed set has exactly 10 directors", () => { - expect(DIRECTOR_IDS).toHaveLength(10); - expect(listDirectors()).toHaveLength(10); + test("closed set has exactly 11 directors", () => { + expect(DIRECTOR_IDS).toHaveLength(11); + expect(listDirectors()).toHaveLength(11); for (const id of DIRECTOR_IDS) { expect(DIRECTOR_REGISTRY[id].id).toBe(id); } @@ -116,7 +116,7 @@ describe("director registry", () => { test("directorProfiles is the spawn catalog (closed set minus dispatch)", () => { const profiles = directorProfiles(); - expect(profiles).toHaveLength(9); + expect(profiles).toHaveLength(10); expect(profiles.map((p) => p.id)).not.toContain("dispatch"); }); @@ -157,7 +157,7 @@ describe("director registry", () => { expect(s.tools?.allow).toContain("write_file"); expect(s.tools?.allow).toContain("edit_file"); expect(s.tools?.allow).toContain("delete_file"); - expect(s.spawn.allowlist).toHaveLength(9); + expect(s.spawn.allowlist).toHaveLength(10); }); test("tier agrees with spawn.maySpawn for every director", () => { @@ -182,6 +182,36 @@ describe("director registry", () => { expect(tierForDirectorId("prober")).toBe("leaf"); }); + test("qa-lead is a hands-on leaf, not prober, with no spawn", () => { + const r = resolveDirector({ agentId: "qa-lead" }); + expect(r.ok).toBe(true); + if (r.ok) { + expect(r.package.id).toBe("qa-lead"); + expect(r.package.tier).toBe("leaf"); + expect(r.package.spawn.maySpawn).toBe(false); + expect(r.package.modelRole).toBe("test"); + expect(r.package.tools?.allow).toContain("write_file"); + expect(r.package.description).toMatch(/QA Lead/); + expect(r.package.systemPrompt).toMatch(/QA Lead/); + expect(r.package.systemPrompt).toMatch(/hands-on/i); + expect(r.package.systemPrompt).toMatch(/corbits exec/i); + expect(r.package.systemPrompt).toMatch(/bun test \.\/e2e/); + expect(r.package.systemPrompt).toMatch(/eval:capability/); + expect(r.package.systemPrompt).toMatch(/occupancy/i); + expect(r.package.systemPrompt).toMatch(/never author unit tests/i); + expect(r.package.systemPrompt).toMatch( + /never measure family\/model latency/i, + ); + expect(r.package.systemPrompt).toMatch(/never fix product code/i); + expect(r.package).not.toBe(DIRECTOR_REGISTRY.prober); + } + expect(isDirectorId("qa-lead")).toBe(true); + expect(isDirectorId("tester")).toBe(false); + expect(tierForDirectorId("qa-lead")).toBe("leaf"); + expect(DIRECTOR_REGISTRY.dispatch.spawn.allowlist).toContain("qa-lead"); + expect(DIRECTOR_REGISTRY.dispatch.spawn.allowlist).not.toContain("tester"); + }); + test("dispatch card has no parallelization cap; duplicate is the same live job", () => { const card = DIRECTOR_REGISTRY.dispatch.systemPrompt; expect(card).toContain("no parallelization cap"); diff --git a/src/agent/directors/registry.ts b/src/agent/directors/registry.ts index 934479596..f2f338a8f 100644 --- a/src/agent/directors/registry.ts +++ b/src/agent/directors/registry.ts @@ -6,6 +6,7 @@ import { dispatchPackage } from "./dispatch/package.js"; import { explorerPackage } from "./explorer/package.js"; import { plannerPackage } from "./planner/package.js"; import { proberPackage } from "./prober/package.js"; +import { qaLeadPackage } from "./qa-lead/package.js"; import { reviewerPackage } from "./reviewer/package.js"; import { shakespearePackage } from "./shakespeare/package.js"; import { wardenPackage } from "./warden/package.js"; @@ -46,6 +47,7 @@ export const DIRECTOR_REGISTRY: Readonly> = warden: wardenPackage, shakespeare: shakespearePackage, prober: proberPackage, + "qa-lead": qaLeadPackage, }; export function isDirectorId(value: unknown): value is DirectorId { diff --git a/src/agent/directors/types.ts b/src/agent/directors/types.ts index be7e7ea02..fbd896526 100644 --- a/src/agent/directors/types.ts +++ b/src/agent/directors/types.ts @@ -14,6 +14,7 @@ export const DIRECTOR_IDS = [ "warden", "shakespeare", "prober", + "qa-lead", ] as const; export type DirectorId = (typeof DIRECTOR_IDS)[number]; diff --git a/src/agent/prompt-sizes.test.ts b/src/agent/prompt-sizes.test.ts index 847f2e24b..c509848b8 100644 --- a/src/agent/prompt-sizes.test.ts +++ b/src/agent/prompt-sizes.test.ts @@ -44,6 +44,7 @@ const PROMPT_SIZE_BASELINE: Record< warden: { chars: 4113, bytes: 4127 }, shakespeare: { chars: 5993, bytes: 6015 }, prober: { chars: 5428, bytes: 5454 }, + "qa-lead": { chars: 5226, bytes: 5246 }, }; /**