Skip to content

Commit 040f0ed

Browse files
Add measure-only prober leaf director (#962)
* Add measure-only prober leaf director * Wire prober into the Skywalker spawn graph * Yield prober report format to the scaffold-owned worker envelope The shared scaffold owns Summary / Findings / Blockers / Paths, so the lane report content is carried as a Findings sentence instead of re-specified headings. * Renumber closed fleet 17->18 for prober (docs prose) * Fix PRODUCT.md table format (oxfmt)
1 parent 07049c3 commit 040f0ed

12 files changed

Lines changed: 230 additions & 14 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ Corbits Code keeps repository guidance and the closed director fleet separate:
198198
- `src/agent/directors/` — closed spawn catalog (`directorProfiles()`). Skywalker
199199
is the primary orchestrator; spawnable directors include builder, explorer,
200200
counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper,
201-
emil, rand, shakespeare, testsmith, tester, and gauntlet. Closed ids cannot be
201+
emil, rand, shakespeare, testsmith, tester, gauntlet, and prober. Closed ids cannot be
202202
overridden by plugins or local files.
203203
- `.agents/agents/` — optional local profile additions; this directory is not
204204
required and may be absent

‎docs/ARCHITECTURE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ Enforcement is runtime code at the existing tool-mount point, not prompt wording
244244

245245
#### Closed director fleet (`src/agent/directors/`)
246246

247-
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 17 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). Skywalker is the primary session identity: `spawn_agent(agent="skywalker")` is refused, and `directorProfiles()` omits it from the spawn catalog.
247+
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 18 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). Skywalker is the primary session identity: `spawn_agent(agent="skywalker")` is refused, and `directorProfiles()` omits it from the spawn catalog.
248248

249249
**Primary**
250250

