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
|
@@ -10,6 +10,24 @@
|
|
|
10
10
|
let _nextX = 100;
|
|
11
11
|
let _nextY = 100;
|
|
12
12
|
|
|
13
|
+
const TEMPLATE_LAYOUT_DEFAULTS = Object.freeze({
|
|
14
|
+
centerX: 480,
|
|
15
|
+
topY: 80,
|
|
16
|
+
columnGap: 280,
|
|
17
|
+
rowGap: 180,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function toFiniteNumber(value, fallback = 0) {
|
|
21
|
+
return Number.isFinite(value) ? value : fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getNodeOriginalPosition(node, fallbackX = 0) {
|
|
25
|
+
return {
|
|
26
|
+
x: toFiniteNumber(node?.position?.x, fallbackX),
|
|
27
|
+
y: toFiniteNumber(node?.position?.y, 0),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
/**
|
|
14
32
|
* Create a workflow node definition with automatic positioning.
|
|
15
33
|
* @param {string} id - Unique node identifier within the template
|
|
@@ -63,3 +81,139 @@ export function resetLayout() {
|
|
|
63
81
|
_nextX = 100;
|
|
64
82
|
_nextY = 100;
|
|
65
83
|
}
|
|
84
|
+
|
|
85
|
+
export function normalizeTemplateLayoutInPlace(template, opts = {}) {
|
|
86
|
+
const nodes = Array.isArray(template?.nodes) ? template.nodes : [];
|
|
87
|
+
const edges = Array.isArray(template?.edges) ? template.edges : [];
|
|
88
|
+
if (nodes.length <= 1) return template;
|
|
89
|
+
|
|
90
|
+
const centerX = toFiniteNumber(opts.centerX, TEMPLATE_LAYOUT_DEFAULTS.centerX);
|
|
91
|
+
const topY = toFiniteNumber(opts.topY, TEMPLATE_LAYOUT_DEFAULTS.topY);
|
|
92
|
+
const columnGap = Math.max(180, toFiniteNumber(opts.columnGap, TEMPLATE_LAYOUT_DEFAULTS.columnGap));
|
|
93
|
+
const rowGap = Math.max(120, toFiniteNumber(opts.rowGap, TEMPLATE_LAYOUT_DEFAULTS.rowGap));
|
|
94
|
+
|
|
95
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
96
|
+
const originalIndex = new Map(nodes.map((node, index) => [node.id, index]));
|
|
97
|
+
const originalPosition = new Map(
|
|
98
|
+
nodes.map((node, index) => [node.id, getNodeOriginalPosition(node, index * columnGap)]),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const incoming = new Map(nodes.map((node) => [node.id, []]));
|
|
102
|
+
const outgoing = new Map(nodes.map((node) => [node.id, []]));
|
|
103
|
+
for (const edgeDef of edges) {
|
|
104
|
+
if (!edgeDef || edgeDef.backEdge === true) continue;
|
|
105
|
+
const source = String(edgeDef.source || "").trim();
|
|
106
|
+
const target = String(edgeDef.target || "").trim();
|
|
107
|
+
if (!nodeIds.has(source) || !nodeIds.has(target)) continue;
|
|
108
|
+
outgoing.get(source).push(target);
|
|
109
|
+
incoming.get(target).push(source);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sortedNodeIds = [...nodeIds].sort((left, right) => {
|
|
113
|
+
const leftPos = originalPosition.get(left) || { x: 0, y: 0 };
|
|
114
|
+
const rightPos = originalPosition.get(right) || { x: 0, y: 0 };
|
|
115
|
+
if (leftPos.y !== rightPos.y) return leftPos.y - rightPos.y;
|
|
116
|
+
if (leftPos.x !== rightPos.x) return leftPos.x - rightPos.x;
|
|
117
|
+
return (originalIndex.get(left) || 0) - (originalIndex.get(right) || 0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const rootIds = sortedNodeIds.filter((nodeId) => {
|
|
121
|
+
const node = nodes[originalIndex.get(nodeId) || 0];
|
|
122
|
+
return (incoming.get(nodeId)?.length || 0) === 0 || String(node?.type || "").startsWith("trigger.");
|
|
123
|
+
});
|
|
124
|
+
const queue = rootIds.length > 0 ? [...rootIds] : [sortedNodeIds[0]];
|
|
125
|
+
const indegree = new Map([...incoming.entries()].map(([nodeId, parents]) => [nodeId, parents.length]));
|
|
126
|
+
const depthById = new Map(queue.map((nodeId) => [nodeId, 0]));
|
|
127
|
+
const queued = new Set(queue);
|
|
128
|
+
|
|
129
|
+
for (const nodeId of sortedNodeIds) {
|
|
130
|
+
if ((indegree.get(nodeId) || 0) === 0 && !queued.has(nodeId)) {
|
|
131
|
+
queue.push(nodeId);
|
|
132
|
+
queued.add(nodeId);
|
|
133
|
+
depthById.set(nodeId, 0);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
while (queue.length > 0) {
|
|
138
|
+
const nodeId = queue.shift();
|
|
139
|
+
const currentDepth = depthById.get(nodeId) || 0;
|
|
140
|
+
for (const targetId of outgoing.get(nodeId) || []) {
|
|
141
|
+
const nextDepth = currentDepth + 1;
|
|
142
|
+
if (nextDepth > (depthById.get(targetId) || 0)) {
|
|
143
|
+
depthById.set(targetId, nextDepth);
|
|
144
|
+
}
|
|
145
|
+
indegree.set(targetId, (indegree.get(targetId) || 0) - 1);
|
|
146
|
+
if ((indegree.get(targetId) || 0) <= 0 && !queued.has(targetId)) {
|
|
147
|
+
queue.push(targetId);
|
|
148
|
+
queued.add(targetId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const unresolved = sortedNodeIds.filter((nodeId) => !depthById.has(nodeId));
|
|
154
|
+
while (unresolved.length > 0) {
|
|
155
|
+
let progressed = false;
|
|
156
|
+
for (let index = unresolved.length - 1; index >= 0; index -= 1) {
|
|
157
|
+
const nodeId = unresolved[index];
|
|
158
|
+
const parents = incoming.get(nodeId) || [];
|
|
159
|
+
if (parents.every((parentId) => depthById.has(parentId))) {
|
|
160
|
+
const nextDepth = parents.length > 0
|
|
161
|
+
? Math.max(...parents.map((parentId) => depthById.get(parentId) || 0)) + 1
|
|
162
|
+
: 0;
|
|
163
|
+
depthById.set(nodeId, nextDepth);
|
|
164
|
+
unresolved.splice(index, 1);
|
|
165
|
+
progressed = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (progressed) continue;
|
|
169
|
+
const nodeId = unresolved.shift();
|
|
170
|
+
const parents = incoming.get(nodeId) || [];
|
|
171
|
+
const knownParentDepths = parents
|
|
172
|
+
.map((parentId) => depthById.get(parentId))
|
|
173
|
+
.filter((depth) => Number.isFinite(depth));
|
|
174
|
+
const fallbackDepth = knownParentDepths.length > 0
|
|
175
|
+
? Math.max(...knownParentDepths) + 1
|
|
176
|
+
: 0;
|
|
177
|
+
depthById.set(nodeId, fallbackDepth);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const layerMap = new Map();
|
|
181
|
+
for (const nodeId of sortedNodeIds) {
|
|
182
|
+
const depth = depthById.get(nodeId) || 0;
|
|
183
|
+
if (!layerMap.has(depth)) layerMap.set(depth, []);
|
|
184
|
+
layerMap.get(depth).push(nodeId);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const assignedX = new Map();
|
|
188
|
+
const sortedLayers = [...layerMap.keys()].sort((left, right) => left - right);
|
|
189
|
+
for (const depth of sortedLayers) {
|
|
190
|
+
const layerNodeIds = layerMap.get(depth) || [];
|
|
191
|
+
layerNodeIds.sort((left, right) => {
|
|
192
|
+
const leftParents = (incoming.get(left) || []).filter((parentId) => assignedX.has(parentId));
|
|
193
|
+
const rightParents = (incoming.get(right) || []).filter((parentId) => assignedX.has(parentId));
|
|
194
|
+
const leftBarycenter = leftParents.length > 0
|
|
195
|
+
? leftParents.reduce((sum, parentId) => sum + assignedX.get(parentId), 0) / leftParents.length
|
|
196
|
+
: (originalPosition.get(left)?.x || 0);
|
|
197
|
+
const rightBarycenter = rightParents.length > 0
|
|
198
|
+
? rightParents.reduce((sum, parentId) => sum + assignedX.get(parentId), 0) / rightParents.length
|
|
199
|
+
: (originalPosition.get(right)?.x || 0);
|
|
200
|
+
if (leftBarycenter !== rightBarycenter) return leftBarycenter - rightBarycenter;
|
|
201
|
+
const leftPos = originalPosition.get(left) || { x: 0, y: 0 };
|
|
202
|
+
const rightPos = originalPosition.get(right) || { x: 0, y: 0 };
|
|
203
|
+
if (leftPos.x !== rightPos.x) return leftPos.x - rightPos.x;
|
|
204
|
+
if (leftPos.y !== rightPos.y) return leftPos.y - rightPos.y;
|
|
205
|
+
return (originalIndex.get(left) || 0) - (originalIndex.get(right) || 0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const startX = centerX - ((layerNodeIds.length - 1) * columnGap) / 2;
|
|
209
|
+
layerNodeIds.forEach((nodeId, index) => {
|
|
210
|
+
const node = nodes[originalIndex.get(nodeId) || 0];
|
|
211
|
+
const x = Math.round(startX + index * columnGap);
|
|
212
|
+
const y = topY + depth * rowGap;
|
|
213
|
+
node.position = { x, y };
|
|
214
|
+
assignedX.set(nodeId, x);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return template;
|
|
219
|
+
}
|
|
@@ -13,6 +13,63 @@
|
|
|
13
13
|
|
|
14
14
|
import { node, edge, resetLayout } from "./_helpers.mjs";
|
|
15
15
|
|
|
16
|
+
const AGENT_SESSION_MONITOR_COMMAND = [
|
|
17
|
+
'node -e "',
|
|
18
|
+
"const fs = require('node:fs');",
|
|
19
|
+
"const path = require('node:path');",
|
|
20
|
+
"const { pathToFileURL } = require('node:url');",
|
|
21
|
+
"const cwd = process.cwd();",
|
|
22
|
+
"const mirrorMarker = `${path.sep}.bosun${path.sep}workspaces${path.sep}`.toLowerCase();",
|
|
23
|
+
"let repoRoot = cwd;",
|
|
24
|
+
"if (cwd.toLowerCase().includes(mirrorMarker)) {",
|
|
25
|
+
"const sourceRepoRoot = path.resolve(cwd, '..', '..', '..', '..');",
|
|
26
|
+
"if (fs.existsSync(path.join(sourceRepoRoot, 'infra', 'session-tracker.mjs'))) repoRoot = sourceRepoRoot;",
|
|
27
|
+
"}",
|
|
28
|
+
"const trackerModuleUrl = pathToFileURL(path.join(repoRoot, 'infra', 'session-tracker.mjs')).href;",
|
|
29
|
+
"import(trackerModuleUrl).then(({ getSessionTracker }) => {",
|
|
30
|
+
"const tracker = getSessionTracker();",
|
|
31
|
+
"const sessions = tracker.getActiveSessions().map((session) => {",
|
|
32
|
+
"const progress = tracker.getProgressStatus(session.taskId);",
|
|
33
|
+
"return {",
|
|
34
|
+
"id: session.taskId,",
|
|
35
|
+
"taskId: session.taskId,",
|
|
36
|
+
"taskTitle: session.taskTitle || null,",
|
|
37
|
+
"status: progress.status,",
|
|
38
|
+
"idleMs: progress.idleMs,",
|
|
39
|
+
"totalEvents: progress.totalEvents,",
|
|
40
|
+
"elapsedMs: progress.elapsedMs,",
|
|
41
|
+
"lastEventType: progress.lastEventType,",
|
|
42
|
+
"recommendation: progress.recommendation,",
|
|
43
|
+
"tokenPercent: null",
|
|
44
|
+
"};",
|
|
45
|
+
"});",
|
|
46
|
+
"console.log(JSON.stringify(sessions));",
|
|
47
|
+
"}).catch((err) => { console.error(err?.stack || String(err)); process.exit(1); });",
|
|
48
|
+
'"',
|
|
49
|
+
].join("");
|
|
50
|
+
|
|
51
|
+
const buildAgentSessionMonitorParseExpression = (rawExpression) => [
|
|
52
|
+
"(() => {",
|
|
53
|
+
`const raw = String(${rawExpression} || '[]');`,
|
|
54
|
+
"const lines = raw.split(/\\r?\\n/).map((line) => line.trim()).filter(Boolean);",
|
|
55
|
+
"const candidate = lines.length ? lines[lines.length - 1] : '[]';",
|
|
56
|
+
"try {",
|
|
57
|
+
"const parsed = JSON.parse(candidate);",
|
|
58
|
+
"return Array.isArray(parsed) ? parsed : [];",
|
|
59
|
+
"} catch {",
|
|
60
|
+
"return [];",
|
|
61
|
+
"}",
|
|
62
|
+
"})()",
|
|
63
|
+
].join("");
|
|
64
|
+
|
|
65
|
+
const AGENT_SESSION_MONITOR_LIST_PARSE_EXPRESSION = buildAgentSessionMonitorParseExpression(
|
|
66
|
+
"$ctx.getNodeOutput('list-sessions')?.output",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const AGENT_SESSION_MONITOR_HEALTH_PARSE_EXPRESSION = buildAgentSessionMonitorParseExpression(
|
|
70
|
+
"$ctx.getNodeOutput('check-health')?.output",
|
|
71
|
+
);
|
|
72
|
+
|
|
16
73
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
74
|
// Frontend Agent with Screenshot Validation
|
|
18
75
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -368,21 +425,21 @@ export const AGENT_SESSION_MONITOR_TEMPLATE = {
|
|
|
368
425
|
}, { x: 400, y: 50 }),
|
|
369
426
|
|
|
370
427
|
node("list-sessions", "action.run_command", "List Active Sessions", {
|
|
371
|
-
command:
|
|
428
|
+
command: AGENT_SESSION_MONITOR_COMMAND,
|
|
372
429
|
continueOnError: true,
|
|
373
430
|
}, { x: 400, y: 200 }),
|
|
374
431
|
|
|
375
432
|
node("has-active", "condition.expression", "Any Active?", {
|
|
376
|
-
expression:
|
|
433
|
+
expression: `${AGENT_SESSION_MONITOR_LIST_PARSE_EXPRESSION}.length > 0`,
|
|
377
434
|
}, { x: 400, y: 340 }),
|
|
378
435
|
|
|
379
436
|
node("check-health", "action.run_command", "Check Session Health", {
|
|
380
|
-
command:
|
|
437
|
+
command: AGENT_SESSION_MONITOR_COMMAND,
|
|
381
438
|
continueOnError: true,
|
|
382
439
|
}, { x: 200, y: 490 }),
|
|
383
440
|
|
|
384
441
|
node("has-issues", "condition.expression", "Any Unhealthy?", {
|
|
385
|
-
expression:
|
|
442
|
+
expression: `(() => { const sessions = ${AGENT_SESSION_MONITOR_HEALTH_PARSE_EXPRESSION}; const maxIdleMs = Number($data?.maxIdleMs || 600000); const maxTokenPercent = Number($data?.maxTokenPercent || 85); return sessions.some((item) => { const status = String(item?.status || '').toLowerCase(); const idleMs = Number(item?.idleMs || 0); const tokenPercent = Number(item?.tokenPercent); return status === 'idle' || status === 'stalled' || status === 'timeout' || idleMs > maxIdleMs || (Number.isFinite(tokenPercent) && tokenPercent >= maxTokenPercent); }); })()`,
|
|
386
443
|
}, { x: 200, y: 640 }),
|
|
387
444
|
|
|
388
445
|
node("auto-continue", "action.continue_session", "Auto-Continue Stalled", {
|
|
@@ -34,7 +34,7 @@ export const CODE_QUALITY_STRIKER_TEMPLATE = {
|
|
|
34
34
|
trigger: "trigger.schedule",
|
|
35
35
|
variables: {
|
|
36
36
|
sessionTimeoutMs: 5400000, // 90 minutes hard cap
|
|
37
|
-
branch: "chore/code-quality-striker
|
|
37
|
+
branch: "chore/code-quality-striker",
|
|
38
38
|
baseBranch: "main",
|
|
39
39
|
sessionLogPath: ".bosun-monitor/code-quality-striker.md",
|
|
40
40
|
maxFilesPerSession: 6, // keep PRs reviewable; prevents mega-diffs
|
|
@@ -184,7 +184,7 @@ If tests fail, **revert your change** (\`git checkout -- <file>\`) and either:
|
|
|
184
184
|
|
|
185
185
|
### Step 5 — Commit, push, and open the PR
|
|
186
186
|
|
|
187
|
-
Branch name: \`
|
|
187
|
+
Branch name: \`{{branch}}\`
|
|
188
188
|
Base branch: \`{{baseBranch}}\`
|
|
189
189
|
|
|
190
190
|
Commit message format:
|
|
@@ -203,7 +203,7 @@ PR body template:
|
|
|
203
203
|
\`\`\`markdown
|
|
204
204
|
## Code Quality Pass
|
|
205
205
|
|
|
206
|
-
**Session**:
|
|
206
|
+
**Session**: {{branch}}
|
|
207
207
|
**Scope**: structural refactor only — zero functional changes
|
|
208
208
|
|
|
209
209
|
### Changes
|
|
@@ -266,7 +266,7 @@ A small, clean, tested PR is always better than nothing.`,
|
|
|
266
266
|
|
|
267
267
|
// ── 7a. Create PR ──────────────────────────────────────────────────────
|
|
268
268
|
node("create-pr", "action.create_pr", "Open Quality PR", {
|
|
269
|
-
title: "refactor: code quality pass
|
|
269
|
+
title: "refactor: code quality pass",
|
|
270
270
|
body: "Automated code-quality session. Structural refactor only — zero functional changes. See `.bosun-monitor/code-quality-striker.md` for session details.",
|
|
271
271
|
branch: "{{branch}}",
|
|
272
272
|
baseBranch: "{{baseBranch}}",
|
|
@@ -274,17 +274,17 @@ A small, clean, tested PR is always better than nothing.`,
|
|
|
274
274
|
}, { x: 200, y: 890 }),
|
|
275
275
|
|
|
276
276
|
node("notify-success", "notify.telegram", "Notify PR Opened", {
|
|
277
|
-
message: ":check: Code quality striker session complete.\nPR opened: **{{branch}}
|
|
277
|
+
message: ":check: Code quality striker session complete.\nPR opened: **{{branch}}**",
|
|
278
278
|
silent: true,
|
|
279
279
|
}, { x: 200, y: 1030 }),
|
|
280
280
|
|
|
281
281
|
// ── 7b. Validation failed — notify and abort ───────────────────────────
|
|
282
282
|
node("notify-failure", "notify.telegram", "Notify — Validation Failed", {
|
|
283
|
-
message: ":alert: Code quality striker **validation failed
|
|
283
|
+
message: ":alert: Code quality striker **validation failed**.\n\nThe agent produced changes that broke tests or build. No PR was created.\nCheck `.bosun-monitor/code-quality-striker.md` for details.",
|
|
284
284
|
}, { x: 600, y: 890 }),
|
|
285
285
|
|
|
286
286
|
node("log-failure", "notify.log", "Log Failure", {
|
|
287
|
-
message: "Code quality striker
|
|
287
|
+
message: "Code quality striker validation failed — no PR created.",
|
|
288
288
|
level: "warn",
|
|
289
289
|
}, { x: 600, y: 1030 }),
|
|
290
290
|
],
|
|
@@ -102,7 +102,7 @@ Respond with JSON: { "action": "<choice>", "reason": "<why>", "message": "<optio
|
|
|
102
102
|
}, { x: 400, y: 520, outputs: ["merge", "prompt-agent", "close", "retry", "escalate", "wait-for-ci", "default"] }),
|
|
103
103
|
|
|
104
104
|
node("do-merge", "action.run_command", "Auto-Merge PR", {
|
|
105
|
-
command: "gh pr merge {{prNumber}} --auto --
|
|
105
|
+
command: "gh pr merge {{prNumber}} --auto --merge",
|
|
106
106
|
failOnError: true,
|
|
107
107
|
maxRetries: "{{maxRetries}}",
|
|
108
108
|
retryDelayMs: 30000,
|
|
@@ -698,7 +698,7 @@ export const BOSUN_PR_PROGRESSOR_TEMPLATE = {
|
|
|
698
698
|
recommended: true,
|
|
699
699
|
trigger: "trigger.workflow_call",
|
|
700
700
|
variables: {
|
|
701
|
-
mergeMethod: "
|
|
701
|
+
mergeMethod: "merge",
|
|
702
702
|
labelNeedsFix: "bosun-needs-fix",
|
|
703
703
|
labelNeedsReview: "bosun-needs-human-review",
|
|
704
704
|
suspiciousDeletionRatio: 3,
|
|
@@ -889,7 +889,7 @@ export const BOSUN_PR_PROGRESSOR_TEMPLATE = {
|
|
|
889
889
|
"const ratio=Number('{{suspiciousDeletionRatio}}')||3;",
|
|
890
890
|
"const minDel=Number('{{minDestructiveDeletions}}')||500;",
|
|
891
891
|
"const labelReview=String('{{labelNeedsReview}}'||'bosun-needs-human-review');",
|
|
892
|
-
"const method=String('{{mergeMethod}}'||'
|
|
892
|
+
"const method=String('{{mergeMethod}}'||'merge').toLowerCase();",
|
|
893
893
|
"function gh(args){return execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();}",
|
|
894
894
|
"if(!repo||!n){console.log(JSON.stringify({mergedCount:0,heldCount:0,skippedCount:1,skipped:[{repo,number:n,reason:'missing_repo_or_pr'}]}));process.exit(0);}",
|
|
895
895
|
"try{",
|
|
@@ -1015,7 +1015,7 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
1015
1015
|
recommended: true,
|
|
1016
1016
|
trigger: "trigger.schedule",
|
|
1017
1017
|
variables: {
|
|
1018
|
-
mergeMethod: "
|
|
1018
|
+
mergeMethod: "merge", // merge | squash | rebase
|
|
1019
1019
|
labelNeedsFix: "bosun-needs-fix", // applied to CI failures and conflicts
|
|
1020
1020
|
labelNeedsReview: "bosun-needs-human-review", // applied when review agent flags a suspicious diff
|
|
1021
1021
|
// auto: active workspace repos from bosun.config.json (fallback current repo)
|
|
@@ -1027,6 +1027,7 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
1027
1027
|
// If net deletions > additions × ratio AND deletions > minDestructiveDeletions → HOLD
|
|
1028
1028
|
suspiciousDeletionRatio: 3, // e.g. deletes 3× more lines than it adds
|
|
1029
1029
|
minDestructiveDeletions: 500, // absolute floor — small PRs are fine even if net negative
|
|
1030
|
+
autoApplySuggestions: true, // auto-commit review suggestions before merge
|
|
1030
1031
|
},
|
|
1031
1032
|
nodes: [
|
|
1032
1033
|
node("trigger", "trigger.schedule", "Poll Every 90s", {
|
|
@@ -1429,7 +1430,7 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
1429
1430
|
"const ratio=Number('{{suspiciousDeletionRatio}}')||3;",
|
|
1430
1431
|
"const minDel=Number('{{minDestructiveDeletions}}')||500;",
|
|
1431
1432
|
"const labelReview=String('{{labelNeedsReview}}'||'bosun-needs-human-review');",
|
|
1432
|
-
"const method=String('{{mergeMethod}}'||'
|
|
1433
|
+
"const method=String('{{mergeMethod}}'||'merge').toLowerCase();",
|
|
1433
1434
|
"const merged=[]; const held=[]; const skipped=[];",
|
|
1434
1435
|
"function gh(args){return execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();}",
|
|
1435
1436
|
"for(const c of candidates){",
|
|
@@ -1464,6 +1465,17 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
1464
1465
|
" });",
|
|
1465
1466
|
" if(hasFailure){skipped.push({repo,number:n,reason:'ci_failed'});continue;}",
|
|
1466
1467
|
" if(hasPending){skipped.push({repo,number:n,reason:'ci_pending'});continue;}",
|
|
1468
|
+
" const doApplySuggestions=String('{{autoApplySuggestions}}'||'true')==='true'&&process.env.BOSUN_AUTO_APPLY_SUGGESTIONS!=='false';",
|
|
1469
|
+
" if(doApplySuggestions){",
|
|
1470
|
+
" try{",
|
|
1471
|
+
" const toolPath=require('path').resolve(process.cwd(),'tools','apply-pr-suggestions.mjs');",
|
|
1472
|
+
" if(require('fs').existsSync(toolPath)){",
|
|
1473
|
+
" const sugOut=execFileSync('node',[toolPath,'--owner',repo.split('/')[0],'--repo',repo.split('/')[1],n,'--json'],{encoding:'utf8',timeout:60000,stdio:['pipe','pipe','pipe']});",
|
|
1474
|
+
" const sugRes=(()=>{try{return JSON.parse(sugOut)}catch{return null}})();",
|
|
1475
|
+
" if(sugRes?.commitSha) console.error('[watchdog] auto-applied '+sugRes.applied+' suggestion(s) on PR #'+n+' → '+sugRes.commitSha.slice(0,8));",
|
|
1476
|
+
" }",
|
|
1477
|
+
" }catch(sugErr){console.error('[watchdog] suggestion auto-apply skipped for PR #'+n+': '+String(sugErr?.message||sugErr).slice(0,120));}",
|
|
1478
|
+
" }",
|
|
1467
1479
|
" const mergeArgs=['pr','merge',n,'--repo',repo,'--delete-branch'];",
|
|
1468
1480
|
" if(method==='rebase') mergeArgs.push('--rebase');",
|
|
1469
1481
|
" else if(method==='merge') mergeArgs.push('--merge');",
|
|
@@ -1488,11 +1500,10 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
1488
1500
|
},
|
|
1489
1501
|
}, { x: 600, y: 700 }),
|
|
1490
1502
|
|
|
1491
|
-
node("notify", "notify.
|
|
1503
|
+
node("notify", "notify.log", "Watchdog Report", {
|
|
1492
1504
|
message:
|
|
1493
|
-
"
|
|
1494
|
-
|
|
1495
|
-
silent: true,
|
|
1505
|
+
"Bosun PR Watchdog cycle complete — see live digest/status board for streaming updates",
|
|
1506
|
+
level: "info",
|
|
1496
1507
|
}, { x: 400, y: 900 }),
|
|
1497
1508
|
|
|
1498
1509
|
node("no-prs", "notify.log", "No Bosun PRs Open", {
|
|
@@ -1992,4 +2003,3 @@ export const SDK_CONFLICT_RESOLVER_TEMPLATE = {
|
|
|
1992
2003
|
},
|
|
1993
2004
|
};
|
|
1994
2005
|
|
|
1995
|
-
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* → loop.for_each (fan-out, maxConcurrent tasks at a time)
|
|
17
17
|
* → sub-workflow: template-task-lifecycle per task
|
|
18
18
|
* → action.set_variable (record batch results)
|
|
19
|
-
* →
|
|
19
|
+
* → condition.expression (any failures?)
|
|
20
|
+
* → notify.telegram (failure alert)
|
|
21
|
+
* → notify.log (summary)
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
import { node, edge, resetLayout } from "./_helpers.mjs";
|
|
@@ -64,7 +66,18 @@ export const TASK_BATCH_PROCESSOR_TEMPLATE = {
|
|
|
64
66
|
node("query-tasks", "action.run_command", "Query Task Backlog", {
|
|
65
67
|
command: "node",
|
|
66
68
|
args: ["-e", `
|
|
67
|
-
|
|
69
|
+
const fs = require("node:fs");
|
|
70
|
+
const path = require("node:path");
|
|
71
|
+
const { pathToFileURL } = require("node:url");
|
|
72
|
+
const cwd = process.cwd();
|
|
73
|
+
const mirrorMarker = (path.sep + ".bosun" + path.sep + "workspaces" + path.sep).toLowerCase();
|
|
74
|
+
let repoRoot = cwd;
|
|
75
|
+
if (cwd.toLowerCase().includes(mirrorMarker)) {
|
|
76
|
+
const sourceRepoRoot = path.resolve(cwd, "..", "..", "..", "..");
|
|
77
|
+
if (fs.existsSync(path.join(sourceRepoRoot, "kanban", "kanban-adapter.mjs"))) repoRoot = sourceRepoRoot;
|
|
78
|
+
}
|
|
79
|
+
const kanbanModuleUrl = pathToFileURL(path.join(repoRoot, "kanban", "kanban-adapter.mjs")).href;
|
|
80
|
+
import(kanbanModuleUrl)
|
|
68
81
|
.then(k => k.listTasks(undefined, { status: "todo" }))
|
|
69
82
|
.then(tasks => {
|
|
70
83
|
const filtered = (tasks || []).filter((task) => {
|
|
@@ -110,11 +123,19 @@ export const TASK_BATCH_PROCESSOR_TEMPLATE = {
|
|
|
110
123
|
value: "{{dispatch-tasks}}",
|
|
111
124
|
}, { x: 400, y: 570 }),
|
|
112
125
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
channel: "{{notifyChannel}}",
|
|
116
|
-
message: "Task batch completed: {{dispatch-tasks.successCount}}/{{dispatch-tasks.totalItems}} succeeded ({{dispatch-tasks.failCount}} failed)",
|
|
126
|
+
node("has-batch-failures", "condition.expression", "Any Batch Failures?", {
|
|
127
|
+
expression: "Number($data?.batchResult?.failCount || 0) > 0",
|
|
117
128
|
}, { x: 400, y: 700 }),
|
|
129
|
+
|
|
130
|
+
node("notify-failures", "notify.telegram", "Batch Failure Alert", {
|
|
131
|
+
channel: "{{notifyChannel}}",
|
|
132
|
+
message: "Task batch needs attention: {{batchResult.failCount}} failed out of {{batchResult.totalItems}} ({{batchResult.successCount}} succeeded)",
|
|
133
|
+
}, { x: 220, y: 830 }),
|
|
134
|
+
|
|
135
|
+
node("log-summary", "notify.log", "Batch Summary", {
|
|
136
|
+
message: "Task batch completed: {{batchResult.successCount}}/{{batchResult.totalItems}} succeeded ({{batchResult.failCount}} failed)",
|
|
137
|
+
level: "info",
|
|
138
|
+
}, { x: 580, y: 830 }),
|
|
118
139
|
],
|
|
119
140
|
edges: [
|
|
120
141
|
edge("trigger", "check-coordinator"),
|
|
@@ -122,7 +143,9 @@ export const TASK_BATCH_PROCESSOR_TEMPLATE = {
|
|
|
122
143
|
edge("query-tasks", "dispatch-tasks"),
|
|
123
144
|
edge("dispatch-tasks", "join-dispatch"),
|
|
124
145
|
edge("join-dispatch", "record-results"),
|
|
125
|
-
edge("record-results", "
|
|
146
|
+
edge("record-results", "has-batch-failures"),
|
|
147
|
+
edge("has-batch-failures", "notify-failures", { condition: "$output?.result === true" }),
|
|
148
|
+
edge("has-batch-failures", "log-summary", { condition: "$output?.result !== true" }),
|
|
126
149
|
],
|
|
127
150
|
metadata: {
|
|
128
151
|
author: "bosun",
|
|
@@ -156,7 +179,6 @@ export const TASK_BATCH_PR_TEMPLATE = {
|
|
|
156
179
|
maxBatchSize: 5,
|
|
157
180
|
defaultBaseBranch: "main",
|
|
158
181
|
draftPR: true,
|
|
159
|
-
notifyChannel: "telegram",
|
|
160
182
|
},
|
|
161
183
|
nodes: [
|
|
162
184
|
// ── Trigger ──────────────────────────────────────────────────────────
|
|
@@ -170,7 +192,18 @@ export const TASK_BATCH_PR_TEMPLATE = {
|
|
|
170
192
|
node("query-tasks", "action.run_command", "List Todo Tasks", {
|
|
171
193
|
command: "node",
|
|
172
194
|
args: ["-e", `
|
|
173
|
-
|
|
195
|
+
const fs = require("node:fs");
|
|
196
|
+
const path = require("node:path");
|
|
197
|
+
const { pathToFileURL } = require("node:url");
|
|
198
|
+
const cwd = process.cwd();
|
|
199
|
+
const mirrorMarker = (path.sep + ".bosun" + path.sep + "workspaces" + path.sep).toLowerCase();
|
|
200
|
+
let repoRoot = cwd;
|
|
201
|
+
if (cwd.toLowerCase().includes(mirrorMarker)) {
|
|
202
|
+
const sourceRepoRoot = path.resolve(cwd, "..", "..", "..", "..");
|
|
203
|
+
if (fs.existsSync(path.join(sourceRepoRoot, "kanban", "kanban-adapter.mjs"))) repoRoot = sourceRepoRoot;
|
|
204
|
+
}
|
|
205
|
+
const kanbanModuleUrl = pathToFileURL(path.join(repoRoot, "kanban", "kanban-adapter.mjs")).href;
|
|
206
|
+
import(kanbanModuleUrl)
|
|
174
207
|
.then(k => k.listTasks(undefined, { status: "todo" }))
|
|
175
208
|
.then(tasks => {
|
|
176
209
|
const filtered = (tasks || []).filter((task) => {
|
|
@@ -262,9 +295,9 @@ export const TASK_BATCH_PR_TEMPLATE = {
|
|
|
262
295
|
}, { x: 400, y: 1230 }),
|
|
263
296
|
|
|
264
297
|
// ── Batch complete notification ──────────────────────────────────────
|
|
265
|
-
node("notify", "notify.
|
|
266
|
-
channel: "{{notifyChannel}}",
|
|
298
|
+
node("notify", "notify.log", "Batch Complete", {
|
|
267
299
|
message: "Task batch PR pipeline complete",
|
|
300
|
+
level: "info",
|
|
268
301
|
}, { x: 400, y: 1220 }),
|
|
269
302
|
],
|
|
270
303
|
edges: [
|
|
@@ -431,19 +431,41 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
431
431
|
taskId: "{{taskId}}",
|
|
432
432
|
}, { x: 600, y: 1090 }),
|
|
433
433
|
|
|
434
|
+
node("wt-failure-blocking", "condition.expression", "Non-Retryable WT Failure?", {
|
|
435
|
+
expression: "$ctx.getNodeOutput('acquire-worktree')?.retryable === false",
|
|
436
|
+
}, { x: 600, y: 1220, outputs: ["yes", "no"] }),
|
|
437
|
+
|
|
438
|
+
node("set-blocked-wt-failed", "action.update_task_status", "Set Blocked (WT Fail)", {
|
|
439
|
+
taskId: "{{taskId}}",
|
|
440
|
+
status: "blocked",
|
|
441
|
+
taskTitle: "{{taskTitle}}",
|
|
442
|
+
}, { x: 470, y: 1350 }),
|
|
443
|
+
|
|
444
|
+
node("annotate-blocked-wt-failed", "action.bosun_function", "Annotate Blocked (WT Fail)", {
|
|
445
|
+
function: "tasks.update",
|
|
446
|
+
args: {
|
|
447
|
+
taskId: "{{taskId}}",
|
|
448
|
+
fields: {
|
|
449
|
+
cooldownUntil: "{{acquire-worktree.retryAt}}",
|
|
450
|
+
blockedReason: "{{acquire-worktree.blockedReason}}",
|
|
451
|
+
meta: "{{(() => { const current = ($data.taskMeta && typeof $data.taskMeta === 'object') ? $data.taskMeta : {}; const output = $ctx.getNodeOutput('acquire-worktree') || {}; return { ...current, autoRecovery: { active: true, reason: 'worktree_failure', failureKind: output.failureKind || 'branch_refresh_conflict', retryAt: output.retryAt || null, recoveryDelayMs: output.autoRecoverDelayMs || null, error: output.error || '', recordedAt: output.recordedAt || null }, worktreeFailure: { failureKind: output.failureKind || 'branch_refresh_conflict', retryable: output.retryable !== false, retryAt: output.retryAt || null, blockedReason: output.blockedReason || '', error: output.error || '', recordedAt: output.recordedAt || null } }; })()}}",
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
}, { x: 470, y: 1480 }),
|
|
455
|
+
|
|
434
456
|
node("set-todo-wt-failed", "action.update_task_status", "Set Todo (WT Fail)", {
|
|
435
457
|
taskId: "{{taskId}}",
|
|
436
458
|
status: "todo",
|
|
437
459
|
taskTitle: "{{taskTitle}}",
|
|
438
|
-
}, { x:
|
|
460
|
+
}, { x: 730, y: 1350 }),
|
|
439
461
|
|
|
440
462
|
node("release-slot-wt-failed", "action.release_slot", "Release Slot (WT Fail)", {
|
|
441
463
|
taskId: "{{taskId}}",
|
|
442
|
-
}, { x: 600, y:
|
|
464
|
+
}, { x: 600, y: 1480 }),
|
|
443
465
|
|
|
444
466
|
node("notify-wt-failed", "notify.telegram", "Notify WT Failed", {
|
|
445
|
-
message: "⚠️ Worktree failed for \"{{taskTitle}}\" ({{taskId}})",
|
|
446
|
-
}, { x: 600, y:
|
|
467
|
+
message: "⚠️ Worktree failed for \"{{taskTitle}}\" ({{taskId}}){{acquire-worktree.recoveryNote}}",
|
|
468
|
+
}, { x: 600, y: 1740 }),
|
|
447
469
|
],
|
|
448
470
|
edges: [
|
|
449
471
|
// Main flow
|
|
@@ -515,7 +537,11 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
515
537
|
|
|
516
538
|
// Worktree failed path
|
|
517
539
|
edge("worktree-ok", "release-claim-wt-failed", { condition: "$output?.result !== true", port: "no" }),
|
|
518
|
-
edge("release-claim-wt-failed", "
|
|
540
|
+
edge("release-claim-wt-failed", "wt-failure-blocking"),
|
|
541
|
+
edge("wt-failure-blocking", "set-blocked-wt-failed", { condition: "$output?.result === true", port: "yes" }),
|
|
542
|
+
edge("wt-failure-blocking", "set-todo-wt-failed", { condition: "$output?.result !== true", port: "no" }),
|
|
543
|
+
edge("set-blocked-wt-failed", "annotate-blocked-wt-failed"),
|
|
544
|
+
edge("annotate-blocked-wt-failed", "release-slot-wt-failed"),
|
|
519
545
|
edge("set-todo-wt-failed", "release-slot-wt-failed"),
|
|
520
546
|
edge("release-slot-wt-failed", "notify-wt-failed"),
|
|
521
547
|
],
|
|
@@ -801,4 +827,3 @@ export const VE_ORCHESTRATOR_LITE_TEMPLATE = {
|
|
|
801
827
|
},
|
|
802
828
|
};
|
|
803
829
|
|
|
804
|
-
|