bosun 0.40.21 → 0.41.1
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 +8 -0
- package/README.md +20 -0
- package/agent/agent-custom-tools.mjs +23 -5
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +131 -30
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/primary-agent.mjs +81 -7
- package/agent/retry-queue.mjs +164 -0
- package/bench/swebench/bosun-swebench.mjs +5 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +267 -8
- package/config/config-doctor.mjs +51 -2
- package/config/config.mjs +232 -5
- package/github/github-auth-manager.mjs +70 -19
- package/infra/library-manager.mjs +894 -60
- package/infra/monitor.mjs +701 -69
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +95 -28
- package/infra/test-runtime.mjs +267 -0
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +30 -8
- package/server/setup-web-server.mjs +29 -1
- package/server/ui-server.mjs +1571 -49
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-claims.mjs +6 -10
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/chat-view.js +18 -1
- package/ui/components/workspace-switcher.js +321 -9
- package/ui/demo-defaults.js +17830 -10433
- package/ui/demo.html +9 -1
- package/ui/modules/router.js +1 -1
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +376 -37
- package/ui/modules/voice-client.js +173 -33
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +571 -1
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/library.js +410 -55
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +1083 -507
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +38 -1
- package/ui/tabs/workflows.js +1275 -402
- package/voice/voice-agents-sdk.mjs +2 -2
- package/voice/voice-relay.mjs +28 -20
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/project-detection.mjs +559 -0
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-contract.mjs +433 -232
- package/workflow/workflow-engine.mjs +510 -47
- package/workflow/workflow-nodes/custom-loader.mjs +251 -0
- package/workflow/workflow-nodes.mjs +2024 -184
- package/workflow/workflow-templates.mjs +118 -24
- package/workflow-templates/agents.mjs +20 -20
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/code-quality.mjs +20 -14
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +27 -10
- package/workflow-templates/task-execution.mjs +752 -0
- package/workflow-templates/task-lifecycle.mjs +117 -14
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +153 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
package/cli.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { fileURLToPath } from "node:url";
|
|
|
28
28
|
import { execFileSync, execSync, fork, spawn } from "node:child_process";
|
|
29
29
|
import os from "node:os";
|
|
30
30
|
import { createDaemonCrashTracker } from "./infra/daemon-restart-policy.mjs";
|
|
31
|
+
import { ensureTestRuntimeSandbox } from "./infra/test-runtime.mjs";
|
|
31
32
|
import {
|
|
32
33
|
applyAllCompatibility,
|
|
33
34
|
detectLegacySetup,
|
|
@@ -76,12 +77,14 @@ function showHelp() {
|
|
|
76
77
|
bosun [options]
|
|
77
78
|
|
|
78
79
|
COMMANDS
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
workflow list List declarative pipeline workflows
|
|
81
|
+
workflow run <name> Run a declarative pipeline workflow
|
|
82
|
+
--setup Launch the web-based setup wizard (default)
|
|
81
83
|
--setup-terminal Run the legacy terminal setup wizard
|
|
82
84
|
--where Show the resolved bosun config directory
|
|
83
85
|
--doctor Validate bosun .env/config setup
|
|
84
86
|
--tool-log <ID|list|prune> Retrieve/list/prune cached tool outputs
|
|
87
|
+
node:create <name> Scaffold a custom workflow node in custom-nodes/
|
|
85
88
|
--context-index [mode] Run context index workflow (run|status|search)
|
|
86
89
|
--context-index-query <text> Query text for context index search mode
|
|
87
90
|
--context-index-limit <n> Max results for context index search (default: 25)
|
|
@@ -140,6 +143,12 @@ function showHelp() {
|
|
|
140
143
|
--workspace-switch <id> Switch active workspace
|
|
141
144
|
--workspace-add-repo Add repo to workspace (interactive)
|
|
142
145
|
--workspace-health Run workspace health diagnostics
|
|
146
|
+
--workspace-pause <id> Pause a workspace (no new workflows)
|
|
147
|
+
--workspace-resume <id> Resume a paused workspace
|
|
148
|
+
--workspace-disable <id> Disable a workspace entirely
|
|
149
|
+
--workspace-status Show state summary of all workspaces
|
|
150
|
+
--workspace-executors <id> Show/set executor config for workspace
|
|
151
|
+
[--max-concurrent N] [--pool shared|dedicated] [--weight N]
|
|
143
152
|
|
|
144
153
|
TASK MANAGEMENT
|
|
145
154
|
task list [--status s] [--json] List tasks with optional filters
|
|
@@ -152,6 +161,12 @@ function showHelp() {
|
|
|
152
161
|
|
|
153
162
|
Run 'bosun task --help' for complete task CLI documentation and examples.
|
|
154
163
|
|
|
164
|
+
WORKFLOWS
|
|
165
|
+
workflow list List built-in and configured workflows
|
|
166
|
+
workflow run <name> Run a declarative fresh-context workflow
|
|
167
|
+
|
|
168
|
+
Run 'bosun workflow --help' for workflow CLI examples.
|
|
169
|
+
|
|
155
170
|
VIBE-KANBAN
|
|
156
171
|
--no-vk-spawn Don't auto-spawn Vibe-Kanban
|
|
157
172
|
--vk-ensure-interval <ms> VK health check interval (default: 60000)
|
|
@@ -252,6 +267,9 @@ function resolveConfigDirForCli() {
|
|
|
252
267
|
const repoLocalConfigDir = resolveRepoLocalBosunDir(repoRoot);
|
|
253
268
|
if (repoLocalConfigDir) return repoLocalConfigDir;
|
|
254
269
|
|
|
270
|
+
const sandbox = ensureTestRuntimeSandbox();
|
|
271
|
+
if (sandbox?.configDir) return sandbox.configDir;
|
|
272
|
+
|
|
255
273
|
const preferWindowsDirs =
|
|
256
274
|
process.platform === "win32" && !isWslInteropRuntime();
|
|
257
275
|
const baseDir = preferWindowsDirs
|
|
@@ -368,10 +386,21 @@ function uniqueResolvedPaths(paths) {
|
|
|
368
386
|
return results;
|
|
369
387
|
}
|
|
370
388
|
|
|
389
|
+
function getWorkspaceScopedCacheDirCandidate(repoRootPath) {
|
|
390
|
+
const bosunDir = process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
391
|
+
if (!bosunDir || !repoRootPath) return null;
|
|
392
|
+
const parts = String(repoRootPath).replace(/\\/g, "/").split("/").filter(Boolean);
|
|
393
|
+
const repoName = parts.at(-1);
|
|
394
|
+
const workspaceName = parts.at(-2);
|
|
395
|
+
if (!repoName || !workspaceName) return null;
|
|
396
|
+
return resolve(bosunDir, "workspaces", workspaceName, repoName, ".cache");
|
|
397
|
+
}
|
|
398
|
+
|
|
371
399
|
function getRuntimeCacheDirCandidates(extraCacheDirs = []) {
|
|
372
400
|
return uniqueResolvedPaths([
|
|
373
401
|
...extraCacheDirs,
|
|
374
402
|
runtimeCacheDir,
|
|
403
|
+
getWorkspaceScopedCacheDirCandidate(runtimeRepoRoot),
|
|
375
404
|
process.env.BOSUN_DIR ? resolve(process.env.BOSUN_DIR, ".cache") : null,
|
|
376
405
|
resolve(__dirname, ".cache"),
|
|
377
406
|
resolve(process.cwd(), ".cache"),
|
|
@@ -625,6 +654,42 @@ function findGhostDaemonPids() {
|
|
|
625
654
|
}
|
|
626
655
|
}
|
|
627
656
|
|
|
657
|
+
function findGhostSentinelPids() {
|
|
658
|
+
if (process.platform === "win32") {
|
|
659
|
+
try {
|
|
660
|
+
const out = execFileSync(
|
|
661
|
+
"powershell",
|
|
662
|
+
[
|
|
663
|
+
"-NoProfile",
|
|
664
|
+
"-Command",
|
|
665
|
+
"Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^(node|electron)(\\.exe)?$' -and $_.CommandLine -match 'telegram-sentinel\\.mjs' } | Select-Object -ExpandProperty ProcessId",
|
|
666
|
+
],
|
|
667
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 },
|
|
668
|
+
).trim();
|
|
669
|
+
if (!out) return [];
|
|
670
|
+
return out
|
|
671
|
+
.split(/\r?\n/)
|
|
672
|
+
.map((s) => parseInt(String(s).trim(), 10))
|
|
673
|
+
.filter((n) => Number.isFinite(n) && n > 0 && n !== process.pid);
|
|
674
|
+
} catch {
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const out = execFileSync(
|
|
680
|
+
"pgrep",
|
|
681
|
+
["-f", "telegram-sentinel\\.mjs"],
|
|
682
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 },
|
|
683
|
+
).trim();
|
|
684
|
+
return out
|
|
685
|
+
.split("\n")
|
|
686
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
687
|
+
.filter((n) => Number.isFinite(n) && n > 0 && n !== process.pid);
|
|
688
|
+
} catch {
|
|
689
|
+
return [];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
628
693
|
function writePidFile(pid) {
|
|
629
694
|
try {
|
|
630
695
|
mkdirSync(dirname(DAEMON_PID_FILE), { recursive: true });
|
|
@@ -852,11 +917,28 @@ function daemonStatus() {
|
|
|
852
917
|
} else {
|
|
853
918
|
// Check for ghost daemon-child processes (alive but no PID file)
|
|
854
919
|
const ghosts = findGhostDaemonPids();
|
|
920
|
+
const ghostSentinels = findGhostSentinelPids();
|
|
855
921
|
if (ghosts.length > 0) {
|
|
856
922
|
console.log(` :alert: bosun daemon is NOT tracked (no PID file), but ${ghosts.length} ghost process(es) found: ${ghosts.join(", ")}`);
|
|
857
923
|
console.log(` The daemon is likely running but its PID file was lost.`);
|
|
858
|
-
|
|
924
|
+
if (ghostSentinels.length > 0) {
|
|
925
|
+
console.log(` Ghost sentinel restart owner(s) detected: ${ghostSentinels.join(", ")}`);
|
|
926
|
+
}
|
|
927
|
+
console.log(` Run --terminate to stop restart owners, then --daemon to restart.`);
|
|
859
928
|
} else {
|
|
929
|
+
const existingMonitorOwner = detectExistingMonitorLockOwner();
|
|
930
|
+
if (existingMonitorOwner) {
|
|
931
|
+
console.log(
|
|
932
|
+
` bosun daemon is not running in daemon mode, but bosun monitor is active (PID ${existingMonitorOwner.pid}).`,
|
|
933
|
+
);
|
|
934
|
+
console.log(
|
|
935
|
+
` Bosun is running in monitor mode with lock file ${existingMonitorOwner.pidFile}.`,
|
|
936
|
+
);
|
|
937
|
+
console.log(
|
|
938
|
+
` Use 'bosun --terminate' to stop it, or 'bosun --daemon' only after it is fully stopped.`,
|
|
939
|
+
);
|
|
940
|
+
process.exit(0);
|
|
941
|
+
}
|
|
860
942
|
// Broader scan: portal, monitor, ui-server, etc. (non-daemon bosun processes)
|
|
861
943
|
const allPids = findAllBosunProcessPids();
|
|
862
944
|
if (allPids.length > 0) {
|
|
@@ -1108,6 +1190,7 @@ async function terminateBosun() {
|
|
|
1108
1190
|
]).map((pidFile) => readAlivePid(pidFile)),
|
|
1109
1191
|
readSentinelPid(),
|
|
1110
1192
|
].filter((pid) => Number.isFinite(pid) && pid > 0);
|
|
1193
|
+
const sentinelGhostPids = findGhostSentinelPids();
|
|
1111
1194
|
const manualStopHoldMs =
|
|
1112
1195
|
Math.max(
|
|
1113
1196
|
0,
|
|
@@ -1119,9 +1202,17 @@ async function terminateBosun() {
|
|
|
1119
1202
|
...daemonPids,
|
|
1120
1203
|
...monitorPids,
|
|
1121
1204
|
...sentinelPids,
|
|
1205
|
+
...sentinelGhostPids,
|
|
1206
|
+
...ghosts,
|
|
1122
1207
|
]);
|
|
1123
1208
|
const restartOwnerPids = Array.from(
|
|
1124
|
-
new Set([
|
|
1209
|
+
new Set([
|
|
1210
|
+
...ancestorPids,
|
|
1211
|
+
...sentinelPids,
|
|
1212
|
+
...sentinelGhostPids,
|
|
1213
|
+
...daemonPids,
|
|
1214
|
+
...ghosts,
|
|
1215
|
+
]),
|
|
1125
1216
|
).filter((pid) => pid !== process.pid);
|
|
1126
1217
|
const tracked = [...restartOwnerPids, ...monitorPids];
|
|
1127
1218
|
const trackedPids = Array.from(new Set([...tracked, ...ghosts])).filter(
|
|
@@ -1251,7 +1342,21 @@ async function main() {
|
|
|
1251
1342
|
process.exit(0);
|
|
1252
1343
|
}
|
|
1253
1344
|
|
|
1254
|
-
|
|
1345
|
+
const workflowFlagIndex = args.indexOf("--workflow");
|
|
1346
|
+
const workflowCommandIndex =
|
|
1347
|
+
args[0] === "workflow"
|
|
1348
|
+
? 0
|
|
1349
|
+
: args[0]?.startsWith("--")
|
|
1350
|
+
? args.indexOf("workflow")
|
|
1351
|
+
: -1;
|
|
1352
|
+
if (workflowCommandIndex >= 0 || workflowFlagIndex >= 0) {
|
|
1353
|
+
const { runWorkflowCli } = await import("./workflow/workflow-cli.mjs");
|
|
1354
|
+
const commandStartIndex = workflowCommandIndex >= 0 ? workflowCommandIndex : workflowFlagIndex;
|
|
1355
|
+
const workflowArgs = args.slice(commandStartIndex + 1);
|
|
1356
|
+
await runWorkflowCli(workflowArgs);
|
|
1357
|
+
process.exit(0);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1255
1360
|
const auditFlagIndex = args.indexOf("--audit");
|
|
1256
1361
|
const auditCommandIndex =
|
|
1257
1362
|
args[0] === "audit"
|
|
@@ -1263,8 +1368,27 @@ async function main() {
|
|
|
1263
1368
|
const { runAuditCli } = await import("./lib/codebase-audit.mjs");
|
|
1264
1369
|
const commandStartIndex = auditCommandIndex >= 0 ? auditCommandIndex : auditFlagIndex;
|
|
1265
1370
|
const auditArgs = args.slice(commandStartIndex + 1);
|
|
1266
|
-
|
|
1267
|
-
process.exit(
|
|
1371
|
+
await runAuditCli(auditArgs);
|
|
1372
|
+
process.exit(0);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
if (args[0] === "node:create" || (args[0] === "node" && args[1] === "create")) {
|
|
1376
|
+
const name = args[0] === "node:create" ? args[1] : args[2];
|
|
1377
|
+
if (!name) {
|
|
1378
|
+
console.error("Usage: bosun node:create <name>");
|
|
1379
|
+
process.exit(1);
|
|
1380
|
+
}
|
|
1381
|
+
const { scaffoldCustomNodeFile } = await import("./workflow/workflow-nodes.mjs");
|
|
1382
|
+
try {
|
|
1383
|
+
const result = scaffoldCustomNodeFile(name, { repoRoot: runtimeRepoRoot });
|
|
1384
|
+
console.log(`\n ✓ Created custom node \"${result.type}\"`);
|
|
1385
|
+
console.log(` File: ${result.filePath}`);
|
|
1386
|
+
console.log("");
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
console.error(` Error: ${err.message}`);
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1391
|
+
process.exit(0);
|
|
1268
1392
|
}
|
|
1269
1393
|
|
|
1270
1394
|
// Handle --help
|
|
@@ -1742,7 +1866,11 @@ async function main() {
|
|
|
1742
1866
|
console.log("\n Workspaces:");
|
|
1743
1867
|
for (const ws of workspaces) {
|
|
1744
1868
|
const marker = ws.id === active?.id ? " ← active" : "";
|
|
1745
|
-
|
|
1869
|
+
const stateIcon = ws.state === "active" ? "●" : ws.state === "paused" ? "◐" : "○";
|
|
1870
|
+
const stateLabel = ws.state !== "active" ? ` [${ws.state}]` : "";
|
|
1871
|
+
console.log(` ${stateIcon} ${ws.name} (${ws.id})${stateLabel}${marker}`);
|
|
1872
|
+
const ex = ws.executors;
|
|
1873
|
+
console.log(` executors: max=${ex.maxConcurrent}, pool=${ex.pool}, weight=${ex.weight}`);
|
|
1746
1874
|
for (const repo of ws.repos || []) {
|
|
1747
1875
|
const primary = repo.primary ? " [primary]" : "";
|
|
1748
1876
|
const exists = repo.exists ? "✓" : "✗";
|
|
@@ -1831,6 +1959,137 @@ async function main() {
|
|
|
1831
1959
|
process.exit(result.ok ? 0 : 1);
|
|
1832
1960
|
}
|
|
1833
1961
|
|
|
1962
|
+
// Handle --workspace-pause
|
|
1963
|
+
if (args.includes("--workspace-pause") || args.includes("workspace-pause")) {
|
|
1964
|
+
const { pauseWorkspace, getWorkspace } = await import("./workspace/workspace-manager.mjs");
|
|
1965
|
+
const configDirArg = getArgValue("--config-dir");
|
|
1966
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1967
|
+
const wsId = getArgValue("--workspace-pause") || getArgValue("workspace-pause");
|
|
1968
|
+
if (!wsId) {
|
|
1969
|
+
console.error(" Error: workspace ID required. Usage: bosun --workspace-pause <id>");
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
}
|
|
1972
|
+
try {
|
|
1973
|
+
pauseWorkspace(configDir, wsId);
|
|
1974
|
+
const ws = getWorkspace(configDir, wsId);
|
|
1975
|
+
console.log(`\n ⏸ Workspace "${ws?.name || wsId}" paused — no new workflows will start\n`);
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
console.error(` Error: ${err.message}`);
|
|
1978
|
+
process.exit(1);
|
|
1979
|
+
}
|
|
1980
|
+
process.exit(0);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// Handle --workspace-resume
|
|
1984
|
+
if (args.includes("--workspace-resume") || args.includes("workspace-resume")) {
|
|
1985
|
+
const { resumeWorkspace, getWorkspace } = await import("./workspace/workspace-manager.mjs");
|
|
1986
|
+
const configDirArg = getArgValue("--config-dir");
|
|
1987
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1988
|
+
const wsId = getArgValue("--workspace-resume") || getArgValue("workspace-resume");
|
|
1989
|
+
if (!wsId) {
|
|
1990
|
+
console.error(" Error: workspace ID required. Usage: bosun --workspace-resume <id>");
|
|
1991
|
+
process.exit(1);
|
|
1992
|
+
}
|
|
1993
|
+
try {
|
|
1994
|
+
resumeWorkspace(configDir, wsId);
|
|
1995
|
+
const ws = getWorkspace(configDir, wsId);
|
|
1996
|
+
console.log(`\n ▶ Workspace "${ws?.name || wsId}" resumed — workflows will trigger normally\n`);
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
console.error(` Error: ${err.message}`);
|
|
1999
|
+
process.exit(1);
|
|
2000
|
+
}
|
|
2001
|
+
process.exit(0);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// Handle --workspace-disable
|
|
2005
|
+
if (args.includes("--workspace-disable") || args.includes("workspace-disable")) {
|
|
2006
|
+
const { disableWorkspace, getWorkspace } = await import("./workspace/workspace-manager.mjs");
|
|
2007
|
+
const configDirArg = getArgValue("--config-dir");
|
|
2008
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
2009
|
+
const wsId = getArgValue("--workspace-disable") || getArgValue("workspace-disable");
|
|
2010
|
+
if (!wsId) {
|
|
2011
|
+
console.error(" Error: workspace ID required. Usage: bosun --workspace-disable <id>");
|
|
2012
|
+
process.exit(1);
|
|
2013
|
+
}
|
|
2014
|
+
try {
|
|
2015
|
+
disableWorkspace(configDir, wsId);
|
|
2016
|
+
const ws = getWorkspace(configDir, wsId);
|
|
2017
|
+
console.log(`\n ⏹ Workspace "${ws?.name || wsId}" disabled — no workflows, no executors\n`);
|
|
2018
|
+
} catch (err) {
|
|
2019
|
+
console.error(` Error: ${err.message}`);
|
|
2020
|
+
process.exit(1);
|
|
2021
|
+
}
|
|
2022
|
+
process.exit(0);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// Handle --workspace-status
|
|
2026
|
+
if (args.includes("--workspace-status") || args.includes("workspace-status")) {
|
|
2027
|
+
const { getWorkspaceStateSummary } = await import("./workspace/workspace-manager.mjs");
|
|
2028
|
+
const configDirArg = getArgValue("--config-dir");
|
|
2029
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
2030
|
+
const summary = getWorkspaceStateSummary(configDir);
|
|
2031
|
+
if (summary.length === 0) {
|
|
2032
|
+
console.log("\n No workspaces configured.\n");
|
|
2033
|
+
} else {
|
|
2034
|
+
console.log("\n Workspace Status:");
|
|
2035
|
+
for (const ws of summary) {
|
|
2036
|
+
const stateIcon = ws.state === "active" ? "●" : ws.state === "paused" ? "◐" : "○";
|
|
2037
|
+
const current = ws.isCurrent ? " ← current" : "";
|
|
2038
|
+
console.log(` ${stateIcon} ${ws.name} (${ws.id}) — ${ws.state}${current}`);
|
|
2039
|
+
const ex = ws.executors;
|
|
2040
|
+
console.log(` executors: max=${ex.maxConcurrent}, pool=${ex.pool}, weight=${ex.weight}`);
|
|
2041
|
+
if (ws.disabledWorkflows.length > 0) {
|
|
2042
|
+
console.log(` disabled workflows: ${ws.disabledWorkflows.join(", ")}`);
|
|
2043
|
+
}
|
|
2044
|
+
if (ws.enabledWorkflows.length > 0) {
|
|
2045
|
+
console.log(` enabled workflows: ${ws.enabledWorkflows.join(", ")}`);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
console.log("");
|
|
2049
|
+
}
|
|
2050
|
+
process.exit(0);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// Handle --workspace-executors
|
|
2054
|
+
if (args.includes("--workspace-executors") || args.includes("workspace-executors")) {
|
|
2055
|
+
const { setWorkspaceExecutors, getWorkspace } = await import("./workspace/workspace-manager.mjs");
|
|
2056
|
+
const configDirArg = getArgValue("--config-dir");
|
|
2057
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
2058
|
+
const wsId = getArgValue("--workspace-executors") || getArgValue("workspace-executors");
|
|
2059
|
+
if (!wsId) {
|
|
2060
|
+
console.error(" Error: workspace ID required. Usage: bosun --workspace-executors <id> [--max-concurrent N] [--pool shared|dedicated] [--weight N]");
|
|
2061
|
+
process.exit(1);
|
|
2062
|
+
}
|
|
2063
|
+
const maxConcurrent = getArgValue("--max-concurrent");
|
|
2064
|
+
const pool = getArgValue("--pool");
|
|
2065
|
+
const weight = getArgValue("--weight");
|
|
2066
|
+
const hasUpdate = maxConcurrent || pool || weight;
|
|
2067
|
+
if (hasUpdate) {
|
|
2068
|
+
try {
|
|
2069
|
+
const opts = {};
|
|
2070
|
+
if (maxConcurrent) opts.maxConcurrent = Number(maxConcurrent);
|
|
2071
|
+
if (pool) opts.pool = pool;
|
|
2072
|
+
if (weight) opts.weight = Number(weight);
|
|
2073
|
+
const result = setWorkspaceExecutors(configDir, wsId, opts);
|
|
2074
|
+
console.log(`\n ✓ Executor config updated for "${wsId}":`, JSON.stringify(result), "\n");
|
|
2075
|
+
} catch (err) {
|
|
2076
|
+
console.error(` Error: ${err.message}`);
|
|
2077
|
+
process.exit(1);
|
|
2078
|
+
}
|
|
2079
|
+
} else {
|
|
2080
|
+
const ws = getWorkspace(configDir, wsId);
|
|
2081
|
+
if (!ws) {
|
|
2082
|
+
console.error(` Error: workspace "${wsId}" not found`);
|
|
2083
|
+
process.exit(1);
|
|
2084
|
+
}
|
|
2085
|
+
console.log(`\n Executor config for "${ws.name}":`);
|
|
2086
|
+
console.log(` maxConcurrent: ${ws.executors.maxConcurrent}`);
|
|
2087
|
+
console.log(` pool: ${ws.executors.pool}`);
|
|
2088
|
+
console.log(` weight: ${ws.executors.weight}\n`);
|
|
2089
|
+
}
|
|
2090
|
+
process.exit(0);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
1834
2093
|
// Handle --setup-terminal (legacy terminal wizard)
|
|
1835
2094
|
if (args.includes("--setup-terminal")) {
|
|
1836
2095
|
const configDirArg = getArgValue("--config-dir");
|
package/config/config-doctor.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { resolve, dirname, isAbsolute, relative, join } from "node:path";
|
|
3
3
|
import { execSync, spawnSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
+
import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
|
|
7
|
+
import { getWorkflowContract } from "../workflow/workflow-contract.mjs";
|
|
6
8
|
|
|
7
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
10
|
const CONFIG_FILES = [
|
|
@@ -10,6 +12,10 @@ const CONFIG_FILES = [
|
|
|
10
12
|
".bosun.json",
|
|
11
13
|
"bosun.json",
|
|
12
14
|
];
|
|
15
|
+
const WORKFLOW_CONTRACT_NODE_TYPES = Object.freeze({
|
|
16
|
+
read: new Set(["read-workflow-contract", "action.read_workflow_contract"]),
|
|
17
|
+
validate: new Set(["workflow-contract-validation", "action.workflow_contract_validation"]),
|
|
18
|
+
});
|
|
13
19
|
|
|
14
20
|
function parseBool(value) {
|
|
15
21
|
return ["1", "true", "yes", "on"].includes(
|
|
@@ -92,6 +98,9 @@ function resolveConfigDir(repoRoot) {
|
|
|
92
98
|
return packageDir;
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
const sandbox = ensureTestRuntimeSandbox();
|
|
102
|
+
if (sandbox?.configDir) return sandbox.configDir;
|
|
103
|
+
|
|
95
104
|
const preferWindowsDirs =
|
|
96
105
|
process.platform === "win32" && !isWslInteropRuntime();
|
|
97
106
|
const baseDir =
|
|
@@ -166,6 +175,36 @@ function findConfigFile(configDir) {
|
|
|
166
175
|
return null;
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
function workflowHasContractNodes(definition) {
|
|
179
|
+
const nodes = Array.isArray(definition?.nodes) ? definition.nodes : [];
|
|
180
|
+
let hasRead = false;
|
|
181
|
+
let hasValidate = false;
|
|
182
|
+
for (const node of nodes) {
|
|
183
|
+
const type = String(node?.type || "").trim();
|
|
184
|
+
if (WORKFLOW_CONTRACT_NODE_TYPES.read.has(type)) hasRead = true;
|
|
185
|
+
if (WORKFLOW_CONTRACT_NODE_TYPES.validate.has(type)) hasValidate = true;
|
|
186
|
+
}
|
|
187
|
+
return hasRead && hasValidate;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hasEnabledWorkflowContractStep(repoRoot) {
|
|
191
|
+
const workflowDir = resolve(repoRoot, ".bosun", "workflows");
|
|
192
|
+
if (!existsSync(workflowDir)) return false;
|
|
193
|
+
|
|
194
|
+
for (const file of readdirSync(workflowDir)) {
|
|
195
|
+
if (!file.endsWith(".json")) continue;
|
|
196
|
+
try {
|
|
197
|
+
const definition = JSON.parse(readFileSync(resolve(workflowDir, file), "utf8"));
|
|
198
|
+
if (definition?.enabled === false) continue;
|
|
199
|
+
if (workflowHasContractNodes(definition)) return true;
|
|
200
|
+
} catch {
|
|
201
|
+
/* ignore malformed workflow files here; other checks can catch them */
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
169
208
|
function validateExecutors(raw, issues) {
|
|
170
209
|
if (!raw) return;
|
|
171
210
|
const entries = String(raw)
|
|
@@ -671,6 +710,17 @@ export function runConfigDoctor(options = {}) {
|
|
|
671
710
|
});
|
|
672
711
|
}
|
|
673
712
|
|
|
713
|
+
const workflowContract = getWorkflowContract(repoRoot);
|
|
714
|
+
if (workflowContract.found && !hasEnabledWorkflowContractStep(repoRoot)) {
|
|
715
|
+
issues.warnings.push({
|
|
716
|
+
code: "WORKFLOW_CONTRACT_STEP_DISABLED",
|
|
717
|
+
message:
|
|
718
|
+
"WORKFLOW.md exists but no enabled workflow includes the read/validate workflow-contract steps.",
|
|
719
|
+
fix:
|
|
720
|
+
"Install or update the session-start workflow (for example template-task-lifecycle) so it includes `read-workflow-contract` and `workflow-contract-validation`.",
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
674
724
|
issues.infos.push({
|
|
675
725
|
code: "PATHS",
|
|
676
726
|
message: `Config directory: ${configDir}`,
|
|
@@ -988,4 +1038,3 @@ export function formatWorkspaceHealthReport(result) {
|
|
|
988
1038
|
return lines.join("\n");
|
|
989
1039
|
}
|
|
990
1040
|
|
|
991
|
-
|