bosun 0.41.2 → 0.41.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/agent-pool.mjs +9 -2
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +119 -6
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +35 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/setup-web-server.mjs +58 -5
- package/server/ui-server.mjs +1394 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +28 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +338 -84
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +43 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +848 -141
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +358 -63
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +44 -11
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
|
@@ -92,6 +92,16 @@ const MAX_PERSISTED_RUNS = readBoundedEnvInt("WORKFLOW_MAX_PERSISTED_RUNS", 2000
|
|
|
92
92
|
min: 20,
|
|
93
93
|
max: 20000,
|
|
94
94
|
});
|
|
95
|
+
const MAX_INTERRUPTED_ORPHAN_SCAN_FILES = readBoundedEnvInt(
|
|
96
|
+
"WORKFLOW_INTERRUPTED_ORPHAN_SCAN_MAX_FILES",
|
|
97
|
+
200,
|
|
98
|
+
{ min: 0, max: 5000 },
|
|
99
|
+
);
|
|
100
|
+
const INTERRUPTED_ORPHAN_SCAN_WINDOW_MS = readBoundedEnvInt(
|
|
101
|
+
"WORKFLOW_INTERRUPTED_ORPHAN_SCAN_WINDOW_MS",
|
|
102
|
+
7 * 24 * 60 * 60 * 1000,
|
|
103
|
+
{ min: 0, max: 90 * 24 * 60 * 60 * 1000 },
|
|
104
|
+
);
|
|
95
105
|
const DEFAULT_RUN_STUCK_THRESHOLD_MS = readBoundedEnvInt(
|
|
96
106
|
"WORKFLOW_RUN_STUCK_THRESHOLD_MS",
|
|
97
107
|
5 * 60 * 1000,
|
|
@@ -312,6 +322,61 @@ export function isPortConnectionCompatible(sourcePort, targetPort) {
|
|
|
312
322
|
};
|
|
313
323
|
}
|
|
314
324
|
|
|
325
|
+
function getExplicitNodeOutputs(node) {
|
|
326
|
+
return Array.isArray(node?.outputs)
|
|
327
|
+
? Array.from(new Set(node.outputs.map((value) => String(value || "").trim()).filter(Boolean)))
|
|
328
|
+
: undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function hydrateWorkflowNode(node, nodeMap) {
|
|
332
|
+
const ports = resolveNodePorts(node);
|
|
333
|
+
const explicitOutputs = getExplicitNodeOutputs(node);
|
|
334
|
+
const nextNode = {
|
|
335
|
+
...node,
|
|
336
|
+
inputPorts: ports.inputs.map((port) => clonePortDescriptor(port)),
|
|
337
|
+
outputPorts: ports.outputs.map((port) => clonePortDescriptor(port)),
|
|
338
|
+
...(explicitOutputs !== undefined ? { outputs: explicitOutputs } : {}),
|
|
339
|
+
};
|
|
340
|
+
nodeMap.set(nextNode.id, nextNode);
|
|
341
|
+
return nextNode;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function buildPortValidationIssue(edge, sourcePort, targetPort, compatibility) {
|
|
345
|
+
return {
|
|
346
|
+
edgeId: edge.id || `${edge.source}->${edge.target}`,
|
|
347
|
+
source: edge.source,
|
|
348
|
+
target: edge.target,
|
|
349
|
+
sourcePort: sourcePort?.name || "default",
|
|
350
|
+
targetPort: targetPort?.name || "default",
|
|
351
|
+
sourceType: sourcePort?.type || null,
|
|
352
|
+
targetType: targetPort?.type || null,
|
|
353
|
+
severity: "error",
|
|
354
|
+
message: compatibility.reason,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function hydrateWorkflowEdge(edge, nodeMap, issues) {
|
|
359
|
+
const sourceNode = nodeMap.get(edge.source);
|
|
360
|
+
const targetNode = nodeMap.get(edge.target);
|
|
361
|
+
const sourcePorts = resolveNodePorts(sourceNode);
|
|
362
|
+
const targetPorts = resolveNodePorts(targetNode);
|
|
363
|
+
const sourcePort = resolvePortByName(sourcePorts.outputs, edge.sourcePort || "default", "output");
|
|
364
|
+
const targetPort = resolvePortByName(targetPorts.inputs, edge.targetPort || "default", "input");
|
|
365
|
+
const compatibility = isPortConnectionCompatible(sourcePort, targetPort);
|
|
366
|
+
|
|
367
|
+
if (!compatibility.compatible) {
|
|
368
|
+
issues.push(buildPortValidationIssue(edge, sourcePort, targetPort, compatibility));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
...edge,
|
|
373
|
+
sourcePort: sourcePort?.name || String(edge.sourcePort || "default").trim() || "default",
|
|
374
|
+
targetPort: targetPort?.name || String(edge.targetPort || "default").trim() || "default",
|
|
375
|
+
sourcePortType: sourcePort?.type || null,
|
|
376
|
+
targetPortType: targetPort?.type || null,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
315
380
|
function hydrateWorkflowDefinition(def, { strict = false } = {}) {
|
|
316
381
|
const normalized = {
|
|
317
382
|
...(def || {}),
|
|
@@ -321,55 +386,10 @@ function hydrateWorkflowDefinition(def, { strict = false } = {}) {
|
|
|
321
386
|
};
|
|
322
387
|
|
|
323
388
|
const nodeMap = new Map();
|
|
324
|
-
normalized.nodes = normalized.nodes.map((node) =>
|
|
325
|
-
const ports = resolveNodePorts(node);
|
|
326
|
-
const explicitOutputs = Array.isArray(node?.outputs)
|
|
327
|
-
? Array.from(new Set(node.outputs.map((value) => String(value || "").trim()).filter(Boolean)))
|
|
328
|
-
: undefined;
|
|
329
|
-
const nextNode = {
|
|
330
|
-
...node,
|
|
331
|
-
inputPorts: ports.inputs.map((port) => clonePortDescriptor(port)),
|
|
332
|
-
outputPorts: ports.outputs.map((port) => clonePortDescriptor(port)),
|
|
333
|
-
...(explicitOutputs !== undefined ? { outputs: explicitOutputs } : {}),
|
|
334
|
-
};
|
|
335
|
-
nodeMap.set(nextNode.id, nextNode);
|
|
336
|
-
return nextNode;
|
|
337
|
-
});
|
|
389
|
+
normalized.nodes = normalized.nodes.map((node) => hydrateWorkflowNode(node, nodeMap));
|
|
338
390
|
|
|
339
391
|
const issues = [];
|
|
340
|
-
|
|
341
|
-
normalized.edges = normalized.edges.map((edge) => {
|
|
342
|
-
const sourceNode = nodeMap.get(edge.source);
|
|
343
|
-
const targetNode = nodeMap.get(edge.target);
|
|
344
|
-
const sourcePorts = resolveNodePorts(sourceNode);
|
|
345
|
-
const targetPorts = resolveNodePorts(targetNode);
|
|
346
|
-
const sourcePort = resolvePortByName(sourcePorts.outputs, edge.sourcePort || "default", "output");
|
|
347
|
-
const targetPort = resolvePortByName(targetPorts.inputs, edge.targetPort || "default", "input");
|
|
348
|
-
const compatibility = isPortConnectionCompatible(sourcePort, targetPort);
|
|
349
|
-
|
|
350
|
-
if (!compatibility.compatible) {
|
|
351
|
-
issues.push({
|
|
352
|
-
edgeId: edge.id || `${edge.source}->${edge.target}`,
|
|
353
|
-
source: edge.source,
|
|
354
|
-
target: edge.target,
|
|
355
|
-
sourcePort: sourcePort?.name || "default",
|
|
356
|
-
targetPort: targetPort?.name || "default",
|
|
357
|
-
sourceType: sourcePort?.type || null,
|
|
358
|
-
targetType: targetPort?.type || null,
|
|
359
|
-
severity: "error",
|
|
360
|
-
message: compatibility.reason,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return {
|
|
365
|
-
...edge,
|
|
366
|
-
sourcePort: sourcePort?.name || String(edge.sourcePort || "default").trim() || "default",
|
|
367
|
-
targetPort: targetPort?.name || String(edge.targetPort || "default").trim() || "default",
|
|
368
|
-
sourcePortType: sourcePort?.type || null,
|
|
369
|
-
targetPortType: targetPort?.type || null,
|
|
370
|
-
};
|
|
371
|
-
});
|
|
372
|
-
|
|
392
|
+
normalized.edges = normalized.edges.map((edge) => hydrateWorkflowEdge(edge, nodeMap, issues));
|
|
373
393
|
normalized.metadata.validationIssues = issues;
|
|
374
394
|
|
|
375
395
|
if (strict && issues.length > 0) {
|
|
@@ -378,7 +398,6 @@ function hydrateWorkflowDefinition(def, { strict = false } = {}) {
|
|
|
378
398
|
|
|
379
399
|
return normalized;
|
|
380
400
|
}
|
|
381
|
-
|
|
382
401
|
/**
|
|
383
402
|
* Register a node type handler.
|
|
384
403
|
* @param {string} type - Node type identifier (e.g., "trigger.task_low", "action.run_agent")
|
|
@@ -982,7 +1001,16 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
982
1001
|
/** Get a single workflow definition */
|
|
983
1002
|
get(id) {
|
|
984
1003
|
if (!this._loaded) this.load();
|
|
985
|
-
|
|
1004
|
+
const workflowId = String(id || "").trim();
|
|
1005
|
+
if (!workflowId) return null;
|
|
1006
|
+
const exact = this._workflows.get(workflowId);
|
|
1007
|
+
if (exact) return exact;
|
|
1008
|
+
for (const workflow of this._workflows.values()) {
|
|
1009
|
+
if (workflow?.metadata?.installedFrom === workflowId) {
|
|
1010
|
+
return workflow;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return null;
|
|
986
1014
|
}
|
|
987
1015
|
|
|
988
1016
|
/** Save (create or update) a workflow definition */
|
|
@@ -2421,6 +2449,24 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2421
2449
|
}
|
|
2422
2450
|
}
|
|
2423
2451
|
|
|
2452
|
+
const countForwardIncomingEdges = (targetNodeId) =>
|
|
2453
|
+
(def.edges || []).filter((edge) => edge.target === targetNodeId && !edge.backEdge).length;
|
|
2454
|
+
|
|
2455
|
+
const propagateSkippedDependencies = (skippedNodeId) => {
|
|
2456
|
+
if (countForwardIncomingEdges(skippedNodeId) > 1) return;
|
|
2457
|
+
const skippedEdges = adjacency.get(skippedNodeId) || [];
|
|
2458
|
+
for (const skippedEdge of skippedEdges) {
|
|
2459
|
+
if (skippedEdge.backEdge) continue;
|
|
2460
|
+
consumeEdgeDependency(skippedEdge.target, false, {
|
|
2461
|
+
reason: "upstream-skipped",
|
|
2462
|
+
payload: {
|
|
2463
|
+
sourceNodeId: skippedNodeId,
|
|
2464
|
+
edgeId: skippedEdge.id || `${skippedEdge.source}->${skippedEdge.target}`,
|
|
2465
|
+
},
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
|
|
2424
2470
|
const consumeEdgeDependency = (targetNodeId, matched, skipInfo = null) => {
|
|
2425
2471
|
const nextDegree = (inDegree.get(targetNodeId) || 1) - 1;
|
|
2426
2472
|
inDegree.set(targetNodeId, nextDegree);
|
|
@@ -2438,6 +2484,7 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2438
2484
|
executed.add(targetNodeId);
|
|
2439
2485
|
const skippedNode = nodeMap.get(targetNodeId);
|
|
2440
2486
|
console.log(`${TAG} node:SKIPPED ${targetNodeId} (${skippedNode?.type || "?"}) [${skippedNode?.label || ""}] — no satisfied edges`);
|
|
2487
|
+
propagateSkippedDependencies(targetNodeId);
|
|
2441
2488
|
}
|
|
2442
2489
|
}
|
|
2443
2490
|
};
|
|
@@ -2710,6 +2757,51 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2710
2757
|
}
|
|
2711
2758
|
}
|
|
2712
2759
|
|
|
2760
|
+
_getInterruptedOrphanRunCandidates() {
|
|
2761
|
+
if (!existsSync(this.runsDir)) return [];
|
|
2762
|
+
if (MAX_INTERRUPTED_ORPHAN_SCAN_FILES <= 0) return [];
|
|
2763
|
+
|
|
2764
|
+
const cutoffMs = INTERRUPTED_ORPHAN_SCAN_WINDOW_MS > 0
|
|
2765
|
+
? Date.now() - INTERRUPTED_ORPHAN_SCAN_WINDOW_MS
|
|
2766
|
+
: 0;
|
|
2767
|
+
const candidates = [];
|
|
2768
|
+
let totalCandidates = 0;
|
|
2769
|
+
|
|
2770
|
+
try {
|
|
2771
|
+
for (const file of readdirSync(this.runsDir)) {
|
|
2772
|
+
if (
|
|
2773
|
+
extname(file) !== ".json" ||
|
|
2774
|
+
file === "index.json" ||
|
|
2775
|
+
file === ACTIVE_RUNS_INDEX
|
|
2776
|
+
) {
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
const detailPath = resolve(this.runsDir, file);
|
|
2780
|
+
let mtimeMs = 0;
|
|
2781
|
+
try {
|
|
2782
|
+
mtimeMs = statSync(detailPath).mtimeMs || 0;
|
|
2783
|
+
} catch {
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
if (cutoffMs > 0 && mtimeMs < cutoffMs) continue;
|
|
2787
|
+
totalCandidates += 1;
|
|
2788
|
+
candidates.push({ file, detailPath, mtimeMs });
|
|
2789
|
+
}
|
|
2790
|
+
} catch {
|
|
2791
|
+
return [];
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2795
|
+
if (candidates.length > MAX_INTERRUPTED_ORPHAN_SCAN_FILES) {
|
|
2796
|
+
console.warn(
|
|
2797
|
+
`${TAG} Orphan interrupted-run scan limited to ${MAX_INTERRUPTED_ORPHAN_SCAN_FILES} recent files ` +
|
|
2798
|
+
`(${totalCandidates} candidate files in retention window)`,
|
|
2799
|
+
);
|
|
2800
|
+
candidates.length = MAX_INTERRUPTED_ORPHAN_SCAN_FILES;
|
|
2801
|
+
}
|
|
2802
|
+
return candidates;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2713
2805
|
_getRunStuckThresholdMs() {
|
|
2714
2806
|
const raw = Number(process.env.WORKFLOW_RUN_STUCK_THRESHOLD_MS);
|
|
2715
2807
|
if (Number.isFinite(raw) && raw > 0) return raw;
|
|
@@ -3074,17 +3166,14 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
3074
3166
|
}
|
|
3075
3167
|
|
|
3076
3168
|
// Tertiary source: orphan detail files with no index entry and no end marker.
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
for (const file of detailFiles) {
|
|
3083
|
-
const runId = basename(file, ".json");
|
|
3169
|
+
// This is bounded to a recent subset so old archived run details cannot
|
|
3170
|
+
// stall startup when workflow-runs contains thousands of historical files.
|
|
3171
|
+
const orphanCandidates = this._getInterruptedOrphanRunCandidates();
|
|
3172
|
+
for (const candidate of orphanCandidates) {
|
|
3173
|
+
const runId = basename(candidate.file, ".json");
|
|
3084
3174
|
if (!runId || this._activeRuns.has(runId) || runsById.has(runId)) continue;
|
|
3085
|
-
const detailPath = resolve(this.runsDir, file);
|
|
3086
3175
|
try {
|
|
3087
|
-
const detail = JSON.parse(readFileSync(detailPath, "utf8"));
|
|
3176
|
+
const detail = JSON.parse(readFileSync(candidate.detailPath, "utf8"));
|
|
3088
3177
|
const hasRunningNode = Object.values(detail?.nodeStatuses || {}).some(
|
|
3089
3178
|
(status) => status === NodeStatus.RUNNING || status === NodeStatus.WAITING,
|
|
3090
3179
|
);
|