bosun 0.40.21 → 0.41.1
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-custom-tools.mjs +23 -5
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +131 -30
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/primary-agent.mjs +81 -7
- package/agent/retry-queue.mjs +164 -0
- package/bench/swebench/bosun-swebench.mjs +5 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +267 -8
- package/config/config-doctor.mjs +51 -2
- package/config/config.mjs +232 -5
- package/github/github-auth-manager.mjs +70 -19
- package/infra/library-manager.mjs +894 -60
- package/infra/monitor.mjs +701 -69
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +95 -28
- package/infra/test-runtime.mjs +267 -0
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +30 -8
- package/server/setup-web-server.mjs +29 -1
- package/server/ui-server.mjs +1571 -49
- 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-claims.mjs +6 -10
- 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/chat-view.js +18 -1
- package/ui/components/workspace-switcher.js +321 -9
- package/ui/demo-defaults.js +17830 -10433
- package/ui/demo.html +9 -1
- package/ui/modules/router.js +1 -1
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +376 -37
- package/ui/modules/voice-client.js +173 -33
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +571 -1
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/library.js +410 -55
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +1083 -507
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +38 -1
- package/ui/tabs/workflows.js +1275 -402
- package/voice/voice-agents-sdk.mjs +2 -2
- package/voice/voice-relay.mjs +28 -20
- 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/project-detection.mjs +559 -0
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-contract.mjs +433 -232
- package/workflow/workflow-engine.mjs +510 -47
- package/workflow/workflow-nodes/custom-loader.mjs +251 -0
- package/workflow/workflow-nodes.mjs +2024 -184
- package/workflow/workflow-templates.mjs +118 -24
- package/workflow-templates/agents.mjs +20 -20
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/code-quality.mjs +20 -14
- 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 +27 -10
- package/workflow-templates/task-execution.mjs +752 -0
- package/workflow-templates/task-lifecycle.mjs +117 -14
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +153 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
|
@@ -31,6 +31,7 @@ export const TASK_PLANNER_TEMPLATE = {
|
|
|
31
31
|
minTodoCount: 3,
|
|
32
32
|
taskCount: 5,
|
|
33
33
|
dedupHours: 24,
|
|
34
|
+
failureCooldownMinutes: 30,
|
|
34
35
|
prompt: "",
|
|
35
36
|
plannerContext:
|
|
36
37
|
"Focus on high-value implementation work. Avoid duplicating existing tasks.",
|
|
@@ -42,7 +43,15 @@ export const TASK_PLANNER_TEMPLATE = {
|
|
|
42
43
|
}, { x: 400, y: 50 }),
|
|
43
44
|
|
|
44
45
|
node("check-dedup", "condition.expression", "Dedup Window", {
|
|
45
|
-
expression:
|
|
46
|
+
expression:
|
|
47
|
+
"(() => {" +
|
|
48
|
+
" const now = Date.now();" +
|
|
49
|
+
" const lastSuccessAt = Number($data?._lastPlannerRun || 0);" +
|
|
50
|
+
" const lastFailureAt = Number($data?._lastPlannerFailureAt || 0);" +
|
|
51
|
+
" const successWindowMs = Number($data?.dedupHours || 24) * 3600000;" +
|
|
52
|
+
" const failureWindowMs = Number($data?.failureCooldownMinutes || 30) * 60000;" +
|
|
53
|
+
" return (now - lastSuccessAt) > successWindowMs && (now - lastFailureAt) > failureWindowMs;" +
|
|
54
|
+
"})()",
|
|
46
55
|
}, { x: 400, y: 180 }),
|
|
47
56
|
|
|
48
57
|
node("log-start", "notify.log", "Log Planner Start", {
|
|
@@ -94,6 +103,14 @@ export const TASK_PLANNER_TEMPLATE = {
|
|
|
94
103
|
message: "Task planner failed to materialize tasks from planner output",
|
|
95
104
|
level: "warn",
|
|
96
105
|
}, { x: 600, y: 830 }),
|
|
106
|
+
|
|
107
|
+
// Cooldown on failure: stamp _lastPlannerRun so the dedup window
|
|
108
|
+
// prevents immediate retry without blocking normal planning for a full day.
|
|
109
|
+
node("set-timestamp-fail", "action.set_variable", "Cooldown After Failure", {
|
|
110
|
+
key: "_lastPlannerFailureAt",
|
|
111
|
+
value: "Date.now()",
|
|
112
|
+
isExpression: true,
|
|
113
|
+
}, { x: 600, y: 960 }),
|
|
97
114
|
],
|
|
98
115
|
edges: [
|
|
99
116
|
edge("trigger", "check-dedup"),
|
|
@@ -105,6 +122,7 @@ export const TASK_PLANNER_TEMPLATE = {
|
|
|
105
122
|
edge("check-result", "set-timestamp", { condition: "$output?.result === true" }),
|
|
106
123
|
edge("check-result", "notify-fail", { condition: "$output?.result !== true" }),
|
|
107
124
|
edge("set-timestamp", "notify-done"),
|
|
125
|
+
edge("notify-fail", "set-timestamp-fail"),
|
|
108
126
|
],
|
|
109
127
|
metadata: {
|
|
110
128
|
author: "bosun",
|
|
@@ -477,12 +495,14 @@ export const WEEKLY_FITNESS_SUMMARY_TEMPLATE = {
|
|
|
477
495
|
}, { x: 420, y: 40 }),
|
|
478
496
|
|
|
479
497
|
node("task-metrics", "action.bosun_cli", "Collect Task Metrics", {
|
|
480
|
-
|
|
498
|
+
subcommand: "task list",
|
|
499
|
+
args: "--json",
|
|
500
|
+
parseJson: true,
|
|
481
501
|
continueOnError: true,
|
|
482
502
|
}, { x: 150, y: 180 }),
|
|
483
503
|
|
|
484
504
|
node("pr-metrics", "action.run_command", "Collect PR Metrics", {
|
|
485
|
-
command: "gh pr list --state all --json number,state,mergedAt,closedAt,title --limit 200",
|
|
505
|
+
command: "gh pr list --state all --json number,state,mergedAt,closedAt,createdAt,updatedAt,title,body --limit 200",
|
|
486
506
|
continueOnError: true,
|
|
487
507
|
}, { x: 420, y: 180 }),
|
|
488
508
|
|
|
@@ -491,21 +511,426 @@ export const WEEKLY_FITNESS_SUMMARY_TEMPLATE = {
|
|
|
491
511
|
continueOnError: true,
|
|
492
512
|
}, { x: 690, y: 180 }),
|
|
493
513
|
|
|
514
|
+
node("read-previous-summary", "action.read_file", "Read Prior Summary", {
|
|
515
|
+
path: ".bosun/workflow-runs/weekly-fitness-summary.latest.json",
|
|
516
|
+
}, { x: 960, y: 180 }),
|
|
517
|
+
|
|
518
|
+
node("summarize-fitness-metrics", "action.set_variable", "Summarize Fitness Metrics", {
|
|
519
|
+
key: "fitnessSummary",
|
|
520
|
+
value:
|
|
521
|
+
"(() => {" +
|
|
522
|
+
" try {" +
|
|
523
|
+
" const now = Date.now();" +
|
|
524
|
+
" const lookbackDays = Math.max(1, Number($data?.lookbackDays || 7));" +
|
|
525
|
+
" const windowMs = lookbackDays * 24 * 60 * 60 * 1000;" +
|
|
526
|
+
" const currentStart = now - windowMs;" +
|
|
527
|
+
" const previousStart = currentStart - windowMs;" +
|
|
528
|
+
" const previousEnd = currentStart;" +
|
|
529
|
+
" const toNumber = (v, fallback = 0) => { const n = Number(v); return Number.isFinite(n) ? n : fallback; };" +
|
|
530
|
+
" const toIso = (ms) => new Date(ms).toISOString();" +
|
|
531
|
+
" const parseJsonSafe = (raw) => { try { return JSON.parse(String(raw)); } catch { return null; } };" +
|
|
532
|
+
" const extractCanonicalItems = (value) => {" +
|
|
533
|
+
" if (!value || typeof value !== 'object') return null;" +
|
|
534
|
+
" const keys = ['items', 'tasks', 'entries', 'records', 'results', 'data'];" +
|
|
535
|
+
" for (const key of keys) {" +
|
|
536
|
+
" if (Array.isArray(value[key])) return value[key].filter(Boolean);" +
|
|
537
|
+
" }" +
|
|
538
|
+
" return null;" +
|
|
539
|
+
" };" +
|
|
540
|
+
" const parseSource = (raw, depth = 0) => {" +
|
|
541
|
+
" if (depth > 3) return { items: [], degraded: true, parsedAny: false, partial: false };" +
|
|
542
|
+
" if (Array.isArray(raw)) return { items: raw.filter(Boolean), degraded: false, parsedAny: raw.length > 0, partial: false };" +
|
|
543
|
+
" if (raw && typeof raw === 'object') {" +
|
|
544
|
+
" const canonical = extractCanonicalItems(raw) ?? extractCanonicalItems(raw.output) ?? extractCanonicalItems(raw.result) ?? extractCanonicalItems(raw.payload);" +
|
|
545
|
+
" if (canonical) return { items: canonical, degraded: false, parsedAny: canonical.length > 0, partial: false };" +
|
|
546
|
+
" const wrappedCandidates = [raw.output, raw.result, raw.payload, raw.data, raw.stdout, raw.content, raw.text, raw.json];" +
|
|
547
|
+
" for (const candidate of wrappedCandidates) {" +
|
|
548
|
+
" if (candidate == null) continue;" +
|
|
549
|
+
" const parsedCandidate = parseSource(candidate, depth + 1);" +
|
|
550
|
+
" if (parsedCandidate.items.length > 0 || parsedCandidate.parsedAny || parsedCandidate.degraded === false) return parsedCandidate;" +
|
|
551
|
+
" }" +
|
|
552
|
+
" return { items: [], degraded: Object.keys(raw).length > 0, parsedAny: false, partial: false };" +
|
|
553
|
+
" }" +
|
|
554
|
+
" if (typeof raw !== 'string') return { items: [], degraded: true, parsedAny: false, partial: false };" +
|
|
555
|
+
" const trimmed = raw.trim();" +
|
|
556
|
+
" if (!trimmed) return { items: [], degraded: false, parsedAny: false, partial: false };" +
|
|
557
|
+
" const parsed = parseJsonSafe(trimmed);" +
|
|
558
|
+
" if (Array.isArray(parsed)) return { items: parsed.filter(Boolean), degraded: false, parsedAny: true, partial: false };" +
|
|
559
|
+
" if (parsed && typeof parsed === 'object') {" +
|
|
560
|
+
" const canonical = extractCanonicalItems(parsed) ?? extractCanonicalItems(parsed.output) ?? extractCanonicalItems(parsed.result) ?? extractCanonicalItems(parsed.payload);" +
|
|
561
|
+
" if (canonical) return { items: canonical, degraded: false, parsedAny: true, partial: false };" +
|
|
562
|
+
" }" +
|
|
563
|
+
" const lines = trimmed.split(/\\r?\\n/).filter((line) => line.trim() !== '');" +
|
|
564
|
+
" const parsedLines = [];" +
|
|
565
|
+
" let failedLines = 0;" +
|
|
566
|
+
" for (const line of lines) {" +
|
|
567
|
+
" const parsedLine = parseJsonSafe(line);" +
|
|
568
|
+
" if (parsedLine == null) { failedLines += 1; continue; }" +
|
|
569
|
+
" if (Array.isArray(parsedLine)) parsedLines.push(...parsedLine.filter(Boolean));" +
|
|
570
|
+
" else parsedLines.push(parsedLine);" +
|
|
571
|
+
" }" +
|
|
572
|
+
" if (parsedLines.length > 0) return { items: parsedLines, degraded: failedLines > 0, parsedAny: true, partial: failedLines > 0 };" +
|
|
573
|
+
" return { items: [], degraded: true, parsedAny: false, partial: false };" +
|
|
574
|
+
" };" +
|
|
575
|
+
" const getTs = (item) => {" +
|
|
576
|
+
" if (!item || typeof item !== 'object') return null;" +
|
|
577
|
+
" const fields = ['completedAt', 'closedAt', 'mergedAt', 'resolvedAt', 'updatedAt', 'createdAt', 'timestamp', 'ts', 'date', 'completed_at', 'closed_at', 'merged_at', 'resolved_at', 'updated_at', 'created_at'];" +
|
|
578
|
+
" for (const key of fields) {" +
|
|
579
|
+
" const value = item[key];" +
|
|
580
|
+
" if (!value) continue;" +
|
|
581
|
+
" const ms = Date.parse(String(value));" +
|
|
582
|
+
" if (Number.isFinite(ms)) return ms;" +
|
|
583
|
+
" if (typeof value === 'number' && Number.isFinite(value)) return value > 1e12 ? value : value * 1000;" +
|
|
584
|
+
" }" +
|
|
585
|
+
" return null;" +
|
|
586
|
+
" };" +
|
|
587
|
+
" const normalizeBucket = (items) => {" +
|
|
588
|
+
" const stamped = [];" +
|
|
589
|
+
" const unstamped = [];" +
|
|
590
|
+
" for (const item of items) {" +
|
|
591
|
+
" const ts = getTs(item);" +
|
|
592
|
+
" if (ts == null) unstamped.push(item); else stamped.push({ item, ts });" +
|
|
593
|
+
" }" +
|
|
594
|
+
" return { stamped, unstamped };" +
|
|
595
|
+
" };" +
|
|
596
|
+
" const splitWindows = (items) => {" +
|
|
597
|
+
" const { stamped, unstamped } = normalizeBucket(items);" +
|
|
598
|
+
" const current = stamped.filter((entry) => entry.ts >= currentStart && entry.ts <= now).map((entry) => entry.item);" +
|
|
599
|
+
" const previous = stamped.filter((entry) => entry.ts >= previousStart && entry.ts < previousEnd).map((entry) => entry.item);" +
|
|
600
|
+
" const usedFallbackWindow = stamped.length === 0 && unstamped.length > 0;" +
|
|
601
|
+
" if (usedFallbackWindow) return { current: unstamped, previous: [], usedFallbackWindow };" +
|
|
602
|
+
" return { current, previous, usedFallbackWindow };" +
|
|
603
|
+
" };" +
|
|
604
|
+
" const metric = (name, value, previous, direction, unit, confidence, status, notes = []) => {" +
|
|
605
|
+
" const hasCurrent = typeof value === 'number' && Number.isFinite(value);" +
|
|
606
|
+
" const hasPrevious = typeof previous === 'number' && Number.isFinite(previous);" +
|
|
607
|
+
" return {" +
|
|
608
|
+
" name," +
|
|
609
|
+
" value: hasCurrent ? value : null," +
|
|
610
|
+
" previous: hasPrevious ? previous : null," +
|
|
611
|
+
" delta: hasCurrent && hasPrevious ? Number((value - previous).toFixed(2)) : null," +
|
|
612
|
+
" direction," +
|
|
613
|
+
" unit," +
|
|
614
|
+
" confidence," +
|
|
615
|
+
" status," +
|
|
616
|
+
" notes: notes.filter(Boolean)," +
|
|
617
|
+
" };" +
|
|
618
|
+
" };" +
|
|
619
|
+
" const sourceStatus = (nodeOut, parsedList, parsedMeta = {}) => {" +
|
|
620
|
+
" const output = nodeOut?.output;" +
|
|
621
|
+
" const hasPayload = (() => {" +
|
|
622
|
+
" if (output == null) return false;" +
|
|
623
|
+
" if (Array.isArray(output)) return true;" +
|
|
624
|
+
" if (typeof output === 'string') return output.trim() !== '';" +
|
|
625
|
+
" if (typeof output === 'object') {" +
|
|
626
|
+
" const wrapped = [output.stdout, output.content, output.text, output.json, output.output, output.result, output.payload, output.data];" +
|
|
627
|
+
" if (wrapped.some((v) => (typeof v === 'string' ? v.trim() !== '' : v != null))) return true;" +
|
|
628
|
+
" return Object.keys(output).length > 0;" +
|
|
629
|
+
" }" +
|
|
630
|
+
" return true;" +
|
|
631
|
+
" })();" +
|
|
632
|
+
" const success = nodeOut?.success !== false;" +
|
|
633
|
+
" if (!hasPayload) return { status: 'missing', confidence: 'low' };" +
|
|
634
|
+
" if (!Array.isArray(parsedList)) return { status: 'degraded', confidence: 'low' };" +
|
|
635
|
+
" if (parsedMeta?.degraded || parsedMeta?.partial) return { status: 'degraded', confidence: parsedList.length > 0 ? 'medium' : 'low' };" +
|
|
636
|
+
" if (!success) return { status: 'degraded', confidence: parsedList.length > 0 ? 'medium' : 'low' };" +
|
|
637
|
+
" return { status: 'ok', confidence: parsedList.length > 0 ? 'high' : 'medium' };" +
|
|
638
|
+
" };" +
|
|
639
|
+
" const taskNode = $ctx.getNodeOutput('task-metrics') || {};" +
|
|
640
|
+
" const prNode = $ctx.getNodeOutput('pr-metrics') || {};" +
|
|
641
|
+
" const debtNode = $ctx.getNodeOutput('debt-metrics') || {};" +
|
|
642
|
+
" const prevNode = $ctx.getNodeOutput('read-previous-summary') || {};" +
|
|
643
|
+
" const taskParsed = parseSource(taskNode.output);" +
|
|
644
|
+
" const prParsed = parseSource(prNode.output);" +
|
|
645
|
+
" const debtParsed = parseSource(debtNode.output);" +
|
|
646
|
+
" const tasks = taskParsed.items;" +
|
|
647
|
+
" const prs = prParsed.items;" +
|
|
648
|
+
" const debt = debtParsed.items;" +
|
|
649
|
+
" const taskHealth = sourceStatus(taskNode, tasks, taskParsed);" +
|
|
650
|
+
" const prHealth = sourceStatus(prNode, prs, prParsed);" +
|
|
651
|
+
" const debtHealth = sourceStatus(debtNode, debt, debtParsed);" +
|
|
652
|
+
" const taskSplit = splitWindows(tasks);" +
|
|
653
|
+
" const prSplit = splitWindows(prs);" +
|
|
654
|
+
" const debtSplit = splitWindows(debt);" +
|
|
655
|
+
" const doneStatuses = new Set(['done', 'closed', 'completed', 'merged', 'resolved']);" +
|
|
656
|
+
" const isDone = (item) => doneStatuses.has(String(item?.status ?? item?.state ?? '').toLowerCase());" +
|
|
657
|
+
" const taskTelemetryUnavailable = taskHealth.status === 'missing' || (taskHealth.status === 'degraded' && tasks.length === 0);" +
|
|
658
|
+
" const throughputCurrent = taskTelemetryUnavailable ? null : taskSplit.current.filter(isDone).length;" +
|
|
659
|
+
" const throughputPrevious = taskTelemetryUnavailable ? null : taskSplit.previous.filter(isDone).length;" +
|
|
660
|
+
" const reopenedCount = (items) => items.filter((item) => {" +
|
|
661
|
+
" if (!item || typeof item !== 'object') return false;" +
|
|
662
|
+
" const reopenCount = toNumber(item.reopenCount ?? item.reopenedCount ?? item.reopen_count ?? item.reopened_count, 0);" +
|
|
663
|
+
" if (reopenCount > 0) return true;" +
|
|
664
|
+
" if (item.reopened === true) return true;" +
|
|
665
|
+
" const status = String(item.status ?? item.state ?? '').toLowerCase();" +
|
|
666
|
+
" return status.includes('reopen');" +
|
|
667
|
+
" }).length;" +
|
|
668
|
+
" const reopenedCurrent = taskTelemetryUnavailable ? null : reopenedCount(taskSplit.current);" +
|
|
669
|
+
" const reopenedPrevious = taskTelemetryUnavailable ? null : reopenedCount(taskSplit.previous);" +
|
|
670
|
+
" const classifyRegression = (pr) => /revert|regression|rollback|hotfix/i.test(String(pr?.title || '') + ' ' + String(pr?.body || ''));" +
|
|
671
|
+
" const regressionCurrentCount = prSplit.current.filter(classifyRegression).length;" +
|
|
672
|
+
" const regressionPreviousCount = prSplit.previous.filter(classifyRegression).length;" +
|
|
673
|
+
" const regressionCurrentRate = prSplit.current.length > 0 ? Number(((regressionCurrentCount / prSplit.current.length) * 100).toFixed(2)) : null;" +
|
|
674
|
+
" const regressionPreviousRate = prSplit.previous.length > 0 ? Number(((regressionPreviousCount / prSplit.previous.length) * 100).toFixed(2)) : null;" +
|
|
675
|
+
" const mergedCount = (items) => items.filter((pr) => String(pr?.state || '').toLowerCase() === 'merged' || Boolean(pr?.mergedAt) || Boolean(pr?.merged_at) || pr?.merged === true).length;" +
|
|
676
|
+
" const closedCount = (items) => items.filter((pr) => {" +
|
|
677
|
+
" const state = String(pr?.state || '').toLowerCase();" +
|
|
678
|
+
" return state === 'closed' || state === 'merged' || Boolean(pr?.closedAt) || Boolean(pr?.closed_at) || Boolean(pr?.mergedAt) || Boolean(pr?.merged_at);" +
|
|
679
|
+
" }).length;" +
|
|
680
|
+
" const mergeClosedCurrent = closedCount(prSplit.current);" +
|
|
681
|
+
" const mergeClosedPrevious = closedCount(prSplit.previous);" +
|
|
682
|
+
" const mergeSuccessCurrent = mergeClosedCurrent > 0 ? Number(((mergedCount(prSplit.current) / mergeClosedCurrent) * 100).toFixed(2)) : null;" +
|
|
683
|
+
" const mergeSuccessPrevious = mergeClosedPrevious > 0 ? Number(((mergedCount(prSplit.previous) / mergeClosedPrevious) * 100).toFixed(2)) : null;" +
|
|
684
|
+
" const debtDelta = (entries) => {" +
|
|
685
|
+
" let total = 0;" +
|
|
686
|
+
" for (const entry of entries) {" +
|
|
687
|
+
" if (entry == null) continue;" +
|
|
688
|
+
" if (typeof entry === 'number') { total += entry; continue; }" +
|
|
689
|
+
" if (typeof entry !== 'object') continue;" +
|
|
690
|
+
" if (Number.isFinite(Number(entry.debtDelta))) { total += Number(entry.debtDelta); continue; }" +
|
|
691
|
+
" if (Number.isFinite(Number(entry.delta))) { total += Number(entry.delta); continue; }" +
|
|
692
|
+
" if (Number.isFinite(Number(entry.netChange))) { total += Number(entry.netChange); continue; }" +
|
|
693
|
+
" const amt = Number.isFinite(Number(entry.amount)) ? Number(entry.amount) : 1;" +
|
|
694
|
+
" const kind = String(entry.type || entry.event || entry.action || '').toLowerCase();" +
|
|
695
|
+
" if (/resolved|burn|paydown|decrease|closed/.test(kind)) total -= amt;" +
|
|
696
|
+
" else if (/created|added|increase|opened|new/.test(kind)) total += amt;" +
|
|
697
|
+
" }" +
|
|
698
|
+
" return Number(total.toFixed(2));" +
|
|
699
|
+
" };" +
|
|
700
|
+
" const debtCurrent = debtSplit.current.length > 0 ? debtDelta(debtSplit.current) : null;" +
|
|
701
|
+
" const debtPrevious = debtSplit.previous.length > 0 ? debtDelta(debtSplit.previous) : null;" +
|
|
702
|
+
" const priorRaw = prevNode?.success === true ? (parseSource(prevNode.content).items?.[0] ?? parseJsonSafe(prevNode.content)) : null;" +
|
|
703
|
+
" const priorParsed = priorRaw?.fitnessSummary && typeof priorRaw.fitnessSummary === 'object' ? priorRaw.fitnessSummary : (priorRaw && typeof priorRaw === 'object' ? priorRaw : null);" +
|
|
704
|
+
" const metricConfidence = (primaryHealth, hasValue, usedFallbackWindow) => {" +
|
|
705
|
+
" if (!hasValue) return 'low';" +
|
|
706
|
+
" if (primaryHealth.status === 'missing') return 'low';" +
|
|
707
|
+
" if (primaryHealth.status === 'degraded') return 'low';" +
|
|
708
|
+
" if (usedFallbackWindow) return 'medium';" +
|
|
709
|
+
" return primaryHealth.confidence || 'medium';" +
|
|
710
|
+
" };" +
|
|
711
|
+
" const throughputMetric = metric('throughput', throughputCurrent, throughputPrevious, 'up_is_good', 'tasks', metricConfidence(taskHealth, throughputCurrent != null, taskSplit.usedFallbackWindow), taskHealth.status, [throughputCurrent == null ? 'Task telemetry unavailable for this window.' : '', taskSplit.usedFallbackWindow ? 'No task timestamps detected; treated all records as current week.' : '']);" +
|
|
712
|
+
" const regressionMetric = metric('regression_rate', regressionCurrentRate, regressionPreviousRate, 'down_is_good', 'percent', metricConfidence(prHealth, regressionCurrentRate != null, prSplit.usedFallbackWindow), prHealth.status, [regressionCurrentRate == null ? 'Insufficient PR sample to compute regression rate.' : '', prSplit.usedFallbackWindow ? 'No PR timestamps detected; treated all records as current week.' : '']);" +
|
|
713
|
+
" const mergeMetric = metric('merge_success', mergeSuccessCurrent, mergeSuccessPrevious, 'up_is_good', 'percent', metricConfidence(prHealth, mergeSuccessCurrent != null, prSplit.usedFallbackWindow), prHealth.status, [mergeSuccessCurrent == null ? 'No closed or merged PRs in scope.' : '', prSplit.usedFallbackWindow ? 'No PR timestamps detected; treated all records as current week.' : '']);" +
|
|
714
|
+
" const reopenedMetric = metric('reopened_tasks', reopenedCurrent, reopenedPrevious, 'down_is_good', 'tasks', metricConfidence(taskHealth, reopenedCurrent != null, taskSplit.usedFallbackWindow), taskHealth.status, [reopenedCurrent == null ? 'Task telemetry unavailable for this window.' : '', taskSplit.usedFallbackWindow ? 'No task timestamps detected; treated all records as current week.' : '']);" +
|
|
715
|
+
" const debtMetric = metric('debt_growth', debtCurrent, debtPrevious, 'down_is_good', 'points', metricConfidence(debtHealth, debtCurrent != null, debtSplit.usedFallbackWindow), debtHealth.status, [debtCurrent == null ? 'No debt ledger events in scope.' : '', debtSplit.usedFallbackWindow ? 'No debt timestamps detected; treated all records as current week.' : '']);" +
|
|
716
|
+
" const metrics = { throughput: throughputMetric, regression_rate: regressionMetric, merge_success: mergeMetric, reopened_tasks: reopenedMetric, debt_growth: debtMetric };" +
|
|
717
|
+
" const metricKeys = ['throughput', 'regression_rate', 'merge_success', 'reopened_tasks', 'debt_growth'];" +
|
|
718
|
+
" const trendDeltas = metricKeys.reduce((acc, key) => { const d = metrics?.[key]?.delta; acc[key] = Number.isFinite(d) ? d : null; return acc; }, {});" +
|
|
719
|
+
" const normalizePriorTrendDelta = (metricName) => {" +
|
|
720
|
+
" const direct = priorParsed?.priorWeekTrendDeltas?.[metricName];" +
|
|
721
|
+
" if (Number.isFinite(Number(direct))) return Number(Number(direct).toFixed(2));" +
|
|
722
|
+
" const trend = priorParsed?.trendDeltas?.[metricName];" +
|
|
723
|
+
" if (Number.isFinite(Number(trend))) return Number(Number(trend).toFixed(2));" +
|
|
724
|
+
" const metricDelta = priorParsed?.metrics?.[metricName]?.delta;" +
|
|
725
|
+
" if (Number.isFinite(Number(metricDelta))) return Number(Number(metricDelta).toFixed(2));" +
|
|
726
|
+
" return null;" +
|
|
727
|
+
" };" +
|
|
728
|
+
" const priorWeekTrendDeltas = metricKeys.reduce((acc, key) => { acc[key] = normalizePriorTrendDelta(key); return acc; }, {});" +
|
|
729
|
+
" const priorWeekDeltas = priorWeekTrendDeltas;" +
|
|
730
|
+
" const priorWeekMetrics = priorParsed?.metrics && typeof priorParsed.metrics === 'object' ? priorParsed.metrics : null;" +
|
|
731
|
+
" const alertThresholds = { throughput: 1, regression_rate: 2.5, merge_success: 2.5, reopened_tasks: 1, debt_growth: 1 };" +
|
|
732
|
+
" const metricTrendAlerts = Object.entries(metrics).flatMap(([metricName, m]) => {" +
|
|
733
|
+
" if (m == null || m.delta == null) return [];" +
|
|
734
|
+
" if (String(m.confidence || '').toLowerCase() === 'low') return [];" +
|
|
735
|
+
" const delta = Number(m.delta);" +
|
|
736
|
+
" const isRegression = (m.direction === 'up_is_good' && delta < 0) || (m.direction === 'down_is_good' && delta > 0);" +
|
|
737
|
+
" if (!isRegression) return [];" +
|
|
738
|
+
" const absDelta = Math.abs(delta);" +
|
|
739
|
+
" const threshold = alertThresholds[metricName] ?? 1;" +
|
|
740
|
+
" const severity = absDelta >= threshold * 2 ? 'high' : absDelta >= threshold ? 'medium' : 'low';" +
|
|
741
|
+
" return [{ metric: metricName, severity, delta, reason: `${metricName} moved in a negative direction by ${delta} ${m.unit}.` }];" +
|
|
742
|
+
" });" +
|
|
743
|
+
" const sourceHealth = {" +
|
|
744
|
+
" tasks: { ...taskHealth, count: tasks.length }," +
|
|
745
|
+
" prs: { ...prHealth, count: prs.length }," +
|
|
746
|
+
" debt: { ...debtHealth, count: debt.length }," +
|
|
747
|
+
" };" +
|
|
748
|
+
" const sourceTelemetryAlerts = Object.entries(sourceHealth).flatMap(([sourceName, health]) => {" +
|
|
749
|
+
" if (health?.status === 'ok') return [];" +
|
|
750
|
+
" const severity = health?.status === 'missing' ? 'high' : 'medium';" +
|
|
751
|
+
" const reason = health?.status === 'missing' ? `${sourceName} telemetry missing; metric interpretation may be limited.` : `${sourceName} telemetry partially parsed; confidence reduced.`;" +
|
|
752
|
+
" return [{ metric: `telemetry:${sourceName}`, severity, delta: null, reason }];" +
|
|
753
|
+
" });" +
|
|
754
|
+
" const trendAlerts = [...metricTrendAlerts, ...sourceTelemetryAlerts];" +
|
|
755
|
+
" const confidenceValues = Object.values(metrics).map((m) => m?.confidence || 'low');" +
|
|
756
|
+
" const overallConfidence = confidenceValues.every((c) => c === 'high') ? 'high' : confidenceValues.some((c) => c === 'low') ? 'low' : 'medium';" +
|
|
757
|
+
" const plannerSignals = {" +
|
|
758
|
+
" schemaVersion: '1.0'," +
|
|
759
|
+
" overallConfidence," +
|
|
760
|
+
" trendAlertCount: trendAlerts.length," +
|
|
761
|
+
" highSeverityAlertCount: trendAlerts.filter((a) => a?.severity === 'high').length," +
|
|
762
|
+
" sourceStatus: Object.fromEntries(Object.entries(sourceHealth).map(([k, v]) => [k, v?.status || 'missing']))," +
|
|
763
|
+
" metricStatus: Object.fromEntries(metricKeys.map((k) => [k, metrics?.[k]?.status || 'missing']))," +
|
|
764
|
+
" metricConfidence: Object.fromEntries(metricKeys.map((k) => [k, metrics?.[k]?.confidence || 'low']))," +
|
|
765
|
+
" metricValues: Object.fromEntries(metricKeys.map((k) => [k, Number.isFinite(metrics?.[k]?.value) ? Number(metrics[k].value) : null]))," +
|
|
766
|
+
" trendDeltas," +
|
|
767
|
+
" priorWeekTrendDeltas," +
|
|
768
|
+
" };" +
|
|
769
|
+
" const plannerArtifact = {" +
|
|
770
|
+
" schemaVersion: '1.0'," +
|
|
771
|
+
" generatedAt: toIso(now)," +
|
|
772
|
+
" lookbackDays," +
|
|
773
|
+
" sourceStatus: plannerSignals.sourceStatus," +
|
|
774
|
+
" metricConfidence: plannerSignals.metricConfidence," +
|
|
775
|
+
" metricValues: plannerSignals.metricValues," +
|
|
776
|
+
" trendDeltas," +
|
|
777
|
+
" priorWeekTrendDeltas," +
|
|
778
|
+
" trendAlertCount: plannerSignals.trendAlertCount," +
|
|
779
|
+
" highSeverityAlertCount: plannerSignals.highSeverityAlertCount," +
|
|
780
|
+
" trendAlerts," +
|
|
781
|
+
" };" +
|
|
782
|
+
" return {" +
|
|
783
|
+
" schemaVersion: '1.0'," +
|
|
784
|
+
" generatedAt: toIso(now)," +
|
|
785
|
+
" lookbackDays," +
|
|
786
|
+
" window: { currentStart: toIso(currentStart), currentEnd: toIso(now), previousStart: toIso(previousStart), previousEnd: toIso(previousEnd) }," +
|
|
787
|
+
" sourceHealth," +
|
|
788
|
+
" metrics," +
|
|
789
|
+
" trendDeltas," +
|
|
790
|
+
" trendAlerts," +
|
|
791
|
+
" priorWeekTrendDeltas," +
|
|
792
|
+
" priorWeekDeltas," +
|
|
793
|
+
" priorWeekMetrics," +
|
|
794
|
+
" plannerSignals," +
|
|
795
|
+
" plannerArtifact," +
|
|
796
|
+
" dataQuality: {" +
|
|
797
|
+
" overallConfidence," +
|
|
798
|
+
" missingSources: Object.entries(sourceHealth).filter(([, v]) => v.status === 'missing').map(([k]) => k)," +
|
|
799
|
+
" degradedSources: Object.entries(sourceHealth).filter(([, v]) => v.status === 'degraded').map(([k]) => k)," +
|
|
800
|
+
" }," +
|
|
801
|
+
" };" +
|
|
802
|
+
" } catch (error) {" +
|
|
803
|
+
" return {" +
|
|
804
|
+
" schemaVersion: '1.0'," +
|
|
805
|
+
" generatedAt: new Date().toISOString()," +
|
|
806
|
+
" lookbackDays: Number($data?.lookbackDays || 7)," +
|
|
807
|
+
" sourceHealth: {" +
|
|
808
|
+
" tasks: { status: 'missing', confidence: 'low', count: 0 }," +
|
|
809
|
+
" prs: { status: 'missing', confidence: 'low', count: 0 }," +
|
|
810
|
+
" debt: { status: 'missing', confidence: 'low', count: 0 }," +
|
|
811
|
+
" }," +
|
|
812
|
+
" metrics: {" +
|
|
813
|
+
" throughput: { value: null, previous: null, delta: null, confidence: 'low', status: 'missing' }," +
|
|
814
|
+
" regression_rate: { value: null, previous: null, delta: null, confidence: 'low', status: 'missing' }," +
|
|
815
|
+
" merge_success: { value: null, previous: null, delta: null, confidence: 'low', status: 'missing' }," +
|
|
816
|
+
" reopened_tasks: { value: null, previous: null, delta: null, confidence: 'low', status: 'missing' }," +
|
|
817
|
+
" debt_growth: { value: null, previous: null, delta: null, confidence: 'low', status: 'missing' }," +
|
|
818
|
+
" }," +
|
|
819
|
+
" trendDeltas: { throughput: null, regression_rate: null, merge_success: null, reopened_tasks: null, debt_growth: null }," +
|
|
820
|
+
" trendAlerts: [{ metric: 'summary', severity: 'high', delta: null, reason: `Fitness summary fallback engaged: ${error?.message || 'unknown error'}` }]," +
|
|
821
|
+
" priorWeekTrendDeltas: { throughput: null, regression_rate: null, merge_success: null, reopened_tasks: null, debt_growth: null }," +
|
|
822
|
+
" priorWeekDeltas: { throughput: null, regression_rate: null, merge_success: null, reopened_tasks: null, debt_growth: null }," +
|
|
823
|
+
" priorWeekMetrics: null," +
|
|
824
|
+
" plannerSignals: {" +
|
|
825
|
+
" schemaVersion: '1.0'," +
|
|
826
|
+
" overallConfidence: 'low'," +
|
|
827
|
+
" trendAlertCount: 1," +
|
|
828
|
+
" highSeverityAlertCount: 1," +
|
|
829
|
+
" sourceStatus: { tasks: 'missing', prs: 'missing', debt: 'missing' }," +
|
|
830
|
+
" metricStatus: { throughput: 'missing', regression_rate: 'missing', merge_success: 'missing', reopened_tasks: 'missing', debt_growth: 'missing' }," +
|
|
831
|
+
" metricConfidence: { throughput: 'low', regression_rate: 'low', merge_success: 'low', reopened_tasks: 'low', debt_growth: 'low' }," +
|
|
832
|
+
" metricValues: { throughput: null, regression_rate: null, merge_success: null, reopened_tasks: null, debt_growth: null }," +
|
|
833
|
+
" trendDeltas: { throughput: null, regression_rate: null, merge_success: null, reopened_tasks: null, debt_growth: null }," +
|
|
834
|
+
" priorWeekTrendDeltas: { throughput: null, regression_rate: null, merge_success: null, reopened_tasks: null, debt_growth: null }," +
|
|
835
|
+
" }," +
|
|
836
|
+
" plannerArtifact: {" +
|
|
837
|
+
" schemaVersion: '1.0'," +
|
|
838
|
+
" generatedAt: new Date().toISOString()," +
|
|
839
|
+
" lookbackDays: Number($data?.lookbackDays || 7)," +
|
|
840
|
+
" sourceStatus: { tasks: 'missing', prs: 'missing', debt: 'missing' }," +
|
|
841
|
+
" metricConfidence: { throughput: 'low', regression_rate: 'low', merge_success: 'low', reopened_tasks: 'low', debt_growth: 'low' }," +
|
|
842
|
+
" metricValues: { throughput: null, regression_rate: null, merge_success: null, reopened_tasks: null, debt_growth: null }," +
|
|
843
|
+
" trendDeltas: { throughput: null, regression_rate: null, merge_success: null, reopened_tasks: null, debt_growth: null }," +
|
|
844
|
+
" priorWeekTrendDeltas: { throughput: null, regression_rate: null, merge_success: null, reopened_tasks: null, debt_growth: null }," +
|
|
845
|
+
" trendAlertCount: 1," +
|
|
846
|
+
" highSeverityAlertCount: 1," +
|
|
847
|
+
" trendAlerts: [{ metric: 'summary', severity: 'high', delta: null, reason: `Fitness summary fallback engaged: ${error?.message || 'unknown error'}` }]," +
|
|
848
|
+
" }," +
|
|
849
|
+
" dataQuality: { overallConfidence: 'low', missingSources: ['tasks', 'prs', 'debt'], degradedSources: [] }," +
|
|
850
|
+
" };" +
|
|
851
|
+
" }" +
|
|
852
|
+
"})()",
|
|
853
|
+
isExpression: true,
|
|
854
|
+
}, { x: 420, y: 360 }),
|
|
855
|
+
|
|
856
|
+
node("serialize-fitness-summary", "action.set_variable", "Serialize Fitness Summary", {
|
|
857
|
+
key: "fitnessSummaryJson",
|
|
858
|
+
value: "(() => JSON.stringify($data?.fitnessSummary || {}, null, 2))()",
|
|
859
|
+
isExpression: true,
|
|
860
|
+
}, { x: 420, y: 500 }),
|
|
861
|
+
|
|
862
|
+
node("render-trend-alerts", "action.set_variable", "Render Trend Alerts", {
|
|
863
|
+
key: "fitnessTrendAlertsText",
|
|
864
|
+
value: "(() => { const alerts = Array.isArray($data?.fitnessSummary?.trendAlerts) ? $data.fitnessSummary.trendAlerts : []; if (!alerts.length) return 'No negative trend alerts this week.'; return alerts.map((a, idx) => `${idx + 1}. ${a.metric} (${a.severity}) - ${a.reason}`).join('\\n'); })()",
|
|
865
|
+
isExpression: true,
|
|
866
|
+
}, { x: 420, y: 640 }),
|
|
867
|
+
|
|
868
|
+
node("persist-fitness-summary", "action.write_file", "Persist Fitness Summary Artifact", {
|
|
869
|
+
path: ".bosun/workflow-runs/weekly-fitness-summary.latest.json",
|
|
870
|
+
content: "{{fitnessSummaryJson}}",
|
|
871
|
+
mkdir: true,
|
|
872
|
+
}, { x: 420, y: 780 }),
|
|
873
|
+
|
|
494
874
|
node("evaluate-fitness", "action.run_agent", "Evaluate Fitness", {
|
|
495
|
-
prompt: `# Weekly Delivery Fitness Evaluation
|
|
875
|
+
prompt: `# Weekly Delivery Fitness Evaluation
|
|
876
|
+
|
|
877
|
+
Evaluate the last {{lookbackDays}} days using this machine-readable summary:
|
|
878
|
+
|
|
879
|
+
Metrics to evaluate:
|
|
880
|
+
- Throughput
|
|
881
|
+
- Regression rate
|
|
882
|
+
- Merge success
|
|
883
|
+
- Reopened tasks
|
|
884
|
+
- Debt growth
|
|
885
|
+
|
|
886
|
+
## Weekly Fitness JSON
|
|
887
|
+
{{fitnessSummaryJson}}
|
|
888
|
+
|
|
889
|
+
## Negative Trend Alerts
|
|
890
|
+
{{fitnessTrendAlertsText}}
|
|
891
|
+
|
|
892
|
+
Focus directive: {{evaluatorFocus}}
|
|
893
|
+
|
|
894
|
+
Requirements:
|
|
895
|
+
- Respect confidence and status on each metric.
|
|
896
|
+
- If a metric has low confidence or missing telemetry, call that out explicitly and avoid overconfident recommendations.
|
|
897
|
+
- Use prior-week deltas when available.
|
|
898
|
+
- If one telemetry source is unavailable, still provide a stable scorecard and best-effort recommendations.
|
|
899
|
+
|
|
900
|
+
Return sections:
|
|
901
|
+
1) Scorecard (0-100) with one line per metric and confidence
|
|
902
|
+
2) Root-cause analysis of the largest drag
|
|
903
|
+
3) Countermeasures ranked by impact/cost
|
|
904
|
+
4) FOLLOW_UP_ACTION lines using format:
|
|
905
|
+
FOLLOW_UP_ACTION: [title] | [description] | [repo_area] | [risk] | [effort]
|
|
906
|
+
|
|
907
|
+
Only include FOLLOW_UP_ACTION lines for changes that are worth implementing this week.`,
|
|
496
908
|
sdk: "auto",
|
|
497
909
|
timeoutMs: 600000,
|
|
498
|
-
}, { x: 420, y:
|
|
910
|
+
}, { x: 420, y: 930 }),
|
|
499
911
|
|
|
500
912
|
node("has-followups", "condition.expression", "Follow-ups Enabled + Present", {
|
|
501
913
|
expression: "($data?.createFollowupTasks === true) && (($ctx.getNodeOutput('evaluate-fitness')?.output || '').includes('FOLLOW_UP_ACTION:'))",
|
|
502
|
-
}, { x: 420, y:
|
|
914
|
+
}, { x: 420, y: 1090 }),
|
|
503
915
|
|
|
504
916
|
node("build-followup-json", "action.run_agent", "Build Follow-up Tasks JSON", {
|
|
505
|
-
prompt: `Convert FOLLOW_UP_ACTION lines below into a single JSON object with shape {
|
|
917
|
+
prompt: `Convert FOLLOW_UP_ACTION lines below into a single JSON object with shape { "tasks": [...] }.
|
|
918
|
+
|
|
919
|
+
Source:
|
|
920
|
+
{{evaluate-fitness.output}}
|
|
921
|
+
|
|
922
|
+
Structured context:
|
|
923
|
+
{{fitnessSummaryJson}}
|
|
924
|
+
|
|
925
|
+
Rules:
|
|
926
|
+
- Generate at most {{maxFollowupTasks}} tasks
|
|
927
|
+
- Include fields: title, description, implementation_steps, acceptance_criteria, verification, priority, tags, base_branch, impact, confidence, risk, estimated_effort, repo_areas, why_now, kill_criteria
|
|
928
|
+
- Use trend deltas from the summary artifact to justify urgency and avoid parse errors
|
|
929
|
+
- Keep tasks implementation-ready and avoid duplicates
|
|
930
|
+
- Return only JSON`,
|
|
506
931
|
sdk: "auto",
|
|
507
932
|
timeoutMs: 300000,
|
|
508
|
-
}, { x: 220, y:
|
|
933
|
+
}, { x: 220, y: 1260 }),
|
|
509
934
|
|
|
510
935
|
node("materialize-followups", "action.materialize_planner_tasks", "Materialize Follow-up Tasks", {
|
|
511
936
|
plannerNodeId: "build-followup-json",
|
|
@@ -514,25 +939,31 @@ export const WEEKLY_FITNESS_SUMMARY_TEMPLATE = {
|
|
|
514
939
|
dedup: true,
|
|
515
940
|
failOnZero: false,
|
|
516
941
|
minCreated: 0,
|
|
517
|
-
}, { x: 220, y:
|
|
942
|
+
}, { x: 220, y: 1420 }),
|
|
518
943
|
|
|
519
944
|
node("notify-summary", "notify.telegram", "Send Weekly Fitness Summary", {
|
|
520
|
-
message: ":chart: Weekly fitness evaluation complete. Follow-up tasks created: {{materialize-followups.createdCount}}
|
|
945
|
+
message: ":chart: Weekly fitness evaluation complete. Follow-up tasks created: {{materialize-followups.createdCount}}\\n\\nTrend alerts:\\n{{fitnessTrendAlertsText}}\\n\\n{{evaluate-fitness.output}}",
|
|
521
946
|
silent: true,
|
|
522
|
-
}, { x: 420, y:
|
|
947
|
+
}, { x: 420, y: 1580 }),
|
|
523
948
|
|
|
524
949
|
node("log-no-followups", "notify.log", "No Follow-up Tasks", {
|
|
525
950
|
message: "Weekly fitness evaluation completed with no follow-up task creation.",
|
|
526
951
|
level: "info",
|
|
527
|
-
}, { x: 620, y:
|
|
952
|
+
}, { x: 620, y: 1260 }),
|
|
528
953
|
],
|
|
529
954
|
edges: [
|
|
530
955
|
edge("trigger", "task-metrics"),
|
|
531
956
|
edge("trigger", "pr-metrics"),
|
|
532
957
|
edge("trigger", "debt-metrics"),
|
|
533
|
-
edge("
|
|
534
|
-
edge("
|
|
535
|
-
edge("
|
|
958
|
+
edge("trigger", "read-previous-summary"),
|
|
959
|
+
edge("task-metrics", "summarize-fitness-metrics"),
|
|
960
|
+
edge("pr-metrics", "summarize-fitness-metrics"),
|
|
961
|
+
edge("debt-metrics", "summarize-fitness-metrics"),
|
|
962
|
+
edge("read-previous-summary", "summarize-fitness-metrics"),
|
|
963
|
+
edge("summarize-fitness-metrics", "serialize-fitness-summary"),
|
|
964
|
+
edge("serialize-fitness-summary", "render-trend-alerts"),
|
|
965
|
+
edge("render-trend-alerts", "persist-fitness-summary"),
|
|
966
|
+
edge("persist-fitness-summary", "evaluate-fitness"),
|
|
536
967
|
edge("evaluate-fitness", "has-followups"),
|
|
537
968
|
edge("has-followups", "build-followup-json", { condition: "$output?.result === true" }),
|
|
538
969
|
edge("has-followups", "log-no-followups", { condition: "$output?.result !== true" }),
|
|
@@ -556,5 +987,3 @@ export const WEEKLY_FITNESS_SUMMARY_TEMPLATE = {
|
|
|
556
987
|
},
|
|
557
988
|
},
|
|
558
989
|
};
|
|
559
|
-
|
|
560
|
-
|