bosun 0.29.2 → 0.29.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -482,7 +482,7 @@ TELEGRAM_MINIAPP_ENABLED=false
482
482
  # CODEX_FEATURES_CHILD_AGENTS_MD=true # Sub-agent discovery via CODEX.md (KEY for sub-agents)
483
483
  # CODEX_FEATURES_MEMORY_TOOL=true # Persistent memory across sessions
484
484
  # CODEX_FEATURES_UNDO=true # Undo/rollback support
485
- # CODEX_FEATURES_COLLAB=true # Collaboration mode
485
+ # CODEX_FEATURES_MULTI_AGENT=true # Multi Agent mode
486
486
  # CODEX_FEATURES_COLLABORATION_MODES=true # Mode selection for collaboration
487
487
  # CODEX_FEATURES_STEER=true # Steering/guidance
488
488
  # CODEX_FEATURES_APPS=true # ChatGPT Apps integration
package/agent-pool.mjs CHANGED
@@ -307,8 +307,26 @@ const SDK_ADAPTERS = {
307
307
  },
308
308
  };
309
309
 
310
- /** Ordered fallback chain for SDK resolution */
311
- const SDK_FALLBACK_ORDER = ["codex", "copilot", "claude"];
310
+ /**
311
+ * Ordered fallback chain for SDK resolution.
312
+ * Configurable via bosun.config.json → agentPool.fallbackOrder
313
+ */
314
+ let SDK_FALLBACK_ORDER = ["codex", "copilot", "claude"];
315
+
316
+ // Attempt to load custom fallback order from config
317
+ try {
318
+ const cfg = loadConfig();
319
+ const customOrder = cfg?.agentPool?.fallbackOrder;
320
+ if (Array.isArray(customOrder) && customOrder.length > 0) {
321
+ // Validate: only accept known SDK names
322
+ const valid = customOrder
323
+ .map((s) => String(s).trim().toLowerCase())
324
+ .filter((s) => SDK_ADAPTERS[s]);
325
+ if (valid.length > 0) SDK_FALLBACK_ORDER = valid;
326
+ }
327
+ } catch {
328
+ // loadConfig not available or no custom order — use default
329
+ }
312
330
 
313
331
  // ---------------------------------------------------------------------------
314
332
  // SDK Resolution & Cache
@@ -570,7 +588,7 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
570
588
  ...(codexOpts.config || {}),
571
589
  features: {
572
590
  child_agents_md: true,
573
- collab: true,
591
+ multi_agent: true,
574
592
  memory_tool: true,
575
593
  undo: true,
576
594
  steer: true,
package/agent-sdk.mjs CHANGED
@@ -21,12 +21,12 @@ const DEFAULT_CAPABILITIES_BY_PRIMARY = {
21
21
  },
22
22
  copilot: {
23
23
  steering: false,
24
- subagents: false,
24
+ subagents: true,
25
25
  vscodeTools: true,
26
26
  },
27
27
  claude: {
28
28
  steering: false,
29
- subagents: false,
29
+ subagents: true,
30
30
  vscodeTools: false,
31
31
  },
32
32
  };
