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 +1 -1
- package/agent-pool.mjs +21 -3
- package/agent-sdk.mjs +2 -2
- package/cli.mjs +12 -0
- package/codex-config.mjs +261 -39
- package/codex-shell.mjs +3 -3
- package/config-doctor.mjs +244 -0
- package/monitor.mjs +31 -0
- package/package.json +1 -1
- package/setup.mjs +227 -75
- package/task-executor.mjs +4 -0
- package/ui/components/chat-view.js +69 -53
- package/ui/components/session-list.js +4 -4
- package/ui/components/shared.js +4 -0
- package/ui/components/workspace-switcher.js +17 -6
- package/ui/demo.html +19 -1
- package/ui/styles/sessions.css +167 -21
- package/ui/styles/workspace-switcher.css +21 -0
- package/ui/tabs/chat.js +10 -0
- package/ui/tabs/dashboard.js +26 -32
- package/ui/tabs/tasks.js +43 -6
- package/ui-server.mjs +12 -0
- package/workspace-manager.mjs +37 -0
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
|
-
#
|
|
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
|
-
/**
|
|
311
|
-
|
|
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
|
-
|
|
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:
|
|
24
|
+
subagents: true,
|
|
25
25
|
vscodeTools: true,
|
|
26
26
|
},
|
|
27
27
|
claude: {
|
|
28
28
|
steering: false,
|
|
29
|
-
subagents:
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
749
|
+
multi_agent: true,
|
|
750
750
|
memory_tool: true,
|
|
751
751
|
undo: true,
|
|
752
752
|
steer: true,
|