Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ Corbits Code keeps repository guidance and the closed director fleet separate:
- `src/agent/directors/` — closed spawn catalog (`directorProfiles()`). Skywalker
is the primary orchestrator; spawnable directors include builder, explorer,
counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper,
emil, rand, shakespeare, testsmith, tester, gauntlet, and prober. Closed ids cannot be
emil, rand, shakespeare, testsmith, tester, gauntlet, prober, and migrator. Closed ids cannot be
overridden by plugins or local files.
- `.agents/agents/` — optional local profile additions; this directory is not
required and may be absent
Expand Down
3 changes: 2 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,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 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.
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 19 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.

**Primary**

Expand All @@ -265,6 +265,7 @@ Every shipped specialist is a **director package** — a prompt-first `DirectorP
| neckbeard | Adversarial hygiene / refactor stress | Real review substitute |
| bruckheimer | Product discovery → PRODUCT/ARCHITECTURE/IMPLEMENTATION-oriented briefs | Eng plan, code |
| gaasbot | Quick CTO opinion voice | Formal review gate, implement |
| migrator | Reversible settings/config/session-state migrations | Live-state execution, new features |

**Design trio (dev perspective)**

Expand Down
16 changes: 8 additions & 8 deletions docs/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,14 @@ 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 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.

| Lane | Directors |
| --------- | -------------------------------------------------------------------------------------- |
| Primary | skywalker |
| Eng | builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot |
| Design | draper, emil, rand |
| Docs / QA | shakespeare, testsmith, tester, gauntlet, prober |
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 19 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.

| Lane | Directors |
| --------- | ------------------------------------------------------------------------------------------------ |
| Primary | skywalker |
| Eng | builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, migrator |
| Design | draper, emil, rand |
| Docs / QA | shakespeare, testsmith, tester, gauntlet, prober |

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.

Expand Down
1 change: 1 addition & 0 deletions src/agent/directors/migrator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { migratorPackage } from "./package.js";
136 changes: 136 additions & 0 deletions src/agent/directors/migrator/package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { describe, expect, test } from "bun:test";
import { migratorPackage } from "./package.js";

describe("migratorPackage", () => {
test("id matches directory / registry id", () => {
expect(migratorPackage.id).toBe("migrator");
});

test("systemPrompt is non-empty and not a Placeholder", () => {
expect(migratorPackage.systemPrompt.length).toBeGreaterThan(0);
expect(migratorPackage.systemPrompt.startsWith("Placeholder")).toBe(false);
});

test("systemPrompt identity is the reversible-migration leaf", () => {
const p = migratorPackage.systemPrompt;
expect(p).toContain("You are MigratorDirector (Migrator)");
expect(p).toMatch(/reversible-migration leaf/);
expect(p).toContain("PRIMARY INTENT");
});

test("systemPrompt owns only settings/config/session-state data changes", () => {
const p = migratorPackage.systemPrompt;
expect(p).toMatch(/settings-schema/);
expect(p).toMatch(/config-key/);
expect(p).toMatch(/run\.json/);
expect(p).toMatch(/context-store-layout/);
expect(p).toMatch(/never bulk renames, never features/);
});

test("systemPrompt requires the three migration artifacts", () => {
const p = migratorPackage.systemPrompt;
expect(p).toMatch(/dry-run output/);
expect(p).toMatch(/forward migration path/);
expect(p).toMatch(/rollback path/);
expect(p).toMatch(/in-flight sessions/);
});

test("systemPrompt verifies rollback by execution and stops when irreversible", () => {
const p = migratorPackage.systemPrompt;
expect(p).toMatch(/scratch copy/);
expect(p).toMatch(/not by inspection/);
expect(p).toMatch(/say so plainly and stop/);
expect(p).toMatch(/do not ship it/);
});

test("systemPrompt scopes run_shell to dry-run/scratch-only, forbids live execution", () => {
const p = migratorPackage.systemPrompt;
expect(p).toMatch(/dry-run and scratch-copy execution ONLY/);
expect(p).toMatch(/never execute the forward migration/);
expect(p).toMatch(/live state/);
});

test("systemPrompt forbids background shells — foreground with timeouts only", () => {
const p = migratorPackage.systemPrompt;
expect(p).toMatch(/Background shells are forbidden/);
expect(p).toMatch(/background: true/);
expect(p).toMatch(/shell_collect/);
expect(p).toMatch(/foreground/);
expect(p).toMatch(/timeouts only/);
});

test("systemPrompt makes scratch auditable under tmp/ with reported path", () => {
const p = migratorPackage.systemPrompt;
expect(p).toMatch(/tmp\//);
expect(p).toMatch(/clean them up afterwards/);
expect(p).toMatch(/scratch path in the delivery/);
});

test("systemPrompt states the report shape", () => {
const p = migratorPackage.systemPrompt;
expect(p).toMatch(
/Report: dry-run output, forward path, rollback path, in-flight impact/,
);
});

test("tools.allow is exactly read_file/grep/lsp/run_shell in order", () => {
expect(migratorPackage.tools?.allow).toEqual([
"read_file",
"grep",
"lsp",
"run_shell",
]);
});

test("tools.allow carries no fleet verbs and no path writes", () => {
const allow = migratorPackage.tools?.allow ?? [];
for (const verb of [
"spawn_agent",
"send_input",
"list_agents",
"search_agents",
"wait_agents",
] as const) {
expect(allow).not.toContain(verb);
}
for (const tool of ["write_file", "edit_file", "delete_file"] as const) {
expect(allow).not.toContain(tool);
}
});

test("spawn.maySpawn is false with no allowlist (leaf)", () => {
expect(migratorPackage.spawn.maySpawn).toBe(false);
expect(migratorPackage.spawn.allowlist).toBeUndefined();
expect(migratorPackage.tier).toBe("leaf");
});

test("modelRole is plan", () => {
expect(migratorPackage.modelRole).toBe("plan");
});

test("primaryIntent ships reversible migrations with evidence and rollback", () => {
expect(migratorPackage.primaryIntent).toBe(
"Ship reversible data migrations with dry-run evidence and a tested rollback path",
);
});

test("outOfLane refuses renames, features, irreversible breaks, orchestration", () => {
expect(migratorPackage.outOfLane).toEqual([
"bulk code renames (ast-grep / refactor skill territory)",
"product features",
"API renames",
"irreversible schema breaks without a rollback path",
"orchestration or spawning workers",
]);
});

test("description names the reversible-migration lane", () => {
expect(migratorPackage.description).toBe(
"Reversible settings, config, and session-state migrations — forward path, rollback path, dry-run evidence",
);
});

test("optionalSkills is empty", () => {
expect(migratorPackage.optionalSkills).toEqual([]);
});
});
30 changes: 30 additions & 0 deletions src/agent/directors/migrator/package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { DirectorPackage } from "../types.js";

/**
* Migrator worker (CL-7671).
* Reversible settings/config/session-state data migrations only — forward
* path + rollback path + dry-run evidence + in-flight session impact.
* Never bulk renames, never features.
*/
export const migratorPackage: DirectorPackage = {
id: "migrator",
primaryIntent:
"Ship reversible data migrations with dry-run evidence and a tested rollback path",
outOfLane: [
"bulk code renames (ast-grep / refactor skill territory)",
"product features",
"API renames",
"irreversible schema breaks without a rollback path",
"orchestration or spawning workers",
],
description:
"Reversible settings, config, and session-state migrations — forward path, rollback path, dry-run evidence",
optionalSkills: [],
tools: { allow: ["read_file", "grep", "lsp", "run_shell"] },
spawn: { maySpawn: false },
tier: "leaf",
modelRole: "plan",
systemPrompt: `PRIMARY INTENT: Ship reversible data migrations with dry-run evidence and a tested rollback path.