@@ -282,6 +282,7 @@ Every shipped specialist is a **director package** — a prompt-first `DirectorP
282282
| testsmith | Test design only (what/how to test) |
283283
| tester | Runtime verification; never fix product code |
284284
| gauntlet | Mutation/vacuity check that named tests can actually fail |
285+
| prober | Measure-only latency/behavior probe per family/model |
285286

286287
**Intent → director** (`spawn_agent(intent=…)` when `agent` is omitted). implement/review (and their default directors) fail closed without non-empty `success_criteria`.
287288

‎docs/PRODUCT.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,14 @@ Capabilities beyond the core toolset are opt-in plugins, enabled per workspace t
155155

156156
## Multi-agent (fleet agents)
157157

158-
The primary session is always **orchestrator** (single-agent mode is gone). Its identity is **Skywalker** (product name remains Corbits Code; when asked its name, answer Skywalker): classify work, DIY tiny/single-file/one-route product edits, dispatch a **closed fleet of 17 directors** for substantial work, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary (CORE / `SKYWALKER_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-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`) tell Skywalker which directors to spawn for substantial work; tiny/bounded edits may run on the primary.
158+
The primary session is always **orchestrator** (single-agent mode is gone). Its identity is **Skywalker** (product name remains Corbits Code; when asked its name, answer Skywalker): classify work, DIY tiny/single-file/one-route product edits, dispatch a **closed fleet of 18 directors** for substantial work, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary (CORE / `SKYWALKER_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-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`) tell Skywalker which directors to spawn for substantial work; tiny/bounded edits may run on the primary.
159159

160160
| Lane | Directors |
161161
| --------- | -------------------------------------------------------------------------------------- |
162162
| Primary | skywalker |
163163
| Eng | builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot |
164164
| Design | draper, emil, rand |
165-
| Docs / QA | shakespeare, testsmith, tester, gauntlet |
165+
| Docs / QA | shakespeare, testsmith, tester, gauntlet, prober |
166166

167167
There is **no catch-all worker**. `spawn_agent` requires `agent=…` or a non-general `intent` (implement/explore/plan/review→critic); bare dispatch and `intent=general` are refused. Named `spawn_agent(agent=…)` selects a director package without requiring a plugin profile, except `skywalker` which is the primary session identity and is refused as a spawned worker. Nested spawn is runtime-enforced: only skywalker (full fleet allowlist) and greybeard (intern/explorer/critic) may spawn; other workers have no fleet tools. Primary omits an allowlist so plugin profiles remain reachable from the main session.
168168

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { proberPackage } from "./package.js";
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { proberPackage } from "./package.js";
3+
4+
describe("proberPackage", () => {
5+
test("id matches directory / registry id", () => {
6+
expect(proberPackage.id).toBe("prober");
7+
});
8+
9+
test("systemPrompt is non-empty and not a Placeholder", () => {
10+
expect(proberPackage.systemPrompt.length).toBeGreaterThan(0);
11+
expect(proberPackage.systemPrompt.startsWith("Placeholder")).toBe(false);
12+
});
13+
14+
test("systemPrompt identity is Prober / ProberDirector", () => {
15+
const p = proberPackage.systemPrompt;
16+
expect(p).toMatch(/ProberDirector \(Prober\)/);
17+
expect(p).toContain("PRIMARY INTENT");
18+
expect(p).toMatch(/measure-only lane/i);
19+
});
20+
21+
test("systemPrompt states the measure-only lane (TTFT, latency, streaks, salvage/nudge)", () => {
22+
const p = proberPackage.systemPrompt;
23+
expect(p).toMatch(/measure latency and behavior distributions/i);
24+
expect(p).toMatch(/TTFT/);
25+
expect(p).toMatch(/per-turn latency/i);
26+
expect(p).toMatch(/tool-only streak/i);
27+
expect(p).toMatch(/salvage counts/i);
28+
expect(p).toMatch(/nudge counts/i);
29+
expect(p).toMatch(/per family\/model/i);
30+
});
31+
32+
test("systemPrompt forbids shipping product code and tuning prompts/policy", () => {
33+
const p = proberPackage.systemPrompt;
34+
expect(p).toMatch(/never ship product code/i);
35+
expect(p).toMatch(/never tune prompts or model-family policy/i);
36+
expect(p).toMatch(/never silent retunes/i);
37+
});
38+
39+
test("systemPrompt consumes the existing harness (no second harness)", () => {
40+
const p = proberPackage.systemPrompt;
41+
expect(p).toContain("bun run eval:capability");
42+
expect(p).toContain("scripts/eval-capability.ts");
43+
expect(p).toContain("src/perf");
44+
expect(p).toContain("rollup.ts");
45+
expect(p).toContain("assert-spans.ts");
46+
expect(p).toContain("attribution-report.ts");
47+
expect(p).toMatch(/do not build a second one/i);
48+
expect(p).toMatch(/do not hand-roll/i);
49+
});
50+
51+
test("systemPrompt routes findings to model-family-policy follow-ups", () => {
52+
const p = proberPackage.systemPrompt;
53+
expect(p).toContain("src/agent/model-family-policy.ts");
54+
expect(p).toMatch(/follow-up tickets/i);
55+
expect(p).toMatch(/do not edit the policy here/i);
56+
});
57+
58+
test("systemPrompt points at the scaffold-owned worker report envelope (no re-spec)", () => {
59+
const p = proberPackage.systemPrompt;
60+
expect(p).toMatch(/Corbits report envelope/);
61+
expect(p).toMatch(/scaffold owns its shape/i);
62+
expect(p).not.toContain("## Summary");
63+
expect(p).not.toContain("## Findings");
64+
expect(p).not.toContain("## Blockers");
65+
expect(p).not.toContain("## Paths");
66+
expect(p).toMatch(/distributions per family\/model/i);
67+
expect(p).toMatch(/follow-up tickets for policy\/prompt owners/i);
68+
expect(p).toMatch(/DONE GATE/i);
69+
expect(p).toMatch(/BLINDERS ON/i);
70+
expect(p).toContain("success_criteria");
71+
});
72+
73+
test("systemPrompt has no tool-schema restatement or fake caps", () => {
74+
const p = proberPackage.systemPrompt;
75+
expect(p).not.toMatch(/parameters?:/i);
76+
expect(p).not.toMatch(/fan-out/i);
77+
expect(p).not.toMatch(/at most \d+/i);
78+
expect(p).not.toMatch(/turn budget/i);
79+
expect(p).not.toMatch(/scheduler/i);
80+
});
81+
82+
test("spawn.maySpawn is false (leaf)", () => {
83+
expect(proberPackage.spawn.maySpawn).toBe(false);
84+
});
85+
86+
test("tier is leaf", () => {
87+
expect(proberPackage.tier).toBe("leaf");
88+
});
89+
90+
test("tools.allow mounts the review surface (lane discipline in prompt)", () => {
91+
const allow = proberPackage.tools?.allow ?? [];
92+
expect(allow).toContain("read_file");
93+
expect(allow).toContain("run_shell");
94+
expect(allow).toContain("write_file");
95+
expect(allow).toContain("edit_file");
96+
expect(allow).toContain("delete_file");
97+
});
98+
99+
test("modelRole is test", () => {
100+
expect(proberPackage.modelRole).toBe("test");
101+
});
102+
103+
test("primaryIntent and outOfLane match the prober lane", () => {
104+
expect(proberPackage.primaryIntent).toMatch(/measure/i);
105+
expect(proberPackage.primaryIntent).toMatch(/never ship product code/i);
106+
expect(proberPackage.primaryIntent).toMatch(/never tune/i);
107+
const joined = proberPackage.outOfLane.join(" ");
108+
expect(joined).toMatch(/shipping product code/i);
109+
expect(joined).toMatch(/tuning prompts or model-family policy/i);
110+
expect(joined).toMatch(/new eval harness/i);
111+
});
112+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { DirectorPackage } from "../types.js";
2+
import { REVIEW_TOOLS } from "../tool-sets.js";
3+
4+
/**
5+
* Prober worker (CL-7656).
6+
* Measure-only latency/behavior prober — report distributions per
7+
* family/model; never ship product code, never tune prompts or policy.
8+
*/
9+
export const proberPackage: DirectorPackage = {
10+
id: "prober",
11+
primaryIntent:
12+
"Measure latency and behavior distributions per family/model; never ship product code, never tune prompts or policy",
13+
outOfLane: [
14+
"shipping product code",
15+
"tuning prompts or model-family policy",
16+
"building a new eval harness",
17+
"fleet orchestration",
18+
"architecture essays without measurements",
19+
],
20+
description: "Measure-only latency/behavior prober per family/model",
21+
systemPrompt: `You are ProberDirector (Prober), a specialist in Corbits Code.
22+
23+
PRIMARY INTENT: measure latency and behavior distributions per family/model and report the numbers with evidence. Never ship product code. Never tune prompts or model-family policy. Findings feed model-family-policy as follow-up tickets — never silent retunes.
24+
25+
You are the measure-only lane — not Builder, not Counsel, not an orchestrator. Do not spawn specialists. Do not edit product code, prompts, or policy to "improve" the numbers mid-probe; a probe that moves the target is not a measurement.
26+
27+
BLINDERS ON: measure what the brief's success_criteria ask for, on the harness below, sliced per family/model. Do not wander into fixes, retunes, or fleet orchestration.
28+
29+
# What to measure
30+
31+
- TTFT (time to first token) and per-turn latency distributions.
32+
- Tool-only streak distribution (consecutive tool-call turns with no text).
33+
- Salvage counts (incomplete-report recoveries) and nudge counts
34+
(wrap-up / stall check-ins) per family/model.
35+
- Always slice by family/model — a global average that hides a family
36+
regression is a failed probe.
37+
38+
# Harness (consume — do not build a second one)
39+
40+
- Capability runs: \`bun run eval:capability\` (\`scripts/eval-capability.ts\`
41+
over \`evals/capability\`) — the product non-TUI path (\`loadConfig\` +
42+
\`runExec\`) against fixture copies with \`verify.sh\` graders.
43+
- Latency spans: the PerfTrace harness under \`src/perf\` — \`rollup.ts\`
44+
(TTFT vs stream split), \`assert-spans.ts\` (span assertions), and
45+
\`attribution-report.ts\` (per-turn attribution).
46+
- Read how they invoke before running; extend neither. If the harness
47+
cannot answer the brief, report that under Blockers — do not hand-roll
48+
a replacement harness.
49+
50+
# Findings route to follow-ups, never silent retunes
51+
52+
- Report numbers with the exact commands, matrix cells, and harness
53+
outputs behind them so a follow-up can reproduce the probe.
54+
- Policy-looking conclusions (e.g. "family X needs a tighter tool-only
55+
nudge") go to Blockers/Findings as follow-up tickets against
56+
\`src/agent/model-family-policy.ts\` — do not edit the policy here.
57+
- Prompt-looking conclusions go the same route — named follow-ups, not
58+
edits.
59+
60+
# Report
61+
62+
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: distributions per family/model (TTFT, per-turn latency, tool-only streaks, salvage/nudge counts), the exact harness commands and matrix behind them, and follow-up tickets for policy/prompt owners.
63+
64+
DONE GATE: stop when the brief's measure ask is answered with evidence
65+
OR explicitly blocked under Blockers. Do not expand into fixes,
66+
retunes, or orchestration.
67+
68+
OUT OF LANE: shipping product code, tuning prompts or model-family
69+
policy (route to follow-up tickets), building a new harness, fleet
70+
orchestration, architecture essays without measurements.`,
71+
tools: { allow: REVIEW_TOOLS },
72+
spawn: { maySpawn: false },
73+
tier: "leaf",
74+
modelRole: "test",
75+
};

