bosun 0.29.3 → 0.29.5
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/agent-pool.mjs +20 -2
- package/agent-sdk.mjs +2 -2
- package/codex-config.mjs +146 -32
- package/package.json +1 -1
- package/setup.mjs +227 -75
- 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 +5 -3
- package/ui/styles/workspace-switcher.css +21 -0
- package/ui/tabs/dashboard.js +26 -32
- package/ui/tabs/tasks.js +43 -6
- package/workspace-manager.mjs +37 -0
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
|
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/codex-config.mjs
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
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
|
*/
|
|
@@ -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].
|
|
@@ -411,7 +434,7 @@ 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();
|
|
@@ -420,6 +443,16 @@ function normalizeWritableRoots(input, { repoRoot, additionalRoots } = {}) {
|
|
|
420
443
|
// at Codex launch time and cause "writable root does not exist" errors
|
|
421
444
|
// (e.g. /home/user/.codex/.git). Only accept absolute paths.
|
|
422
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;
|
|
423
456
|
roots.add(trimmed);
|
|
424
457
|
};
|
|
425
458
|
if (Array.isArray(input)) {
|
|
@@ -437,15 +470,18 @@ function normalizeWritableRoots(input, { repoRoot, additionalRoots } = {}) {
|
|
|
437
470
|
if (!repo) return;
|
|
438
471
|
const r = String(repo).trim();
|
|
439
472
|
if (!r || !r.startsWith("/")) return;
|
|
440
|
-
|
|
473
|
+
// Repo root and parent are always added (they're primary working dirs)
|
|
474
|
+
addPrimaryRoot(r);
|
|
441
475
|
const gitDir = resolve(r, ".git");
|
|
442
476
|
// Only add .git if it actually exists (prevents phantom writable roots)
|
|
443
477
|
if (existsSync(gitDir)) addRoot(gitDir);
|
|
444
478
|
const cacheWorktrees = resolve(r, ".cache", "worktrees");
|
|
445
|
-
|
|
446
|
-
|
|
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);
|
|
447
483
|
const parent = dirname(r);
|
|
448
|
-
if (parent && parent !== r)
|
|
484
|
+
if (parent && parent !== r) addPrimaryRoot(parent);
|
|
449
485
|
};
|
|
450
486
|
|
|
451
487
|
addRepoRootPaths(repoRoot);
|
|
@@ -515,13 +551,17 @@ export function ensureGitAncestor(dir) {
|
|
|
515
551
|
* @returns {string[]} Merged writable roots
|
|
516
552
|
*/
|
|
517
553
|
export function buildTaskWritableRoots({ worktreePath, repoRoot, existingRoots = [] } = {}) {
|
|
518
|
-
const roots = new Set(existingRoots.filter(r => r && r.startsWith("/")));
|
|
554
|
+
const roots = new Set(existingRoots.filter(r => r && r.startsWith("/") && existsSync(r)));
|
|
519
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) => {
|
|
520
560
|
if (p && p.startsWith("/")) roots.add(p);
|
|
521
561
|
};
|
|
522
562
|
|
|
523
563
|
if (worktreePath) {
|
|
524
|
-
|
|
564
|
+
addRoot(worktreePath); // Worktree dir itself may be about to be created
|
|
525
565
|
// Worktrees have a .git file pointing to main repo — resolve the actual git dir
|
|
526
566
|
const ancestor = ensureGitAncestor(worktreePath);
|
|
527
567
|
if (ancestor.gitDir) addIfExists(ancestor.gitDir);
|
|
@@ -531,7 +571,7 @@ export function buildTaskWritableRoots({ worktreePath, repoRoot, existingRoots =
|
|
|
531
571
|
}
|
|
532
572
|
}
|
|
533
573
|
if (repoRoot) {
|
|
534
|
-
|
|
574
|
+
addRoot(repoRoot);
|
|
535
575
|
const gitDir = resolve(repoRoot, ".git");
|
|
536
576
|
if (existsSync(gitDir)) addIfExists(gitDir);
|
|
537
577
|
addIfExists(resolve(repoRoot, ".cache", "worktrees"));
|
|
@@ -557,7 +597,7 @@ export function ensureSandboxWorkspaceWrite(toml, options = {}) {
|
|
|
557
597
|
excludeSlashTmp = false,
|
|
558
598
|
} = options;
|
|
559
599
|
|
|
560
|
-
const desiredRoots = normalizeWritableRoots(writableRoots, { repoRoot, additionalRoots });
|
|
600
|
+
const desiredRoots = normalizeWritableRoots(writableRoots, { repoRoot, additionalRoots, validateExistence: true });
|
|
561
601
|
if (!hasSandboxWorkspaceWrite(toml)) {
|
|
562
602
|
if (desiredRoots.length === 0) {
|
|
563
603
|
return { toml, changed: false, added: false, rootsAdded: [] };
|
|
@@ -609,13 +649,18 @@ export function ensureSandboxWorkspaceWrite(toml, options = {}) {
|
|
|
609
649
|
const match = section.match(rootsRegex);
|
|
610
650
|
if (match) {
|
|
611
651
|
const existingRoots = parseTomlArrayLiteral(match[1]);
|
|
612
|
-
|
|
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 });
|
|
613
655
|
for (const root of desiredRoots) {
|
|
614
656
|
if (!merged.includes(root)) {
|
|
615
657
|
merged.push(root);
|
|
616
658
|
rootsAdded.push(root);
|
|
617
659
|
}
|
|
618
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;
|
|
619
664
|
const formatted = formatTomlArray(merged);
|
|
620
665
|
if (formatted !== match[1]) {
|
|
621
666
|
section = section.replace(rootsRegex, `writable_roots = ${formatted}`);
|
|
@@ -642,6 +687,39 @@ export function ensureSandboxWorkspaceWrite(toml, options = {}) {
|
|
|
642
687
|
};
|
|
643
688
|
}
|
|
644
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
|
+
|
|
645
723
|
/**
|
|
646
724
|
* Build the [shell_environment_policy] section.
|
|
647
725
|
* Default: inherit = "all" so .NET, Go, Node etc. env vars are visible.
|
|
@@ -810,9 +888,12 @@ export function hasAgentSdkConfig(toml) {
|
|
|
810
888
|
|
|
811
889
|
/**
|
|
812
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}
|
|
813
894
|
*/
|
|
814
|
-
export function buildAgentSdkBlock() {
|
|
815
|
-
return
|
|
895
|
+
export function buildAgentSdkBlock({ primary = "codex" } = {}) {
|
|
896
|
+
return buildDefaultAgentSdkBlock(primary);
|
|
816
897
|
}
|
|
817
898
|
|
|
818
899
|
/**
|
|
@@ -1054,12 +1135,14 @@ export function ensureRetrySettings(toml, providerName) {
|
|
|
1054
1135
|
* @param {boolean} [opts.skipVk]
|
|
1055
1136
|
* @param {boolean} [opts.dryRun] If true, returns result without writing
|
|
1056
1137
|
* @param {object} [opts.env] Environment overrides (defaults to process.env)
|
|
1138
|
+
* @param {string} [opts.primarySdk] Primary agent SDK: "codex", "copilot", or "claude"
|
|
1057
1139
|
*/
|
|
1058
1140
|
export function ensureCodexConfig({
|
|
1059
1141
|
vkBaseUrl = "http://127.0.0.1:54089",
|
|
1060
1142
|
skipVk = false,
|
|
1061
1143
|
dryRun = false,
|
|
1062
1144
|
env = process.env,
|
|
1145
|
+
primarySdk,
|
|
1063
1146
|
} = {}) {
|
|
1064
1147
|
const result = {
|
|
1065
1148
|
path: CONFIG_PATH,
|
|
@@ -1075,6 +1158,7 @@ export function ensureCodexConfig({
|
|
|
1075
1158
|
sandboxWorkspaceAdded: false,
|
|
1076
1159
|
sandboxWorkspaceUpdated: false,
|
|
1077
1160
|
sandboxWorkspaceRootsAdded: [],
|
|
1161
|
+
sandboxStaleRootsRemoved: [],
|
|
1078
1162
|
shellEnvAdded: false,
|
|
1079
1163
|
commonMcpAdded: false,
|
|
1080
1164
|
profileProvidersAdded: [],
|
|
@@ -1155,8 +1239,21 @@ export function ensureCodexConfig({
|
|
|
1155
1239
|
|
|
1156
1240
|
// ── 1b. Ensure agent SDK selection block ──────────────────
|
|
1157
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
|
+
|
|
1158
1255
|
if (!hasAgentSdkConfig(toml)) {
|
|
1159
|
-
toml += buildAgentSdkBlock();
|
|
1256
|
+
toml += buildAgentSdkBlock({ primary: resolvedPrimary });
|
|
1160
1257
|
result.agentSdkAdded = true;
|
|
1161
1258
|
}
|
|
1162
1259
|
|
|
@@ -1241,6 +1338,14 @@ export function ensureCodexConfig({
|
|
|
1241
1338
|
result.sandboxWorkspaceUpdated = !ensured.added;
|
|
1242
1339
|
result.sandboxWorkspaceRootsAdded = ensured.rootsAdded || [];
|
|
1243
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
|
+
}
|
|
1244
1349
|
}
|
|
1245
1350
|
|
|
1246
1351
|
// ── 1g. Ensure shell environment policy ───────────────────
|
|
@@ -1408,6 +1513,15 @@ export function printConfigSummary(result, log = console.log) {
|
|
|
1408
1513
|
);
|
|
1409
1514
|
}
|
|
1410
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
|
+
|
|
1411
1525
|
if (result.shellEnvAdded) {
|
|
1412
1526
|
log(" ✅ Added shell environment policy (inherit=all)");
|
|
1413
1527
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.5",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
package/setup.mjs
CHANGED
|
@@ -1500,6 +1500,15 @@ const EXECUTOR_PRESETS = {
|
|
|
1500
1500
|
role: "primary",
|
|
1501
1501
|
},
|
|
1502
1502
|
],
|
|
1503
|
+
"claude-only": [
|
|
1504
|
+
{
|
|
1505
|
+
name: "claude-default",
|
|
1506
|
+
executor: "CLAUDE",
|
|
1507
|
+
variant: "CLAUDE_OPUS_4",
|
|
1508
|
+
weight: 100,
|
|
1509
|
+
role: "primary",
|
|
1510
|
+
},
|
|
1511
|
+
],
|
|
1503
1512
|
triple: [
|
|
1504
1513
|
{
|
|
1505
1514
|
name: "copilot-claude",
|
|
@@ -1817,6 +1826,16 @@ function normalizeSetupConfiguration({
|
|
|
1817
1826
|
enabled: executor.enabled !== false,
|
|
1818
1827
|
}));
|
|
1819
1828
|
|
|
1829
|
+
// Derive PRIMARY_AGENT from executor config for SDK resolution
|
|
1830
|
+
{
|
|
1831
|
+
const primaryExec = configJson.executors.find((e) => e.role === "primary");
|
|
1832
|
+
if (primaryExec) {
|
|
1833
|
+
const sdkMap = { CODEX: "codex-sdk", COPILOT: "copilot-sdk", CLAUDE: "claude-sdk" };
|
|
1834
|
+
env.PRIMARY_AGENT = env.PRIMARY_AGENT ||
|
|
1835
|
+
sdkMap[String(primaryExec.executor).toUpperCase()] || "codex-sdk";
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1820
1839
|
configJson.failover = {
|
|
1821
1840
|
strategy: normalizeEnum(
|
|
1822
1841
|
configJson.failover?.strategy || env.FAILOVER_STRATEGY || "next-in-line",
|
|
@@ -2460,6 +2479,7 @@ async function main() {
|
|
|
2460
2479
|
"Codex only",
|
|
2461
2480
|
"Copilot + Codex (50/50 split)",
|
|
2462
2481
|
"Copilot only (Claude Opus 4.6)",
|
|
2482
|
+
"Claude only (direct API)",
|
|
2463
2483
|
"Triple (Copilot Claude 40%, Codex 35%, Copilot GPT 25%)",
|
|
2464
2484
|
"Custom — I'll define my own executors",
|
|
2465
2485
|
]
|
|
@@ -2467,6 +2487,7 @@ async function main() {
|
|
|
2467
2487
|
"Codex only",
|
|
2468
2488
|
"Copilot + Codex (50/50 split)",
|
|
2469
2489
|
"Copilot only (Claude Opus 4.6)",
|
|
2490
|
+
"Claude only (direct API)",
|
|
2470
2491
|
"Triple (Copilot Claude 40%, Codex 35%, Copilot GPT 25%)",
|
|
2471
2492
|
];
|
|
2472
2493
|
|
|
@@ -2477,8 +2498,8 @@ async function main() {
|
|
|
2477
2498
|
);
|
|
2478
2499
|
|
|
2479
2500
|
const presetNames = isAdvancedSetup
|
|
2480
|
-
? ["codex-only", "copilot-codex", "copilot-only", "triple", "custom"]
|
|
2481
|
-
: ["codex-only", "copilot-codex", "copilot-only", "triple"];
|
|
2501
|
+
? ["codex-only", "copilot-codex", "copilot-only", "claude-only", "triple", "custom"]
|
|
2502
|
+
: ["codex-only", "copilot-codex", "copilot-only", "claude-only", "triple"];
|
|
2482
2503
|
const presetKey = presetNames[presetIdx] || "codex-only";
|
|
2483
2504
|
|
|
2484
2505
|
if (presetKey === "custom") {
|
|
@@ -2629,92 +2650,140 @@ async function main() {
|
|
|
2629
2650
|
if (!wantCodexFallback) env.CODEX_SDK_DISABLED = "true";
|
|
2630
2651
|
}
|
|
2631
2652
|
|
|
2632
|
-
// ── Step 5: AI Provider
|
|
2633
|
-
headingStep(5, "AI
|
|
2653
|
+
// ── Step 5: AI Provider Keys ─────────────────────────────
|
|
2654
|
+
headingStep(5, "AI Provider Keys", markSetupProgress);
|
|
2634
2655
|
console.log(
|
|
2635
|
-
"
|
|
2656
|
+
" Configure API keys for the agent SDKs in your executor preset.\n",
|
|
2636
2657
|
);
|
|
2637
2658
|
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2659
|
+
// Determine which SDK families are needed
|
|
2660
|
+
const needsCodexSdk = usedSdks.has("CODEX") || env.CODEX_SDK_DISABLED !== "true";
|
|
2661
|
+
const needsCopilotSdk = usedSdks.has("COPILOT") || env.COPILOT_SDK_DISABLED !== "true";
|
|
2662
|
+
const needsClaudeSdk = usedSdks.has("CLAUDE") || env.CLAUDE_SDK_DISABLED !== "true";
|
|
2663
|
+
|
|
2664
|
+
// ── 5a. Copilot / GitHub Token ──────────────────────
|
|
2665
|
+
if (needsCopilotSdk) {
|
|
2666
|
+
console.log(chalk.bold(" Copilot SDK") + chalk.dim(" (uses GitHub token)\n"));
|
|
2667
|
+
const existingGhToken = process.env.COPILOT_CLI_TOKEN || process.env.GITHUB_TOKEN || "";
|
|
2668
|
+
if (existingGhToken) {
|
|
2669
|
+
info(`GitHub token detected (${existingGhToken.slice(0, 8)}…). Copilot SDK will use it.`);
|
|
2670
|
+
} else {
|
|
2671
|
+
const ghToken = await prompt.ask(
|
|
2672
|
+
"GitHub Token (GITHUB_TOKEN or COPILOT_CLI_TOKEN, blank to skip)",
|
|
2673
|
+
"",
|
|
2674
|
+
);
|
|
2675
|
+
if (ghToken) env.GITHUB_TOKEN = ghToken;
|
|
2676
|
+
}
|
|
2677
|
+
// Copilot permission defaults
|
|
2678
|
+
env.COPILOT_NO_ALLOW_ALL = env.COPILOT_NO_ALLOW_ALL || "false";
|
|
2679
|
+
env.COPILOT_ENABLE_ASK_USER = env.COPILOT_ENABLE_ASK_USER || "false";
|
|
2680
|
+
env.COPILOT_AGENT_MAX_REQUESTS = env.COPILOT_AGENT_MAX_REQUESTS || "500";
|
|
2655
2681
|
}
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
);
|
|
2673
|
-
env.CODEX_MODEL = await prompt.ask("Model name", "codex");
|
|
2674
|
-
} else if (providerIdx === 3) {
|
|
2675
|
-
env.OPENAI_BASE_URL = await prompt.ask("API Base URL", "");
|
|
2676
|
-
env.CODEX_MODEL = await prompt.ask("Model name", "");
|
|
2677
|
-
} else if (providerIdx === 4) {
|
|
2678
|
-
env.CODEX_SDK_DISABLED = "true";
|
|
2682
|
+
|
|
2683
|
+
// ── 5b. Claude / Anthropic Key ──────────────────────
|
|
2684
|
+
if (needsClaudeSdk) {
|
|
2685
|
+
console.log(chalk.bold("\n Claude SDK") + chalk.dim(" (uses Anthropic API key)\n"));
|
|
2686
|
+
const existingAnthropicKey = process.env.ANTHROPIC_API_KEY || "";
|
|
2687
|
+
if (existingAnthropicKey) {
|
|
2688
|
+
info(`Anthropic API key detected (${existingAnthropicKey.slice(0, 8)}…). Claude SDK will use it.`);
|
|
2689
|
+
} else {
|
|
2690
|
+
const anthropicKey = await prompt.ask(
|
|
2691
|
+
"Anthropic API Key (ANTHROPIC_API_KEY, blank to skip)",
|
|
2692
|
+
"",
|
|
2693
|
+
);
|
|
2694
|
+
if (anthropicKey) env.ANTHROPIC_API_KEY = anthropicKey;
|
|
2695
|
+
}
|
|
2696
|
+
// Claude always runs in bypass mode under Bosun
|
|
2697
|
+
env.CLAUDE_PERMISSION_MODE = env.CLAUDE_PERMISSION_MODE || "bypassPermissions";
|
|
2679
2698
|
}
|
|
2680
2699
|
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2700
|
+
// ── 5c. Codex / OpenAI Key ──────────────────────────
|
|
2701
|
+
if (needsCodexSdk) {
|
|
2702
|
+
console.log(chalk.bold("\n Codex SDK") + chalk.dim(" (uses OpenAI API key)\n"));
|
|
2703
|
+
|
|
2704
|
+
const providerIdx = await prompt.choose(
|
|
2705
|
+
"Select AI provider for Codex:",
|
|
2706
|
+
[
|
|
2707
|
+
"OpenAI (default)",
|
|
2708
|
+
"Azure OpenAI",
|
|
2709
|
+
"Local model (Ollama, vLLM, etc.)",
|
|
2710
|
+
"Other OpenAI-compatible endpoint",
|
|
2711
|
+
"None — disable Codex SDK",
|
|
2712
|
+
],
|
|
2713
|
+
0,
|
|
2685
2714
|
);
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2715
|
+
|
|
2716
|
+
if (providerIdx < 4) {
|
|
2717
|
+
env.OPENAI_API_KEY = await prompt.ask(
|
|
2718
|
+
"API Key",
|
|
2719
|
+
process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_API_KEY || "",
|
|
2691
2720
|
);
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
env.
|
|
2696
|
-
"
|
|
2697
|
-
process.env.
|
|
2698
|
-
process.env.CODEX_MODEL ||
|
|
2699
|
-
"gpt-5.3-codex",
|
|
2721
|
+
}
|
|
2722
|
+
if (providerIdx === 1) {
|
|
2723
|
+
env.AZURE_OPENAI_API_KEY = env.OPENAI_API_KEY;
|
|
2724
|
+
env.OPENAI_BASE_URL = await prompt.ask(
|
|
2725
|
+
"Azure endpoint URL",
|
|
2726
|
+
process.env.OPENAI_BASE_URL || "",
|
|
2700
2727
|
);
|
|
2701
|
-
env.
|
|
2702
|
-
"
|
|
2703
|
-
process.env.
|
|
2728
|
+
env.CODEX_MODEL = await prompt.ask(
|
|
2729
|
+
"Deployment/model name",
|
|
2730
|
+
process.env.CODEX_MODEL || "",
|
|
2704
2731
|
);
|
|
2732
|
+
} else if (providerIdx === 2) {
|
|
2733
|
+
env.OPENAI_API_KEY = env.OPENAI_API_KEY || "ollama";
|
|
2734
|
+
env.OPENAI_BASE_URL = await prompt.ask(
|
|
2735
|
+
"Local API URL",
|
|
2736
|
+
"http://localhost:11434/v1",
|
|
2737
|
+
);
|
|
2738
|
+
env.CODEX_MODEL = await prompt.ask("Model name", "codex");
|
|
2739
|
+
} else if (providerIdx === 3) {
|
|
2740
|
+
env.OPENAI_BASE_URL = await prompt.ask("API Base URL", "");
|
|
2741
|
+
env.CODEX_MODEL = await prompt.ask("Model name", "");
|
|
2742
|
+
} else if (providerIdx === 4) {
|
|
2743
|
+
env.CODEX_SDK_DISABLED = "true";
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
if (providerIdx < 4) {
|
|
2747
|
+
const configureProfiles = await prompt.confirm(
|
|
2748
|
+
"Configure model profiles (xl/m) for one-click switching?",
|
|
2749
|
+
true,
|
|
2750
|
+
);
|
|
2751
|
+
if (configureProfiles) {
|
|
2752
|
+
const activeProfileIdx = await prompt.choose(
|
|
2753
|
+
"Default active profile:",
|
|
2754
|
+
["xl (high quality)", "m (faster/cheaper)"],
|
|
2755
|
+
0,
|
|
2756
|
+
);
|
|
2757
|
+
env.CODEX_MODEL_PROFILE = activeProfileIdx === 0 ? "xl" : "m";
|
|
2758
|
+
env.CODEX_MODEL_PROFILE_SUBAGENT = activeProfileIdx === 0 ? "m" : "xl";
|
|
2759
|
+
|
|
2760
|
+
env.CODEX_MODEL_PROFILE_XL_MODEL = await prompt.ask(
|
|
2761
|
+
"XL profile model",
|
|
2762
|
+
process.env.CODEX_MODEL_PROFILE_XL_MODEL ||
|
|
2763
|
+
process.env.CODEX_MODEL ||
|
|
2764
|
+
"gpt-5.3-codex",
|
|
2765
|
+
);
|
|
2766
|
+
env.CODEX_MODEL_PROFILE_M_MODEL = await prompt.ask(
|
|
2767
|
+
"M profile model",
|
|
2768
|
+
process.env.CODEX_MODEL_PROFILE_M_MODEL || "gpt-5.1-codex-mini",
|
|
2769
|
+
);
|
|
2705
2770
|
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2771
|
+
const providerName =
|
|
2772
|
+
providerIdx === 1 ? "azure" : providerIdx === 3 ? "compatible" : "openai";
|
|
2773
|
+
env.CODEX_MODEL_PROFILE_XL_PROVIDER =
|
|
2774
|
+
process.env.CODEX_MODEL_PROFILE_XL_PROVIDER || providerName;
|
|
2775
|
+
env.CODEX_MODEL_PROFILE_M_PROVIDER =
|
|
2776
|
+
process.env.CODEX_MODEL_PROFILE_M_PROVIDER || providerName;
|
|
2712
2777
|
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2778
|
+
if (!env.CODEX_SUBAGENT_MODEL) {
|
|
2779
|
+
env.CODEX_SUBAGENT_MODEL =
|
|
2780
|
+
env.CODEX_MODEL_PROFILE_M_MODEL || "gpt-5.1-codex-mini";
|
|
2781
|
+
}
|
|
2716
2782
|
}
|
|
2717
2783
|
}
|
|
2784
|
+
} else {
|
|
2785
|
+
// Codex not needed — skip OpenAI key prompts entirely
|
|
2786
|
+
info("Codex SDK not in executor preset — skipping OpenAI configuration.");
|
|
2718
2787
|
}
|
|
2719
2788
|
|
|
2720
2789
|
// ── Step 6: Telegram ──────────────────────────────────
|
|
@@ -4568,6 +4637,13 @@ async function runNonInteractive({
|
|
|
4568
4637
|
env.COPILOT_AGENT_MAX_REQUESTS =
|
|
4569
4638
|
process.env.COPILOT_AGENT_MAX_REQUESTS || "500";
|
|
4570
4639
|
|
|
4640
|
+
// Claude SDK: permission mode and API key passthrough
|
|
4641
|
+
env.CLAUDE_PERMISSION_MODE =
|
|
4642
|
+
process.env.CLAUDE_PERMISSION_MODE || "bypassPermissions";
|
|
4643
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
4644
|
+
env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4571
4647
|
// Parse EXECUTORS env if set, else use default preset
|
|
4572
4648
|
if (process.env.EXECUTORS) {
|
|
4573
4649
|
const entries = process.env.EXECUTORS.split(",").map((e) => e.trim());
|
|
@@ -4589,7 +4665,33 @@ async function runNonInteractive({
|
|
|
4589
4665
|
}
|
|
4590
4666
|
}
|
|
4591
4667
|
if (!configJson.executors.length) {
|
|
4592
|
-
|
|
4668
|
+
// Smart default: pick preset based on available API keys
|
|
4669
|
+
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
|
4670
|
+
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
|
4671
|
+
const hasGitHub = !!process.env.GITHUB_TOKEN || !!process.env.COPILOT_CLI_TOKEN;
|
|
4672
|
+
const presetEnv = (process.env.EXECUTOR_PRESET || "").toLowerCase();
|
|
4673
|
+
if (presetEnv && EXECUTOR_PRESETS[presetEnv]) {
|
|
4674
|
+
configJson.executors = EXECUTOR_PRESETS[presetEnv];
|
|
4675
|
+
} else if (hasGitHub) {
|
|
4676
|
+
configJson.executors = hasOpenAI
|
|
4677
|
+
? EXECUTOR_PRESETS["copilot-codex"]
|
|
4678
|
+
: EXECUTOR_PRESETS["copilot-only"];
|
|
4679
|
+
} else if (hasAnthropic) {
|
|
4680
|
+
configJson.executors = EXECUTOR_PRESETS["claude-only"];
|
|
4681
|
+
} else {
|
|
4682
|
+
configJson.executors = EXECUTOR_PRESETS["codex-only"];
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
|
|
4686
|
+
// Derive PRIMARY_AGENT from executor preset's primary role
|
|
4687
|
+
{
|
|
4688
|
+
const primaryExec = (configJson.executors || []).find(
|
|
4689
|
+
(e) => e.role === "primary",
|
|
4690
|
+
);
|
|
4691
|
+
if (primaryExec) {
|
|
4692
|
+
const sdkMap = { CODEX: "codex-sdk", COPILOT: "copilot-sdk", CLAUDE: "claude-sdk" };
|
|
4693
|
+
env.PRIMARY_AGENT = sdkMap[String(primaryExec.executor).toUpperCase()] || "codex-sdk";
|
|
4694
|
+
}
|
|
4593
4695
|
}
|
|
4594
4696
|
|
|
4595
4697
|
configJson.projectName = env.PROJECT_NAME;
|
|
@@ -4803,6 +4905,42 @@ async function writeConfigFiles({ env, configJson, repoRoot, configDir }) {
|
|
|
4803
4905
|
warn(`Could not update Copilot MCP config: ${copilotMcpResult.error}`);
|
|
4804
4906
|
}
|
|
4805
4907
|
|
|
4908
|
+
// ── Repo-level AI configs for all workspace repos ──────
|
|
4909
|
+
heading("Repo-Level AI Configs");
|
|
4910
|
+
try {
|
|
4911
|
+
const { ensureRepoConfigs, printRepoConfigSummary } = await import("./repo-config.mjs");
|
|
4912
|
+
// Apply to the primary repo
|
|
4913
|
+
const repoResult = ensureRepoConfigs(repoRoot);
|
|
4914
|
+
printRepoConfigSummary(repoResult, (msg) => console.log(msg));
|
|
4915
|
+
|
|
4916
|
+
// Also apply to all workspace repos under $BOSUN_DIR/workspaces/
|
|
4917
|
+
const bosunDir = env.BOSUN_DIR || configDir || resolve(homedir(), "bosun");
|
|
4918
|
+
const bosunConfigPath = resolve(bosunDir, "bosun.config.json");
|
|
4919
|
+
if (existsSync(bosunConfigPath)) {
|
|
4920
|
+
try {
|
|
4921
|
+
const bosunCfg = JSON.parse(readFileSync(bosunConfigPath, "utf8"));
|
|
4922
|
+
const wsDir = resolve(bosunDir, "workspaces");
|
|
4923
|
+
for (const ws of bosunCfg.workspaces || []) {
|
|
4924
|
+
for (const repo of ws.repos || []) {
|
|
4925
|
+
const wsRepoPath = resolve(wsDir, ws.id || ws.name || "", repo.name);
|
|
4926
|
+
if (wsRepoPath !== repoRoot && existsSync(wsRepoPath)) {
|
|
4927
|
+
const wsResult = ensureRepoConfigs(wsRepoPath);
|
|
4928
|
+
const anyChange = Object.values(wsResult).some((r) => r.created || r.updated);
|
|
4929
|
+
if (anyChange) {
|
|
4930
|
+
info(`Workspace repo: ${repo.name}`);
|
|
4931
|
+
printRepoConfigSummary(wsResult, (msg) => console.log(msg));
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
} catch (wsErr) {
|
|
4937
|
+
warn(`Could not update workspace repo configs: ${wsErr.message}`);
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
} catch (rcErr) {
|
|
4941
|
+
warn(`Could not apply repo-level configs: ${rcErr.message}`);
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4806
4944
|
// ── Codex CLI config.toml ─────────────────────────────
|
|
4807
4945
|
heading("Codex CLI Config");
|
|
4808
4946
|
|
|
@@ -4814,10 +4952,24 @@ async function writeConfigFiles({ env, configJson, repoRoot, configDir }) {
|
|
|
4814
4952
|
const kanbanIsVk =
|
|
4815
4953
|
(env.KANBAN_BACKEND || "internal").toLowerCase() === "vk" ||
|
|
4816
4954
|
["vk", "hybrid"].includes((env.EXECUTOR_MODE || "internal").toLowerCase());
|
|
4955
|
+
// Derive primary SDK from executor configuration
|
|
4956
|
+
const primaryExecutor = (configJson.executors || []).find(
|
|
4957
|
+
(e) => e.role === "primary",
|
|
4958
|
+
);
|
|
4959
|
+
const executorToPrimarySdk = {
|
|
4960
|
+
CODEX: "codex",
|
|
4961
|
+
COPILOT: "copilot",
|
|
4962
|
+
CLAUDE: "claude",
|
|
4963
|
+
};
|
|
4964
|
+
const primarySdk = primaryExecutor
|
|
4965
|
+
? executorToPrimarySdk[String(primaryExecutor.executor).toUpperCase()] || "codex"
|
|
4966
|
+
: "codex";
|
|
4967
|
+
|
|
4817
4968
|
const tomlResult = ensureCodexConfig({
|
|
4818
4969
|
vkBaseUrl,
|
|
4819
4970
|
skipVk: !kanbanIsVk,
|
|
4820
4971
|
dryRun: false,
|
|
4972
|
+
primarySdk,
|
|
4821
4973
|
env: {
|
|
4822
4974
|
...process.env,
|
|
4823
4975
|
...env,
|
|
@@ -278,9 +278,9 @@ function SwipeableSessionItem({
|
|
|
278
278
|
const x = e.clientX || e.touches?.[0]?.clientX || 0;
|
|
279
279
|
currentX.current = x;
|
|
280
280
|
const dx = x - startX.current;
|
|
281
|
-
// Only allow left swipe (negative), cap at -
|
|
281
|
+
// Only allow left swipe (negative), cap at -140
|
|
282
282
|
if (dx < 0) {
|
|
283
|
-
setOffset(Math.max(dx, -
|
|
283
|
+
setOffset(Math.max(dx, -140));
|
|
284
284
|
} else {
|
|
285
285
|
setOffset(0);
|
|
286
286
|
}
|
|
@@ -290,7 +290,7 @@ function SwipeableSessionItem({
|
|
|
290
290
|
swiping.current = false;
|
|
291
291
|
if (offset < -50) {
|
|
292
292
|
// Snap open to reveal actions
|
|
293
|
-
setOffset(-
|
|
293
|
+
setOffset(-140);
|
|
294
294
|
if (onToggleActions) onToggleActions(s.id);
|
|
295
295
|
} else {
|
|
296
296
|
setOffset(0);
|
|
@@ -440,7 +440,7 @@ function SwipeableSessionItem({
|
|
|
440
440
|
e.stopPropagation();
|
|
441
441
|
if (onToggleActions) {
|
|
442
442
|
onToggleActions(s.id);
|
|
443
|
-
setOffset(-
|
|
443
|
+
setOffset(-140);
|
|
444
444
|
}
|
|
445
445
|
}}
|
|
446
446
|
>
|
package/ui/components/shared.js
CHANGED
|
@@ -206,6 +206,8 @@ export function Modal({ title, open = true, onClose, children, contentClassName
|
|
|
206
206
|
}, []);
|
|
207
207
|
|
|
208
208
|
const handleTouchStart = useCallback((e) => {
|
|
209
|
+
// Don't intercept touches on the close button
|
|
210
|
+
if (e.target.closest?.(".modal-close-btn")) return;
|
|
209
211
|
const el = contentRef.current;
|
|
210
212
|
if (!el) return;
|
|
211
213
|
const rect = el.getBoundingClientRect();
|
|
@@ -244,6 +246,8 @@ export function Modal({ title, open = true, onClose, children, contentClassName
|
|
|
244
246
|
|
|
245
247
|
const handlePointerDown = useCallback((e) => {
|
|
246
248
|
if (e.pointerType === "touch") return;
|
|
249
|
+
// Don't intercept clicks on the close button
|
|
250
|
+
if (e.target.closest?.(".modal-close-btn")) return;
|
|
247
251
|
const el = contentRef.current;
|
|
248
252
|
if (!el) return;
|
|
249
253
|
const rect = el.getBoundingClientRect();
|
|
@@ -469,19 +469,30 @@ export function WorkspaceSwitcher() {
|
|
|
469
469
|
|
|
470
470
|
const activeWs = workspaces.value.find((ws) => ws.id === activeWorkspaceId.value);
|
|
471
471
|
const wsList = workspaces.value;
|
|
472
|
+
const isLoading = workspacesLoading.value;
|
|
472
473
|
|
|
473
|
-
|
|
474
|
+
// Always render — even with no workspaces, show a manage button so users can add one
|
|
474
475
|
|
|
475
476
|
return html`
|
|
476
477
|
<div class="ws-switcher">
|
|
477
478
|
<button
|
|
478
|
-
class="ws-switcher-btn"
|
|
479
|
-
onClick=${(e) => {
|
|
480
|
-
|
|
479
|
+
class="ws-switcher-btn ${!wsList.length ? 'ws-switcher-btn-empty' : ''}"
|
|
480
|
+
onClick=${(e) => {
|
|
481
|
+
e.stopPropagation();
|
|
482
|
+
haptic("light");
|
|
483
|
+
if (!wsList.length) {
|
|
484
|
+
setManagerOpen(true);
|
|
485
|
+
} else {
|
|
486
|
+
setOpen(!open);
|
|
487
|
+
}
|
|
488
|
+
}}
|
|
489
|
+
title=${wsList.length ? "Switch workspace" : "Set up a workspace"}
|
|
481
490
|
>
|
|
482
491
|
<span class="ws-switcher-icon">⬡</span>
|
|
483
|
-
<span class="ws-switcher-name"
|
|
484
|
-
|
|
492
|
+
<span class="ws-switcher-name">
|
|
493
|
+
${isLoading ? "Loading…" : (activeWs?.name || (wsList.length ? "Select Workspace" : "Set up workspace"))}
|
|
494
|
+
</span>
|
|
495
|
+
${wsList.length ? html`<span class="ws-switcher-chevron ${open ? "open" : ""}">${open ? "▴" : "▾"}</span>` : null}
|
|
485
496
|
</button>
|
|
486
497
|
|
|
487
498
|
${open && html`
|
package/ui/demo.html
CHANGED
|
@@ -237,6 +237,24 @@
|
|
|
237
237
|
version: '0.26.2',
|
|
238
238
|
tasks: { completed: 13, failed: 1, retried: 2, active: 2, queued: 3 },
|
|
239
239
|
prs: { created: 15, merged: 11, pending: 2, ciFailures: 1 },
|
|
240
|
+
// counts consumed by dashboard metrics
|
|
241
|
+
counts: {
|
|
242
|
+
running: 2,
|
|
243
|
+
inprogress: 2,
|
|
244
|
+
inreview: 1,
|
|
245
|
+
review: 1,
|
|
246
|
+
done: 2,
|
|
247
|
+
todo: 2,
|
|
248
|
+
draft: 1,
|
|
249
|
+
error: 0,
|
|
250
|
+
},
|
|
251
|
+
backlog_remaining: 2,
|
|
252
|
+
// consumed by dashboard quality section
|
|
253
|
+
success_metrics: {
|
|
254
|
+
first_shot_rate: 72,
|
|
255
|
+
needed_fix: 3,
|
|
256
|
+
failed: 1,
|
|
257
|
+
},
|
|
240
258
|
};
|
|
241
259
|
|
|
242
260
|
const SEED_EXECUTORS = [
|
|
@@ -668,7 +686,7 @@
|
|
|
668
686
|
if (route === '/api/status')
|
|
669
687
|
return { data: STATE.status };
|
|
670
688
|
if (route === '/api/executor')
|
|
671
|
-
return { data: { ...STATE.status, maxParallel: STATE.maxParallel, paused: STATE.paused, executors: STATE.executors } };
|
|
689
|
+
return { data: { ...STATE.status, maxParallel: STATE.maxParallel, paused: STATE.paused, executors: STATE.executors, activeSlots: STATE.executors.filter(e => e.status === 'active').length, sdk: 'auto' } };
|
|
672
690
|
if (route === '/api/telemetry/summary')
|
|
673
691
|
return { data: { status: 'ok', updatedAt: Date.now(), totals: { tasks: STATE.tasks.length, executors: STATE.executors.length } } };
|
|
674
692
|
if (route === '/api/telemetry/errors')
|
package/ui/styles/sessions.css
CHANGED
|
@@ -953,7 +953,8 @@
|
|
|
953
953
|
position: absolute;
|
|
954
954
|
left: 0;
|
|
955
955
|
right: 0;
|
|
956
|
-
|
|
956
|
+
/* position above the input bar (~60px) + gap */
|
|
957
|
+
bottom: 72px;
|
|
957
958
|
display: flex;
|
|
958
959
|
justify-content: center;
|
|
959
960
|
pointer-events: none;
|
|
@@ -1585,8 +1586,8 @@ ul.md-list li::before {
|
|
|
1585
1586
|
justify-content: center;
|
|
1586
1587
|
border: none;
|
|
1587
1588
|
cursor: pointer;
|
|
1588
|
-
padding: 0
|
|
1589
|
-
min-width:
|
|
1589
|
+
padding: 0 10px;
|
|
1590
|
+
min-width: 68px;
|
|
1590
1591
|
font-size: 11px;
|
|
1591
1592
|
font-family: inherit;
|
|
1592
1593
|
gap: 2px;
|
|
@@ -1627,6 +1628,7 @@ ul.md-list li::before {
|
|
|
1627
1628
|
font-weight: 600;
|
|
1628
1629
|
text-transform: uppercase;
|
|
1629
1630
|
letter-spacing: 0.3px;
|
|
1631
|
+
white-space: nowrap;
|
|
1630
1632
|
}
|
|
1631
1633
|
|
|
1632
1634
|
@keyframes session-pulse {
|
|
@@ -27,6 +27,17 @@
|
|
|
27
27
|
border-color: var(--color-primary, #10b981);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
.ws-switcher-btn.ws-switcher-btn-empty {
|
|
31
|
+
border-style: dashed;
|
|
32
|
+
opacity: 0.8;
|
|
33
|
+
font-style: italic;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.ws-switcher-btn.ws-switcher-btn-empty:hover {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
font-style: normal;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
.ws-switcher-icon {
|
|
31
42
|
font-size: 14px;
|
|
32
43
|
opacity: 0.7;
|
|
@@ -660,6 +671,16 @@
|
|
|
660
671
|
to { transform: rotate(360deg); }
|
|
661
672
|
}
|
|
662
673
|
|
|
674
|
+
/* ── Modal toolbar (inside shared Modal body) ──────────────── */
|
|
675
|
+
.ws-manager-modal-toolbar {
|
|
676
|
+
display: flex;
|
|
677
|
+
align-items: center;
|
|
678
|
+
gap: 8px;
|
|
679
|
+
margin-bottom: 12px;
|
|
680
|
+
padding-bottom: 12px;
|
|
681
|
+
border-bottom: 1px solid var(--border);
|
|
682
|
+
}
|
|
683
|
+
|
|
663
684
|
/* ── Responsive ──────────────────────────────────────────── */
|
|
664
685
|
@media (max-width: 540px) {
|
|
665
686
|
.ws-manager-panel {
|
package/ui/tabs/dashboard.js
CHANGED
|
@@ -521,39 +521,33 @@ export function DashboardTab() {
|
|
|
521
521
|
`,
|
|
522
522
|
)}
|
|
523
523
|
</div>
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
<span
|
|
539
|
-
class="dashboard-work-dot"
|
|
540
|
-
style="background: ${item.color}"
|
|
541
|
-
></span>
|
|
542
|
-
<span class="dashboard-work-label">${item.label}</span>
|
|
524
|
+
${segments.length > 0 && html`
|
|
525
|
+
<div class="dashboard-work-layout" style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border)">
|
|
526
|
+
<div class="dashboard-work-list">
|
|
527
|
+
${workItems.map(
|
|
528
|
+
(item) => html`
|
|
529
|
+
<div class="dashboard-work-item">
|
|
530
|
+
<div class="dashboard-work-left">
|
|
531
|
+
<span
|
|
532
|
+
class="dashboard-work-dot"
|
|
533
|
+
style="background: ${item.color}"
|
|
534
|
+
></span>
|
|
535
|
+
<span class="dashboard-work-label">${item.label}</span>
|
|
536
|
+
</div>
|
|
537
|
+
<span class="dashboard-work-value">${item.value}</span>
|
|
543
538
|
</div>
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
539
|
+
`,
|
|
540
|
+
)}
|
|
541
|
+
</div>
|
|
542
|
+
<div class="dashboard-work-chart">
|
|
543
|
+
<${DonutChart} segments=${segments} size=${90} strokeWidth=${9} />
|
|
544
|
+
<div class="dashboard-work-meta">
|
|
545
|
+
${progressPct}% engaged
|
|
546
|
+
</div>
|
|
547
|
+
<${ProgressBar} percent=${progressPct} />
|
|
553
548
|
</div>
|
|
554
|
-
<${ProgressBar} percent=${progressPct} />
|
|
555
549
|
</div>
|
|
556
|
-
|
|
550
|
+
`}
|
|
557
551
|
<//>
|
|
558
552
|
|
|
559
553
|
<${Card}
|
|
@@ -683,8 +677,8 @@ export function DashboardTab() {
|
|
|
683
677
|
${truncate(task.title || "(untitled)", 50)}
|
|
684
678
|
</div>
|
|
685
679
|
<div class="meta-text">
|
|
686
|
-
${task.id}${task.updated_at
|
|
687
|
-
? ` · ${formatRelative(task.updated_at)}`
|
|
680
|
+
${task.id}${(task.updated_at || task.updated)
|
|
681
|
+
? ` · ${formatRelative(task.updated_at || task.updated)}`
|
|
688
682
|
: ""}
|
|
689
683
|
</div>
|
|
690
684
|
</div>
|
package/ui/tabs/tasks.js
CHANGED
|
@@ -89,6 +89,15 @@ const SORT_OPTIONS = [
|
|
|
89
89
|
{ value: "title", label: "Title" },
|
|
90
90
|
];
|
|
91
91
|
|
|
92
|
+
/* Maps snapshot-bar labels → tasksFilter values */
|
|
93
|
+
const SNAPSHOT_STATUS_MAP = {
|
|
94
|
+
Backlog: "todo",
|
|
95
|
+
Active: "inprogress",
|
|
96
|
+
Review: "inreview",
|
|
97
|
+
Done: "done",
|
|
98
|
+
Errors: "error",
|
|
99
|
+
};
|
|
100
|
+
|
|
92
101
|
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, "": 4 };
|
|
93
102
|
|
|
94
103
|
const SYSTEM_TAGS = new Set([
|
|
@@ -994,8 +1003,10 @@ export function TasksTab() {
|
|
|
994
1003
|
return dir * (av - bv);
|
|
995
1004
|
}
|
|
996
1005
|
if (listSortCol === "updated") {
|
|
997
|
-
|
|
998
|
-
|
|
1006
|
+
const tsA = a.updated_at || a.updated;
|
|
1007
|
+
const tsB = b.updated_at || b.updated;
|
|
1008
|
+
av = tsA ? (typeof tsA === 'number' ? tsA : new Date(tsA).getTime()) : 0;
|
|
1009
|
+
bv = tsB ? (typeof tsB === 'number' ? tsB : new Date(tsB).getTime()) : 0;
|
|
999
1010
|
return dir * (av - bv);
|
|
1000
1011
|
}
|
|
1001
1012
|
if (listSortCol === "status") { av = a.status || ""; bv = b.status || ""; }
|
|
@@ -1609,11 +1620,20 @@ export function TasksTab() {
|
|
|
1609
1620
|
|
|
1610
1621
|
<div class="snapshot-bar">
|
|
1611
1622
|
${summaryMetrics.map((m) => html`
|
|
1612
|
-
<
|
|
1623
|
+
<button
|
|
1624
|
+
key=${m.label}
|
|
1625
|
+
class="snapshot-pill snapshot-pill-btn ${!isKanban && filterVal === SNAPSHOT_STATUS_MAP[m.label] ? 'snapshot-pill-active' : ''}"
|
|
1626
|
+
onClick=${() => {
|
|
1627
|
+
if (isKanban) return;
|
|
1628
|
+
const statusVal = SNAPSHOT_STATUS_MAP[m.label];
|
|
1629
|
+
if (statusVal !== undefined) handleFilter(filterVal === statusVal ? 'all' : statusVal);
|
|
1630
|
+
}}
|
|
1631
|
+
title=${isKanban ? m.label : `Filter by ${m.label}`}
|
|
1632
|
+
>
|
|
1613
1633
|
<span class="snapshot-dot" style="background:${m.color};" />
|
|
1614
1634
|
<strong class="snapshot-val">${m.value}</strong>
|
|
1615
1635
|
<span class="snapshot-lbl">${m.label}</span>
|
|
1616
|
-
</
|
|
1636
|
+
</button>
|
|
1617
1637
|
`)}
|
|
1618
1638
|
<span class="snapshot-view-tag">${isKanban ? "⬛ Board" : "☰ List"}</span>
|
|
1619
1639
|
</div>
|
|
@@ -1632,6 +1652,23 @@ export function TasksTab() {
|
|
|
1632
1652
|
cursor:pointer;
|
|
1633
1653
|
}
|
|
1634
1654
|
.actions-dropdown-item:hover { background:var(--hover-bg, rgba(255,255,255,.08)); }
|
|
1655
|
+
.snapshot-pill-btn {
|
|
1656
|
+
background: none;
|
|
1657
|
+
border: 1px solid transparent;
|
|
1658
|
+
border-radius: 999px;
|
|
1659
|
+
cursor: pointer;
|
|
1660
|
+
padding: 2px 8px;
|
|
1661
|
+
transition: border-color 0.15s, background 0.15s;
|
|
1662
|
+
font: inherit;
|
|
1663
|
+
}
|
|
1664
|
+
.snapshot-pill-btn:hover {
|
|
1665
|
+
border-color: var(--border);
|
|
1666
|
+
background: var(--bg-card-hover, rgba(255,255,255,0.05));
|
|
1667
|
+
}
|
|
1668
|
+
.snapshot-pill-active {
|
|
1669
|
+
border-color: var(--accent) !important;
|
|
1670
|
+
background: rgba(59,130,246,0.12) !important;
|
|
1671
|
+
}
|
|
1635
1672
|
@media (max-width: 640px) {
|
|
1636
1673
|
.actions-label { display:none; }
|
|
1637
1674
|
}
|
|
@@ -1706,8 +1743,8 @@ export function TasksTab() {
|
|
|
1706
1743
|
: html`<span class="task-td-empty">—</span>`}
|
|
1707
1744
|
</td>
|
|
1708
1745
|
<td class="task-td task-td-updated">
|
|
1709
|
-
${task.updated_at
|
|
1710
|
-
? html`<span class="task-td-date">${formatRelative(task.updated_at)}</span>`
|
|
1746
|
+
${(task.updated_at || task.updated)
|
|
1747
|
+
? html`<span class="task-td-date">${formatRelative(task.updated_at || task.updated)}</span>`
|
|
1711
1748
|
: html`<span class="task-td-empty">—</span>`}
|
|
1712
1749
|
</td>
|
|
1713
1750
|
</tr>
|
package/workspace-manager.mjs
CHANGED
|
@@ -26,6 +26,39 @@ import { execSync, spawnSync } from "node:child_process";
|
|
|
26
26
|
|
|
27
27
|
const TAG = "[workspace-manager]";
|
|
28
28
|
|
|
29
|
+
// Lazy-loaded reference to repo-config.mjs (resolved on first use)
|
|
30
|
+
let _repoConfigModule = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensure repo-level AI executor configs exist after clone/pull.
|
|
34
|
+
* Uses synchronous import cache — repo-config.mjs is pure sync.
|
|
35
|
+
* @param {string} repoPath Absolute path to the repo directory
|
|
36
|
+
*/
|
|
37
|
+
function ensureRepoAIConfigs(repoPath) {
|
|
38
|
+
try {
|
|
39
|
+
if (!_repoConfigModule) {
|
|
40
|
+
// repo-config.mjs is ESM but fully synchronous internally.
|
|
41
|
+
// We pre-populate the cache via dynamic import at module init.
|
|
42
|
+
return; // Will be populated after first async import
|
|
43
|
+
}
|
|
44
|
+
const { ensureRepoConfigs, printRepoConfigSummary } = _repoConfigModule;
|
|
45
|
+
const result = ensureRepoConfigs(repoPath);
|
|
46
|
+
// Only log if something was created/updated
|
|
47
|
+
const anyChange = Object.values(result).some((r) => r.created || r.updated);
|
|
48
|
+
if (anyChange) {
|
|
49
|
+
console.log(TAG, `Repo-level AI configs for ${basename(repoPath)}:`);
|
|
50
|
+
printRepoConfigSummary(result, (msg) => console.log(TAG, msg));
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.warn(TAG, `Could not ensure repo AI configs: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Pre-load repo-config.mjs asynchronously at module init time
|
|
58
|
+
import("./repo-config.mjs")
|
|
59
|
+
.then((mod) => { _repoConfigModule = mod; })
|
|
60
|
+
.catch(() => { /* repo-config not available — skip */ });
|
|
61
|
+
|
|
29
62
|
// ── Path Helpers ─────────────────────────────────────────────────────────────
|
|
30
63
|
|
|
31
64
|
/**
|
|
@@ -313,8 +346,12 @@ export function addRepoToWorkspace(configDir, workspaceId, { url, name, branch,
|
|
|
313
346
|
}
|
|
314
347
|
cloned = true;
|
|
315
348
|
console.log(TAG, `Cloned ${repoName} successfully`);
|
|
349
|
+
// Ensure repo-level AI executor configs exist after fresh clone
|
|
350
|
+
ensureRepoAIConfigs(repoPath);
|
|
316
351
|
} else {
|
|
317
352
|
console.log(TAG, `Repository ${repoName} already exists at ${repoPath}`);
|
|
353
|
+
// Ensure repo-level configs are up to date even for existing repos
|
|
354
|
+
ensureRepoAIConfigs(repoPath);
|
|
318
355
|
}
|
|
319
356
|
|
|
320
357
|
const slug = extractSlug(url);
|