From abefb6b7db6cade21094bd0414fa0a7c1c5cdead Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 18:11:41 -0700 Subject: [PATCH 1/5] Add Migrator director for reversible data migrations --- src/agent/directors/migrator/index.ts | 1 + src/agent/directors/migrator/package.test.ts | 113 ++++++++++++++++++ src/agent/directors/migrator/package.ts | 30 +++++ src/agent/directors/registry.test.ts | 27 +++-- src/agent/directors/registry.ts | 2 + src/agent/directors/skywalker/package.test.ts | 3 +- src/agent/directors/skywalker/package.ts | 6 +- src/agent/directors/types.ts | 1 + src/agent/prompt-sizes.test.ts | 2 + 9 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 src/agent/directors/migrator/index.ts create mode 100644 src/agent/directors/migrator/package.test.ts create mode 100644 src/agent/directors/migrator/package.ts diff --git a/src/agent/directors/migrator/index.ts b/src/agent/directors/migrator/index.ts new file mode 100644 index 000000000..fc01065a2 --- /dev/null +++ b/src/agent/directors/migrator/index.ts @@ -0,0 +1 @@ +export { migratorPackage } from "./package.js"; diff --git a/src/agent/directors/migrator/package.test.ts b/src/agent/directors/migrator/package.test.ts new file mode 100644 index 000000000..916fa1ef8 --- /dev/null +++ b/src/agent/directors/migrator/package.test.ts @@ -0,0 +1,113 @@ +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 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 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([]); + }); +}); diff --git a/src/agent/directors/migrator/package.ts b/src/agent/directors/migrator/package.ts new file mode 100644 index 000000000..5fad827a2 --- /dev/null +++ b/src/agent/directors/migrator/package.ts @@ -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 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. 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.`, +}; diff --git a/src/agent/directors/registry.test.ts b/src/agent/directors/registry.test.ts index 22655713e..1ad411875 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 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); } @@ -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"); }); @@ -133,6 +133,19 @@ describe("director registry", () => { expect(packageToProfile(g).orchestrator).toBe(true); }); + test("migrator is a read-only 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); + 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", @@ -196,11 +209,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", () => { diff --git a/src/agent/directors/registry.ts b/src/agent/directors/registry.ts index bd846d751..4eee09664 100644 --- a/src/agent/directors/registry.ts +++ b/src/agent/directors/registry.ts @@ -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"; @@ -62,6 +63,7 @@ export const DIRECTOR_REGISTRY: Readonly> = tester: testerPackage, gauntlet: gauntletPackage, prober: proberPackage, + migrator: migratorPackage, }; export function isDirectorId(value: unknown): value is DirectorId { diff --git a/src/agent/directors/skywalker/package.test.ts b/src/agent/directors/skywalker/package.test.ts index 3a1022486..3586ae08e 100644 --- a/src/agent/directors/skywalker/package.test.ts +++ b/src/agent/directors/skywalker/package.test.ts @@ -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", @@ -47,6 +47,7 @@ describe("skywalkerPackage", () => { "tester", "gauntlet", "prober", + "migrator", ]); }); diff --git a/src/agent/directors/skywalker/package.ts b/src/agent/directors/skywalker/package.ts index 469c20ded..0d00c9058 100644 --- a/src/agent/directors/skywalker/package.ts +++ b/src/agent/directors/skywalker/package.ts @@ -31,7 +31,7 @@ Example chains: - feature: explorer → plan → implement → critic - "why / how / is this stalled": answer yourself; at most one explorer if a single unknown blocks you -Closed directors (use search_agents / registry; each id is a spawn agent= target): builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper, emil, rand, shakespeare, testsmith, tester, gauntlet, prober. +Closed directors (use search_agents / registry; each id is a spawn agent= target): builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper, emil, rand, shakespeare, testsmith, tester, gauntlet, prober, migrator. No catch-all worker. If unsure, reclassify — do not spawn a blob agent. Quick routing: @@ -45,6 +45,7 @@ Quick routing: - testsmith = design permanent test cases - gauntlet = mutation-check that tests can actually fail (tree clean) - prober = measure-only latency/behavior probe per family/model +- migrator = reversible settings/config/session-state migrations - shakespeare = PRODUCT/ARCHITECTURE/IMPLEMENTATION docs - rand = DESIGN.md only - draper = brand/design critique (visual, copy, interactive) @@ -145,7 +146,7 @@ Do not reclassify COMMUNICATION as ORCHESTRATION just to justify parallel spawn # Spawn graph Skywalker = full closed set. Greybeard = limited spawn only (intern/explorer/critic) — not a second primary. -You may spawn: builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper, emil, rand, shakespeare, testsmith, tester, gauntlet, prober. +You may spawn: builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper, emil, rand, shakespeare, testsmith, tester, gauntlet, prober, migrator. When spawning, pass a typed brief. success_criteria is required for implement/review and their default directors; recommended otherwise: - intent — explore | implement | plan | review @@ -207,6 +208,7 @@ export const skywalkerPackage: DirectorPackage = { "tester", "gauntlet", "prober", + "migrator", ], }, modelRole: "orchestrator", diff --git a/src/agent/directors/types.ts b/src/agent/directors/types.ts index bbb54643a..cd8c5a1a0 100644 --- a/src/agent/directors/types.ts +++ b/src/agent/directors/types.ts @@ -22,6 +22,7 @@ export const DIRECTOR_IDS = [ "tester", "gauntlet", "prober", + "migrator", ] 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 b2e6380c3..001e3c9a2 100644 --- a/src/agent/prompt-sizes.test.ts +++ b/src/agent/prompt-sizes.test.ts @@ -41,6 +41,7 @@ const CHAR_BUDGET: Record = { // CL-7656: grok family is the max (13711 chars); budget = measured + // 2000 allowance, ceiling to 100. prober: 15800, + migrator: 12300, }; const BYTE_BUDGET: Record = { @@ -70,6 +71,7 @@ const BYTE_BUDGET: Record = { // CL-7656: grok family is the max (13779 bytes); budget = measured + // 3000 allowance, ceiling to 100. prober: 16800, + migrator: 13400, }; function budgetMessage( From 8a7a02f192dc6a9c4a694e392e3a5fb32ffdfc2f Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 18:32:37 -0700 Subject: [PATCH 2/5] Scope Migrator shell to dry-run and scratch-copy execution run_shell is dry-run/scratch-copy ONLY: never execute the forward migration against live state, no background shells (shell_collect unmounted; foreground with timeouts only). Scratch copies live under tmp/, are cleaned up afterwards, and the scratch path is reported in the delivery. Rename the registry leaf test to dry-run-scoped and raise the migrator prompt budgets to 12800 chars / 13900 bytes (measured-max + 2000/+3000, ceiling 100). --- src/agent/directors/migrator/package.test.ts | 23 ++++++++++++++++++++ src/agent/directors/migrator/package.ts | 2 +- src/agent/directors/registry.test.ts | 4 +++- src/agent/prompt-sizes.test.ts | 8 +++++-- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/agent/directors/migrator/package.test.ts b/src/agent/directors/migrator/package.test.ts index 916fa1ef8..646d69013 100644 --- a/src/agent/directors/migrator/package.test.ts +++ b/src/agent/directors/migrator/package.test.ts @@ -43,6 +43,29 @@ describe("migratorPackage", () => { 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( diff --git a/src/agent/directors/migrator/package.ts b/src/agent/directors/migrator/package.ts index 5fad827a2..d8d0d4c39 100644 --- a/src/agent/directors/migrator/package.ts +++ b/src/agent/directors/migrator/package.ts @@ -26,5 +26,5 @@ export const migratorPackage: DirectorPackage = { modelRole: "plan", systemPrompt: `PRIMARY INTENT: Ship reversible data migrations with dry-run evidence and a tested rollback path. -You are 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. 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.`, +You are 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.`, }; diff --git a/src/agent/directors/registry.test.ts b/src/agent/directors/registry.test.ts index 1ad411875..d167dc6df 100644 --- a/src/agent/directors/registry.test.ts +++ b/src/agent/directors/registry.test.ts @@ -133,7 +133,7 @@ describe("director registry", () => { expect(packageToProfile(g).orchestrator).toBe(true); }); - test("migrator is a read-only leaf with no fleet verbs (CL-7671)", () => { + 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"); @@ -141,6 +141,8 @@ describe("director registry", () => { 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"); diff --git a/src/agent/prompt-sizes.test.ts b/src/agent/prompt-sizes.test.ts index 001e3c9a2..4d9a0dbfe 100644 --- a/src/agent/prompt-sizes.test.ts +++ b/src/agent/prompt-sizes.test.ts @@ -41,7 +41,9 @@ const CHAR_BUDGET: Record = { // CL-7656: grok family is the max (13711 chars); budget = measured + // 2000 allowance, ceiling to 100. prober: 15800, - migrator: 12300, + // CL-7671 scope-honesty sentences grew migrator past the 12300-char + // placeholder: measured-max + 2000 allowance, ceiling to 100. + migrator: 12800, }; const BYTE_BUDGET: Record = { @@ -71,7 +73,9 @@ const BYTE_BUDGET: Record = { // CL-7656: grok family is the max (13779 bytes); budget = measured + // 3000 allowance, ceiling to 100. prober: 16800, - migrator: 13400, + // CL-7671 scope-honesty sentences grew migrator past the 13400-byte + // placeholder: measured-max + 3000 allowance, ceiling to 100. + migrator: 13900, }; function budgetMessage( From b8f02fbf4de132ac81c92dc9591c7810ffd478d4 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 18:37:38 -0700 Subject: [PATCH 3/5] Measure migrator budgets off the final scoped prompt --- src/agent/prompt-sizes.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agent/prompt-sizes.test.ts b/src/agent/prompt-sizes.test.ts index 4d9a0dbfe..80f516e19 100644 --- a/src/agent/prompt-sizes.test.ts +++ b/src/agent/prompt-sizes.test.ts @@ -42,8 +42,8 @@ const CHAR_BUDGET: Record = { // 2000 allowance, ceiling to 100. prober: 15800, // CL-7671 scope-honesty sentences grew migrator past the 12300-char - // placeholder: measured-max + 2000 allowance, ceiling to 100. - migrator: 12800, + // placeholder: measured-max (11202) + 2000 allowance, ceiling to 100. + migrator: 13300, }; const BYTE_BUDGET: Record = { @@ -74,8 +74,8 @@ const BYTE_BUDGET: Record = { // 3000 allowance, ceiling to 100. prober: 16800, // CL-7671 scope-honesty sentences grew migrator past the 13400-byte - // placeholder: measured-max + 3000 allowance, ceiling to 100. - migrator: 13900, + // placeholder: measured-max (11258) + 3000 allowance, ceiling to 100. + migrator: 14300, }; function budgetMessage( From af4352184b5226523feaad6f342e0dc0d341a5f5 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 21:49:12 -0700 Subject: [PATCH 4/5] Use MigratorDirector identity in migrator system prompt --- src/agent/directors/migrator/package.test.ts | 2 +- src/agent/directors/migrator/package.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agent/directors/migrator/package.test.ts b/src/agent/directors/migrator/package.test.ts index 646d69013..ad13c8fe2 100644 --- a/src/agent/directors/migrator/package.test.ts +++ b/src/agent/directors/migrator/package.test.ts @@ -13,7 +13,7 @@ describe("migratorPackage", () => { test("systemPrompt identity is the reversible-migration leaf", () => { const p = migratorPackage.systemPrompt; - expect(p).toContain("You are Migrator"); + expect(p).toContain("You are MigratorDirector (Migrator)"); expect(p).toMatch(/reversible-migration leaf/); expect(p).toContain("PRIMARY INTENT"); }); diff --git a/src/agent/directors/migrator/package.ts b/src/agent/directors/migrator/package.ts index d8d0d4c39..4990297e9 100644 --- a/src/agent/directors/migrator/package.ts +++ b/src/agent/directors/migrator/package.ts @@ -26,5 +26,5 @@ export const migratorPackage: DirectorPackage = { modelRole: "plan", systemPrompt: `PRIMARY INTENT: Ship reversible data migrations with dry-run evidence and a tested rollback path. -You are 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.`, +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.`, }; From 8749842b873242557168fa61a73bdd538e17a46b Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 22:43:36 -0700 Subject: [PATCH 5/5] =?UTF-8?q?Add=20Migrator=20eng-lane=20docs=20rows;=20?= =?UTF-8?q?fleet=20counts=2018=20=E2=86=92=2019?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/ARCHITECTURE.md | 3 ++- docs/PRODUCT.md | 16 ++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 30115bb2c..41399370f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b46da1319..84d71da93 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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** @@ -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)** diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index ddda8f9c8..589e95fae 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -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.