package/cli.mjs CHANGED
@@ -118,6 +118,7 @@ function showHelp() {
118
118
  --workspace-add <name> Create a new workspace
119
119
  --workspace-switch <id> Switch active workspace
120
120
  --workspace-add-repo Add repo to workspace (interactive)
121
+ --workspace-health Run workspace health diagnostics
121
122
 
122
123
  TASK MANAGEMENT
123
124
  task list [--status s] [--json] List tasks with optional filters
@@ -914,6 +915,17 @@ async function main() {
914
915
  process.exit(0);
915
916
  }
916
917
 
918
+ // Handle --workspace-health / --verify-workspace
919
+ if (args.includes("--workspace-health") || args.includes("--verify-workspace") || args.includes("workspace-health")) {
920
+ const { runWorkspaceHealthCheck, formatWorkspaceHealthReport } =
921
+ await import("./config-doctor.mjs");
922
+ const configDirArg = getArgValue("--config-dir");
923
+ const configDir = configDirArg || process.env.BOSUN_DIR || resolve(os.homedir(), "bosun");
924
+ const result = runWorkspaceHealthCheck({ configDir });
925
+ console.log(formatWorkspaceHealthReport(result));
926
+ process.exit(result.ok ? 0 : 1);
927
+ }
928
+
917
929
  // Handle --setup
918
930
  if (args.includes("--setup") || args.includes("setup")) {
919
931
  const configDirArg = getArgValue("--config-dir");
package/codex-config.mjs CHANGED
@@ -9,12 +9,22 @@
9
9
  * 5. Sandbox permissions and shell environment policy
10
10
  * 6. Common MCP servers (context7, microsoft-docs)
11
11
  *
12
+ * SCOPE: This manages the GLOBAL ~/.codex/config.toml which contains:
13
+ * - Model provider configs (API keys, base URLs) — MUST be global
14
+ * - Stream timeouts & retry settings — per-provider, global
15
+ * - Sandbox workspace-write (writable_roots) — spans multiple repos, global
16
+ * - Feature flags, MCP servers, agent_sdk — kept as FALLBACK defaults
17
+ * (prefer repo-level .codex/config.toml via repo-config.mjs)
18
+ *
19
+ * For project-scoped settings, see repo-config.mjs which generates
20
+ * .codex/config.toml at the repo level.
21
+ *
12
22
  * Uses string-based TOML manipulation (no parser dependency) — we only
13
23
  * append or patch well-known sections rather than rewriting the whole file.
14
24
  */
15
25
 
16
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
17
- import { resolve, dirname } from "node:path";
26
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
27
+ import { resolve, dirname, parse } from "node:path";
18
28
  import { homedir } from "node:os";
19
29
  import { fileURLToPath } from "node:url";
20
30
  import { resolveCodexProfileRuntime } from "./codex-model-profiles.mjs";
@@ -55,25 +65,38 @@ const AGENT_SDK_CAPS_HEADER = "[agent_sdk.capabilities]";
55
65
  const AGENTS_HEADER = "[agents]";
56
66
  const DEFAULT_AGENT_MAX_THREADS = 12;
57
67
 
58
- const DEFAULT_AGENT_SDK_BLOCK = [
59
- "",
60
- "# ── Agent SDK selection (added by bosun) ──",
61
- AGENT_SDK_HEADER,
62
- "# Primary agent SDK used for in-process automation.",
63
- '# Supported: "codex", "copilot", "claude"',
64
- 'primary = "codex"',
65
- "# Max concurrent agent threads per Codex session.",
66
- `max_threads = ${DEFAULT_AGENT_MAX_THREADS}`,
67
- "",
68
- AGENT_SDK_CAPS_HEADER,
69
- "# Live steering updates during an active run.",
70
- "steering = true",
71
- "# Ability to spawn subagents/child tasks.",
72
- "subagents = true",
73
- "# Access to VS Code tools (Copilot extension).",
74
- "vscode_tools = false",
75
- "",
76
- ].join("\n");
68
+ /**
69
+ * Build the default [agent_sdk] TOML block.
70
+ * @param {string} [primary="codex"] The primary SDK: "codex", "copilot", or "claude"
71
+ * @returns {string}
72
+ */
73
+ function buildDefaultAgentSdkBlock(primary = "codex") {
74
+ const caps = {
75
+ codex: { steering: true, subagents: true, vscodeTools: false },
76
+ copilot: { steering: false, subagents: true, vscodeTools: true },
77
+ claude: { steering: false, subagents: true, vscodeTools: false },
78
+ };
79
+ const c = caps[primary] || caps.codex;
80
+ return [
81
+ "",
82
+ "# ── Agent SDK selection (added by bosun) ──",
83
+ AGENT_SDK_HEADER,
84
+ "# Primary agent SDK used for in-process automation.",
85
+ '# Supported: "codex", "copilot", "claude"',
86
+ `primary = "${primary}"`,
87
+ "# Max concurrent agent threads per Codex session.",
88
+ `max_threads = ${DEFAULT_AGENT_MAX_THREADS}`,
89
+ "",
90
+ AGENT_SDK_CAPS_HEADER,
91
+ "# Live steering updates during an active run.",
92
+ `steering = ${c.steering}`,
93
+ "# Ability to spawn subagents/child tasks.",
94
+ `subagents = ${c.subagents}`,
95
+ "# Access to VS Code tools (Copilot extension).",
96
+ `vscode_tools = ${c.vscodeTools}`,
97
+ "",
98
+ ].join("\n");
99
+ }
77
100
 
78
101
  /**
79
102
  * @deprecated No longer used — max_threads is now managed under [agent_sdk].
@@ -100,7 +123,7 @@ const buildAgentsBlock = (_maxThreads) =>
100
123
  const RECOMMENDED_FEATURES = {
101
124
  // Sub-agents & collaboration
102
125
  child_agents_md: { default: true, envVar: "CODEX_FEATURES_CHILD_AGENTS_MD", comment: "Enable sub-agent discovery via CODEX.md" },
103
- collab: { default: true, envVar: "CODEX_FEATURES_COLLAB", comment: "Enable collaboration mode" },
126
+ multi_agent: { default: true, envVar: "CODEX_FEATURES_MULTI_AGENT", comment: "Enable collaboration mode" },
104
127
  collaboration_modes: { default: true, envVar: "CODEX_FEATURES_COLLABORATION_MODES", comment: "Enable collaboration mode selection" },
105
128
 
106
129
  // Continuity & recovery
@@ -128,7 +151,7 @@ const RECOMMENDED_FEATURES = {
128
151
  const CRITICAL_ALWAYS_ON_FEATURES = new Set([
129
152
  "child_agents_md",
130
153
  "memory_tool",
131
- "collab",
154
+ "multi_agent",
132
155
  "collaboration_modes",
133
156
  "shell_tool",
134
157
  "unified_exec",
@@ -411,11 +434,25 @@ function formatTomlArray(values) {
411
434
  return `[${values.map((value) => `"${String(value).replace(/"/g, '\\"')}"`).join(", ")}]`;
412
435
  }
413
436
 
414
- function normalizeWritableRoots(input, { repoRoot, additionalRoots } = {}) {
437
+ function normalizeWritableRoots(input, { repoRoot, additionalRoots, validateExistence = false } = {}) {
415
438
  const roots = new Set();
416
439
  const addRoot = (value) => {
417
440
  const trimmed = String(value || "").trim();
418
441
  if (!trimmed) return;
442
+ // Reject bare relative paths like ".git" — they resolve relative to CWD
443
+ // at Codex launch time and cause "writable root does not exist" errors
444
+ // (e.g. /home/user/.codex/.git). Only accept absolute paths.
445
+ if (!trimmed.startsWith("/")) return;
446
+ // When validateExistence is true, skip paths that don't exist on disk.
447
+ // This prevents the sandbox from failing to start with phantom roots.
448
+ if (validateExistence && !existsSync(trimmed)) return;
449
+ roots.add(trimmed);
450
+ };
451
+ // Always-add: these are primary roots (repo root, parent) that should be
452
+ // present even if validateExistence is true — they're the intended CWD.
453
+ const addPrimaryRoot = (value) => {
454
+ const trimmed = String(value || "").trim();
455
+ if (!trimmed || !trimmed.startsWith("/")) return;
419
456
  roots.add(trimmed);
420
457
  };
421
458
  if (Array.isArray(input)) {
@@ -428,17 +465,23 @@ function normalizeWritableRoots(input, { repoRoot, additionalRoots } = {}) {
428
465
  .forEach(addRoot);
429
466
  }
430
467
 
431
- // Add paths for primary repo root
468
+ // Add paths for a repo root — only if it's a non-empty absolute path
432
469
  const addRepoRootPaths = (repo) => {
433
470
  if (!repo) return;
434
- const r = String(repo);
435
- if (!r) return;
436
- addRoot(r);
437
- addRoot(resolve(r, ".git"));
438
- addRoot(resolve(r, ".cache", "worktrees"));
439
- addRoot(resolve(r, ".cache"));
471
+ const r = String(repo).trim();
472
+ if (!r || !r.startsWith("/")) return;
473
+ // Repo root and parent are always added (they're primary working dirs)
474
+ addPrimaryRoot(r);
475
+ const gitDir = resolve(r, ".git");
476
+ // Only add .git if it actually exists (prevents phantom writable roots)
477
+ if (existsSync(gitDir)) addRoot(gitDir);
478
+ const cacheWorktrees = resolve(r, ".cache", "worktrees");
479
+ // Only add .cache subdirs if they exist — avoid phantom roots
480
+ if (existsSync(cacheWorktrees)) addRoot(cacheWorktrees);
481
+ const cacheDir = resolve(r, ".cache");
482
+ if (existsSync(cacheDir)) addRoot(cacheDir);
440
483
  const parent = dirname(r);
441
- if (parent && parent !== r) addRoot(parent);
484
+ if (parent && parent !== r) addPrimaryRoot(parent);
442
485
  };
443
486
 
444
487
  addRepoRootPaths(repoRoot);
@@ -451,11 +494,95 @@ function normalizeWritableRoots(input, { repoRoot, additionalRoots } = {}) {
451
494
  }
452
495
 
453
496
  // /tmp is needed for sandbox temp files, pip installs, etc.
454
- addRoot("/tmp");
497
+ roots.add("/tmp");
455
498
 
456
499
  return Array.from(roots);
457
500
  }
458
501
 
502
+ /**
503
+ * Validate that a directory has a valid .git ancestor (regular repo or worktree).
504
+ * In worktrees, .git is a file containing "gitdir: <path>" — follow the reference
505
+ * to find the actual git object store. Returns the resolved .git directory path
506
+ * or null if not found.
507
+ * @param {string} dir - Directory to check
508
+ * @returns {{ gitDir: string|null, mainWorktreeRoot: string|null, isWorktree: boolean }}
509
+ */
510
+ export function ensureGitAncestor(dir) {
511
+ if (!dir) return { gitDir: null, mainWorktreeRoot: null, isWorktree: false };
512
+ let current = resolve(dir);
513
+ const { root } = parse(current);
514
+ while (current !== root) {
515
+ const gitPath = resolve(current, ".git");
516
+ if (existsSync(gitPath)) {
517
+ try {
518
+ const stat = statSync(gitPath);
519
+ if (stat.isDirectory()) {
520
+ // Regular git repo
521
+ return { gitDir: gitPath, mainWorktreeRoot: current, isWorktree: false };
522
+ }
523
+ if (stat.isFile()) {
524
+ // Worktree: .git is a file with "gitdir: <relative-or-absolute-path>"
525
+ const content = readFileSync(gitPath, "utf8").trim();
526
+ const match = content.match(/^gitdir:\s*(.+)$/);
527
+ if (match) {
528
+ const gitdirRef = resolve(current, match[1].trim());
529
+ // Worktree gitdir points to <main-repo>/.git/worktrees/<name>
530
+ // Walk up 2 levels to get the main .git directory
531
+ const mainGitDir = resolve(gitdirRef, "..", "..");
532
+ const mainRepoRoot = dirname(mainGitDir);
533
+ return {
534
+ gitDir: mainGitDir,
535
+ mainWorktreeRoot: mainRepoRoot,
536
+ isWorktree: true,
537
+ };
538
+ }
539
+ }
540
+ } catch { /* permission error, race, etc. */ }
541
+ }
542
+ current = dirname(current);
543
+ }
544
+ return { gitDir: null, mainWorktreeRoot: null, isWorktree: false };
545
+ }
546
+
547
+ /**
548
+ * Build writable roots for a specific task's execution context.
549
+ * Combines the global config roots with task-specific paths (worktree, repo root).
550
+ * @param {{ worktreePath?: string, repoRoot?: string, existingRoots?: string[] }} opts
551
+ * @returns {string[]} Merged writable roots
552
+ */
553
+ export function buildTaskWritableRoots({ worktreePath, repoRoot, existingRoots = [] } = {}) {
554
+ const roots = new Set(existingRoots.filter(r => r && r.startsWith("/") && existsSync(r)));
555
+ const addIfExists = (p) => {
556
+ if (p && p.startsWith("/") && existsSync(p)) roots.add(p);
557
+ };
558
+ // Add path even if it doesn't exist yet (will be created by the task)
559
+ const addRoot = (p) => {
560
+ if (p && p.startsWith("/")) roots.add(p);
561
+ };
562
+
563
+ if (worktreePath) {
564
+ addRoot(worktreePath); // Worktree dir itself may be about to be created
565
+ // Worktrees have a .git file pointing to main repo — resolve the actual git dir
566
+ const ancestor = ensureGitAncestor(worktreePath);
567
+ if (ancestor.gitDir) addIfExists(ancestor.gitDir);
568
+ if (ancestor.mainWorktreeRoot) {
569
+ addIfExists(resolve(ancestor.mainWorktreeRoot, ".git"));
570
+ addIfExists(resolve(ancestor.mainWorktreeRoot, ".cache", "worktrees"));
571
+ }
572
+ }
573
+ if (repoRoot) {
574
+ addRoot(repoRoot);
575
+ const gitDir = resolve(repoRoot, ".git");
576
+ if (existsSync(gitDir)) addIfExists(gitDir);
577
+ addIfExists(resolve(repoRoot, ".cache", "worktrees"));
578
+ addIfExists(resolve(repoRoot, ".cache"));
579
+ const parent = dirname(repoRoot);
580
+ if (parent && parent !== repoRoot) addIfExists(parent);
581
+ }
582
+ roots.add("/tmp");
583
+ return Array.from(roots);
584
+ }
585
+
459
586
  export function hasSandboxWorkspaceWrite(toml) {
460
587
  return /^\[sandbox_workspace_write\]/m.test(toml);
461
588
  }
@@ -470,7 +597,7 @@ export function ensureSandboxWorkspaceWrite(toml, options = {}) {
470
597
  excludeSlashTmp = false,
471
598
  } = options;
472
599
 
473
- const desiredRoots = normalizeWritableRoots(writableRoots, { repoRoot, additionalRoots });
600
+ const desiredRoots = normalizeWritableRoots(writableRoots, { repoRoot, additionalRoots, validateExistence: true });
474
601
  if (!hasSandboxWorkspaceWrite(toml)) {
475
602
  if (desiredRoots.length === 0) {
476
603
  return { toml, changed: false, added: false, rootsAdded: [] };
@@ -522,13 +649,18 @@ export function ensureSandboxWorkspaceWrite(toml, options = {}) {
522
649
  const match = section.match(rootsRegex);
523
650
  if (match) {
524
651
  const existingRoots = parseTomlArrayLiteral(match[1]);
525
- const merged = normalizeWritableRoots(existingRoots, { repoRoot });
652
+ // Filter out stale roots that no longer exist on disk
653
+ const validExisting = existingRoots.filter((r) => r === "/tmp" || existsSync(r));
654
+ const merged = normalizeWritableRoots(validExisting, { repoRoot, validateExistence: true });
526
655
  for (const root of desiredRoots) {
527
656
  if (!merged.includes(root)) {
528
657
  merged.push(root);
529
658
  rootsAdded.push(root);
530
659
  }
531
660
  }
661
+ // Track any roots that were removed due to non-existence
662
+ const staleRemoved = existingRoots.filter((r) => r !== "/tmp" && !existsSync(r));
663
+ if (staleRemoved.length > 0) changed = true;
532
664
  const formatted = formatTomlArray(merged);
533
665
  if (formatted !== match[1]) {
534
666
  section = section.replace(rootsRegex, `writable_roots = ${formatted}`);
@@ -555,6 +687,39 @@ export function ensureSandboxWorkspaceWrite(toml, options = {}) {
555
687
  };
556
688
  }
557
689
 
690
+ /**
691
+ * Prune writable_roots in [sandbox_workspace_write] that no longer exist on disk.
692
+ * Returns the updated TOML and a list of removed paths.
693
+ * @param {string} toml
694
+ * @returns {{ toml: string, changed: boolean, removed: string[] }}
695
+ */
696
+ export function pruneStaleSandboxRoots(toml) {
697
+ if (!hasSandboxWorkspaceWrite(toml)) {
698
+ return { toml, changed: false, removed: [] };
699
+ }
700
+ const header = "[sandbox_workspace_write]";
701
+ const headerIdx = toml.indexOf(header);
702
+ if (headerIdx === -1) return { toml, changed: false, removed: [] };
703
+ const afterHeader = headerIdx + header.length;
704
+ const nextSection = toml.indexOf("\n[", afterHeader);
705
+ const sectionEnd = nextSection === -1 ? toml.length : nextSection;
706
+ let section = toml.substring(afterHeader, sectionEnd);
707
+
708
+ const rootsRegex = /^writable_roots\s*=\s*(\[[^\]]*\])\s*$/m;
709
+ const match = section.match(rootsRegex);
710
+ if (!match) return { toml, changed: false, removed: [] };
711
+
712
+ const existing = parseTomlArrayLiteral(match[1]);
713
+ const valid = existing.filter((r) => r === "/tmp" || existsSync(r));
714
+ const removed = existing.filter((r) => r !== "/tmp" && !existsSync(r));
715
+ if (removed.length === 0) return { toml, changed: false, removed: [] };
716
+
717
+ section = section.replace(rootsRegex, `writable_roots = ${formatTomlArray(valid)}`);
718
+ const updatedToml =
719
+ toml.substring(0, afterHeader) + section + toml.substring(sectionEnd);
720
+ return { toml: updatedToml, changed: true, removed };
721
+ }
722
+
558
723
  /**
559
724
  * Build the [shell_environment_policy] section.
560
725
  * Default: inherit = "all" so .NET, Go, Node etc. env vars are visible.
@@ -723,9 +888,12 @@ export function hasAgentSdkConfig(toml) {
723
888
 
724
889
  /**
725
890
  * Build the default agent SDK block.
891
+ * @param {object} [opts]
892
+ * @param {string} [opts.primary="codex"] Primary SDK: "codex", "copilot", or "claude"
893
+ * @returns {string}
726
894
  */
727
- export function buildAgentSdkBlock() {
728
- return DEFAULT_AGENT_SDK_BLOCK;
895
+ export function buildAgentSdkBlock({ primary = "codex" } = {}) {
896
+ return buildDefaultAgentSdkBlock(primary);
729
897
  }
730
898
 
731
899
  /**
@@ -967,12 +1135,14 @@ export function ensureRetrySettings(toml, providerName) {
967
1135
  * @param {boolean} [opts.skipVk]
968
1136
  * @param {boolean} [opts.dryRun] If true, returns result without writing
969
1137
  * @param {object} [opts.env] Environment overrides (defaults to process.env)
1138
+ * @param {string} [opts.primarySdk] Primary agent SDK: "codex", "copilot", or "claude"
970
1139
  */
971
1140
  export function ensureCodexConfig({
972
1141
  vkBaseUrl = "http://127.0.0.1:54089",
973
1142
  skipVk = false,
974
1143
  dryRun = false,
975
1144
  env = process.env,
1145
+ primarySdk,
976
1146
  } = {}) {
977
1147
  const result = {
978
1148
  path: CONFIG_PATH,
@@ -988,6 +1158,7 @@ export function ensureCodexConfig({
988
1158
  sandboxWorkspaceAdded: false,
989
1159
  sandboxWorkspaceUpdated: false,
990
1160
  sandboxWorkspaceRootsAdded: [],
1161
+ sandboxStaleRootsRemoved: [],
991
1162
  shellEnvAdded: false,
992
1163
  commonMcpAdded: false,
993
1164
  profileProvidersAdded: [],
@@ -1068,8 +1239,21 @@ export function ensureCodexConfig({
1068
1239
 
1069
1240
  // ── 1b. Ensure agent SDK selection block ──────────────────
1070
1241
 
1242
+ // Resolve which SDK should be primary:
1243
+ // 1. Explicit primarySdk parameter
1244
+ // 2. PRIMARY_AGENT env var (e.g. "copilot-sdk" → "copilot")
1245
+ // 3. Default: "codex"
1246
+ const resolvedPrimary = (() => {
1247
+ if (primarySdk && ["codex", "copilot", "claude"].includes(primarySdk)) {
1248
+ return primarySdk;
1249
+ }
1250
+ const envPrimary = (env.PRIMARY_AGENT || "").trim().toLowerCase().replace(/-sdk$/, "");
1251
+ if (["codex", "copilot", "claude"].includes(envPrimary)) return envPrimary;
1252
+ return "codex";
1253
+ })();
1254
+
1071
1255
  if (!hasAgentSdkConfig(toml)) {
1072
- toml += buildAgentSdkBlock();
1256
+ toml += buildAgentSdkBlock({ primary: resolvedPrimary });
1073
1257
  result.agentSdkAdded = true;
1074
1258
  }
1075
1259
 
@@ -1114,7 +1298,7 @@ export function ensureCodexConfig({
1114
1298
  // ── 1f. Ensure sandbox workspace-write defaults ───────────
1115
1299
 
1116
1300
  {
1117
- // Determine primary repo root and any workspace roots
1301
+ // Determine primary repo root prefer workspace agent root
1118
1302
  const primaryRepoRoot = env.BOSUN_AGENT_REPO_ROOT || env.REPO_ROOT || "";
1119
1303
  const additionalRoots = [];
1120
1304
  // If agent repo root differs from REPO_ROOT, include both
@@ -1122,6 +1306,27 @@ export function ensureCodexConfig({
1122
1306
  env.BOSUN_AGENT_REPO_ROOT !== env.REPO_ROOT) {
1123
1307
  additionalRoots.push(env.REPO_ROOT);
1124
1308
  }
1309
+ // Enumerate ALL workspace repo paths so every repo's .git/.cache is writable
1310
+ try {
1311
+ // Inline workspace config read (sync) — avoids async import in sync function
1312
+ const configDirGuess = env.BOSUN_DIR || resolve(homedir(), "bosun");
1313
+ const bosunConfigPath = resolve(configDirGuess, "bosun.config.json");
1314
+ if (existsSync(bosunConfigPath)) {
1315
+ const bosunCfg = JSON.parse(readFileSync(bosunConfigPath, "utf8"));
1316
+ const wsDir = resolve(configDirGuess, "workspaces");
1317
+ const allWs = Array.isArray(bosunCfg.workspaces) ? bosunCfg.workspaces : [];
1318
+ for (const ws of allWs) {
1319
+ const wsPath = resolve(wsDir, ws.id || ws.name || "");
1320
+ for (const repo of ws.repos || []) {
1321
+ const repoPath = resolve(wsPath, repo.name);
1322
+ if (repoPath && !additionalRoots.includes(repoPath) &&
1323
+ repoPath !== primaryRepoRoot) {
1324
+ additionalRoots.push(repoPath);
1325
+ }
1326
+ }
1327
+ }
1328
+ }
1329
+ } catch { /* workspace config read failed — skip */ }
1125
1330
  const ensured = ensureSandboxWorkspaceWrite(toml, {
1126
1331
  writableRoots: env.CODEX_SANDBOX_WRITABLE_ROOTS || "",
1127
1332
  repoRoot: primaryRepoRoot,
@@ -1133,6 +1338,14 @@ export function ensureCodexConfig({
1133
1338
  result.sandboxWorkspaceUpdated = !ensured.added;
1134
1339
  result.sandboxWorkspaceRootsAdded = ensured.rootsAdded || [];
1135
1340
  }
1341
+
1342
+ // Prune any writable_roots that no longer exist on disk
1343
+ const pruned = pruneStaleSandboxRoots(toml);
1344
+ if (pruned.changed) {
1345
+ toml = pruned.toml;
1346
+ result.sandboxWorkspaceUpdated = true;
1347
+ result.sandboxStaleRootsRemoved = pruned.removed;
1348
+ }
1136
1349
  }
1137
1350
 
1138
1351
  // ── 1g. Ensure shell environment policy ───────────────────
@@ -1300,6 +1513,15 @@ export function printConfigSummary(result, log = console.log) {
1300
1513
  );
1301
1514
  }
1302
1515
 
1516
+ if (result.sandboxStaleRootsRemoved && result.sandboxStaleRootsRemoved.length > 0) {
1517
+ log(
1518
+ ` 🗑️ Pruned ${result.sandboxStaleRootsRemoved.length} stale writable root(s) that no longer exist`,
1519
+ );
1520
+ for (const r of result.sandboxStaleRootsRemoved) {
1521
+ log(` - ${r}`);
1522
+ }
1523
+ }
1524
+
1303
1525
  if (result.shellEnvAdded) {
1304
1526
  log(" ✅ Added shell environment policy (inherit=all)");
1305
1527
  }
package/codex-shell.mjs CHANGED
@@ -222,7 +222,7 @@ const THREAD_OPTIONS = {
222
222
  skipGitRepoCheck: true,
223
223
  webSearchMode: "live",
224
224
  approvalPolicy: "never",
225
- // Note: sub-agent features (child_agents_md, collab, memory_tool, etc.)
225
+ // Note: sub-agent features (child_agents_md, multi_agent, memory_tool, etc.)
226
226
  // are configured via ~/.codex/config.toml [features] section, not SDK ThreadOptions.
227
227
  // codex-config.mjs ensureFeatureFlags() handles this during setup.
228
228
  };
@@ -247,7 +247,7 @@ async function getThread() {
247
247
  config: {
248
248
  features: {
249
249
  child_agents_md: true,
250
- collab: true,
250
+ multi_agent: true,
251
251
  memory_tool: true,
252
252
  undo: true,
253
253
  steer: true,
@@ -746,7 +746,7 @@ export async function initCodexShell() {
746
746
  config: {
747
747
  features: {
748
748
  child_agents_md: true,
749
- collab: true,
749
+ multi_agent: true,
750
750
  memory_tool: true,
751
751
  undo: true,
752
752
  steer: true,