You are MigratorDirector (Migrator), the reversible-migration leaf. You own settings-schema, config-key, run.json, and context-store-layout data changes ONLY — never bulk renames, never features. Every change ships three artifacts: (1) dry-run output showing exactly what would change, (2) the forward migration path, (3) the rollback path back to the prior shape. State what happens to in-flight sessions on both paths. Verify the rollback by executing it in a scratch copy (temporary test, cleaned up afterwards), not by inspection. run_shell is for dry-run and scratch-copy execution ONLY — never execute the forward migration (or anything else) against live state. Background shells are forbidden (background: true starts are uncollectable without shell_collect, which is deliberately not mounted) — use foreground calls with timeouts only. Keep scratch copies under tmp/, clean them up afterwards, and report the scratch path in the delivery. If a change cannot be rolled back, say so plainly and stop — do not ship it. Report: dry-run output, forward path, rollback path, in-flight impact.`,
};
29 changes: 22 additions & 7 deletions src/agent/directors/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import {
} from "./registry.js";

describe("director registry", () => {
test("closed set has exactly 18 directors", () => {
expect(DIRECTOR_IDS).toHaveLength(18);
expect(listDirectors()).toHaveLength(18);
test("closed set has exactly 19 directors", () => {
expect(DIRECTOR_IDS).toHaveLength(19);
expect(listDirectors()).toHaveLength(19);
for (const id of DIRECTOR_IDS) {
expect(DIRECTOR_REGISTRY[id].id).toBe(id);
}
Expand Down Expand Up @@ -116,8 +116,8 @@ describe("director registry", () => {

test("directorProfiles is the spawn catalog (closed set minus skywalker)", () => {
const profiles = directorProfiles();
expect(profiles).toHaveLength(17);
expect(new Set(profiles.map((p) => p.id)).size).toBe(17);
expect(profiles).toHaveLength(18);
expect(new Set(profiles.map((p) => p.id)).size).toBe(18);
expect(profiles.map((p) => p.id)).not.toContain("skywalker");
});

Expand All @@ -133,6 +133,21 @@ describe("director registry", () => {
expect(packageToProfile(g).orchestrator).toBe(true);
});

test("migrator is a dry-run-scoped leaf with no fleet verbs (CL-7671)", () => {
const m = DIRECTOR_REGISTRY.migrator;
expect(m.id).toBe("migrator");
expect(m.tier).toBe("leaf");
expect(m.spawn.maySpawn).toBe(false);
expect(m.modelRole).toBe("plan");
expect(m.tools?.allow).toEqual(["read_file", "grep", "lsp", "run_shell"]);
expect(packageToProfile(m).orchestrator).toBe(false);
expect(m.systemPrompt).toMatch(/dry-run and scratch-copy execution ONLY/);
expect(m.systemPrompt).toMatch(/Background shells are forbidden/);
const r = resolveDirector({ agentId: "migrator" });
expect(r.ok).toBe(true);
if (r.ok) expect(r.package.id).toBe("migrator");
});

test("closed directors mount product write tools", () => {
for (const id of [
"critic",
Expand Down Expand Up @@ -196,11 +211,11 @@ 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(17);
expect(s.spawn.allowlist).toHaveLength(18);
});

// CL-6941: tier and spawn.maySpawn independently encode "may this package
// spawn", hand-set across 18 files. This pins their agreement so drift
// spawn", hand-set across 19 files. This pins their agreement so drift
// (adding maySpawn: true without bumping tier, or vice versa) fails a test
// instead of surfacing as an unexplained FleetAuthorityError at dispatch.
test("tier agrees with spawn.maySpawn for every director", () => {
Expand Down
2 changes: 2 additions & 0 deletions src/agent/directors/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { gaasbotPackage } from "./gaasbot/index.js";
import { greybeardPackage } from "./greybeard/index.js";
import { builderPackage } from "./builder/index.js";
import { internPackage } from "./intern/index.js";
import { migratorPackage } from "./migrator/index.js";
import { neckbeardPackage } from "./neckbeard/index.js";
import { counselPackage } from "./counsel/index.js";
import { shakespearePackage } from "./shakespeare/index.js";
Expand Down Expand Up @@ -62,6 +63,7 @@ export const DIRECTOR_REGISTRY: Readonly<Record<DirectorId, DirectorPackage>> =
tester: testerPackage,
gauntlet: gauntletPackage,
prober: proberPackage,
migrator: migratorPackage,
};

export function isDirectorId(value: unknown): value is DirectorId {
Expand Down
3 changes: 2 additions & 1 deletion src/agent/directors/skywalker/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("skywalkerPackage", () => {

test("maySpawn true with full closed allowlist", () => {
expect(skywalkerPackage.spawn.maySpawn).toBe(true);
expect(skywalkerPackage.spawn.allowlist).toHaveLength(17);
expect(skywalkerPackage.spawn.allowlist).toHaveLength(18);
expect(skywalkerPackage.spawn.allowlist).toEqual([
"builder",
"explorer",
Expand All @@ -47,6 +47,7 @@ describe("skywalkerPackage", () => {
"tester",
"gauntlet",
"prober",
"migrator",
]);
});

Expand Down
Loading
Loading