‎src/agent/directors/registry.test.ts‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import {
1313
} from "./registry.js";
1414

1515
describe("director registry", () => {
16-
test("closed set has exactly 17 directors", () => {
17-
expect(DIRECTOR_IDS).toHaveLength(17);
18-
expect(listDirectors()).toHaveLength(17);
16+
test("closed set has exactly 18 directors", () => {
17+
expect(DIRECTOR_IDS).toHaveLength(18);
18+
expect(listDirectors()).toHaveLength(18);
1919
for (const id of DIRECTOR_IDS) {
2020
expect(DIRECTOR_REGISTRY[id].id).toBe(id);
2121
}
@@ -116,8 +116,8 @@ describe("director registry", () => {
116116

117117
test("directorProfiles is the spawn catalog (closed set minus skywalker)", () => {
118118
const profiles = directorProfiles();
119-
expect(profiles).toHaveLength(16);
120-
expect(new Set(profiles.map((p) => p.id)).size).toBe(16);
119+
expect(profiles).toHaveLength(17);
120+
expect(new Set(profiles.map((p) => p.id)).size).toBe(17);
121121
expect(profiles.map((p) => p.id)).not.toContain("skywalker");
122122
});
123123

@@ -150,6 +150,7 @@ describe("director registry", () => {
150150
"shakespeare",
151151
"bruckheimer",
152152
"rand",
153+
"prober",
153154
"skywalker",
154155
"gauntlet",
155156
] as const) {
@@ -195,11 +196,11 @@ describe("director registry", () => {
195196
expect(s.tools?.allow).toContain("write_file");
196197
expect(s.tools?.allow).toContain("edit_file");
197198
expect(s.tools?.allow).toContain("delete_file");
198-
expect(s.spawn.allowlist).toHaveLength(16);
199+
expect(s.spawn.allowlist).toHaveLength(17);
199200
});
200201

201202
// CL-6941: tier and spawn.maySpawn independently encode "may this package
202-
// spawn", hand-set across 17 files. This pins their agreement so drift
203+
// spawn", hand-set across 18 files. This pins their agreement so drift
203204
// (adding maySpawn: true without bumping tier, or vice versa) fails a test
204205
// instead of surfacing as an unexplained FleetAuthorityError at dispatch.
205206
test("tier agrees with spawn.maySpawn for every director", () => {
@@ -226,6 +227,20 @@ describe("director registry", () => {
226227
expect(tierForDirectorId("gauntlet")).toBe("leaf");
227228
});
228229

230+
test("prober is a measure-only leaf (CL-7656)", () => {
231+
const r = resolveDirector({ agentId: "prober" });
232+
expect(r.ok).toBe(true);
233+
if (r.ok) {
234+
expect(r.package.id).toBe("prober");
235+
expect(r.package.tier).toBe("leaf");
236+
expect(r.package.spawn.maySpawn).toBe(false);
237+
expect(r.package.modelRole).toBe("test");
238+
expect(r.package.primaryIntent).toMatch(/measure/i);
239+
}
240+
expect(isDirectorId("prober")).toBe(true);
241+
expect(tierForDirectorId("prober")).toBe("leaf");
242+
});
243+
229244
test("every director profile declares matching agent id in system prompt", () => {
230245
for (const id of DIRECTOR_IDS) {
231246
const profile = packageToProfile(DIRECTOR_REGISTRY[id]);

‎src/agent/directors/registry.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { skywalkerPackage } from "./skywalker/index.js";
1616
import { testerPackage } from "./tester/index.js";
1717
import { testsmithPackage } from "./testsmith/index.js";
1818
import { gauntletPackage } from "./gauntlet/index.js";
19+
import { proberPackage } from "./prober/index.js";
1920
import { formatDirectorSystemPrompt } from "./identity.js";
2021
import {
2122
DIRECTOR_IDS,
@@ -60,6 +61,7 @@ export const DIRECTOR_REGISTRY: Readonly<Record<DirectorId, DirectorPackage>> =
6061
testsmith: testsmithPackage,
6162
tester: testerPackage,
6263
gauntlet: gauntletPackage,
64+
prober: proberPackage,
6365
};
6466

6567
export function isDirectorId(value: unknown): value is DirectorId {

‎src/agent/directors/skywalker/package.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe("skywalkerPackage", () => {
2828

2929
test("maySpawn true with full closed allowlist", () => {
3030
expect(skywalkerPackage.spawn.maySpawn).toBe(true);
31-
expect(skywalkerPackage.spawn.allowlist).toHaveLength(16);
31+
expect(skywalkerPackage.spawn.allowlist).toHaveLength(17);
3232
expect(skywalkerPackage.spawn.allowlist).toEqual([
3333
"builder",
3434
"explorer",
@@ -46,6 +46,7 @@ describe("skywalkerPackage", () => {
4646
"testsmith",
4747
"tester",
4848
"gauntlet",
49+
"prober",
4950
]);
5051
});
5152

0 commit comments

Comments
 (0)