bosun 0.41.0 → 0.41.2
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-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- 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-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/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- 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/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- 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 +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
|
@@ -46,12 +46,30 @@ export const ERROR_RECOVERY_TEMPLATE = {
|
|
|
46
46
|
}, { x: 400, y: 180 }),
|
|
47
47
|
|
|
48
48
|
node("analyze-error", "action.run_agent", "Analyze Failure", {
|
|
49
|
-
prompt:
|
|
49
|
+
prompt:
|
|
50
|
+
"Analyze the following task failure and suggest the most likely minimal fix.\n\n" +
|
|
51
|
+
"Task: {{taskTitle}} ({{taskId}})\n" +
|
|
52
|
+
"Retry attempt: {{$data?.retryCount || 0}}/{{$data?.maxRetries || 3}}\n" +
|
|
53
|
+
"Branch: {{branch}}\n" +
|
|
54
|
+
"Base branch: {{baseBranch}}\n" +
|
|
55
|
+
"Worktree: {{worktreePath}}\n\n" +
|
|
56
|
+
"Last error:\n{{lastError}}",
|
|
50
57
|
timeoutMs: 300000,
|
|
51
58
|
}, { x: 200, y: 330 }),
|
|
52
59
|
|
|
53
60
|
node("retry-task", "action.run_agent", "Retry Task", {
|
|
54
|
-
prompt:
|
|
61
|
+
prompt:
|
|
62
|
+
"{{taskExecutorRetryPrompt}}\n\n" +
|
|
63
|
+
"Failure context:\n" +
|
|
64
|
+
"- taskId: {{taskId}}\n" +
|
|
65
|
+
"- taskTitle: {{taskTitle}}\n" +
|
|
66
|
+
"- branch: {{branch}}\n" +
|
|
67
|
+
"- baseBranch: {{baseBranch}}\n" +
|
|
68
|
+
"- worktreePath: {{worktreePath}}\n" +
|
|
69
|
+
"- retryCount: {{$data?.retryCount || 0}}/{{$data?.maxRetries || 3}}\n" +
|
|
70
|
+
"- lastError: {{lastError}}\n" +
|
|
71
|
+
"- recoveryAnalysis: {{$ctx.getNodeOutput('analyze-error')?.output || ''}}\n\n" +
|
|
72
|
+
"Use the analysis to choose a different approach if the previous attempt failed.",
|
|
55
73
|
timeoutMs: 3600000,
|
|
56
74
|
failOnError: true,
|
|
57
75
|
maxRetries: "{{maxRetries}}",
|
|
@@ -69,13 +87,17 @@ export const ERROR_RECOVERY_TEMPLATE = {
|
|
|
69
87
|
}, { x: 90, y: 760 }),
|
|
70
88
|
|
|
71
89
|
node("escalate", "notify.telegram", "Escalate to Human", {
|
|
72
|
-
message:
|
|
90
|
+
message:
|
|
91
|
+
":alert: Task **{{taskTitle}}** failed after {{maxRetries}} attempts. Manual intervention needed.\n\n" +
|
|
92
|
+
"Last error: {{lastError}}\n\n" +
|
|
93
|
+
"Recovery analysis: {{$ctx.getNodeOutput('analyze-error')?.output || ''}}",
|
|
73
94
|
}, { x: 600, y: 620 }),
|
|
74
95
|
|
|
75
96
|
node("chain-repair", "action.execute_workflow", "Trigger Repair Workflow", {
|
|
76
97
|
workflowId: "template-task-repair-worktree",
|
|
77
98
|
mode: "dispatch",
|
|
78
|
-
input:
|
|
99
|
+
input:
|
|
100
|
+
"(() => { const analysisRaw = String($ctx.getNodeOutput('analyze-error')?.output || '').trim(); const retryOutputRaw = String($ctx.getNodeOutput('retry-task')?.output || '').trim(); const retryErrorRaw = String($ctx.getNodeOutput('retry-task')?.error || '').trim(); const truncate = (value, limit = 2000) => value.length > limit ? `${value.slice(0, limit)}...` : value; const diagnostics = [String($data?.lastError || '').trim(), analysisRaw ? `Recovery analysis:\n${truncate(analysisRaw)}` : '', retryOutputRaw ? `Retry output:\n${truncate(retryOutputRaw)}` : '', retryErrorRaw ? `Retry error:\n${truncate(retryErrorRaw)}` : ''].filter(Boolean).join('\n\n'); return { taskId: $data?.taskId, taskTitle: $data?.taskTitle, worktreePath: $data?.worktreePath, branch: $data?.branch, baseBranch: $data?.baseBranch, error: diagnostics || String($data?.lastError || ''), recoveryAnalysis: truncate(analysisRaw), retryResult: { success: $ctx.getNodeOutput('retry-task')?.success === true, output: truncate(retryOutputRaw), error: truncate(retryErrorRaw) } }; })()",
|
|
79
101
|
}, { x: 400, y: 760 }),
|
|
80
102
|
],
|
|
81
103
|
edges: [
|
|
@@ -92,7 +114,7 @@ export const ERROR_RECOVERY_TEMPLATE = {
|
|
|
92
114
|
author: "bosun",
|
|
93
115
|
version: 1,
|
|
94
116
|
createdAt: "2025-02-24T00:00:00Z",
|
|
95
|
-
templateVersion: "1.0
|
|
117
|
+
templateVersion: "1.1.0",
|
|
96
118
|
tags: ["error", "recovery", "autofix"],
|
|
97
119
|
requiredTemplates: ["template-task-repair-worktree"],
|
|
98
120
|
replaces: {
|
|
@@ -458,6 +480,20 @@ export const TASK_FINALIZATION_GUARD_TEMPLATE = {
|
|
|
458
480
|
},
|
|
459
481
|
}, { x: 240, y: 900 }),
|
|
460
482
|
|
|
483
|
+
node("handoff-pr-progressor", "action.execute_workflow", "Dispatch PR Progressor", {
|
|
484
|
+
workflowId: "template-bosun-pr-progressor",
|
|
485
|
+
mode: "dispatch",
|
|
486
|
+
input: {
|
|
487
|
+
taskId: "{{taskId}}",
|
|
488
|
+
taskTitle: "{{taskTitle}}",
|
|
489
|
+
branch: "{{branch}}",
|
|
490
|
+
baseBranch: "{{baseBranch}}",
|
|
491
|
+
prNumber: "{{$data?.prNumber ?? $ctx.getNodeOutput('create-pr')?.prNumber ?? null}}",
|
|
492
|
+
prUrl: "{{$data?.prUrl || $ctx.getNodeOutput('create-pr')?.prUrl || ''}}",
|
|
493
|
+
repo: "{{$data?.repo || $data?.repoSlug || $data?.repository || $ctx.getNodeOutput('create-pr')?.repoSlug || ''}}",
|
|
494
|
+
},
|
|
495
|
+
}, { x: 240, y: 1040 }),
|
|
496
|
+
|
|
461
497
|
node("mark-todo-failed", "action.update_task_status", "Mark Todo (Checks Failed)", {
|
|
462
498
|
taskId: "{{taskId}}",
|
|
463
499
|
status: "todo",
|
|
@@ -497,13 +533,13 @@ export const TASK_FINALIZATION_GUARD_TEMPLATE = {
|
|
|
497
533
|
node("notify-pass", "notify.log", "Log Finalization Success", {
|
|
498
534
|
message: "Task {{taskId}} finalization passed — moved to inreview",
|
|
499
535
|
level: "info",
|
|
500
|
-
}, { x: 240, y:
|
|
536
|
+
}, { x: 240, y: 1180 }),
|
|
501
537
|
|
|
502
538
|
node("chain-archiver", "flow.universal", "Queue Archival", {
|
|
503
539
|
workflowId: "template-task-archiver",
|
|
504
540
|
mode: "dispatch",
|
|
505
541
|
input: "({taskId: $data?.taskId, taskTitle: $data?.taskTitle, completedAt: new Date().toISOString(), taskJson: JSON.stringify($data?.task || {id: $data?.taskId, title: $data?.taskTitle})})",
|
|
506
|
-
}, { x: 240, y:
|
|
542
|
+
}, { x: 240, y: 1320 }),
|
|
507
543
|
|
|
508
544
|
node("end-success", "flow.end", "End Success", {
|
|
509
545
|
status: "completed",
|
|
@@ -513,7 +549,7 @@ export const TASK_FINALIZATION_GUARD_TEMPLATE = {
|
|
|
513
549
|
taskId: "{{taskId}}",
|
|
514
550
|
taskTitle: "{{taskTitle}}",
|
|
515
551
|
},
|
|
516
|
-
}, { x: 240, y:
|
|
552
|
+
}, { x: 240, y: 1450 }),
|
|
517
553
|
|
|
518
554
|
node("notify-fail", "notify.telegram", "Notify Finalization Failure", {
|
|
519
555
|
message: ":alert: Task finalization failed for **{{taskTitle}}** ({{taskId}}). Repair workflow handoff triggered.",
|
|
@@ -543,7 +579,8 @@ export const TASK_FINALIZATION_GUARD_TEMPLATE = {
|
|
|
543
579
|
edge("create-pr", "create-pr-success"),
|
|
544
580
|
edge("create-pr-success", "mark-inreview", { condition: "$output?.result === true", port: "yes" }),
|
|
545
581
|
edge("create-pr-success", "mark-todo-failed", { condition: "$output?.result !== true", port: "no" }),
|
|
546
|
-
edge("mark-inreview", "
|
|
582
|
+
edge("mark-inreview", "handoff-pr-progressor"),
|
|
583
|
+
edge("handoff-pr-progressor", "notify-pass"),
|
|
547
584
|
edge("notify-skip-missing-context", "end-success"),
|
|
548
585
|
edge("notify-pass", "chain-archiver"),
|
|
549
586
|
edge("chain-archiver", "end-success"),
|
|
@@ -557,7 +594,7 @@ export const TASK_FINALIZATION_GUARD_TEMPLATE = {
|
|
|
557
594
|
createdAt: "2026-02-26T00:00:00Z",
|
|
558
595
|
templateVersion: "1.0.1",
|
|
559
596
|
tags: ["finalization", "quality-gate", "prepush", "handoff", "reliability"],
|
|
560
|
-
requiredTemplates: ["template-task-archiver"],
|
|
597
|
+
requiredTemplates: ["template-task-archiver", "template-bosun-pr-progressor"],
|
|
561
598
|
replaces: {
|
|
562
599
|
module: "task-executor.mjs",
|
|
563
600
|
functions: ["_handleTaskResult finalization gate"],
|
|
@@ -659,6 +696,20 @@ export const TASK_REPAIR_WORKTREE_TEMPLATE = {
|
|
|
659
696
|
},
|
|
660
697
|
}, { x: 250, y: 1020 }),
|
|
661
698
|
|
|
699
|
+
node("handoff-pr-progressor", "action.execute_workflow", "Dispatch PR Progressor", {
|
|
700
|
+
workflowId: "template-bosun-pr-progressor",
|
|
701
|
+
mode: "dispatch",
|
|
702
|
+
input: {
|
|
703
|
+
taskId: "{{taskId}}",
|
|
704
|
+
taskTitle: "{{taskTitle}}",
|
|
705
|
+
branch: "{{branch}}",
|
|
706
|
+
baseBranch: "{{baseBranch}}",
|
|
707
|
+
prNumber: "{{$data?.prNumber ?? $ctx.getNodeOutput('create-pr')?.prNumber ?? null}}",
|
|
708
|
+
prUrl: "{{$data?.prUrl || $ctx.getNodeOutput('create-pr')?.prUrl || ''}}",
|
|
709
|
+
repo: "{{$data?.repo || $data?.repoSlug || $data?.repository || $ctx.getNodeOutput('create-pr')?.repoSlug || ''}}",
|
|
710
|
+
},
|
|
711
|
+
}, { x: 250, y: 1160 }),
|
|
712
|
+
|
|
662
713
|
node("mark-todo", "action.update_task_status", "Mark Todo (Repair Failed)", {
|
|
663
714
|
taskId: "{{taskId}}",
|
|
664
715
|
status: "todo",
|
|
@@ -676,7 +727,7 @@ export const TASK_REPAIR_WORKTREE_TEMPLATE = {
|
|
|
676
727
|
node("notify-success", "notify.telegram", "Notify Repair Success", {
|
|
677
728
|
message: ":check: Repair workflow recovered **{{taskTitle}}** ({{taskId}}) and moved it to inreview.",
|
|
678
729
|
silent: true,
|
|
679
|
-
}, { x: 250, y:
|
|
730
|
+
}, { x: 250, y: 1300 }),
|
|
680
731
|
|
|
681
732
|
node("notify-escalate", "notify.telegram", "Escalate Repair Failure", {
|
|
682
733
|
message: ":alert: Repair workflow could not recover **{{taskTitle}}** ({{taskId}}). Manual intervention required.",
|
|
@@ -700,7 +751,8 @@ export const TASK_REPAIR_WORKTREE_TEMPLATE = {
|
|
|
700
751
|
edge("create-pr", "create-pr-success"),
|
|
701
752
|
edge("create-pr-success", "mark-inreview", { condition: "$output?.result === true", port: "yes" }),
|
|
702
753
|
edge("create-pr-success", "mark-todo", { condition: "$output?.result !== true", port: "no" }),
|
|
703
|
-
edge("mark-inreview", "
|
|
754
|
+
edge("mark-inreview", "handoff-pr-progressor"),
|
|
755
|
+
edge("handoff-pr-progressor", "notify-success"),
|
|
704
756
|
edge("mark-todo", "notify-escalate"),
|
|
705
757
|
edge("no-worktree", "notify-escalate"),
|
|
706
758
|
],
|
|
@@ -710,6 +762,7 @@ export const TASK_REPAIR_WORKTREE_TEMPLATE = {
|
|
|
710
762
|
createdAt: "2026-02-26T00:00:00Z",
|
|
711
763
|
templateVersion: "1.0.1",
|
|
712
764
|
tags: ["repair", "recovery", "worktree", "resilience", "automation"],
|
|
765
|
+
requiredTemplates: ["template-bosun-pr-progressor"],
|
|
713
766
|
replaces: {
|
|
714
767
|
module: "task-executor.mjs",
|
|
715
768
|
functions: ["retry/escalation recovery path"],
|
|
@@ -51,7 +51,7 @@ export const TASK_BATCH_PROCESSOR_TEMPLATE = {
|
|
|
51
51
|
// ── Trigger: Tasks available for processing ──────────────────────────
|
|
52
52
|
node("trigger", "trigger.task_available", "Tasks Available?", {
|
|
53
53
|
maxParallel: "{{maxConcurrent}}",
|
|
54
|
-
pollIntervalMs:
|
|
54
|
+
pollIntervalMs: 15000,
|
|
55
55
|
status: "{{pollStatus}}",
|
|
56
56
|
}, { x: 400, y: 50 }),
|
|
57
57
|
|
|
@@ -91,7 +91,7 @@ export const TASK_BATCH_PROCESSOR_TEMPLATE = {
|
|
|
91
91
|
|
|
92
92
|
// ── Fan-out: dispatch each task to the lifecycle workflow ─────────────
|
|
93
93
|
node("dispatch-tasks", "loop.for_each", "Dispatch Tasks", {
|
|
94
|
-
items: "{{
|
|
94
|
+
items: "{{query-tasks.output}}",
|
|
95
95
|
itemVariable: "currentTask",
|
|
96
96
|
indexVariable: "taskIndex",
|
|
97
97
|
maxConcurrent: "{{maxConcurrent}}",
|
|
@@ -113,7 +113,7 @@ export const TASK_BATCH_PROCESSOR_TEMPLATE = {
|
|
|
113
113
|
// ── Notify on completion ─────────────────────────────────────────────
|
|
114
114
|
node("notify-complete", "notify.telegram", "Batch Summary", {
|
|
115
115
|
channel: "{{notifyChannel}}",
|
|
116
|
-
message: "Task batch completed: {{
|
|
116
|
+
message: "Task batch completed: {{dispatch-tasks.successCount}}/{{dispatch-tasks.totalItems}} succeeded ({{dispatch-tasks.failCount}} failed)",
|
|
117
117
|
}, { x: 400, y: 700 }),
|
|
118
118
|
],
|
|
119
119
|
edges: [
|
|
@@ -162,7 +162,7 @@ export const TASK_BATCH_PR_TEMPLATE = {
|
|
|
162
162
|
// ── Trigger ──────────────────────────────────────────────────────────
|
|
163
163
|
node("trigger", "trigger.task_available", "Tasks Available?", {
|
|
164
164
|
maxParallel: "{{maxConcurrent}}",
|
|
165
|
-
pollIntervalMs:
|
|
165
|
+
pollIntervalMs: 15000,
|
|
166
166
|
status: "{{pollStatus}}",
|
|
167
167
|
}, { x: 400, y: 50 }),
|
|
168
168
|
|
|
@@ -195,7 +195,7 @@ export const TASK_BATCH_PR_TEMPLATE = {
|
|
|
195
195
|
|
|
196
196
|
// ── Fan-out: per-task agent + PR ─────────────────────────────────────
|
|
197
197
|
node("for-each-task", "loop.for_each", "Process Each Task", {
|
|
198
|
-
items: "{{
|
|
198
|
+
items: "{{query-tasks.output}}",
|
|
199
199
|
itemVariable: "task",
|
|
200
200
|
indexVariable: "idx",
|
|
201
201
|
maxConcurrent: "{{maxConcurrent}}",
|
|
@@ -241,11 +241,25 @@ export const TASK_BATCH_PR_TEMPLATE = {
|
|
|
241
241
|
status: "inreview",
|
|
242
242
|
}, { x: 400, y: 1090 }),
|
|
243
243
|
|
|
244
|
+
node("handoff-pr-progressor", "action.execute_workflow", "Dispatch PR Progressor", {
|
|
245
|
+
workflowId: "template-bosun-pr-progressor",
|
|
246
|
+
mode: "dispatch",
|
|
247
|
+
input: {
|
|
248
|
+
taskId: "{{task.taskId}}",
|
|
249
|
+
taskTitle: "{{task.taskTitle}}",
|
|
250
|
+
branch: "{{task.branch}}",
|
|
251
|
+
baseBranch: "{{defaultBaseBranch}}",
|
|
252
|
+
prNumber: "{{$ctx.getNodeOutput('create-pr')?.prNumber ?? null}}",
|
|
253
|
+
prUrl: "{{$ctx.getNodeOutput('create-pr')?.prUrl || ''}}",
|
|
254
|
+
repo: "{{$ctx.getNodeOutput('create-pr')?.repoSlug || $data?.repo || $data?.repoSlug || $data?.repository || ''}}",
|
|
255
|
+
},
|
|
256
|
+
}, { x: 400, y: 1160 }),
|
|
257
|
+
|
|
244
258
|
node("join-batch-outcomes", "flow.join", "Join Batch Outcomes", {
|
|
245
259
|
mode: "all",
|
|
246
|
-
sourceNodeIds: ["detect-commits", "
|
|
260
|
+
sourceNodeIds: ["detect-commits", "handoff-pr-progressor"],
|
|
247
261
|
includeSkipped: true,
|
|
248
|
-
}, { x: 400, y:
|
|
262
|
+
}, { x: 400, y: 1230 }),
|
|
249
263
|
|
|
250
264
|
// ── Batch complete notification ──────────────────────────────────────
|
|
251
265
|
node("notify", "notify.telegram", "Batch Complete", {
|
|
@@ -263,7 +277,8 @@ export const TASK_BATCH_PR_TEMPLATE = {
|
|
|
263
277
|
edge("push-branch", "create-pr"),
|
|
264
278
|
edge("create-pr", "set-inreview"),
|
|
265
279
|
edge("detect-commits", "join-batch-outcomes", { condition: "result.hasNewCommits !== true" }),
|
|
266
|
-
edge("set-inreview", "
|
|
280
|
+
edge("set-inreview", "handoff-pr-progressor"),
|
|
281
|
+
edge("handoff-pr-progressor", "join-batch-outcomes"),
|
|
267
282
|
edge("join-batch-outcomes", "notify"),
|
|
268
283
|
],
|
|
269
284
|
metadata: {
|
|
@@ -272,5 +287,6 @@ export const TASK_BATCH_PR_TEMPLATE = {
|
|
|
272
287
|
createdAt: "2026-03-15T00:00:00Z",
|
|
273
288
|
templateVersion: "1.0.0",
|
|
274
289
|
tags: ["task", "batch", "pr", "agent", "autonomous"],
|
|
290
|
+
requiredTemplates: ["template-bosun-pr-progressor"],
|
|
275
291
|
},
|
|
276
292
|
};
|
|
@@ -65,6 +65,10 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
65
65
|
defaultSdk: "auto",
|
|
66
66
|
defaultTargetBranch: "origin/main",
|
|
67
67
|
taskTimeoutMs: 21600000, // 6 hours
|
|
68
|
+
prePrValidationEnabled: true,
|
|
69
|
+
prePrValidationCommand: "npm run prepush:check",
|
|
70
|
+
autoMergeOnCreate: false,
|
|
71
|
+
autoMergeMethod: "squash",
|
|
68
72
|
maxRetries: 2,
|
|
69
73
|
maxContinues: 3,
|
|
70
74
|
protectedBranches: ["main", "master", "develop", "production"],
|
|
@@ -228,12 +232,43 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
228
232
|
expression: "$ctx.getNodeOutput('detect-commits')?.hasCommits === true",
|
|
229
233
|
}, { x: 120, y: 1870, outputs: ["yes", "no"] }),
|
|
230
234
|
|
|
235
|
+
// ── SUCCESS PATH: Local quality gate before push/PR ──────────────────
|
|
236
|
+
node("pre-pr-validation", "action.run_command", "Pre-PR Validation", {
|
|
237
|
+
command: "{{prePrValidationCommand}}",
|
|
238
|
+
cwd: "{{worktreePath}}",
|
|
239
|
+
failOnError: false,
|
|
240
|
+
}, { x: -120, y: 1940 }),
|
|
241
|
+
|
|
242
|
+
node("pre-pr-validation-ok", "condition.expression", "Validation Passed?", {
|
|
243
|
+
expression:
|
|
244
|
+
"(() => {" +
|
|
245
|
+
"const enabled = $data?.prePrValidationEnabled !== false;" +
|
|
246
|
+
"if (!enabled) return true;" +
|
|
247
|
+
"const out = $ctx.getNodeOutput('pre-pr-validation');" +
|
|
248
|
+
"if (!out) return false;" +
|
|
249
|
+
"if (out.success === true) return true;" +
|
|
250
|
+
"const code = Number(out.exitCode);" +
|
|
251
|
+
"return Number.isFinite(code) && code === 0;" +
|
|
252
|
+
"})()",
|
|
253
|
+
}, { x: -120, y: 2060, outputs: ["yes", "no"] }),
|
|
254
|
+
|
|
255
|
+
node("log-validation-failed", "notify.log", "Log Validation Failed", {
|
|
256
|
+
message: "Task \"{{taskTitle}}\" ({{taskId}}) — pre-PR validation failed, returning to todo",
|
|
257
|
+
level: "warn",
|
|
258
|
+
}, { x: 300, y: 2000 }),
|
|
259
|
+
|
|
260
|
+
node("set-todo-validation-failed", "action.update_task_status", "Set Todo (Validation Fail)", {
|
|
261
|
+
taskId: "{{taskId}}",
|
|
262
|
+
status: "todo",
|
|
263
|
+
taskTitle: "{{taskTitle}}",
|
|
264
|
+
}, { x: 300, y: 2130 }),
|
|
231
265
|
// ── SUCCESS PATH: Push branch (with rebase + empty-diff guard) ───────
|
|
232
266
|
node("push-branch", "action.push_branch", "Push Branch", {
|
|
233
267
|
worktreePath: "{{worktreePath}}",
|
|
234
268
|
branch: "{{branch}}",
|
|
235
269
|
baseBranch: "{{baseBranch}}",
|
|
236
270
|
rebaseBeforePush: true,
|
|
271
|
+
skipHooks: true,
|
|
237
272
|
emptyDiffGuard: true,
|
|
238
273
|
protectedBranches: "{{protectedBranches}}",
|
|
239
274
|
}, { x: 0, y: 2000 }),
|
|
@@ -250,6 +285,8 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
250
285
|
base: "{{baseBranch}}",
|
|
251
286
|
branch: "{{branch}}",
|
|
252
287
|
cwd: "{{worktreePath}}",
|
|
288
|
+
enableAutoMerge: "{{autoMergeOnCreate}}",
|
|
289
|
+
autoMergeMethod: "{{autoMergeMethod}}",
|
|
253
290
|
}, { x: 0, y: 2260 }),
|
|
254
291
|
|
|
255
292
|
node("pr-created", "condition.expression", "PR Linked?", {
|
|
@@ -263,11 +300,25 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
263
300
|
taskTitle: "{{taskTitle}}",
|
|
264
301
|
}, { x: 0, y: 2390 }),
|
|
265
302
|
|
|
303
|
+
node("handoff-pr-progressor", "action.execute_workflow", "Handoff PR Progressor", {
|
|
304
|
+
workflowId: "template-bosun-pr-progressor",
|
|
305
|
+
mode: "dispatch",
|
|
306
|
+
input: {
|
|
307
|
+
taskId: "{{taskId}}",
|
|
308
|
+
taskTitle: "{{taskTitle}}",
|
|
309
|
+
branch: "{{branch}}",
|
|
310
|
+
baseBranch: "{{baseBranch}}",
|
|
311
|
+
prNumber: "{{$ctx.getNodeOutput('create-pr')?.prNumber ?? $data?.prNumber ?? null}}",
|
|
312
|
+
prUrl: "{{$ctx.getNodeOutput('create-pr')?.prUrl || $data?.prUrl || ''}}",
|
|
313
|
+
repo: "{{$ctx.getNodeOutput('create-pr')?.repoSlug || $data?.repo || $data?.repoSlug || $data?.repository || ''}}",
|
|
314
|
+
},
|
|
315
|
+
}, { x: -120, y: 2520 }),
|
|
316
|
+
|
|
266
317
|
// ── SUCCESS PATH: Log success ────────────────────────────────────────
|
|
267
318
|
node("log-success", "notify.log", "Log Success", {
|
|
268
319
|
message: "Task \"{{taskTitle}}\" ({{taskId}}) completed — PR created",
|
|
269
320
|
level: "info",
|
|
270
|
-
}, { x:
|
|
321
|
+
}, { x: -120, y: 2650 }),
|
|
271
322
|
|
|
272
323
|
// ── NO COMMITS PATH: Log no-commit ───────────────────────────────────
|
|
273
324
|
node("log-no-commits", "notify.log", "Log No Commits", {
|
|
@@ -297,6 +348,8 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
297
348
|
branch: "{{branch}}",
|
|
298
349
|
cwd: "{{worktreePath}}",
|
|
299
350
|
failOnError: false,
|
|
351
|
+
enableAutoMerge: "{{autoMergeOnCreate}}",
|
|
352
|
+
autoMergeMethod: "{{autoMergeMethod}}",
|
|
300
353
|
}, { x: 400, y: 1740 }),
|
|
301
354
|
|
|
302
355
|
node("pr-created-stolen", "condition.expression", "PR Linked After Claim Loss?", {
|
|
@@ -309,10 +362,24 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
309
362
|
taskTitle: "{{taskTitle}}",
|
|
310
363
|
}, { x: 250, y: 2000 }),
|
|
311
364
|
|
|
365
|
+
node("handoff-pr-progressor-stolen", "action.execute_workflow", "Handoff PR Progressor (Recovered)", {
|
|
366
|
+
workflowId: "template-bosun-pr-progressor",
|
|
367
|
+
mode: "dispatch",
|
|
368
|
+
input: {
|
|
369
|
+
taskId: "{{taskId}}",
|
|
370
|
+
taskTitle: "{{taskTitle}}",
|
|
371
|
+
branch: "{{branch}}",
|
|
372
|
+
baseBranch: "{{baseBranch}}",
|
|
373
|
+
prNumber: "{{$ctx.getNodeOutput('create-pr-retry')?.prNumber ?? $data?.prNumber ?? null}}",
|
|
374
|
+
prUrl: "{{$ctx.getNodeOutput('create-pr-retry')?.prUrl || $data?.prUrl || ''}}",
|
|
375
|
+
repo: "{{$ctx.getNodeOutput('create-pr-retry')?.repoSlug || $data?.repo || $data?.repoSlug || $data?.repository || ''}}",
|
|
376
|
+
},
|
|
377
|
+
}, { x: 120, y: 2130 }),
|
|
378
|
+
|
|
312
379
|
node("log-claim-stolen-recovered", "notify.log", "Log Claim Loss Recovery", {
|
|
313
380
|
message: "Task \"{{taskTitle}}\" ({{taskId}}) — claim lost after PR link recovery, keeping inreview",
|
|
314
381
|
level: "warn",
|
|
315
|
-
}, { x:
|
|
382
|
+
}, { x: 120, y: 2260 }),
|
|
316
383
|
|
|
317
384
|
node("log-claim-stolen", "notify.log", "Log Claim Stolen", {
|
|
318
385
|
message: "Task \"{{taskTitle}}\" ({{taskId}}) — claim was stolen, aborting",
|
|
@@ -328,7 +395,7 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
328
395
|
|
|
329
396
|
node("join-outcomes", "flow.join", "Join Outcome Paths", {
|
|
330
397
|
mode: "all",
|
|
331
|
-
sourceNodeIds: ["log-success", "set-todo-push-failed", "set-todo-cooldown", "set-todo-stolen", "log-claim-stolen-recovered"],
|
|
398
|
+
sourceNodeIds: ["log-success", "set-todo-push-failed", "set-todo-cooldown", "set-todo-validation-failed", "set-todo-stolen", "log-claim-stolen-recovered"],
|
|
332
399
|
includeSkipped: true,
|
|
333
400
|
}, { x: 200, y: 2560 }),
|
|
334
401
|
|
|
@@ -402,13 +469,19 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
402
469
|
edge("detect-commits", "has-commits"),
|
|
403
470
|
|
|
404
471
|
// Success path (has commits)
|
|
405
|
-
edge("has-commits", "
|
|
472
|
+
edge("has-commits", "pre-pr-validation", { condition: "$output?.result === true", port: "yes" }),
|
|
473
|
+
edge("pre-pr-validation", "pre-pr-validation-ok"),
|
|
474
|
+
edge("pre-pr-validation-ok", "push-branch", { condition: "$output?.result === true", port: "yes" }),
|
|
475
|
+
edge("pre-pr-validation-ok", "log-validation-failed", { condition: "$output?.result !== true", port: "no" }),
|
|
476
|
+
edge("log-validation-failed", "set-todo-validation-failed"),
|
|
477
|
+
edge("set-todo-validation-failed", "join-outcomes"),
|
|
406
478
|
edge("push-branch", "push-ok"),
|
|
407
479
|
edge("push-ok", "create-pr", { condition: "$output?.result === true", port: "yes" }),
|
|
408
480
|
edge("create-pr", "pr-created"),
|
|
409
481
|
edge("pr-created", "set-inreview", { condition: "$output?.result === true", port: "yes" }),
|
|
410
482
|
edge("pr-created", "set-todo-push-failed", { condition: "$output?.result !== true", port: "no" }),
|
|
411
|
-
edge("set-inreview", "
|
|
483
|
+
edge("set-inreview", "handoff-pr-progressor"),
|
|
484
|
+
edge("handoff-pr-progressor", "log-success"),
|
|
412
485
|
edge("log-success", "join-outcomes"),
|
|
413
486
|
|
|
414
487
|
// Push failed path
|
|
@@ -424,7 +497,8 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
424
497
|
edge("claim-stolen", "create-pr-retry", { condition: "$output?.result === true", port: "yes" }),
|
|
425
498
|
edge("create-pr-retry", "pr-created-stolen"),
|
|
426
499
|
edge("pr-created-stolen", "set-inreview-stolen", { condition: "$output?.result === true", port: "yes" }),
|
|
427
|
-
edge("set-inreview-stolen", "
|
|
500
|
+
edge("set-inreview-stolen", "handoff-pr-progressor-stolen"),
|
|
501
|
+
edge("handoff-pr-progressor-stolen", "log-claim-stolen-recovered"),
|
|
428
502
|
edge("log-claim-stolen-recovered", "join-outcomes"),
|
|
429
503
|
edge("pr-created-stolen", "log-claim-stolen", { condition: "$output?.result !== true", port: "no" }),
|
|
430
504
|
edge("log-claim-stolen", "set-todo-stolen"),
|
|
@@ -451,6 +525,7 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
451
525
|
createdAt: "2026-03-01T00:00:00Z",
|
|
452
526
|
templateVersion: "2.0.0",
|
|
453
527
|
tags: ["task", "lifecycle", "executor", "workflow-first", "core"],
|
|
528
|
+
requiredTemplates: ["template-bosun-pr-progressor"],
|
|
454
529
|
replaces: {
|
|
455
530
|
module: "task-executor.mjs",
|
|
456
531
|
functions: [
|
|
@@ -725,3 +800,5 @@ export const VE_ORCHESTRATOR_LITE_TEMPLATE = {
|
|
|
725
800
|
},
|
|
726
801
|
},
|
|
727
802
|
};
|
|
803
|
+
|
|
804
|
+
|
|
@@ -856,16 +856,57 @@ function extractCommandText(item) {
|
|
|
856
856
|
return "";
|
|
857
857
|
}
|
|
858
858
|
|
|
859
|
-
function
|
|
860
|
-
|
|
861
|
-
const commandText = extractCommandText(item);
|
|
862
|
-
return `${toolName} ${commandText}`
|
|
859
|
+
function normalizeGitTokenSource(value) {
|
|
860
|
+
return String(value || "")
|
|
863
861
|
.toLowerCase()
|
|
864
|
-
.replaceAll(/
|
|
862
|
+
.replaceAll(/[^a-z0-9]+/g, " ")
|
|
865
863
|
.replaceAll(/\s+/g, " ")
|
|
866
864
|
.trim();
|
|
867
865
|
}
|
|
868
866
|
|
|
867
|
+
function inferGitCommandSignals(toolName, commandText) {
|
|
868
|
+
const normalizedToolName = normalizeGitTokenSource(toolName);
|
|
869
|
+
const normalizedCommand = normalizeGitTokenSource(commandText);
|
|
870
|
+
const merged = `${normalizedCommand} ${normalizedToolName}`.trim();
|
|
871
|
+
|
|
872
|
+
const commandLooksGit = /\bgit\b/.test(normalizedCommand);
|
|
873
|
+
const toolLooksGit = normalizedToolName.includes("git");
|
|
874
|
+
if (!toolLooksGit && !commandLooksGit) {
|
|
875
|
+
return {
|
|
876
|
+
eligible: false,
|
|
877
|
+
boundedDiff: false,
|
|
878
|
+
hasLog: false,
|
|
879
|
+
hasShortlog: false,
|
|
880
|
+
hasReflog: false,
|
|
881
|
+
hasDiff: false,
|
|
882
|
+
hasShow: false,
|
|
883
|
+
hasStatus: false,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
let commandBody = normalizedCommand.startsWith("git ")
|
|
888
|
+
? normalizedCommand.slice(4).trim()
|
|
889
|
+
: normalizedCommand;
|
|
890
|
+
|
|
891
|
+
if (!commandBody && toolLooksGit) {
|
|
892
|
+
commandBody = normalizedToolName
|
|
893
|
+
.replaceAll(/\bgit\b/g, " ")
|
|
894
|
+
.replaceAll(/\s+/g, " ")
|
|
895
|
+
.trim();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const source = `${commandBody} ${normalizedToolName}`.trim();
|
|
899
|
+
const hasShortlog = /\bshortlog\b/.test(source);
|
|
900
|
+
const hasReflog = /\breflog\b/.test(source);
|
|
901
|
+
const hasLog = !hasShortlog && !hasReflog && /\blog\b/.test(source);
|
|
902
|
+
const hasDiff = /\bdiff\b/.test(source);
|
|
903
|
+
const hasShow = /\bshow\b/.test(source);
|
|
904
|
+
const hasStatus = /\bstatus\b/.test(source);
|
|
905
|
+
const boundedDiff = /(?:^|\s)--(?:stat|shortstat|numstat|name-only|name-status|summary)\b/.test(commandBody)
|
|
906
|
+
|| /\bdiff(?:\s|-)*(?:stat|shortstat|numstat|name only|name status|summary)\b/.test(merged);
|
|
907
|
+
return { eligible: true, boundedDiff, hasLog, hasShortlog, hasReflog, hasDiff, hasShow, hasStatus };
|
|
908
|
+
}
|
|
909
|
+
|
|
869
910
|
function classifyImmediateGitOutput(item, opts) {
|
|
870
911
|
const maxChars = opts?.gitOutputMaxChars ?? DEFAULT_GIT_OUTPUT_MAX_CHARS;
|
|
871
912
|
if (!Number.isFinite(maxChars) || maxChars <= 0) return null;
|
|
@@ -873,17 +914,25 @@ function classifyImmediateGitOutput(item, opts) {
|
|
|
873
914
|
const text = getItemText(item);
|
|
874
915
|
if (typeof text !== "string" || text.length <= maxChars) return null;
|
|
875
916
|
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
917
|
+
const toolName = extractToolName(item);
|
|
918
|
+
const commandText = extractCommandText(item);
|
|
919
|
+
const {
|
|
920
|
+
eligible,
|
|
921
|
+
boundedDiff,
|
|
922
|
+
hasLog,
|
|
923
|
+
hasShortlog,
|
|
924
|
+
hasReflog,
|
|
925
|
+
hasDiff,
|
|
926
|
+
hasShow,
|
|
927
|
+
hasStatus,
|
|
928
|
+
} = inferGitCommandSignals(toolName, commandText);
|
|
929
|
+
if (!eligible) return null;
|
|
930
|
+
|
|
931
|
+
if (hasStatus || hasShow) return null;
|
|
932
|
+
if (hasShortlog) return { kind: "shortlog", text };
|
|
933
|
+
if (hasReflog) return { kind: "reflog", text };
|
|
934
|
+
if (hasLog) return { kind: "log", text };
|
|
935
|
+
if (hasDiff && !boundedDiff) return { kind: "diff", text };
|
|
887
936
|
|
|
888
937
|
return null;
|
|
889
938
|
}
|
|
@@ -899,7 +948,7 @@ function compressImmediateGitText(text, logId, opts) {
|
|
|
899
948
|
const tail = tailChars > 0 ? text.slice(-tailChars) : "";
|
|
900
949
|
const omitted = text.length - headChars - (tailChars > 0 ? tailChars : 0);
|
|
901
950
|
const lineCount = text.length === 0 ? 0 : (text.match(/\n/g) || []).length + 1;
|
|
902
|
-
const note = `\n\n[…git capped: ${lineCount} lines, ${omitted} chars
|
|
951
|
+
const note = `\n\n[…git capped: ${lineCount} lines, ${omitted} chars suppressed. Full: bosun --tool-log ${logId}]\n\n`;
|
|
903
952
|
return head + note + tail;
|
|
904
953
|
}
|
|
905
954
|
|
|
@@ -2413,4 +2462,3 @@ export async function maybeCompressSessionItems(
|
|
|
2413
2462
|
return compressedItems;
|
|
2414
2463
|
}
|
|
2415
2464
|
|
|
2416
|
-
|
|
@@ -357,12 +357,13 @@ function buildGitPullFailureDetails(err, repoPath, childProcess) {
|
|
|
357
357
|
const stderr = normalizeSingleLine(err?.stderr || "");
|
|
358
358
|
const stdout = normalizeSingleLine(err?.stdout || "");
|
|
359
359
|
const message = normalizeSingleLine(err?.message || err || "");
|
|
360
|
+
const preferred = normalizeSingleLine(err?.stderr || err?.stdout || err?.message || "");
|
|
360
361
|
const parts = [];
|
|
361
362
|
if (Number.isFinite(err?.status)) parts.push(`status=${err.status}`);
|
|
362
363
|
if (err?.signal) parts.push(`signal=${err.signal}`);
|
|
363
364
|
if (err?.code) parts.push(`code=${err.code}`);
|
|
364
365
|
|
|
365
|
-
let details = stderr || stdout || message || "git pull --rebase failed";
|
|
366
|
+
let details = preferred || stderr || stdout || message || "git pull --rebase failed";
|
|
366
367
|
if (!details || isGenericGitPullFailure(details)) {
|
|
367
368
|
const fallback = message && !isGenericGitPullFailure(message)
|
|
368
369
|
? message
|