Skip to content

Commit 4593e8d

Browse files
committed
Add an exec flag to run as a chosen director
1 parent 1c05034 commit 4593e8d

8 files changed

Lines changed: 201 additions & 20 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
2424
write-free. Shell file-writes stay denied. Spawn is a judgment call,
2525
not a tool ban.
2626

27+
- **Exec and capability evals can run as a chosen primary director.**
28+
`corbits exec --director <id>` (and eval `--director`) overlays that
29+
package's system prompt and tool allowlist on the product exec path.
30+
Omit / skywalker keep the default Skywalker session. Directors that
31+
cannot spawn (for example implement) do not mount `task`. This is an
32+
exec/eval/CI override, not a TUI or single-agent mode.
33+
2734
## [0.2.99] - 2026-08-21
2835

2936
Skywalker is the primary orchestrator over a closed director fleet: product write tools stay off the primary, and you cannot spawn Skywalker as a task leaf. Workers are not done until they return the four-heading report. First-party action skills ship as slashes; eval runners require an explicit provider/model pair; the style skill no longer refuses non-git folders.

‎evals/capability/README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ bun run eval:capability -- --provider <name> --model <id> \
129129
bun run eval:capability -- --provider <name> --model <id> --repeats 5 \
130130
--out evals/capability/results/candidate.json \
131131
--baseline evals/capability/results/baseline-0286.json
132+
133+
# Overlay a closed-fleet director on the product exec path (eval/CI override,
134+
# not single-agent mode). Omit / skywalker keep the default Skywalker session.
135+
bun run eval:capability -- --provider <name> --model <id> --director implement
132136
```
133137

134138
## Confirmation gate for behavior changes
@@ -166,6 +170,7 @@ Flags:
166170
| `--verify-timeout-ms <n>` | Wall-clock limit for `verify.sh` (default `120000`, env `CORBITS_EVAL_VERIFY_TIMEOUT_MS`) |
167171
| `--repeats <n>` | Runs per case×variant cell (default `1`; gate runs use `5`, baseline freezes `3`). Results record every repeat plus per-cell aggregates |
168172
| `--dry-run` | Load cases × variants and print plan; no inference. Still requires `--provider`/`--model` or `--matrix` |
173+
| `--director <id>` | Exec overlay: run the product `corbits exec` path as this closed-fleet director (default: skywalker). Eval/CI override, not single-agent mode. Directors that cannot spawn (for example `implement`) do not mount `task`. |
169174

170175
## Case format
171176

‎scripts/eval-capability.test.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ describe("parseArgs", () => {
6161
expect(pair.provider).toBe("foo");
6262
expect(pair.model).toBe("bar");
6363
});
64+
65+
test("--director implement is parsed", () => {
66+
const opts = parseArgs(["--provider", "foo", "--model", "bar", "--director", "implement"]);
67+
expect(opts.director).toBe("implement");
68+
});
69+
70+
test("omitted --director stays undefined", () => {
71+
const opts = parseArgs(["--provider", "foo", "--model", "bar"]);
72+
expect(opts.director).toBeUndefined();
73+
});
74+
75+
test("--director without a value throws", () => {
76+
expect(() => parseArgs(["--provider", "foo", "--model", "bar", "--director"])).toThrow(
77+
"--director requires a value",
78+
);
79+
});
6480
});
6581

6682
describe("initEvalGitRepo", () => {

‎scripts/eval-capability.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ type CliOptions = {
7575
* differs from what was requested, instead of hard-failing.
7676
*/
7777
allowProviderFallback: boolean;
78+
/**
79+
* Exec overlay: run the product path as this closed-fleet director.
80+
* Eval/CI override, not single-agent mode. Omitted = skywalker default.
81+
*/
82+
director?: string;
7883
};
7984

8085
function printUsage(): void {
@@ -96,6 +101,8 @@ function printUsage(): void {
96101
--dry-run List cases × variants only (still requires --provider/--model or --matrix)
97102
--allow-provider-fallback Allow resolved provider/model to differ from
98103
what was requested (default: hard-fail)
104+
--director <id> Exec overlay: run as this director (default: skywalker).
105+
Eval/CI override, not single-agent mode
99106
-h, --help Show help
100107
`);
101108
}
@@ -181,6 +188,9 @@ export function parseArgs(argv: readonly string[]): CliOptions {
181188
case "--allow-provider-fallback":
182189
opts.allowProviderFallback = true;
183190
break;
191+
case "--director":
192+
opts.director = next();
193+
break;
184194
default:
185195
throw new Error(`Unknown argument: ${a}`);
186196
}
@@ -559,6 +569,7 @@ async function runCase(
559569
if (opts.configPath !== undefined) argv.push("--config", opts.configPath);
560570
if (opts.skipPermissions) argv.push("--dangerously-skip-permissions");
561571
argv.push("--force");
572+
if (opts.director !== undefined) argv.push("--director", opts.director);
562573

563574
const maxTurns = opts.maxTurnsOverride ?? caseDef.maxTurns ?? null;
564575
// maxTurns is a soft post-run budget (case fails if exceeded). It does not

‎src/config.test.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
44
import { join } from "node:path";
55

66
import { buildBifrostSource, buildOpenAISource, buildXaiSource, buildProviderCatalog, catalogEntryAsProviderSettings, CliHelpError, CLI_HELP_TEXT, KEYLESS_API_KEY, loadConfig, providerCatalogToSettings, runtimeSettingsWithCatalog, SOURCE_MAX_TOKENS } from "./config/index.js";
7+
import { DIRECTOR_IDS } from "./agent/directors/types.js";
78
import type { Config, UnconfiguredConfig } from "./config/index.js";
89
import { mergeProviderIntoSettings, type ResolvedProvider, type Settings } from "./config/settings.js";
910
import { OPENCODE_GO_BASE_URL } from "../packages/opencode-go/src/index.js";
@@ -183,6 +184,55 @@ describe("loadConfig", () => {
183184
}
184185
});
185186

