bosun 0.35.2 → 0.35.3
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/README.md +14 -1
- package/agent-hooks.mjs +7 -1
- package/agent-pool.mjs +16 -0
- package/agent-prompts.mjs +190 -4
- package/agent-sdk.mjs +6 -1
- package/agent-work-analyzer.mjs +48 -9
- package/autofix.mjs +32 -18
- package/bosun.schema.json +1 -1
- package/kanban-adapter.mjs +62 -12
- package/monitor.mjs +25 -6
- package/opencode-shell.mjs +881 -0
- package/package.json +5 -2
- package/primary-agent.mjs +43 -0
- package/setup.mjs +33 -4
- package/task-executor.mjs +43 -14
- package/ui/app.js +10 -7
- package/ui/components/chat-view.js +31 -9
- package/ui/components/session-list.js +20 -4
- package/ui/modules/router.js +2 -0
- package/ui/tabs/agents.js +66 -8
- package/ui-server.mjs +142 -5
- package/workflow-engine.mjs +664 -10
- package/workflow-nodes.mjs +250 -1
- package/workflow-templates/github.mjs +389 -71
- package/workflow-templates/planning.mjs +31 -11
- package/workflow-templates.mjs +3 -0
|
@@ -299,12 +299,17 @@ export const PR_CONFLICT_RESOLVER_TEMPLATE = {
|
|
|
299
299
|
id: "template-pr-conflict-resolver",
|
|
300
300
|
name: "PR Conflict Resolver",
|
|
301
301
|
description:
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
"
|
|
302
|
+
"⚠️ SUPERSEDED for bosun-managed repos — use the Bosun PR Watchdog " +
|
|
303
|
+
"(template-bosun-pr-watchdog) instead. The Watchdog consolidates conflict " +
|
|
304
|
+
"resolution, CI-failure repair, diff-safety review, and merge into one " +
|
|
305
|
+
"cycle with a single gh API call and a mandatory review gate before any merge. " +
|
|
306
|
+
"This template is kept for repos that do not use the bosun-attached label " +
|
|
307
|
+
"convention. It ONLY touches PRs labelled bosun-attached and never " +
|
|
308
|
+
"auto-merges directly — it resolves conflicts and then defers to the " +
|
|
309
|
+
"Watchdog's review gate for the actual merge decision.",
|
|
305
310
|
category: "github",
|
|
306
|
-
enabled:
|
|
307
|
-
recommended:
|
|
311
|
+
enabled: false,
|
|
312
|
+
recommended: false,
|
|
308
313
|
trigger: "trigger.schedule",
|
|
309
314
|
variables: {
|
|
310
315
|
checkIntervalMs: 1800000,
|
|
@@ -317,113 +322,138 @@ export const PR_CONFLICT_RESOLVER_TEMPLATE = {
|
|
|
317
322
|
cron: "*/30 * * * *",
|
|
318
323
|
}, { x: 400, y: 50 }),
|
|
319
324
|
|
|
320
|
-
|
|
321
|
-
|
|
325
|
+
// Only fetch bosun-attached PRs — never touch external-contributor PRs.
|
|
326
|
+
// Includes labels so we can skip PRs already tagged bosun-needs-fix (watchdog owns those).
|
|
327
|
+
node("list-prs", "action.run_command", "List Bosun-Attached Conflicting PRs", {
|
|
328
|
+
command:
|
|
329
|
+
"gh pr list --label bosun-attached --state open " +
|
|
330
|
+
"--json number,title,headRefName,baseRefName,mergeable,labels --limit 20",
|
|
322
331
|
}, { x: 400, y: 180 }),
|
|
323
332
|
|
|
324
333
|
node("target-pr", "action.set_variable", "Pick Conflict PR", {
|
|
325
334
|
key: "targetPrNumber",
|
|
326
335
|
value:
|
|
327
|
-
"(() => {
|
|
336
|
+
"(() => {" +
|
|
337
|
+
" const raw = $ctx.getNodeOutput('list-prs')?.output || '[]';" +
|
|
338
|
+
" let prs = [];" +
|
|
339
|
+
" try { prs = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { return ''; }" +
|
|
340
|
+
" if (!Array.isArray(prs)) return '';" +
|
|
341
|
+
" const CONFLICT = new Set(['CONFLICTING', 'BEHIND', 'DIRTY']);" +
|
|
342
|
+
" // Skip PRs already owned by the watchdog fix agent" +
|
|
343
|
+
" const pr = prs.find((p) =>" +
|
|
344
|
+
" CONFLICT.has(String(p?.mergeable || '').toUpperCase()) &&" +
|
|
345
|
+
" !(p.labels || []).some((l) => l.name === 'bosun-needs-fix')" +
|
|
346
|
+
" );" +
|
|
347
|
+
" return pr?.number ? String(pr.number) : '';" +
|
|
348
|
+
"})()",
|
|
328
349
|
isExpression: true,
|
|
329
350
|
}, { x: 400, y: 260 }),
|
|
330
351
|
|
|
331
352
|
node("target-branch", "action.set_variable", "Capture Conflict Branch", {
|
|
332
353
|
key: "targetPrBranch",
|
|
333
354
|
value:
|
|
334
|
-
"(() => {
|
|
355
|
+
"(() => {" +
|
|
356
|
+
" const raw = $ctx.getNodeOutput('list-prs')?.output || '[]';" +
|
|
357
|
+
" let prs = [];" +
|
|
358
|
+
" try { prs = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { return ''; }" +
|
|
359
|
+
" if (!Array.isArray(prs)) return '';" +
|
|
360
|
+
" const pr = prs.find((p) => String(p?.number || '') === String($data?.targetPrNumber || ''));" +
|
|
361
|
+
" return pr?.headRefName || '';" +
|
|
362
|
+
"})()",
|
|
335
363
|
isExpression: true,
|
|
336
364
|
}, { x: 400, y: 340 }),
|
|
337
365
|
|
|
366
|
+
node("target-base", "action.set_variable", "Capture Base Branch", {
|
|
367
|
+
key: "targetPrBase",
|
|
368
|
+
value:
|
|
369
|
+
"(() => {" +
|
|
370
|
+
" const raw = $ctx.getNodeOutput('list-prs')?.output || '[]';" +
|
|
371
|
+
" let prs = [];" +
|
|
372
|
+
" try { prs = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { return 'main'; }" +
|
|
373
|
+
" if (!Array.isArray(prs)) return 'main';" +
|
|
374
|
+
" const pr = prs.find((p) => String(p?.number || '') === String($data?.targetPrNumber || ''));" +
|
|
375
|
+
" return pr?.baseRefName || 'main';" +
|
|
376
|
+
"})()",
|
|
377
|
+
isExpression: true,
|
|
378
|
+
}, { x: 400, y: 420 }),
|
|
379
|
+
|
|
338
380
|
node("has-conflicts", "condition.expression", "Any Conflicts?", {
|
|
339
381
|
expression: "Boolean($data?.targetPrNumber)",
|
|
340
|
-
}, { x: 400, y:
|
|
382
|
+
}, { x: 400, y: 510 }),
|
|
383
|
+
|
|
384
|
+
// Label the PR so the watchdog knows it is being worked on
|
|
385
|
+
node("label-fixing", "action.run_command", "Label bosun-needs-fix", {
|
|
386
|
+
command: "gh pr edit {{targetPrNumber}} --add-label bosun-needs-fix",
|
|
387
|
+
continueOnError: true,
|
|
388
|
+
}, { x: 200, y: 650 }),
|
|
341
389
|
|
|
342
390
|
node("resolve-conflicts", "action.run_agent", "Resolve Conflicts", {
|
|
343
|
-
prompt:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
391
|
+
prompt:
|
|
392
|
+
"You are a merge conflict resolution agent for PR #{{targetPrNumber}} " +
|
|
393
|
+
"on branch {{targetPrBranch}} (base: {{targetPrBase}}).\n\n" +
|
|
394
|
+
"Steps:\n" +
|
|
395
|
+
"1. git fetch origin\n" +
|
|
396
|
+
"2. git checkout {{targetPrBranch}}\n" +
|
|
397
|
+
"3. git rebase origin/{{targetPrBase}} (fall back to merge if rebase is too complex)\n" +
|
|
398
|
+
"4. Resolve all merge conflicts, preserving the intent of both sides.\n" +
|
|
399
|
+
"5. Run the repo's build and test suite to confirm nothing is broken.\n" +
|
|
400
|
+
"6. git push --force-with-lease origin {{targetPrBranch}}\n" +
|
|
401
|
+
"7. Remove the bosun-needs-fix label: gh pr edit {{targetPrNumber}} --remove-label bosun-needs-fix\n\n" +
|
|
402
|
+
"Rules:\n" +
|
|
403
|
+
"- Only make minimal conflict-resolution changes. No unrelated refactors.\n" +
|
|
404
|
+
"- Do NOT merge, close, or approve the PR — the Bosun PR Watchdog handles merging.\n" +
|
|
405
|
+
"- Do NOT touch PRs that do not have the bosun-attached label.",
|
|
351
406
|
sdk: "auto",
|
|
352
407
|
timeoutMs: 1800000,
|
|
353
408
|
failOnError: true,
|
|
354
409
|
maxRetries: "{{maxRetries}}",
|
|
355
410
|
retryDelayMs: 30000,
|
|
356
411
|
continueOnError: true,
|
|
357
|
-
}, { x: 200, y:
|
|
358
|
-
|
|
359
|
-
node("verify-ci", "action.run_command", "Verify CI Green", {
|
|
360
|
-
command: "gh pr checks {{targetPrNumber}} --json name,state",
|
|
361
|
-
failOnError: true,
|
|
362
|
-
maxRetries: "{{maxRetries}}",
|
|
363
|
-
retryDelayMs: 30000,
|
|
364
|
-
continueOnError: true,
|
|
365
|
-
}, { x: 200, y: 750 }),
|
|
366
|
-
|
|
367
|
-
node("ci-passed", "condition.expression", "CI Passed?", {
|
|
368
|
-
expression:
|
|
369
|
-
"(() => { const out = $ctx.getNodeOutput('verify-ci'); if (!out || out.success !== true) return false; let checks = []; try { checks = JSON.parse(out.output || '[]'); } catch { return false; } if (!Array.isArray(checks) || checks.length === 0) return false; const ok = new Set(['SUCCESS', 'PASSED', 'PASS', 'COMPLETED', 'NEUTRAL', 'SKIPPED']); return checks.every((c) => ok.has(String(c?.state || '').toUpperCase())); })()",
|
|
370
|
-
}, { x: 200, y: 910 }),
|
|
371
|
-
|
|
372
|
-
node("do-merge", "action.run_command", "Auto-Merge", {
|
|
373
|
-
command: "gh pr merge {{targetPrNumber}} --auto --squash",
|
|
374
|
-
failOnError: true,
|
|
375
|
-
maxRetries: "{{maxRetries}}",
|
|
376
|
-
retryDelayMs: 20000,
|
|
377
|
-
continueOnError: true,
|
|
378
|
-
}, { x: 100, y: 1040 }),
|
|
379
|
-
|
|
380
|
-
node("merge-succeeded", "condition.expression", "Merge Succeeded?", {
|
|
381
|
-
expression: "$ctx.getNodeOutput('do-merge')?.success === true",
|
|
382
|
-
}, { x: 100, y: 1140 }),
|
|
412
|
+
}, { x: 200, y: 800 }),
|
|
383
413
|
|
|
384
|
-
node("notify-fixed", "notify.telegram", "Notify
|
|
385
|
-
message: "🔧 PR #{{targetPrNumber}}
|
|
414
|
+
node("notify-fixed", "notify.telegram", "Notify Resolved", {
|
|
415
|
+
message: "🔧 PR #{{targetPrNumber}} conflict resolved — awaiting CI and Watchdog review before merge",
|
|
386
416
|
silent: true,
|
|
387
|
-
}, { x:
|
|
417
|
+
}, { x: 200, y: 960 }),
|
|
388
418
|
|
|
389
|
-
node("notify-failed", "notify.log", "Log
|
|
390
|
-
message: "PR #{{targetPrNumber}} conflict
|
|
419
|
+
node("notify-failed", "notify.log", "Log Resolution Failed", {
|
|
420
|
+
message: "PR #{{targetPrNumber}} conflict could not be resolved cleanly — manual review required",
|
|
391
421
|
level: "warn",
|
|
392
|
-
}, { x:
|
|
422
|
+
}, { x: 450, y: 800 }),
|
|
393
423
|
|
|
394
424
|
node("skip", "notify.log", "No Conflicts", {
|
|
395
|
-
message: "
|
|
425
|
+
message: "PR Conflict Resolver: no unhandled bosun-attached conflicts found",
|
|
396
426
|
level: "info",
|
|
397
|
-
}, { x: 620, y:
|
|
427
|
+
}, { x: 620, y: 510 }),
|
|
398
428
|
],
|
|
399
429
|
edges: [
|
|
400
|
-
edge("trigger",
|
|
401
|
-
edge("list-prs",
|
|
402
|
-
edge("target-pr",
|
|
403
|
-
edge("target-branch",
|
|
404
|
-
edge("
|
|
405
|
-
edge("has-conflicts",
|
|
406
|
-
edge("
|
|
407
|
-
edge("
|
|
408
|
-
edge("
|
|
409
|
-
edge("
|
|
410
|
-
edge("do-merge", "merge-succeeded"),
|
|
411
|
-
edge("merge-succeeded", "notify-fixed", { condition: "$output?.result === true" }),
|
|
412
|
-
edge("merge-succeeded", "notify-failed", { condition: "$output?.result !== true" }),
|
|
430
|
+
edge("trigger", "list-prs"),
|
|
431
|
+
edge("list-prs", "target-pr"),
|
|
432
|
+
edge("target-pr", "target-branch"),
|
|
433
|
+
edge("target-branch", "target-base"),
|
|
434
|
+
edge("target-base", "has-conflicts"),
|
|
435
|
+
edge("has-conflicts", "label-fixing", { condition: "$output?.result === true" }),
|
|
436
|
+
edge("has-conflicts", "skip", { condition: "$output?.result !== true" }),
|
|
437
|
+
edge("label-fixing", "resolve-conflicts"),
|
|
438
|
+
edge("resolve-conflicts", "notify-fixed", { condition: "$ctx.getNodeOutput('resolve-conflicts')?.success === true" }),
|
|
439
|
+
edge("resolve-conflicts", "notify-failed", { condition: "$ctx.getNodeOutput('resolve-conflicts')?.success !== true" }),
|
|
413
440
|
],
|
|
414
441
|
metadata: {
|
|
415
442
|
author: "bosun",
|
|
416
|
-
version:
|
|
443
|
+
version: 2,
|
|
417
444
|
createdAt: "2025-02-25T00:00:00Z",
|
|
418
|
-
templateVersion: "
|
|
419
|
-
tags: ["github", "pr", "conflict", "rebase", "automation"],
|
|
445
|
+
templateVersion: "2.0.0",
|
|
446
|
+
tags: ["github", "pr", "conflict", "rebase", "automation", "bosun-attached"],
|
|
420
447
|
replaces: {
|
|
421
448
|
module: "pr-cleanup-daemon.mjs",
|
|
422
449
|
functions: ["PRCleanupDaemon.run", "processCleanup", "resolveConflicts"],
|
|
423
450
|
calledFrom: ["monitor.mjs:startProcess"],
|
|
424
|
-
description:
|
|
425
|
-
"
|
|
426
|
-
"
|
|
451
|
+
description:
|
|
452
|
+
"v2: Restricted to bosun-attached PRs only — never touches external-contributor PRs. " +
|
|
453
|
+
"Removed direct auto-merge: this template now only resolves the conflict and pushes; " +
|
|
454
|
+
"the Bosun PR Watchdog (template-bosun-pr-watchdog) owns the merge decision with its " +
|
|
455
|
+
"diff-safety review gate. Skips PRs already tagged bosun-needs-fix (watchdog owns those). " +
|
|
456
|
+
"Labels PR with bosun-needs-fix during resolution so watchdog knows it is in-flight.",
|
|
427
457
|
},
|
|
428
458
|
},
|
|
429
459
|
};
|
|
@@ -611,3 +641,291 @@ Omit empty sections. Include contributor attribution. Be concise.`,
|
|
|
611
641
|
tags: ["github", "release", "notes", "changelog", "draft"],
|
|
612
642
|
},
|
|
613
643
|
};
|
|
644
|
+
|
|
645
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
646
|
+
// Bosun PR Watchdog
|
|
647
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
648
|
+
|
|
649
|
+
resetLayout();
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Bosun PR Watchdog — opt-in, scheduled CI poller for bosun-owned PRs.
|
|
653
|
+
*
|
|
654
|
+
* Only acts on PRs labelled `bosun-attached` (applied by the
|
|
655
|
+
* .github/workflows/bosun-pr-attach.yml GitHub Action when Bosun opens a PR).
|
|
656
|
+
* External-contributor and human PRs that lack that label are never touched.
|
|
657
|
+
*
|
|
658
|
+
* Per cycle:
|
|
659
|
+
* 1. List all open bosun-attached PRs.
|
|
660
|
+
* 2. Merge any PR whose CI checks are all passing (not draft, not pending).
|
|
661
|
+
* 3. Label any PR whose CI checks have failures with `bosun-needs-fix` and
|
|
662
|
+
* dispatch a repair agent to fix the branch.
|
|
663
|
+
*
|
|
664
|
+
* Disable: set `enabled: false` in your bosun config, or delete the workflow.
|
|
665
|
+
* Interval: default 5 min — change `intervalMs` / `cron` variables.
|
|
666
|
+
*/
|
|
667
|
+
export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
668
|
+
id: "template-bosun-pr-watchdog",
|
|
669
|
+
name: "Bosun PR Watchdog",
|
|
670
|
+
description:
|
|
671
|
+
"Scans open bosun-attached PRs on a schedule. Makes ONE gh API call to " +
|
|
672
|
+
"fetch and classify all PRs, then: labels conflicting or failing-CI PRs " +
|
|
673
|
+
"with bosun-needs-fix and dispatches a repair agent; sends merge candidates " +
|
|
674
|
+
"through a MANDATORY agent review gate that checks diff stats before any " +
|
|
675
|
+
"merge — preventing destructive PRs (e.g. -183k lines) from being silently " +
|
|
676
|
+
"auto-merged. External-contributor PRs without bosun-attached are never touched.",
|
|
677
|
+
category: "github",
|
|
678
|
+
enabled: false,
|
|
679
|
+
trigger: "trigger.schedule",
|
|
680
|
+
variables: {
|
|
681
|
+
mergeMethod: "squash", // squash | merge | rebase
|
|
682
|
+
labelNeedsFix: "bosun-needs-fix", // applied to CI failures and conflicts
|
|
683
|
+
labelNeedsReview: "bosun-needs-human-review", // applied when review agent flags a suspicious diff
|
|
684
|
+
maxPrs: 25,
|
|
685
|
+
intervalMs: 300_000, // 5 minutes
|
|
686
|
+
// Merge-safety thresholds checked by the review agent:
|
|
687
|
+
// If net deletions > additions × ratio AND deletions > minDestructiveDeletions → HOLD
|
|
688
|
+
suspiciousDeletionRatio: 3, // e.g. deletes 3× more lines than it adds
|
|
689
|
+
minDestructiveDeletions: 500, // absolute floor — small PRs are fine even if net negative
|
|
690
|
+
},
|
|
691
|
+
nodes: [
|
|
692
|
+
node("trigger", "trigger.schedule", "Poll Every 5 min", {
|
|
693
|
+
intervalMs: "{{intervalMs}}",
|
|
694
|
+
cron: "*/5 * * * *",
|
|
695
|
+
}, { x: 400, y: 50 }),
|
|
696
|
+
|
|
697
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
698
|
+
// STEP 1: ONE gh API call — fetch all fields we need in a single request.
|
|
699
|
+
// classify-and-label does all subsequent classification + labeling inline
|
|
700
|
+
// from this one response, so no repeated gh pr list calls later.
|
|
701
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
702
|
+
node("fetch-and-classify", "action.run_command", "Fetch, Classify & Label PRs", {
|
|
703
|
+
// Fetches all open bosun-attached PRs with every field needed for
|
|
704
|
+
// classification. Pipes into an inline Node script that:
|
|
705
|
+
// • Classifies each PR into: ready | conflict | ci_failure | pending | draft
|
|
706
|
+
// • Labels conflict/ci_failure PRs with bosun-needs-fix (skips if already present)
|
|
707
|
+
// • Outputs a JSON summary used by all downstream nodes/agents
|
|
708
|
+
// Total gh API calls this node makes: 1 list + N edits (only for newly-broken PRs)
|
|
709
|
+
command: [
|
|
710
|
+
"gh pr list --label bosun-attached --state open",
|
|
711
|
+
"--json number,title,headRefName,baseRefName,isDraft,mergeable,statusCheckRollup,labels,url",
|
|
712
|
+
"--limit {{maxPrs}}",
|
|
713
|
+
"| node -e \"",
|
|
714
|
+
"const LABEL_FIX='{{labelNeedsFix}}';",
|
|
715
|
+
"const {execSync}=require('child_process');",
|
|
716
|
+
"let raw='';",
|
|
717
|
+
"process.stdin.on('data',c=>raw+=c);",
|
|
718
|
+
"process.stdin.on('end',()=>{",
|
|
719
|
+
" let prs=[];",
|
|
720
|
+
" try{prs=JSON.parse(raw);}catch(e){console.log(JSON.stringify({error:e.message,total:0}));return;}",
|
|
721
|
+
" const FAIL_STATES=new Set(['FAILURE','ERROR','TIMED_OUT','CANCELLED','STARTUP_FAILURE']);",
|
|
722
|
+
" const PEND_STATES=new Set(['PENDING','IN_PROGRESS','QUEUED','WAITING','REQUESTED','EXPECTED']);",
|
|
723
|
+
" const CONFLICT_MERGEABLES=new Set(['CONFLICTING','BEHIND','DIRTY']);",
|
|
724
|
+
" const readyCandidates=[],conflicts=[],ciFailures=[],pending=[],drafted=[];",
|
|
725
|
+
" let newlyLabeled=0;",
|
|
726
|
+
" for(const pr of prs){",
|
|
727
|
+
" const labels=(pr.labels||[]).map(l=>l.name);",
|
|
728
|
+
" const hasFixLabel=labels.includes(LABEL_FIX);",
|
|
729
|
+
" const checks=pr.statusCheckRollup||[];",
|
|
730
|
+
" const hasFail=checks.some(c=>FAIL_STATES.has(c.conclusion||c.state||''));",
|
|
731
|
+
" const hasPend=checks.some(c=>PEND_STATES.has(c.conclusion||c.state||''));",
|
|
732
|
+
" const isConflict=CONFLICT_MERGEABLES.has(String(pr.mergeable||'').toUpperCase());",
|
|
733
|
+
" const isDraft=pr.isDraft===true;",
|
|
734
|
+
" if(isDraft){drafted.push(pr.number);continue;}",
|
|
735
|
+
" if(isConflict){",
|
|
736
|
+
" conflicts.push({n:pr.number,branch:pr.headRefName,base:pr.baseRefName,url:pr.url});",
|
|
737
|
+
" if(!hasFixLabel){",
|
|
738
|
+
" try{execSync('gh pr edit '+pr.number+' --add-label '+LABEL_FIX,{stdio:'pipe'});newlyLabeled++;}",
|
|
739
|
+
" catch(e){process.stderr.write('label err #'+pr.number+': '+e.message+'\\n');}",
|
|
740
|
+
" }",
|
|
741
|
+
" } else if(hasFail){",
|
|
742
|
+
" ciFailures.push({n:pr.number,branch:pr.headRefName,url:pr.url});",
|
|
743
|
+
" if(!hasFixLabel){",
|
|
744
|
+
" try{execSync('gh pr edit '+pr.number+' --add-label '+LABEL_FIX,{stdio:'pipe'});newlyLabeled++;}",
|
|
745
|
+
" catch(e){process.stderr.write('label err #'+pr.number+': '+e.message+'\\n');}",
|
|
746
|
+
" }",
|
|
747
|
+
" } else if(hasPend){",
|
|
748
|
+
" pending.push(pr.number);",
|
|
749
|
+
" } else if(checks.length>0&&!hasFixLabel){",
|
|
750
|
+
" // CI all-passing, no conflicts, not draft — a review candidate",
|
|
751
|
+
" readyCandidates.push({n:pr.number,branch:pr.headRefName,base:pr.baseRefName,url:pr.url,title:pr.title});",
|
|
752
|
+
" }",
|
|
753
|
+
" }",
|
|
754
|
+
" console.log(JSON.stringify({",
|
|
755
|
+
" total:prs.length,",
|
|
756
|
+
" readyCandidates,",
|
|
757
|
+
" conflicts,",
|
|
758
|
+
" ciFailures,",
|
|
759
|
+
" pending:pending.length,",
|
|
760
|
+
" drafted:drafted.length,",
|
|
761
|
+
" newlyLabeled,",
|
|
762
|
+
" fixNeeded:conflicts.length+ciFailures.length",
|
|
763
|
+
" }));",
|
|
764
|
+
"});",
|
|
765
|
+
"\"",
|
|
766
|
+
].join(" "),
|
|
767
|
+
continueOnError: false,
|
|
768
|
+
}, { x: 400, y: 200 }),
|
|
769
|
+
|
|
770
|
+
node("has-prs", "condition.expression", "Any Bosun PRs?", {
|
|
771
|
+
expression:
|
|
772
|
+
"(()=>{try{" +
|
|
773
|
+
"const o=$ctx.getNodeOutput('fetch-and-classify')?.output;" +
|
|
774
|
+
"return (JSON.parse(o||'{}').total||0)>0;" +
|
|
775
|
+
"}catch(e){return false;}})()",
|
|
776
|
+
}, { x: 400, y: 370 }),
|
|
777
|
+
|
|
778
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
779
|
+
// STEP 2a: Fix path — dispatch ONE agent for all conflicts + CI failures.
|
|
780
|
+
// The agent handles rebase/conflict-resolution AND CI lint/test fixes.
|
|
781
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
782
|
+
node("fix-needed", "condition.expression", "Fix Needed?", {
|
|
783
|
+
expression:
|
|
784
|
+
"(()=>{try{" +
|
|
785
|
+
"const o=$ctx.getNodeOutput('fetch-and-classify')?.output;" +
|
|
786
|
+
"return (JSON.parse(o||'{}').fixNeeded||0)>0;" +
|
|
787
|
+
"}catch(e){return false;}})()",
|
|
788
|
+
}, { x: 200, y: 530 }),
|
|
789
|
+
|
|
790
|
+
node("dispatch-fix", "action.run_agent", "Dispatch Fix Agent", {
|
|
791
|
+
prompt:
|
|
792
|
+
"You are a Bosun PR repair agent. A watchdog workflow has identified " +
|
|
793
|
+
"bosun-attached PRs that need fixing.\n\n" +
|
|
794
|
+
"Run this single command to get the current list of PRs needing work:\n" +
|
|
795
|
+
" gh pr list --label bosun-needs-fix --label bosun-attached --state open \\\n" +
|
|
796
|
+
" --json number,title,headRefName,baseRefName,mergeable,statusCheckRollup,labels,url \\\n" +
|
|
797
|
+
" --limit 10\n\n" +
|
|
798
|
+
"For each PR returned, follow the appropriate path:\n\n" +
|
|
799
|
+
"CONFLICT (mergeable is CONFLICTING, BEHIND, or DIRTY):\n" +
|
|
800
|
+
"1. git fetch origin\n" +
|
|
801
|
+
"2. git checkout <headRefName>\n" +
|
|
802
|
+
"3. git rebase origin/<baseRefName> (or git merge origin/<baseRefName> if rebase is too complex)\n" +
|
|
803
|
+
"4. Resolve merge conflicts, preserving the intent of both sides.\n" +
|
|
804
|
+
"5. Run the repo's build + test suite to confirm nothing is broken.\n" +
|
|
805
|
+
"6. git push --force-with-lease origin <headRefName>\n\n" +
|
|
806
|
+
"CI FAILURE (statusCheckRollup has FAILURE/ERROR/TIMED_OUT entries):\n" +
|
|
807
|
+
"1. git checkout <headRefName>\n" +
|
|
808
|
+
"2. Inspect CI logs: gh run list --branch <headRefName> --limit 3\n" +
|
|
809
|
+
" Then: gh run view <run-id> --log-failed\n" +
|
|
810
|
+
"3. Fix the root cause (lint, type error, failing test, build error).\n" +
|
|
811
|
+
"4. Commit: fix(<scope>): <description> (conventional commit format)\n" +
|
|
812
|
+
"5. Push the branch — CI will re-trigger automatically.\n\n" +
|
|
813
|
+
"AFTER fixing either type:\n" +
|
|
814
|
+
"- Remove the bosun-needs-fix label: gh pr edit <number> --remove-label bosun-needs-fix\n\n" +
|
|
815
|
+
"STRICT RULES:\n" +
|
|
816
|
+
"- Fix only what breaks CI or causes the conflict. No scope creep.\n" +
|
|
817
|
+
"- Do NOT merge, close, or approve any PR.\n" +
|
|
818
|
+
"- Do NOT touch PRs that do not have the bosun-attached label.\n" +
|
|
819
|
+
"- If a conflict cannot be resolved cleanly, leave it labeled and add a comment explaining why.",
|
|
820
|
+
sdk: "auto",
|
|
821
|
+
timeoutMs: 1_800_000,
|
|
822
|
+
maxRetries: 2,
|
|
823
|
+
retryDelayMs: 30_000,
|
|
824
|
+
continueOnError: true,
|
|
825
|
+
}, { x: 200, y: 700 }),
|
|
826
|
+
|
|
827
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
828
|
+
// STEP 2b: Review gate — MANDATORY before any merge.
|
|
829
|
+
// The review agent checks diff stats per candidate and is the ONLY thing
|
|
830
|
+
// that can call `gh pr merge`. It blocks suspicious/destructive diffs.
|
|
831
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
832
|
+
node("review-needed", "condition.expression", "Review Candidates?", {
|
|
833
|
+
expression:
|
|
834
|
+
"(()=>{try{" +
|
|
835
|
+
"const o=$ctx.getNodeOutput('fetch-and-classify')?.output;" +
|
|
836
|
+
"return (JSON.parse(o||'{}').readyCandidates||[]).length>0;" +
|
|
837
|
+
"}catch(e){return false;}})()",
|
|
838
|
+
}, { x: 600, y: 530 }),
|
|
839
|
+
|
|
840
|
+
node("dispatch-review", "action.run_agent", "Review Gate: Inspect & Merge", {
|
|
841
|
+
prompt:
|
|
842
|
+
"You are the Bosun PR merge review agent — the LAST LINE OF DEFENCE before " +
|
|
843
|
+
"any PR is merged. Your job is to inspect each merge candidate and decide " +
|
|
844
|
+
"whether it is safe to merge.\n\n" +
|
|
845
|
+
"MERGE CANDIDATES (CI passing, no conflicts, bosun-attached):\n" +
|
|
846
|
+
" Run: gh pr list --label bosun-attached --state open \\\n" +
|
|
847
|
+
" --json number,title,headRefName,isDraft,statusCheckRollup,labels,url \\\n" +
|
|
848
|
+
" --limit {{maxPrs}}\n" +
|
|
849
|
+
" Filter to PRs where: not isDraft, CI all-passing, no bosun-needs-fix label.\n\n" +
|
|
850
|
+
"FOR EACH CANDIDATE — before merging, run:\n" +
|
|
851
|
+
" gh pr view <number> --json number,title,additions,deletions,changedFiles,body,baseRefName\n\n" +
|
|
852
|
+
"SAFETY CHECKS (ALL must pass before merging):\n\n" +
|
|
853
|
+
"1. DESTRUCTIVE DIFF CHECK:\n" +
|
|
854
|
+
" If (deletions > additions × {{suspiciousDeletionRatio}}) AND (deletions > {{minDestructiveDeletions}}):\n" +
|
|
855
|
+
" → This PR deletes far more than it adds — HOLD IT.\n" +
|
|
856
|
+
" → Run: gh pr edit <number> --add-label {{labelNeedsReview}}\n" +
|
|
857
|
+
" → Run: gh pr comment <number> --body '⚠️ **Bosun Review Agent: merge held** — " +
|
|
858
|
+
"This PR deletes significantly more lines than it adds (deletions: <X>, additions: <Y>). " +
|
|
859
|
+
"A human should verify this is intentional before merging.'\n" +
|
|
860
|
+
" → Do NOT merge this PR. Move to next candidate.\n\n" +
|
|
861
|
+
"2. DIFF SANITY CHECK (for PRs that pass the ratio check):\n" +
|
|
862
|
+
" Run: gh pr diff <number> | head -200\n" +
|
|
863
|
+
" Look for: mass file deletions, removal of entire modules/directories, " +
|
|
864
|
+
" files changed that are unrelated to the PR description.\n" +
|
|
865
|
+
" If something looks wrong → HOLD with bosun-needs-human-review label + comment.\n\n" +
|
|
866
|
+
"3. CI STATUS RECONFIRM:\n" +
|
|
867
|
+
" Run: gh pr checks <number> --json name,state,conclusion\n" +
|
|
868
|
+
" Ensure ALL checks have conclusion SUCCESS/SKIPPED/NEUTRAL. " +
|
|
869
|
+
" If any are pending or failing → do NOT merge (CI may still be running).\n\n" +
|
|
870
|
+
"MERGE (only if ALL checks pass):\n" +
|
|
871
|
+
" gh pr merge <number> --{{mergeMethod}} --delete-branch\n" +
|
|
872
|
+
" Log: ✅ Merged PR #<number> — <title>\n\n" +
|
|
873
|
+
"STRICT RULES:\n" +
|
|
874
|
+
"- NEVER merge if ANY safety check fails. When in doubt, HOLD.\n" +
|
|
875
|
+
"- NEVER merge PRs without the bosun-attached label.\n" +
|
|
876
|
+
"- NEVER merge draft PRs.\n" +
|
|
877
|
+
"- The bosun-needs-human-review label means a human must look at it before bosun touches it again.\n" +
|
|
878
|
+
"- After processing all candidates, output a summary: { merged: N, held: N, skipped: N }",
|
|
879
|
+
sdk: "auto",
|
|
880
|
+
timeoutMs: 1_200_000,
|
|
881
|
+
maxRetries: 1,
|
|
882
|
+
retryDelayMs: 30_000,
|
|
883
|
+
continueOnError: true,
|
|
884
|
+
}, { x: 600, y: 700 }),
|
|
885
|
+
|
|
886
|
+
node("notify", "notify.telegram", "Watchdog Report", {
|
|
887
|
+
message:
|
|
888
|
+
"🐕 Bosun PR Watchdog cycle complete — " +
|
|
889
|
+
"fix-dispatched: {{fixNeeded}} | candidates-reviewed: {{readyCandidates}}",
|
|
890
|
+
silent: true,
|
|
891
|
+
}, { x: 400, y: 900 }),
|
|
892
|
+
|
|
893
|
+
node("no-prs", "notify.log", "No Bosun PRs Open", {
|
|
894
|
+
message: "Bosun PR Watchdog: no open bosun-attached PRs found — idle",
|
|
895
|
+
level: "info",
|
|
896
|
+
}, { x: 700, y: 370 }),
|
|
897
|
+
],
|
|
898
|
+
edges: [
|
|
899
|
+
edge("trigger", "fetch-and-classify"),
|
|
900
|
+
edge("fetch-and-classify","has-prs"),
|
|
901
|
+
edge("has-prs", "fix-needed", { condition: "$output?.result === true" }),
|
|
902
|
+
edge("has-prs", "no-prs", { condition: "$output?.result !== true" }),
|
|
903
|
+
// Fix path (conflicts + CI failures)
|
|
904
|
+
edge("fix-needed", "dispatch-fix", { condition: "$output?.result === true" }),
|
|
905
|
+
edge("fix-needed", "review-needed", { condition: "$output?.result !== true" }),
|
|
906
|
+
edge("dispatch-fix", "review-needed"),
|
|
907
|
+
// Review gate (merge candidates)
|
|
908
|
+
edge("review-needed", "dispatch-review", { condition: "$output?.result === true" }),
|
|
909
|
+
edge("review-needed", "notify", { condition: "$output?.result !== true" }),
|
|
910
|
+
edge("dispatch-review", "notify"),
|
|
911
|
+
],
|
|
912
|
+
metadata: {
|
|
913
|
+
author: "bosun",
|
|
914
|
+
version: 2,
|
|
915
|
+
createdAt: "2025-07-01T00:00:00Z",
|
|
916
|
+
templateVersion: "2.0.0",
|
|
917
|
+
tags: ["github", "pr", "ci", "merge", "watchdog", "bosun-attached", "safety"],
|
|
918
|
+
replaces: {
|
|
919
|
+
module: "agent-hooks.mjs",
|
|
920
|
+
functions: ["registerBuiltinHooks (PostPR block)"],
|
|
921
|
+
calledFrom: [],
|
|
922
|
+
description:
|
|
923
|
+
"v2: Consolidates all gh API calls into ONE gh pr list fetch per cycle. " +
|
|
924
|
+
"Adds mandatory review gate agent that checks diff stats (additions/deletions " +
|
|
925
|
+
"ratio) and diff content before any merge — preventing destructive PRs from " +
|
|
926
|
+
"being auto-merged. Adds conflict detection via the 'mergeable' field. " +
|
|
927
|
+
"Single fix agent handles both conflict resolution and CI failures. " +
|
|
928
|
+
"All external PRs (no bosun-attached label) are never touched.",
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
};
|
|
@@ -53,20 +53,29 @@ export const TASK_PLANNER_TEMPLATE = {
|
|
|
53
53
|
dedup: true,
|
|
54
54
|
}, { x: 400, y: 440 }),
|
|
55
55
|
|
|
56
|
-
node("
|
|
57
|
-
|
|
56
|
+
node("materialize-tasks", "action.materialize_planner_tasks", "Create Tasks", {
|
|
57
|
+
plannerNodeId: "run-planner",
|
|
58
|
+
maxTasks: 5,
|
|
59
|
+
status: "todo",
|
|
60
|
+
dedup: true,
|
|
61
|
+
failOnZero: true,
|
|
62
|
+
minCreated: 1,
|
|
58
63
|
}, { x: 400, y: 570 }),
|
|
59
64
|
|
|
65
|
+
node("check-result", "condition.expression", "Planner Succeeded?", {
|
|
66
|
+
expression: "$ctx.getNodeOutput('materialize-tasks')?.success === true && ($ctx.getNodeOutput('materialize-tasks')?.createdCount || 0) > 0",
|
|
67
|
+
}, { x: 400, y: 700 }),
|
|
68
|
+
|
|
60
69
|
node("set-timestamp", "action.set_variable", "Update Last Run", {
|
|
61
70
|
key: "_lastPlannerRun",
|
|
62
71
|
value: "Date.now()",
|
|
63
72
|
isExpression: true,
|
|
64
|
-
}, { x: 200, y:
|
|
73
|
+
}, { x: 200, y: 830 }),
|
|
65
74
|
|
|
66
75
|
node("notify-done", "notify.telegram", "Notify Tasks Created", {
|
|
67
|
-
message: "🗂️ Task planner
|
|
76
|
+
message: "🗂️ Task planner created {{materialize-tasks.createdCount}} backlog tasks (skipped {{materialize-tasks.skippedCount}} duplicates).",
|
|
68
77
|
silent: true,
|
|
69
|
-
}, { x: 200, y:
|
|
78
|
+
}, { x: 200, y: 960 }),
|
|
70
79
|
|
|
71
80
|
node("notify-skip", "notify.log", "Log Dedup Skip", {
|
|
72
81
|
message: "Task planner skipped: within dedup window",
|
|
@@ -74,16 +83,17 @@ export const TASK_PLANNER_TEMPLATE = {
|
|
|
74
83
|
}, { x: 650, y: 180 }),
|
|
75
84
|
|
|
76
85
|
node("notify-fail", "notify.log", "Log Planner Failure", {
|
|
77
|
-
message: "Task planner failed to
|
|
86
|
+
message: "Task planner failed to materialize tasks from planner output",
|
|
78
87
|
level: "warn",
|
|
79
|
-
}, { x: 600, y:
|
|
88
|
+
}, { x: 600, y: 830 }),
|
|
80
89
|
],
|
|
81
90
|
edges: [
|
|
82
91
|
edge("trigger", "check-dedup"),
|
|
83
92
|
edge("check-dedup", "log-start", { condition: "$output?.result === true" }),
|
|
84
93
|
edge("check-dedup", "notify-skip", { condition: "$output?.result !== true" }),
|
|
85
94
|
edge("log-start", "run-planner"),
|
|
86
|
-
edge("run-planner", "
|
|
95
|
+
edge("run-planner", "materialize-tasks"),
|
|
96
|
+
edge("materialize-tasks", "check-result"),
|
|
87
97
|
edge("check-result", "set-timestamp", { condition: "$output?.result === true" }),
|
|
88
98
|
edge("check-result", "notify-fail", { condition: "$output?.result !== true" }),
|
|
89
99
|
edge("set-timestamp", "notify-done"),
|
|
@@ -143,10 +153,19 @@ export const TASK_REPLENISH_TEMPLATE = {
|
|
|
143
153
|
context: "Scheduled replenishment run. Prioritize implementation tasks that build on recent PRs.",
|
|
144
154
|
}, { x: 400, y: 440 }),
|
|
145
155
|
|
|
156
|
+
node("materialize-tasks", "action.materialize_planner_tasks", "Create Tasks", {
|
|
157
|
+
plannerNodeId: "run-planner",
|
|
158
|
+
maxTasks: 8,
|
|
159
|
+
status: "todo",
|
|
160
|
+
dedup: true,
|
|
161
|
+
failOnZero: true,
|
|
162
|
+
minCreated: 1,
|
|
163
|
+
}, { x: 400, y: 570 }),
|
|
164
|
+
|
|
146
165
|
node("notify", "notify.telegram", "Notify", {
|
|
147
|
-
message: "🔄 Scheduled
|
|
166
|
+
message: "🔄 Scheduled replenishment created {{materialize-tasks.createdCount}} tasks (skipped {{materialize-tasks.skippedCount}}).",
|
|
148
167
|
silent: true,
|
|
149
|
-
}, { x: 400, y:
|
|
168
|
+
}, { x: 400, y: 700 }),
|
|
150
169
|
|
|
151
170
|
node("skip-log", "notify.log", "No Replenish Needed", {
|
|
152
171
|
message: "Scheduled replenishment check: backlog sufficient, skipping",
|
|
@@ -157,7 +176,8 @@ export const TASK_REPLENISH_TEMPLATE = {
|
|
|
157
176
|
edge("check-backlog", "needs-tasks"),
|
|
158
177
|
edge("needs-tasks", "run-planner", { condition: "$output?.result === true" }),
|
|
159
178
|
edge("needs-tasks", "skip-log", { condition: "$output?.result !== true" }),
|
|
160
|
-
edge("run-planner", "
|
|
179
|
+
edge("run-planner", "materialize-tasks"),
|
|
180
|
+
edge("materialize-tasks", "notify"),
|
|
161
181
|
],
|
|
162
182
|
metadata: {
|
|
163
183
|
author: "bosun",
|
package/workflow-templates.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
PR_CONFLICT_RESOLVER_TEMPLATE,
|
|
44
44
|
STALE_PR_REAPER_TEMPLATE,
|
|
45
45
|
RELEASE_DRAFTER_TEMPLATE,
|
|
46
|
+
BOSUN_PR_WATCHDOG_TEMPLATE,
|
|
46
47
|
} from "./workflow-templates/github.mjs";
|
|
47
48
|
|
|
48
49
|
// Agents
|
|
@@ -95,6 +96,7 @@ export {
|
|
|
95
96
|
PR_CONFLICT_RESOLVER_TEMPLATE,
|
|
96
97
|
STALE_PR_REAPER_TEMPLATE,
|
|
97
98
|
RELEASE_DRAFTER_TEMPLATE,
|
|
99
|
+
BOSUN_PR_WATCHDOG_TEMPLATE,
|
|
98
100
|
FRONTEND_AGENT_TEMPLATE,
|
|
99
101
|
REVIEW_AGENT_TEMPLATE,
|
|
100
102
|
CUSTOM_AGENT_TEMPLATE,
|
|
@@ -141,6 +143,7 @@ export const WORKFLOW_TEMPLATES = Object.freeze([
|
|
|
141
143
|
PR_CONFLICT_RESOLVER_TEMPLATE,
|
|
142
144
|
STALE_PR_REAPER_TEMPLATE,
|
|
143
145
|
RELEASE_DRAFTER_TEMPLATE,
|
|
146
|
+
BOSUN_PR_WATCHDOG_TEMPLATE,
|
|
144
147
|
// ── Agents ──
|
|
145
148
|
REVIEW_AGENT_TEMPLATE,
|
|
146
149
|
FRONTEND_AGENT_TEMPLATE,
|