bosun 0.37.1 → 0.37.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 +4 -1
- package/agent-tool-config.mjs +14 -3
- package/bosun-skills.mjs +59 -4
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +48 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +2 -1
- package/setup-web-server.mjs +71 -10
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +110 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +3 -0
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +260 -38
- package/ui/modules/voice-client.js +662 -58
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +219 -9
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +22 -5
- package/ui-server.mjs +961 -103
- package/voice-relay.mjs +119 -11
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
|
@@ -244,14 +244,26 @@ export const PR_TRIAGE_TEMPLATE = {
|
|
|
244
244
|
command: "gh pr edit {{prNumber}} --add-label \"size/L\"",
|
|
245
245
|
}, { x: 650, y: 480 }),
|
|
246
246
|
|
|
247
|
-
node("detect-breaking", "
|
|
248
|
-
|
|
249
|
-
|
|
247
|
+
node("detect-breaking", "condition.expression", "Detect Breaking Changes", {
|
|
248
|
+
expression:
|
|
249
|
+
"(() => {" +
|
|
250
|
+
" const raw=$ctx.getNodeOutput('get-stats')?.output||'{}';" +
|
|
251
|
+
" let stats={};" +
|
|
252
|
+
" try{stats=typeof raw==='string'?JSON.parse(raw):raw;}catch{return false;}" +
|
|
253
|
+
" const title=String(stats?.title||'').toLowerCase();" +
|
|
254
|
+
" const body=String(stats?.body||'').toLowerCase();" +
|
|
255
|
+
" const files=Array.isArray(stats?.files)?stats.files.map((f)=>String(f?.path||f?.filename||f||'').toLowerCase()):[];" +
|
|
256
|
+
" const text=title+'\\n'+body;" +
|
|
257
|
+
" const explicit=/\\bbreaking\\b|\\bbreaking change\\b|\\bmajor\\b|\\bbackward incompatible\\b/.test(text);" +
|
|
258
|
+
" const apiTouch=files.some((f)=>f.includes('api/')||f.includes('/proto/')||f.includes('openapi')||f.includes('schema'));" +
|
|
259
|
+
" const contractWords=/\\bremove\\b|\\brename\\b|\\bdeprecate\\b|\\bdrop\\b/.test(text);" +
|
|
260
|
+
" return explicit || (apiTouch && contractWords);" +
|
|
261
|
+
"})()",
|
|
250
262
|
}, { x: 400, y: 630 }),
|
|
251
263
|
|
|
252
264
|
node("is-breaking", "condition.expression", "Breaking?", {
|
|
253
265
|
expression:
|
|
254
|
-
"(() => {
|
|
266
|
+
"(() => { return $ctx.getNodeOutput('detect-breaking')?.result === true; })()",
|
|
255
267
|
}, { x: 400, y: 780, outputs: ["yes", "no"] }),
|
|
256
268
|
|
|
257
269
|
node("label-breaking", "action.run_command", "Label: Breaking", {
|
|
@@ -876,43 +888,73 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
876
888
|
"}catch(e){return false;}})()",
|
|
877
889
|
}, { x: 200, y: 530 }),
|
|
878
890
|
|
|
879
|
-
node("
|
|
891
|
+
node("programmatic-fix", "action.run_command", "Programmatic Fix Pass", {
|
|
892
|
+
command: [
|
|
893
|
+
"node -e \"",
|
|
894
|
+
"const {execFileSync}=require('child_process');",
|
|
895
|
+
"const raw=String(process.env.BOSUN_FETCH_AND_CLASSIFY||'');",
|
|
896
|
+
"const payload=(()=>{try{return JSON.parse(raw||'{}')}catch{return {}}})();",
|
|
897
|
+
"const ciFailures=Array.isArray(payload.ciFailures)?payload.ciFailures:[];",
|
|
898
|
+
"const conflicts=Array.isArray(payload.conflicts)?payload.conflicts:[];",
|
|
899
|
+
"const needsAgent=[];",
|
|
900
|
+
"let rerunRequested=0;",
|
|
901
|
+
"function runGh(args){return execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();}",
|
|
902
|
+
"for(const item of ciFailures){",
|
|
903
|
+
" const repo=String(item?.repo||'').trim();",
|
|
904
|
+
" const branch=String(item?.branch||'').trim();",
|
|
905
|
+
" const n=String(item?.n||'').trim();",
|
|
906
|
+
" if(!repo||!branch){needsAgent.push({repo,number:n,reason:'missing_repo_or_branch'});continue;}",
|
|
907
|
+
" try{",
|
|
908
|
+
" const listRaw=runGh(['run','list','--repo',repo,'--branch',branch,'--json','databaseId,conclusion,status','--limit','8']);",
|
|
909
|
+
" const runs=(()=>{try{return JSON.parse(listRaw||'[]')}catch{return []}})();",
|
|
910
|
+
" const failed=(Array.isArray(runs)?runs:[]).find((r)=>{",
|
|
911
|
+
" const c=String(r?.conclusion||'').toUpperCase();",
|
|
912
|
+
" return c==='FAILURE'||c==='ERROR'||c==='TIMED_OUT'||c==='CANCELLED';",
|
|
913
|
+
" });",
|
|
914
|
+
" if(failed?.databaseId){runGh(['run','rerun',String(failed.databaseId),'--repo',repo]);rerunRequested++;}",
|
|
915
|
+
" else{needsAgent.push({repo,number:n,reason:'no_rerunnable_failed_run_found'});}",
|
|
916
|
+
" }catch(e){needsAgent.push({repo,number:n,reason:'ci_rerun_failed',error:String(e?.message||e)});}",
|
|
917
|
+
"}",
|
|
918
|
+
"for(const item of conflicts){",
|
|
919
|
+
" needsAgent.push({repo:String(item?.repo||'').trim(),number:String(item?.n||'').trim(),branch:String(item?.branch||'').trim(),base:String(item?.base||'').trim(),reason:'merge_conflict_requires_code_resolution'});",
|
|
920
|
+
"}",
|
|
921
|
+
"console.log(JSON.stringify({rerunRequested,ciFailureCount:ciFailures.length,conflictCount:conflicts.length,needsAgentCount:needsAgent.length,needsAgent}));",
|
|
922
|
+
"\"",
|
|
923
|
+
].join(" "),
|
|
924
|
+
continueOnError: true,
|
|
925
|
+
failOnError: false,
|
|
926
|
+
env: {
|
|
927
|
+
BOSUN_FETCH_AND_CLASSIFY:
|
|
928
|
+
"(()=>{const r=$ctx.getNodeOutput('fetch-and-classify');return r&&r.success!==false?String(r.output||'{}'):'{}';})()",
|
|
929
|
+
},
|
|
930
|
+
}, { x: 200, y: 640 }),
|
|
931
|
+
|
|
932
|
+
node("fix-agent-needed", "condition.expression", "Needs Agent Fix?", {
|
|
933
|
+
expression:
|
|
934
|
+
"(()=>{try{" +
|
|
935
|
+
"const o=$ctx.getNodeOutput('programmatic-fix')?.output;" +
|
|
936
|
+
"return (JSON.parse(o||'{}').needsAgentCount||0)>0;" +
|
|
937
|
+
"}catch(e){return false;}})()",
|
|
938
|
+
}, { x: 200, y: 750 }),
|
|
939
|
+
|
|
940
|
+
node("dispatch-fix-agent", "action.run_agent", "Dispatch Fix Agent (Fallback)", {
|
|
880
941
|
prompt:
|
|
881
|
-
"You are a Bosun PR repair agent. A
|
|
882
|
-
"
|
|
883
|
-
"
|
|
884
|
-
"
|
|
885
|
-
"
|
|
886
|
-
"
|
|
887
|
-
" --limit 10\n\n" +
|
|
888
|
-
"For each PR returned, follow the appropriate path:\n\n" +
|
|
889
|
-
"CONFLICT (mergeable is CONFLICTING, BEHIND, or DIRTY):\n" +
|
|
890
|
-
"1. git fetch origin\n" +
|
|
891
|
-
"2. git checkout <headRefName>\n" +
|
|
892
|
-
"3. git rebase origin/<baseRefName> (or git merge origin/<baseRefName> if rebase is too complex)\n" +
|
|
893
|
-
"4. Resolve merge conflicts, preserving the intent of both sides.\n" +
|
|
894
|
-
"5. Run the repo's build + test suite to confirm nothing is broken.\n" +
|
|
895
|
-
"6. git push --force-with-lease origin <headRefName>\n\n" +
|
|
896
|
-
"CI FAILURE (statusCheckRollup has FAILURE/ERROR/TIMED_OUT entries):\n" +
|
|
897
|
-
"1. git checkout <headRefName>\n" +
|
|
898
|
-
"2. Inspect CI logs: gh run list --repo <owner/repo> --branch <headRefName> --limit 3\n" +
|
|
899
|
-
" Then: gh run view <run-id> --repo <owner/repo> --log-failed\n" +
|
|
900
|
-
"3. Fix the root cause (lint, type error, failing test, build error).\n" +
|
|
901
|
-
"4. Commit: fix(<scope>): <description> (conventional commit format)\n" +
|
|
902
|
-
"5. Push the branch — CI will re-trigger automatically.\n\n" +
|
|
903
|
-
"AFTER fixing either type:\n" +
|
|
904
|
-
"- Remove the bosun-needs-fix label: gh pr edit <number> --repo <owner/repo> --remove-label bosun-needs-fix\n\n" +
|
|
942
|
+
"You are a Bosun PR repair fallback agent. A deterministic CLI fix pass has already run. " +
|
|
943
|
+
"Only work unresolved items from this JSON:\n\n" +
|
|
944
|
+
"{{$ctx.getNodeOutput('programmatic-fix')?.output}}\n\n" +
|
|
945
|
+
"For conflict items: rebase/merge branch onto base, resolve conflicts, run tests, push with --force-with-lease if needed.\n" +
|
|
946
|
+
"For CI-failure items: inspect failed logs, apply minimal fix, commit and push.\n" +
|
|
947
|
+
"After successful repair remove bosun-needs-fix label.\n\n" +
|
|
905
948
|
"STRICT RULES:\n" +
|
|
906
|
-
"- Fix only
|
|
907
|
-
"- Do NOT merge
|
|
908
|
-
"- Do NOT touch PRs
|
|
909
|
-
"- If a conflict cannot be resolved cleanly, leave it labeled and add a comment explaining why.",
|
|
949
|
+
"- Fix only CI/conflict issues. No scope creep.\n" +
|
|
950
|
+
"- Do NOT merge/close/approve PRs.\n" +
|
|
951
|
+
"- Do NOT touch PRs without bosun-attached.",
|
|
910
952
|
sdk: "auto",
|
|
911
953
|
timeoutMs: 1_800_000,
|
|
912
954
|
maxRetries: 2,
|
|
913
955
|
retryDelayMs: 30_000,
|
|
914
956
|
continueOnError: true,
|
|
915
|
-
}, { x: 200, y:
|
|
957
|
+
}, { x: 200, y: 860 }),
|
|
916
958
|
|
|
917
959
|
// ─────────────────────────────────────────────────────────────────────────
|
|
918
960
|
// STEP 2b: Review gate — MANDATORY before any merge.
|
|
@@ -927,50 +969,65 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
927
969
|
"}catch(e){return false;}})()",
|
|
928
970
|
}, { x: 600, y: 530 }),
|
|
929
971
|
|
|
930
|
-
node("
|
|
931
|
-
|
|
932
|
-
"
|
|
933
|
-
"
|
|
934
|
-
"
|
|
935
|
-
"
|
|
936
|
-
"
|
|
937
|
-
"
|
|
938
|
-
"
|
|
939
|
-
"
|
|
940
|
-
"
|
|
941
|
-
"
|
|
942
|
-
"
|
|
943
|
-
"
|
|
944
|
-
"
|
|
945
|
-
"
|
|
946
|
-
"
|
|
947
|
-
"
|
|
948
|
-
"
|
|
949
|
-
"
|
|
950
|
-
"
|
|
951
|
-
"
|
|
952
|
-
"
|
|
953
|
-
"
|
|
954
|
-
"
|
|
955
|
-
"
|
|
956
|
-
"
|
|
957
|
-
"
|
|
958
|
-
"
|
|
959
|
-
"
|
|
960
|
-
"
|
|
961
|
-
"
|
|
962
|
-
"
|
|
963
|
-
"
|
|
964
|
-
"
|
|
965
|
-
"
|
|
966
|
-
"
|
|
967
|
-
"
|
|
968
|
-
"
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
972
|
+
node("programmatic-review", "action.run_command", "Review Gate: Programmatic Merge", {
|
|
973
|
+
command: [
|
|
974
|
+
"node -e \"",
|
|
975
|
+
"const {execFileSync}=require('child_process');",
|
|
976
|
+
"const raw=String(process.env.BOSUN_FETCH_AND_CLASSIFY||'');",
|
|
977
|
+
"const payload=(()=>{try{return JSON.parse(raw||'{}')}catch{return {}}})();",
|
|
978
|
+
"const candidates=Array.isArray(payload.readyCandidates)?payload.readyCandidates:[];",
|
|
979
|
+
"const ratio=Number('{{suspiciousDeletionRatio}}')||3;",
|
|
980
|
+
"const minDel=Number('{{minDestructiveDeletions}}')||500;",
|
|
981
|
+
"const labelReview=String('{{labelNeedsReview}}'||'bosun-needs-human-review');",
|
|
982
|
+
"const method=String('{{mergeMethod}}'||'squash').toLowerCase();",
|
|
983
|
+
"const merged=[]; const held=[]; const skipped=[];",
|
|
984
|
+
"function gh(args){return execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();}",
|
|
985
|
+
"for(const c of candidates){",
|
|
986
|
+
" const repo=String(c?.repo||'').trim();",
|
|
987
|
+
" const n=String(c?.n||'').trim();",
|
|
988
|
+
" if(!repo||!n){skipped.push({repo,number:n,reason:'missing_repo_or_pr'});continue;}",
|
|
989
|
+
" try{",
|
|
990
|
+
" const viewRaw=gh(['pr','view',n,'--repo',repo,'--json','number,title,additions,deletions,changedFiles,isDraft']);",
|
|
991
|
+
" const view=(()=>{try{return JSON.parse(viewRaw||'{}')}catch{return {}}})();",
|
|
992
|
+
" if(view?.isDraft===true){skipped.push({repo,number:n,reason:'draft'});continue;}",
|
|
993
|
+
" const add=Number(view?.additions||0);",
|
|
994
|
+
" const del=Number(view?.deletions||0);",
|
|
995
|
+
" const changed=Number(view?.changedFiles||0);",
|
|
996
|
+
" const destructive=(del>(add*ratio))&&(del>minDel);",
|
|
997
|
+
" const tooWide=changed>250;",
|
|
998
|
+
" if(destructive||tooWide){",
|
|
999
|
+
" gh(['pr','edit',n,'--repo',repo,'--add-label',labelReview]);",
|
|
1000
|
+
" gh(['pr','comment',n,'--repo',repo,'--body',':warning: Bosun held this PR for human review due to suspicious diff footprint.']);",
|
|
1001
|
+
" held.push({repo,number:n,reason:destructive?'destructive_diff':'changed_files_too_large',additions:add,deletions:del,changedFiles:changed});",
|
|
1002
|
+
" continue;",
|
|
1003
|
+
" }",
|
|
1004
|
+
" const checksRaw=gh(['pr','checks',n,'--repo',repo,'--json','name,state,conclusion']);",
|
|
1005
|
+
" const checks=(()=>{try{return JSON.parse(checksRaw||'[]')}catch{return []}})();",
|
|
1006
|
+
" const bad=(Array.isArray(checks)?checks:[]).some((x)=>{",
|
|
1007
|
+
" const c=String(x?.conclusion||'').toUpperCase();",
|
|
1008
|
+
" const s=String(x?.state||'').toUpperCase();",
|
|
1009
|
+
" return ['FAILURE','ERROR','TIMED_OUT','CANCELLED'].includes(c) || ['PENDING','IN_PROGRESS','QUEUED','WAITING','REQUESTED','EXPECTED'].includes(s);",
|
|
1010
|
+
" });",
|
|
1011
|
+
" if(bad){skipped.push({repo,number:n,reason:'ci_not_green'});continue;}",
|
|
1012
|
+
" const mergeArgs=['pr','merge',n,'--repo',repo,'--delete-branch'];",
|
|
1013
|
+
" if(method==='rebase') mergeArgs.push('--rebase');",
|
|
1014
|
+
" else if(method==='merge') mergeArgs.push('--merge');",
|
|
1015
|
+
" else mergeArgs.push('--squash');",
|
|
1016
|
+
" gh(mergeArgs);",
|
|
1017
|
+
" merged.push({repo,number:n,title:String(view?.title||'')});",
|
|
1018
|
+
" }catch(e){",
|
|
1019
|
+
" held.push({repo,number:n,reason:'merge_attempt_failed',error:String(e?.message||e)});",
|
|
1020
|
+
" }",
|
|
1021
|
+
"}",
|
|
1022
|
+
"console.log(JSON.stringify({mergedCount:merged.length,heldCount:held.length,skippedCount:skipped.length,merged,held,skipped}));",
|
|
1023
|
+
"\"",
|
|
1024
|
+
].join(" "),
|
|
973
1025
|
continueOnError: true,
|
|
1026
|
+
failOnError: false,
|
|
1027
|
+
env: {
|
|
1028
|
+
BOSUN_FETCH_AND_CLASSIFY:
|
|
1029
|
+
"(()=>{const r=$ctx.getNodeOutput('fetch-and-classify');return r&&r.success!==false?String(r.output||'{}'):'{}';})()",
|
|
1030
|
+
},
|
|
974
1031
|
}, { x: 600, y: 700 }),
|
|
975
1032
|
|
|
976
1033
|
node("notify", "notify.telegram", "Watchdog Report", {
|
|
@@ -991,31 +1048,33 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
991
1048
|
edge("has-prs", "fix-needed", { condition: "$output?.result === true" }),
|
|
992
1049
|
edge("has-prs", "no-prs", { condition: "$output?.result !== true" }),
|
|
993
1050
|
// Fix path (conflicts + CI failures)
|
|
994
|
-
edge("fix-needed", "
|
|
1051
|
+
edge("fix-needed", "programmatic-fix",{ condition: "$output?.result === true" }),
|
|
995
1052
|
edge("fix-needed", "review-needed", { condition: "$output?.result !== true" }),
|
|
996
|
-
edge("
|
|
1053
|
+
edge("programmatic-fix", "fix-agent-needed"),
|
|
1054
|
+
edge("fix-agent-needed", "dispatch-fix-agent", { condition: "$output?.result === true" }),
|
|
1055
|
+
edge("fix-agent-needed", "review-needed", { condition: "$output?.result !== true" }),
|
|
1056
|
+
edge("dispatch-fix-agent","review-needed"),
|
|
997
1057
|
// Review gate (merge candidates)
|
|
998
|
-
edge("review-needed", "
|
|
1058
|
+
edge("review-needed", "programmatic-review", { condition: "$output?.result === true" }),
|
|
999
1059
|
edge("review-needed", "notify", { condition: "$output?.result !== true" }),
|
|
1000
|
-
edge("
|
|
1060
|
+
edge("programmatic-review","notify"),
|
|
1001
1061
|
],
|
|
1002
1062
|
metadata: {
|
|
1003
1063
|
author: "bosun",
|
|
1004
|
-
version:
|
|
1064
|
+
version: 4,
|
|
1005
1065
|
createdAt: "2025-07-01T00:00:00Z",
|
|
1006
|
-
templateVersion: "2.
|
|
1066
|
+
templateVersion: "2.2.0",
|
|
1007
1067
|
tags: ["github", "pr", "ci", "merge", "watchdog", "bosun-attached", "safety"],
|
|
1008
1068
|
replaces: {
|
|
1009
1069
|
module: "agent-hooks.mjs",
|
|
1010
1070
|
functions: ["registerBuiltinHooks (PostPR block)"],
|
|
1011
1071
|
calledFrom: [],
|
|
1012
1072
|
description:
|
|
1013
|
-
"v2: Consolidates PR polling into one gh pr list fetch per target repo per cycle. " +
|
|
1014
|
-
"
|
|
1015
|
-
"
|
|
1016
|
-
"
|
|
1017
|
-
"
|
|
1018
|
-
"All external PRs (no bosun-attached label) are never touched.",
|
|
1073
|
+
"v2.2: Consolidates PR polling into one gh pr list fetch per target repo per cycle. " +
|
|
1074
|
+
"Uses deterministic-first remediation and review/merge command nodes; " +
|
|
1075
|
+
"agent execution is now fallback-only for unresolved conflicts or failed " +
|
|
1076
|
+
"automatic remediation attempts. All external PRs (no bosun-attached label) " +
|
|
1077
|
+
"remain untouched.",
|
|
1019
1078
|
},
|
|
1020
1079
|
},
|
|
1021
1080
|
};
|
|
@@ -1087,10 +1146,59 @@ export const GITHUB_KANBAN_SYNC_TEMPLATE = {
|
|
|
1087
1146
|
"}catch{return false;}})()",
|
|
1088
1147
|
}, { x: 400, y: 370 }),
|
|
1089
1148
|
|
|
1090
|
-
node("sync-
|
|
1149
|
+
node("sync-programmatic", "action.run_command", "Sync PR State → Kanban (Programmatic)", {
|
|
1150
|
+
command: [
|
|
1151
|
+
"node -e \"",
|
|
1152
|
+
"const {execFileSync}=require('child_process');",
|
|
1153
|
+
"const fs=require('fs');",
|
|
1154
|
+
"const raw=String(process.env.BOSUN_FETCH_PR_STATE||'');",
|
|
1155
|
+
"const data=(()=>{try{return JSON.parse(raw||'{}')}catch{return {}}})();",
|
|
1156
|
+
"const merged=Array.isArray(data.merged)?data.merged:[];",
|
|
1157
|
+
"const open=Array.isArray(data.open)?data.open:[];",
|
|
1158
|
+
"const updates=[]; const unresolved=[];",
|
|
1159
|
+
"const taskCli=fs.existsSync('task-cli.mjs')?'task-cli.mjs':'';",
|
|
1160
|
+
"if(!taskCli){",
|
|
1161
|
+
" console.log(JSON.stringify({updated:0,unresolved:[{reason:'task_cli_missing'}],needsAgent:true}));",
|
|
1162
|
+
" process.exit(0);",
|
|
1163
|
+
"}",
|
|
1164
|
+
"function runTask(args){return execFileSync('node',[taskCli,...args],{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();}",
|
|
1165
|
+
"for(const item of merged){",
|
|
1166
|
+
" const id=String(item?.taskId||'').trim();",
|
|
1167
|
+
" if(!id) continue;",
|
|
1168
|
+
" try{runTask(['update',id,'--status','done']);updates.push({taskId:id,status:'done'});}catch(e){unresolved.push({taskId:id,status:'done',error:String(e?.message||e)});}",
|
|
1169
|
+
"}",
|
|
1170
|
+
"for(const item of open){",
|
|
1171
|
+
" const id=String(item?.taskId||'').trim();",
|
|
1172
|
+
" if(!id) continue;",
|
|
1173
|
+
" try{runTask(['update',id,'--status','inreview']);updates.push({taskId:id,status:'inreview'});}catch(e){unresolved.push({taskId:id,status:'inreview',error:String(e?.message||e)});}",
|
|
1174
|
+
"}",
|
|
1175
|
+
"console.log(JSON.stringify({updated:updates.length,updates,unresolved,needsAgent:unresolved.length>0}));",
|
|
1176
|
+
"\"",
|
|
1177
|
+
].join(" "),
|
|
1178
|
+
continueOnError: true,
|
|
1179
|
+
failOnError: false,
|
|
1180
|
+
env: {
|
|
1181
|
+
BOSUN_FETCH_PR_STATE:
|
|
1182
|
+
"(()=>{const r=$ctx.getNodeOutput('fetch-pr-state');return r&&r.success!==false?String(r.output||'{}'):'{}';})()",
|
|
1183
|
+
},
|
|
1184
|
+
}, { x: 400, y: 530 }),
|
|
1185
|
+
|
|
1186
|
+
node("sync-agent-needed", "condition.expression", "Needs Agent Sync?", {
|
|
1187
|
+
expression:
|
|
1188
|
+
"(()=>{try{" +
|
|
1189
|
+
"const raw=$ctx.getNodeOutput('sync-programmatic')?.output||'{}';" +
|
|
1190
|
+
"const d=JSON.parse(raw);" +
|
|
1191
|
+
"return d?.needsAgent===true || (Array.isArray(d?.unresolved)&&d.unresolved.length>0);" +
|
|
1192
|
+
"}catch{return true;}})()",
|
|
1193
|
+
}, { x: 400, y: 615 }),
|
|
1194
|
+
|
|
1195
|
+
node("sync-agent", "action.run_agent", "Sync PR State → Kanban (Fallback)", {
|
|
1091
1196
|
prompt:
|
|
1092
|
-
"You are the Bosun GitHub-Kanban sync agent.
|
|
1093
|
-
"
|
|
1197
|
+
"You are the Bosun GitHub-Kanban sync fallback agent. A deterministic sync pass already ran.\n\n" +
|
|
1198
|
+
"Programmatic sync output:\n" +
|
|
1199
|
+
"{{$ctx.getNodeOutput('sync-programmatic')?.output}}\n\n" +
|
|
1200
|
+
"Now complete only unresolved updates.\n\n" +
|
|
1201
|
+
"GitHub PR state:\n" +
|
|
1094
1202
|
"PR state (JSON from fetch-pr-state node output):\n" +
|
|
1095
1203
|
"{{$ctx.getNodeOutput('fetch-pr-state')?.output}}\n\n" +
|
|
1096
1204
|
"RULES:\n" +
|
|
@@ -1107,7 +1215,7 @@ export const GITHUB_KANBAN_SYNC_TEMPLATE = {
|
|
|
1107
1215
|
sdk: "auto",
|
|
1108
1216
|
timeoutMs: 300_000,
|
|
1109
1217
|
continueOnError: true,
|
|
1110
|
-
}, { x: 400, y:
|
|
1218
|
+
}, { x: 400, y: 700 }),
|
|
1111
1219
|
|
|
1112
1220
|
node("done", "notify.log", "Sync Complete", {
|
|
1113
1221
|
message: "GitHub ↔ Kanban sync cycle complete",
|
|
@@ -1122,8 +1230,11 @@ export const GITHUB_KANBAN_SYNC_TEMPLATE = {
|
|
|
1122
1230
|
edges: [
|
|
1123
1231
|
edge("trigger", "fetch-pr-state"),
|
|
1124
1232
|
edge("fetch-pr-state", "has-updates"),
|
|
1125
|
-
edge("has-updates", "sync-
|
|
1233
|
+
edge("has-updates", "sync-programmatic", { condition: "$output?.result === true" }),
|
|
1126
1234
|
edge("has-updates", "skip", { condition: "$output?.result !== true" }),
|
|
1235
|
+
edge("sync-programmatic", "sync-agent-needed"),
|
|
1236
|
+
edge("sync-agent-needed", "sync-agent", { condition: "$output?.result === true" }),
|
|
1237
|
+
edge("sync-agent-needed", "done", { condition: "$output?.result !== true" }),
|
|
1127
1238
|
edge("sync-agent", "done"),
|
|
1128
1239
|
],
|
|
1129
1240
|
metadata: {
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-batch.mjs — Task Batch Processor Workflow Template
|
|
3
|
+
*
|
|
4
|
+
* Picks up multiple tasks from the kanban backlog and dispatches them in
|
|
5
|
+
* parallel using the loop.for_each fan-out node. Each task is executed via
|
|
6
|
+
* the Task Lifecycle sub-workflow (template-task-lifecycle).
|
|
7
|
+
*
|
|
8
|
+
* Templates:
|
|
9
|
+
* - TASK_BATCH_PROCESSOR_TEMPLATE (primary batch dispatch)
|
|
10
|
+
* - TASK_BATCH_PR_TEMPLATE (batch → agent → PR shortcut)
|
|
11
|
+
*
|
|
12
|
+
* DAG overview:
|
|
13
|
+
* trigger.task_low
|
|
14
|
+
* → condition.expression (is coordinator or solo?)
|
|
15
|
+
* → action.run_command (list todo tasks)
|
|
16
|
+
* → loop.for_each (fan-out, maxConcurrent tasks at a time)
|
|
17
|
+
* → sub-workflow: template-task-lifecycle per task
|
|
18
|
+
* → action.set_variable (record batch results)
|
|
19
|
+
* → notify.log (summary)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { node, edge, resetLayout } from "./_helpers.mjs";
|
|
23
|
+
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
// Task Batch Processor — Parallel Task Dispatch
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
|
|
28
|
+
resetLayout();
|
|
29
|
+
|
|
30
|
+
export const TASK_BATCH_PROCESSOR_TEMPLATE = {
|
|
31
|
+
id: "template-task-batch-processor",
|
|
32
|
+
name: "Task Batch Processor",
|
|
33
|
+
description:
|
|
34
|
+
"Monitors the task backlog and dispatches multiple tasks in parallel " +
|
|
35
|
+
"using the Task Lifecycle sub-workflow. Automatically picks up tasks " +
|
|
36
|
+
"when backlog drops below threshold, fans out execution across " +
|
|
37
|
+
"available slots, and reports batch results.",
|
|
38
|
+
category: "lifecycle",
|
|
39
|
+
enabled: true,
|
|
40
|
+
recommended: true,
|
|
41
|
+
trigger: "trigger.task_low",
|
|
42
|
+
variables: {
|
|
43
|
+
backlogThreshold: 3,
|
|
44
|
+
maxConcurrent: 3,
|
|
45
|
+
pollStatus: "todo",
|
|
46
|
+
maxBatchSize: 10,
|
|
47
|
+
subWorkflow: "template-task-lifecycle",
|
|
48
|
+
notifyChannel: "telegram",
|
|
49
|
+
},
|
|
50
|
+
nodes: [
|
|
51
|
+
// ── Trigger: Backlog drops below threshold ───────────────────────────
|
|
52
|
+
node("trigger", "trigger.task_low", "Backlog Low?", {
|
|
53
|
+
threshold: "{{backlogThreshold}}",
|
|
54
|
+
status: "{{pollStatus}}",
|
|
55
|
+
}, { x: 400, y: 50 }),
|
|
56
|
+
|
|
57
|
+
// ── Gate: Fleet coordinator check (skip if not coordinator) ──────────
|
|
58
|
+
node("check-coordinator", "condition.expression", "Is Coordinator?", {
|
|
59
|
+
expression: "$data?.isCoordinator !== false",
|
|
60
|
+
}, { x: 400, y: 180 }),
|
|
61
|
+
|
|
62
|
+
// ── Query kanban for available tasks ─────────────────────────────────
|
|
63
|
+
node("query-tasks", "action.run_command", "Query Task Backlog", {
|
|
64
|
+
command: "node",
|
|
65
|
+
args: ["-e", `
|
|
66
|
+
import("./kanban-adapter.mjs")
|
|
67
|
+
.then(k => k.listTasks(undefined, { status: "todo" }))
|
|
68
|
+
.then(tasks => {
|
|
69
|
+
const batch = (tasks || []).slice(0, parseInt(process.env.MAX_BATCH || "10"));
|
|
70
|
+
console.log(JSON.stringify(batch.map(t => ({
|
|
71
|
+
taskId: t.id,
|
|
72
|
+
taskTitle: t.title || t.id,
|
|
73
|
+
status: t.status,
|
|
74
|
+
branch: t.branch || t.metadata?.branch || null,
|
|
75
|
+
scope: t.scope || t.metadata?.scope || null,
|
|
76
|
+
}))));
|
|
77
|
+
})
|
|
78
|
+
.catch(e => { console.error(e.message); process.exit(1); });
|
|
79
|
+
`],
|
|
80
|
+
env: { MAX_BATCH: "{{maxBatchSize}}" },
|
|
81
|
+
parseJson: true,
|
|
82
|
+
}, { x: 400, y: 310 }),
|
|
83
|
+
|
|
84
|
+
// ── Fan-out: dispatch each task to the lifecycle workflow ─────────────
|
|
85
|
+
node("dispatch-tasks", "loop.for_each", "Dispatch Tasks", {
|
|
86
|
+
items: "{{queryResult}}",
|
|
87
|
+
itemVariable: "currentTask",
|
|
88
|
+
indexVariable: "taskIndex",
|
|
89
|
+
maxConcurrent: "{{maxConcurrent}}",
|
|
90
|
+
workflowId: "{{subWorkflow}}",
|
|
91
|
+
}, { x: 400, y: 440 }),
|
|
92
|
+
|
|
93
|
+
// ── Record batch results ─────────────────────────────────────────────
|
|
94
|
+
node("record-results", "action.set_variable", "Record Results", {
|
|
95
|
+
key: "batchResult",
|
|
96
|
+
value: "{{dispatchResult}}",
|
|
97
|
+
}, { x: 400, y: 570 }),
|
|
98
|
+
|
|
99
|
+
// ── Notify on completion ─────────────────────────────────────────────
|
|
100
|
+
node("notify-complete", "notify.telegram", "Batch Summary", {
|
|
101
|
+
channel: "{{notifyChannel}}",
|
|
102
|
+
message: "Task batch completed: {{batchResult.successCount}}/{{batchResult.totalItems}} succeeded",
|
|
103
|
+
}, { x: 400, y: 700 }),
|
|
104
|
+
],
|
|
105
|
+
edges: [
|
|
106
|
+
edge("trigger", "check-coordinator"),
|
|
107
|
+
edge("check-coordinator", "query-tasks", { condition: "result.result === true" }),
|
|
108
|
+
edge("query-tasks", "dispatch-tasks"),
|
|
109
|
+
edge("dispatch-tasks", "record-results"),
|
|
110
|
+
edge("record-results", "notify-complete"),
|
|
111
|
+
],
|
|
112
|
+
metadata: {
|
|
113
|
+
author: "bosun",
|
|
114
|
+
version: 1,
|
|
115
|
+
createdAt: "2026-03-15T00:00:00Z",
|
|
116
|
+
templateVersion: "1.0.0",
|
|
117
|
+
tags: ["task", "batch", "parallel", "dispatch", "lifecycle"],
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
122
|
+
// Task Batch → PR Shortcut — Pick tasks, run agent, create PRs
|
|
123
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
124
|
+
|
|
125
|
+
resetLayout();
|
|
126
|
+
|
|
127
|
+
export const TASK_BATCH_PR_TEMPLATE = {
|
|
128
|
+
id: "template-task-batch-pr",
|
|
129
|
+
name: "Task Batch → PR",
|
|
130
|
+
description:
|
|
131
|
+
"Simplified batch processor that picks todo tasks, runs the agent on " +
|
|
132
|
+
"each, and creates pull requests for any that produce commits. Ideal " +
|
|
133
|
+
"for autonomous mode where tasks should flow straight to PRs.",
|
|
134
|
+
category: "lifecycle",
|
|
135
|
+
enabled: true,
|
|
136
|
+
recommended: false,
|
|
137
|
+
trigger: "trigger.task_low",
|
|
138
|
+
variables: {
|
|
139
|
+
backlogThreshold: 3,
|
|
140
|
+
maxConcurrent: 2,
|
|
141
|
+
pollStatus: "todo",
|
|
142
|
+
maxBatchSize: 5,
|
|
143
|
+
defaultBaseBranch: "main",
|
|
144
|
+
draftPR: true,
|
|
145
|
+
notifyChannel: "telegram",
|
|
146
|
+
},
|
|
147
|
+
nodes: [
|
|
148
|
+
// ── Trigger ──────────────────────────────────────────────────────────
|
|
149
|
+
node("trigger", "trigger.task_low", "Backlog Low?", {
|
|
150
|
+
threshold: "{{backlogThreshold}}",
|
|
151
|
+
status: "{{pollStatus}}",
|
|
152
|
+
}, { x: 400, y: 50 }),
|
|
153
|
+
|
|
154
|
+
// ── Query for tasks ──────────────────────────────────────────────────
|
|
155
|
+
node("query-tasks", "action.run_command", "List Todo Tasks", {
|
|
156
|
+
command: "node",
|
|
157
|
+
args: ["-e", `
|
|
158
|
+
import("./kanban-adapter.mjs")
|
|
159
|
+
.then(k => k.listTasks(undefined, { status: "todo" }))
|
|
160
|
+
.then(tasks => {
|
|
161
|
+
const batch = (tasks || []).slice(0, parseInt(process.env.MAX_BATCH || "5"));
|
|
162
|
+
console.log(JSON.stringify(batch.map(t => ({
|
|
163
|
+
taskId: t.id,
|
|
164
|
+
taskTitle: t.title || t.id,
|
|
165
|
+
branch: t.branch || t.metadata?.branch || null,
|
|
166
|
+
}))));
|
|
167
|
+
})
|
|
168
|
+
.catch(e => { console.error(e.message); process.exit(1); });
|
|
169
|
+
`],
|
|
170
|
+
env: { MAX_BATCH: "{{maxBatchSize}}" },
|
|
171
|
+
parseJson: true,
|
|
172
|
+
}, { x: 400, y: 180 }),
|
|
173
|
+
|
|
174
|
+
// ── Fan-out: per-task agent + PR ─────────────────────────────────────
|
|
175
|
+
node("for-each-task", "loop.for_each", "Process Each Task", {
|
|
176
|
+
items: "{{queryResult}}",
|
|
177
|
+
itemVariable: "task",
|
|
178
|
+
indexVariable: "idx",
|
|
179
|
+
maxConcurrent: "{{maxConcurrent}}",
|
|
180
|
+
}, { x: 400, y: 310 }),
|
|
181
|
+
|
|
182
|
+
// ── Per-task: set status to inprogress ───────────────────────────────
|
|
183
|
+
node("set-inprogress", "action.update_task_status", "Mark In-Progress", {
|
|
184
|
+
taskId: "{{task.taskId}}",
|
|
185
|
+
status: "inprogress",
|
|
186
|
+
}, { x: 400, y: 440 }),
|
|
187
|
+
|
|
188
|
+
// ── Per-task: run agent ──────────────────────────────────────────────
|
|
189
|
+
node("run-agent", "action.run_agent", "Run Agent", {
|
|
190
|
+
taskId: "{{task.taskId}}",
|
|
191
|
+
taskTitle: "{{task.taskTitle}}",
|
|
192
|
+
branch: "{{task.branch}}",
|
|
193
|
+
}, { x: 400, y: 570 }),
|
|
194
|
+
|
|
195
|
+
// ── Per-task: detect commits ─────────────────────────────────────────
|
|
196
|
+
node("detect-commits", "action.detect_new_commits", "Check Commits", {
|
|
197
|
+
taskId: "{{task.taskId}}",
|
|
198
|
+
worktreePath: "{{worktreePath}}",
|
|
199
|
+
failOnError: false,
|
|
200
|
+
}, { x: 400, y: 700 }),
|
|
201
|
+
|
|
202
|
+
// ── Per-task: push + create PR ───────────────────────────────────────
|
|
203
|
+
node("push-branch", "action.git_operations", "Push Branch", {
|
|
204
|
+
operation: "push",
|
|
205
|
+
cwd: "{{worktreePath}}",
|
|
206
|
+
}, { x: 400, y: 830 }),
|
|
207
|
+
|
|
208
|
+
node("create-pr", "action.create_pr", "Create PR", {
|
|
209
|
+
title: "{{task.taskTitle}}",
|
|
210
|
+
body: "Automated PR for task {{task.taskId}}",
|
|
211
|
+
base: "{{defaultBaseBranch}}",
|
|
212
|
+
branch: "{{task.branch}}",
|
|
213
|
+
draft: "{{draftPR}}",
|
|
214
|
+
}, { x: 400, y: 960 }),
|
|
215
|
+
|
|
216
|
+
// ── Per-task: mark review ────────────────────────────────────────────
|
|
217
|
+
node("set-inreview", "action.update_task_status", "Mark In-Review", {
|
|
218
|
+
taskId: "{{task.taskId}}",
|
|
219
|
+
status: "inreview",
|
|
220
|
+
}, { x: 400, y: 1090 }),
|
|
221
|
+
|
|
222
|
+
// ── Batch complete notification ──────────────────────────────────────
|
|
223
|
+
node("notify", "notify.telegram", "Batch Complete", {
|
|
224
|
+
channel: "{{notifyChannel}}",
|
|
225
|
+
message: "Task batch PR pipeline complete",
|
|
226
|
+
}, { x: 400, y: 1220 }),
|
|
227
|
+
],
|
|
228
|
+
edges: [
|
|
229
|
+
edge("trigger", "query-tasks"),
|
|
230
|
+
edge("query-tasks", "for-each-task"),
|
|
231
|
+
edge("for-each-task", "set-inprogress"),
|
|
232
|
+
edge("set-inprogress", "run-agent"),
|
|
233
|
+
edge("run-agent", "detect-commits"),
|
|
234
|
+
edge("detect-commits", "push-branch", { condition: "result.hasNewCommits === true" }),
|
|
235
|
+
edge("push-branch", "create-pr"),
|
|
236
|
+
edge("create-pr", "set-inreview"),
|
|
237
|
+
edge("detect-commits", "notify", { condition: "result.hasNewCommits !== true" }),
|
|
238
|
+
edge("set-inreview", "notify"),
|
|
239
|
+
],
|
|
240
|
+
metadata: {
|
|
241
|
+
author: "bosun",
|
|
242
|
+
version: 1,
|
|
243
|
+
createdAt: "2026-03-15T00:00:00Z",
|
|
244
|
+
templateVersion: "1.0.0",
|
|
245
|
+
tags: ["task", "batch", "pr", "agent", "autonomous"],
|
|
246
|
+
},
|
|
247
|
+
};
|