187+
test("parses exec --director implement", async () => {
188+
const cwd = await emptyCwd();
189+
try {
190+
const globalPath = await writeGlobalSettings(cwd);
191+
const config = await loadConfig(["exec", "--cwd", cwd, "--director", "implement", "ship it"], {
192+
globalSettingsPath: globalPath,
193+
});
194+
assertConfigured(config);
195+
expect(config.command).toBe("exec");
196+
expect(config.director).toBe("implement");
197+
expect(config.task).toBe("ship it");
198+
} finally {
199+
await rm(cwd, { recursive: true, force: true });
200+
}
201+
});
202+
203+
test("omits director as undefined (skywalker default)", async () => {
204+
const cwd = await emptyCwd();
205+
try {
206+
const globalPath = await writeGlobalSettings(cwd);
207+
const config = await loadConfig(["exec", "--cwd", cwd, "ship it"], {
208+
globalSettingsPath: globalPath,
209+
});
210+
assertConfigured(config);
211+
expect(config.command).toBe("exec");
212+
expect(config.director).toBeUndefined();
213+
} finally {
214+
await rm(cwd, { recursive: true, force: true });
215+
}
216+
});
217+
218+
test("unknown --director id errors listing DIRECTOR_IDS", async () => {
219+
await expect(
220+
loadConfig(["exec", "--director", "nope", "ship it"], { globalSettingsPath: NO_SETTINGS }),
221+
).rejects.toThrow(new RegExp(`Unknown director "nope".*${DIRECTOR_IDS.join(", ")}`));
222+
});
223+
224+
test("--director without a value errors", async () => {
225+
await expect(loadConfig(["exec", "--director"], { globalSettingsPath: NO_SETTINGS })).rejects.toThrow(
226+
"--director requires a value",
227+
);
228+
});
229+
230+
test("--director without exec/run is rejected", async () => {
231+
await expect(
232+
loadConfig(["--director", "implement", "ship it"], { globalSettingsPath: NO_SETTINGS }),
233+
).rejects.toThrow("--director is only available in exec mode");
234+
});
235+
186236
test("resume --pick opens the session picker without requiring prior sessions", async () => {
187237
const cwd = await emptyCwd();
188238
try {

‎src/config/index.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { generateSessionId, isSessionId, migrateLegacySessionIfNeeded } from "..
55
import { loadState } from "../session/state.js";
66

77

8+
import { isDirectorId } from "../agent/directors/registry.js";
9+
import { DIRECTOR_IDS, type DirectorId } from "../agent/directors/types.js";
810
import { validateEffort, type ReasoningEffort } from "../provider/reasoning-effort.js";
911
import { bootstrapPricingMetadata } from "../cost/pricing-metadata.js";
1012
import { defaultPricingCachePath, type PricingFetcherOptions } from "../cost/pricing-fetcher.js";
@@ -278,6 +280,11 @@ export type Config = {
278280
force: boolean;
279281
dangerouslySkipPermissions: boolean;
280282
auto: boolean;
283+
/**
284+
* Exec-only chosen primary director. Omitted = Skywalker (product default).
285+
* `--director` is rejected in TUI mode.
286+
*/
287+
director?: DirectorId;
281288
/**
282289
* Entry mode. `"tui"` is the interactive Ink shell; `"exec"` is the non-TUI
283290
* product agent path (`corbits exec "prompt"`). Same directors/tools/permissions.
@@ -343,6 +350,8 @@ export type UnconfiguredConfig = {
343350
dangerouslySkipPermissions: boolean;
344351
auto: boolean;
345352
command: "tui" | "exec";
353+
/** Exec-only chosen primary. Omitted on the unconfigured path too. */
354+
director?: DirectorId;
346355
// Path where the onboarding flow should write the new settings.
347356
globalSettingsPath: string;
348357
// The original error message, used for non-TUI (exec) error output.
@@ -377,6 +386,7 @@ Flags:
377386
--profile <name> settings profile
378387
--resume interactive session picker
379388
--force override an existing run state
389+
--director <id> exec-only: run as this director (default: skywalker)
380390
--dangerously-skip-permissions
381391
--auto / --no-auto auto mode on/off
382392
--help, -h show this help
@@ -474,6 +484,7 @@ export async function loadConfig(
474484
// to ask-on-every-write. There is currently no in-session key to toggle auto;
475485
// Shift+Tab in the TUI cycles reasoning effort instead.
476486
let auto = true;
487+
let director: DirectorId | undefined;
477488
let configPath: string | undefined;
478489
let provider: string | undefined;
479490
let model: string | undefined;
@@ -515,6 +526,17 @@ export async function loadConfig(
515526
force = true;
516527
continue;
517528
}
529+
if (arg === "--director") {
530+
const value = requireValue("--director", args[++i]);
531+
if (command !== "exec") {
532+
throw new Error("--director is only available in exec mode");
533+
}
534+
if (!isDirectorId(value)) {
535+
throw new Error(`Unknown director "${value}". Use one of: ${DIRECTOR_IDS.join(", ")}.`);
536+
}
537+
director = value;
538+
continue;
539+
}
518540

519541
if (arg === "--dangerously-skip-permissions") {
520542
dangerouslySkipPermissions = true;
@@ -633,6 +655,7 @@ export async function loadConfig(
633655
dangerouslySkipPermissions,
634656
auto,
635657
command,
658+
...(director !== undefined ? { director } : {}),
636659
globalSettingsPath: effectiveSettingsPath,
637660
providerError: err instanceof Error ? err.message : String(err),
638661
// Keep diagnostics even when provider setup fails early so junk local
@@ -684,6 +707,7 @@ export async function loadConfig(
684707
dangerouslySkipPermissions,
685708
auto,
686709
command,
710+
...(director !== undefined ? { director } : {}),
687711
globalSettingsPath: effectiveSettingsPath,
688712
sessionId,
689713
noWorkflow,

‎src/exec/runner.ts‎

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import {
2727
} from "../config/settings.js";
2828
import { codexProfileFromProviderName } from "../config/codex-providers.js";
2929
import { xaiProfileFromProviderName } from "../config/xai-providers.js";
30+
import { formatDirectorSystemPrompt } from "../agent/directors/identity.js";
31+
import { DIRECTOR_REGISTRY } from "../agent/directors/registry.js";
32+
import type { DirectorId } from "../agent/directors/types.js";
3033
import { createInferenceDependencies } from "../provider/inference-dependencies.js";
3134
import { getValidCodexToken } from "../auth/codex/session.js";
3235
import { getValidXaiToken } from "../auth/xai/session.js";
@@ -111,6 +114,40 @@ export function formatCaughtError(err: unknown): string {
111114
return err instanceof Error ? err.message : String(err);
112115
}
113116

117+
/**
118+
* Exec-primary director overlay. Omit / skywalker keep the product default
119+
* (`loadSessionChatPrompt` + advertised session tools). Any other closed-fleet
120+
* id uses the package prompt and allowlist. Worker effort/nudge are not applied.
121+
*/
122+
export type ExecDirectorOverlay = {
123+
/** Package system prompt; omitted on the skywalker default path. */
124+
systemPrompt?: string;
125+
/** `pkg.tools.allow` (task stripped when `maySpawn` is false). */
126+
advertisedAllow?: readonly string[];
127+
mountTask: boolean;
128+
};
129+
130+
export function resolveExecDirectorOverlay(
131+
director: DirectorId | undefined,
132+
): ExecDirectorOverlay {
133+
if (director === undefined || director === "skywalker") {
134+
return { mountTask: true };
135+
}
136+
const pkg = DIRECTOR_REGISTRY[director];
137+
const allow = pkg.tools?.allow;
138+
const advertisedAllow =
139+
allow !== undefined && allow.length > 0
140+
? pkg.spawn.maySpawn
141+
? [...allow]
142+
: allow.filter((name) => name !== "task")
143+
: undefined;
144+
return {
145+
systemPrompt: formatDirectorSystemPrompt(pkg),
146+
...(advertisedAllow !== undefined ? { advertisedAllow } : {}),
147+
mountTask: pkg.spawn.maySpawn,
148+
};
149+
}
150+
114151
/** Content-less inbound used after compact so the reactor re-enters (matches TUI). */
115152
export function buildCompactionContinuationMessage(): InboundMessage {
116153
return {
@@ -348,6 +385,8 @@ export async function runExec(config: Config): Promise<ExecResult> {
348385

349386
let currentAgent: Agent | null = null;
350387

388+
const overlay = resolveExecDirectorOverlay(config.director);
389+
351390
const agentToolset = await createAgentToolset({
352391
cwd: config.cwd,
353392
permissionGate,
@@ -384,30 +423,39 @@ export async function runExec(config: Config): Promise<ExecResult> {
384423
);
385424
return result.kind === "option" && result.index === 0;
386425
},
387-
subAgent: {
388-
provider: () => liveSubAgentProvider.current,
389-
sessions: subAgentSessions,
390-
getWorkdirBase: () => sessionDir(config.cwd, sessionId),
391-
onProgress: () => undefined,
392-
...(config.settings !== undefined ? { settings: () => config.settings! } : {}),
393-
catalog: () => config.providers,
394-
profiles: () => liveAgentProfiles,
395-
},
426+
...(overlay.mountTask
427+
? {
428+
subAgent: {
429+
provider: () => liveSubAgentProvider.current,
430+
sessions: subAgentSessions,
431+
getWorkdirBase: () => sessionDir(config.cwd, sessionId),
432+
onProgress: () => undefined,
433+
...(config.settings !== undefined ? { settings: () => config.settings! } : {}),
434+
catalog: () => config.providers,
435+
profiles: () => liveAgentProfiles,
436+
},
437+
}
438+
: {}),
396439
...(extraToolPlugins.length > 0 ? { extraToolPlugins } : {}),
397440
});
398441
toolset = agentToolset;
399442

400-
const { systemPrompt } = await loadSessionChatPrompt({
401-
cwd: config.cwd,
402-
skillDirs,
403-
...(config.systemPromptExtensions !== undefined
404-
? { systemPromptExtensions: config.systemPromptExtensions }
405-
: {}),
406-
sessionMode,
407-
toolAvailability,
408-
});
443+
const systemPrompt =
444+
overlay.systemPrompt ??
445+
(
446+
await loadSessionChatPrompt({
447+
cwd: config.cwd,
448+
skillDirs,
449+
...(config.systemPromptExtensions !== undefined
450+
? { systemPromptExtensions: config.systemPromptExtensions }
451+
: {}),
452+
sessionMode,
453+
toolAvailability,
454+
})
455+
).systemPrompt;
409456

410-
const advertisedBuiltInPrefix = advertisedToolNamesForSessionMode(sessionMode, toolAvailability);
457+
const advertisedBuiltInPrefix =
458+
overlay.advertisedAllow ?? advertisedToolNamesForSessionMode(sessionMode, toolAvailability);
411459
const activatedToolNames = createActivatedToolTracker();
412460
// Advertise then family-gate wire schemas (kimi gets a non-recursive present).
413461
const computeAdvertised = (all: readonly ToolDefinition[]): ToolDefinition[] =>

‎tests/unit/exec/runner.test.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, test } from "bun:test";
22
import type { Config } from "../../../src/config/index.js";
3-
import { formatCaughtError, runExec } from "../../../src/exec/runner.js";
3+
import { formatCaughtError, resolveExecDirectorOverlay, runExec } from "../../../src/exec/runner.js";
4+
import { IMPLEMENT_TOOLS } from "../../../src/agent/directors/tool-sets.js";
45

56
function bareConfig(task: string): Config {
67
// Minimal unconfigured-shaped object is not enough — runExec only needs
@@ -48,3 +49,22 @@ describe("runExec", () => {
4849
}
4950
});
5051
});
52+
53+
describe("resolveExecDirectorOverlay", () => {
54+
test("implement exec primary does not mount task", () => {
55+
const overlay = resolveExecDirectorOverlay("implement");
56+
expect(overlay.mountTask).toBe(false);
57+
expect(overlay.advertisedAllow).toBeDefined();
58+
expect(overlay.advertisedAllow).not.toContain("task");
59+
expect(overlay.advertisedAllow).toEqual([...IMPLEMENT_TOOLS]);
60+
expect(overlay.systemPrompt).toContain("ImplementDirector");
61+
});
62+
63+
test("skywalker default still can mount task", () => {
64+
expect(resolveExecDirectorOverlay(undefined).mountTask).toBe(true);
65+
expect(resolveExecDirectorOverlay(undefined).systemPrompt).toBeUndefined();
66+
expect(resolveExecDirectorOverlay(undefined).advertisedAllow).toBeUndefined();
67+
expect(resolveExecDirectorOverlay("skywalker").mountTask).toBe(true);
68+
expect(resolveExecDirectorOverlay("skywalker").systemPrompt).toBeUndefined();
69+
});
70+
});

0 commit comments

Comments
 (0)