bosun 0.42.2 → 0.42.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -0
- package/agent/agent-event-bus.mjs +10 -0
- package/agent/agent-supervisor.mjs +20 -0
- package/bosun-tui.mjs +107 -105
- package/cli.mjs +10 -0
- package/config/config.mjs +25 -0
- package/config/executor-config.mjs +124 -1
- package/infra/container-runner.mjs +565 -1
- package/infra/monitor.mjs +18 -0
- package/infra/tracing.mjs +544 -240
- package/infra/tui-bridge.mjs +13 -1
- package/kanban/kanban-adapter.mjs +128 -4
- package/lib/repo-map.mjs +114 -3
- package/package.json +11 -4
- package/server/ui-server.mjs +3 -0
- package/task/task-archiver.mjs +18 -6
- package/task/task-attachments.mjs +14 -10
- package/task/task-cli.mjs +24 -4
- package/task/task-executor.mjs +19 -0
- package/task/task-store.mjs +194 -37
- package/telegram/telegram-bot.mjs +4 -1
- package/tui/app.mjs +131 -171
- package/tui/components/status-header.mjs +178 -75
- package/tui/lib/header-config.mjs +68 -0
- package/tui/lib/ws-bridge.mjs +61 -9
- package/tui/screens/agents.mjs +127 -0
- package/tui/screens/tasks.mjs +1 -48
- package/ui/app.js +8 -5
- package/ui/components/kanban-board.js +65 -3
- package/ui/components/session-list.js +18 -32
- package/ui/demo-defaults.js +52 -2
- package/ui/modules/session-api.js +100 -0
- package/ui/modules/state.js +71 -15
- package/ui/tabs/workflows.js +25 -1
- package/ui/tui/App.js +298 -0
- package/ui/tui/TasksScreen.js +564 -0
- package/ui/tui/constants.js +55 -0
- package/ui/tui/tasks-screen-helpers.js +301 -0
- package/ui/tui/useTasks.js +61 -0
- package/ui/tui/useWebSocket.js +166 -0
- package/ui/tui/useWorkflows.js +30 -0
- package/workflow/workflow-engine.mjs +412 -7
- package/workflow/workflow-nodes.mjs +616 -75
- package/workflow-templates/agents.mjs +3 -0
- package/workflow-templates/planning.mjs +7 -0
- package/workflow-templates/sub-workflows.mjs +5 -0
- package/workflow-templates/task-execution.mjs +3 -0
- package/workspace/command-diagnostics.mjs +1 -1
- package/workspace/context-cache.mjs +182 -9
package/infra/tui-bridge.mjs
CHANGED
|
@@ -18,6 +18,9 @@ const RATE_LIMIT_BUCKET_SCHEMA = {
|
|
|
18
18
|
primary: { type: ["number", "null"] },
|
|
19
19
|
secondary: { type: ["number", "null"] },
|
|
20
20
|
credits: { type: ["number", "null"] },
|
|
21
|
+
primaryLimit: { type: ["number", "null"] },
|
|
22
|
+
secondaryLimit: { type: ["number", "null"] },
|
|
23
|
+
creditsLimit: { type: ["number", "null"] },
|
|
21
24
|
unit: { type: "string", minLength: 1 },
|
|
22
25
|
},
|
|
23
26
|
};
|
|
@@ -74,6 +77,7 @@ export const TUI_EVENT_SCHEMAS = Object.freeze({
|
|
|
74
77
|
"tokensIn",
|
|
75
78
|
"tokensOut",
|
|
76
79
|
"tokensTotal",
|
|
80
|
+
"totalTokens",
|
|
77
81
|
"throughputTps",
|
|
78
82
|
"uptimeMs",
|
|
79
83
|
"rateLimits",
|
|
@@ -85,6 +89,7 @@ export const TUI_EVENT_SCHEMAS = Object.freeze({
|
|
|
85
89
|
tokensIn: { type: "number", minimum: 0 },
|
|
86
90
|
tokensOut: { type: "number", minimum: 0 },
|
|
87
91
|
tokensTotal: { type: "number", minimum: 0 },
|
|
92
|
+
totalTokens: { type: "number", minimum: 0 },
|
|
88
93
|
throughputTps: { type: "number", minimum: 0 },
|
|
89
94
|
uptimeMs: { type: "number", minimum: 0 },
|
|
90
95
|
rateLimits: {
|
|
@@ -208,6 +213,9 @@ function normalizeRateLimits(rateLimits = {}) {
|
|
|
208
213
|
primary: Number.isFinite(Number(bucket.primary)) ? Number(bucket.primary) : null,
|
|
209
214
|
secondary: Number.isFinite(Number(bucket.secondary)) ? Number(bucket.secondary) : null,
|
|
210
215
|
credits: bucket.credits == null ? null : (Number.isFinite(Number(bucket.credits)) ? Number(bucket.credits) : null),
|
|
216
|
+
primaryLimit: bucket.primaryLimit == null ? null : (Number.isFinite(Number(bucket.primaryLimit)) ? Number(bucket.primaryLimit) : null),
|
|
217
|
+
secondaryLimit: bucket.secondaryLimit == null ? null : (Number.isFinite(Number(bucket.secondaryLimit)) ? Number(bucket.secondaryLimit) : null),
|
|
218
|
+
creditsLimit: bucket.creditsLimit == null ? null : (Number.isFinite(Number(bucket.creditsLimit)) ? Number(bucket.creditsLimit) : null),
|
|
211
219
|
unit: String(bucket.unit || "count").trim() || "count",
|
|
212
220
|
};
|
|
213
221
|
}
|
|
@@ -237,7 +245,10 @@ export function buildMonitorStatsPayload({ agentPool, runtimeStats = {}, uptimeM
|
|
|
237
245
|
agentPoolStats.tokensOut,
|
|
238
246
|
runtimeStats.totalOutputTokens ?? runtimeTokenTotals.tokensOut,
|
|
239
247
|
);
|
|
240
|
-
const tokensTotal = nonNegativeNumber(
|
|
248
|
+
const tokensTotal = nonNegativeNumber(
|
|
249
|
+
agentPoolStats.tokensTotal ?? agentPoolStats.totalTokens,
|
|
250
|
+
tokensIn + tokensOut,
|
|
251
|
+
);
|
|
241
252
|
const resolvedUptimeMs = nonNegativeNumber(
|
|
242
253
|
uptimeMs,
|
|
243
254
|
runtimeStats.startedAt ? Date.now() - Number(runtimeStats.startedAt) : 0,
|
|
@@ -252,6 +263,7 @@ export function buildMonitorStatsPayload({ agentPool, runtimeStats = {}, uptimeM
|
|
|
252
263
|
tokensIn,
|
|
253
264
|
tokensOut,
|
|
254
265
|
tokensTotal,
|
|
266
|
+
totalTokens: tokensTotal,
|
|
255
267
|
throughputTps,
|
|
256
268
|
uptimeMs: resolvedUptimeMs,
|
|
257
269
|
rateLimits: normalizeRateLimits(agentPoolStats.rateLimits),
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
setTaskStatus as setInternalTaskStatus,
|
|
63
63
|
removeTask as removeInternalTask,
|
|
64
64
|
updateTask as patchInternalTask,
|
|
65
|
+
waitForStoreWrites,
|
|
65
66
|
} from "../task/task-store.mjs";
|
|
66
67
|
import {
|
|
67
68
|
listTaskAttachments,
|
|
@@ -673,6 +674,101 @@ function extractMarkdownLinks(text) {
|
|
|
673
674
|
return results;
|
|
674
675
|
}
|
|
675
676
|
|
|
677
|
+
function normalizePrLinkageEntry(entry = {}, fallback = {}) {
|
|
678
|
+
const safeEntry = entry && typeof entry === "object" ? entry : {};
|
|
679
|
+
const safeFallback = fallback && typeof fallback === "object" ? fallback : {};
|
|
680
|
+
const branchName = typeof (safeEntry.branchName ?? safeFallback.branchName) === "string"
|
|
681
|
+
? String(safeEntry.branchName ?? safeFallback.branchName).trim()
|
|
682
|
+
: "";
|
|
683
|
+
const prUrl = typeof (safeEntry.prUrl ?? safeFallback.prUrl) === "string"
|
|
684
|
+
? String(safeEntry.prUrl ?? safeFallback.prUrl).trim()
|
|
685
|
+
: "";
|
|
686
|
+
const parsedPrNumber = Number.parseInt(String(safeEntry.prNumber ?? safeFallback.prNumber ?? ""), 10);
|
|
687
|
+
const prNumber = Number.isFinite(parsedPrNumber) && parsedPrNumber > 0 ? parsedPrNumber : null;
|
|
688
|
+
if (!branchName && !prUrl && !prNumber) return null;
|
|
689
|
+
return {
|
|
690
|
+
branchName: branchName || null,
|
|
691
|
+
prUrl: prUrl || null,
|
|
692
|
+
prNumber,
|
|
693
|
+
source: typeof (safeEntry.source ?? safeFallback.source) === "string" && String(safeEntry.source ?? safeFallback.source).trim()
|
|
694
|
+
? String(safeEntry.source ?? safeFallback.source).trim()
|
|
695
|
+
: null,
|
|
696
|
+
freshness: typeof (safeEntry.freshness ?? safeFallback.freshness) === "string" && String(safeEntry.freshness ?? safeFallback.freshness).trim()
|
|
697
|
+
? String(safeEntry.freshness ?? safeFallback.freshness).trim()
|
|
698
|
+
: null,
|
|
699
|
+
linkedAt: typeof (safeEntry.linkedAt ?? safeFallback.linkedAt) === "string" && String(safeEntry.linkedAt ?? safeFallback.linkedAt).trim()
|
|
700
|
+
? String(safeEntry.linkedAt ?? safeFallback.linkedAt).trim()
|
|
701
|
+
: null,
|
|
702
|
+
updatedAt: typeof (safeEntry.updatedAt ?? safeFallback.updatedAt) === "string" && String(safeEntry.updatedAt ?? safeFallback.updatedAt).trim()
|
|
703
|
+
? String(safeEntry.updatedAt ?? safeFallback.updatedAt).trim()
|
|
704
|
+
: null,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function mergePrLinkageRecords(...sources) {
|
|
709
|
+
const merged = [];
|
|
710
|
+
const indexByKey = new Map();
|
|
711
|
+
const buildKey = (entry) => {
|
|
712
|
+
const branchName = String(entry?.branchName || "").trim().toLowerCase();
|
|
713
|
+
const prUrl = String(entry?.prUrl || "").trim().toLowerCase();
|
|
714
|
+
const prNumber = Number.isFinite(entry?.prNumber) ? entry.prNumber : "";
|
|
715
|
+
return [branchName, prNumber, prUrl].join("|");
|
|
716
|
+
};
|
|
717
|
+
for (const source of sources) {
|
|
718
|
+
const entries = Array.isArray(source) ? source : [];
|
|
719
|
+
for (const rawEntry of entries) {
|
|
720
|
+
const entry = normalizePrLinkageEntry(rawEntry);
|
|
721
|
+
if (!entry) continue;
|
|
722
|
+
const key = buildKey(entry);
|
|
723
|
+
if (!key) continue;
|
|
724
|
+
if (indexByKey.has(key)) {
|
|
725
|
+
const idx = indexByKey.get(key);
|
|
726
|
+
merged[idx] = normalizePrLinkageEntry({ ...merged[idx], ...entry }, merged[idx]);
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
indexByKey.set(key, merged.length);
|
|
730
|
+
merged.push(entry);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return merged;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function buildPrLinkagePatch(options = {}, currentTask = null) {
|
|
737
|
+
const branchName = typeof options?.branchName === "string" ? options.branchName.trim() : "";
|
|
738
|
+
const prUrl = typeof options?.prUrl === "string" ? options.prUrl.trim() : "";
|
|
739
|
+
const prNumber = options?.prNumber == null || options?.prNumber === ""
|
|
740
|
+
? null
|
|
741
|
+
: Number.parseInt(String(options.prNumber), 10);
|
|
742
|
+
const source = typeof options?.source === "string" && options.source.trim() ? options.source.trim() : null;
|
|
743
|
+
const freshness = typeof options?.freshness === "string" && options.freshness.trim() ? options.freshness.trim() : null;
|
|
744
|
+
const currentMeta = currentTask?.meta && typeof currentTask.meta === "object" ? currentTask.meta : {};
|
|
745
|
+
const currentLinkage = mergePrLinkageRecords(currentTask?.prLinkage, currentMeta?.prLinkage);
|
|
746
|
+
const nextEntry = normalizePrLinkageEntry({
|
|
747
|
+
branchName,
|
|
748
|
+
prUrl,
|
|
749
|
+
prNumber: Number.isFinite(prNumber) && prNumber > 0 ? prNumber : null,
|
|
750
|
+
source,
|
|
751
|
+
freshness,
|
|
752
|
+
updatedAt: new Date().toISOString(),
|
|
753
|
+
linkedAt: currentLinkage[0]?.linkedAt || new Date().toISOString(),
|
|
754
|
+
}, currentTask || {});
|
|
755
|
+
if (!nextEntry) return {};
|
|
756
|
+
const prLinkage = mergePrLinkageRecords(currentLinkage, [nextEntry]);
|
|
757
|
+
return {
|
|
758
|
+
...(nextEntry.branchName ? { branchName: nextEntry.branchName } : {}),
|
|
759
|
+
...(nextEntry.prUrl ? { prUrl: nextEntry.prUrl } : {}),
|
|
760
|
+
...(Number.isFinite(nextEntry.prNumber) && nextEntry.prNumber > 0 ? { prNumber: nextEntry.prNumber } : {}),
|
|
761
|
+
prLinkage,
|
|
762
|
+
meta: {
|
|
763
|
+
...currentMeta,
|
|
764
|
+
prLinkage,
|
|
765
|
+
prLinkageSource: nextEntry.source || currentMeta.prLinkageSource || null,
|
|
766
|
+
prLinkageFreshness: nextEntry.freshness || currentMeta.prLinkageFreshness || null,
|
|
767
|
+
prLinkageUpdatedAt: nextEntry.updatedAt || currentMeta.prLinkageUpdatedAt || null,
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
676
772
|
function extractAttachmentsFromText(text, meta = {}) {
|
|
677
773
|
const links = extractMarkdownLinks(text);
|
|
678
774
|
const attachments = [];
|
|
@@ -792,6 +888,8 @@ class InternalAdapter {
|
|
|
792
888
|
existingAttachments,
|
|
793
889
|
localAttachments,
|
|
794
890
|
);
|
|
891
|
+
const prLinkage = mergePrLinkageRecords(task.prLinkage, task.meta?.prLinkage, [normalizePrLinkageEntry(task, task)]);
|
|
892
|
+
const primaryPrLinkage = prLinkage[0] || null;
|
|
795
893
|
return {
|
|
796
894
|
id: String(task.id || ""),
|
|
797
895
|
title: recoveredTitle || "",
|
|
@@ -816,9 +914,10 @@ class InternalAdapter {
|
|
|
816
914
|
? task.meta.repositories
|
|
817
915
|
: [],
|
|
818
916
|
baseBranch,
|
|
819
|
-
branchName: task.branchName || null,
|
|
820
|
-
|
|
821
|
-
|
|
917
|
+
branchName: task.branchName || primaryPrLinkage?.branchName || null,
|
|
918
|
+
prLinkage,
|
|
919
|
+
prNumber: task.prNumber || primaryPrLinkage?.prNumber || null,
|
|
920
|
+
prUrl: task.prUrl || primaryPrLinkage?.prUrl || null,
|
|
822
921
|
taskUrl: task.taskUrl || null,
|
|
823
922
|
createdAt: task.createdAt || null,
|
|
824
923
|
updatedAt: task.updatedAt || null,
|
|
@@ -836,6 +935,10 @@ class InternalAdapter {
|
|
|
836
935
|
statusHistory: Array.isArray(task.statusHistory) ? task.statusHistory : (Array.isArray(task.meta?.statusHistory) ? task.meta.statusHistory : []),
|
|
837
936
|
comments: normalizedComments,
|
|
838
937
|
attachments: mergedAttachments,
|
|
938
|
+
prLinkage,
|
|
939
|
+
prLinkageSource: primaryPrLinkage?.source || task.meta?.prLinkageSource || null,
|
|
940
|
+
prLinkageFreshness: primaryPrLinkage?.freshness || task.meta?.prLinkageFreshness || null,
|
|
941
|
+
prLinkageUpdatedAt: primaryPrLinkage?.updatedAt || task.meta?.prLinkageUpdatedAt || null,
|
|
839
942
|
},
|
|
840
943
|
};
|
|
841
944
|
}
|
|
@@ -895,7 +998,8 @@ class InternalAdapter {
|
|
|
895
998
|
if (!updated) {
|
|
896
999
|
throw new Error(`[kanban] internal task not found: ${normalizedId}`);
|
|
897
1000
|
}
|
|
898
|
-
const
|
|
1001
|
+
const current = getInternalTask(normalizedId);
|
|
1002
|
+
const linkagePatch = buildPrLinkagePatch(options, current);
|
|
899
1003
|
const branchName =
|
|
900
1004
|
typeof options?.branchName === "string" ? options.branchName.trim() : "";
|
|
901
1005
|
const prUrl = typeof options?.prUrl === "string" ? options.prUrl.trim() : "";
|
|
@@ -908,8 +1012,10 @@ class InternalAdapter {
|
|
|
908
1012
|
if (Number.isFinite(prNumber) && prNumber > 0) linkagePatch.prNumber = prNumber;
|
|
909
1013
|
if (Object.keys(linkagePatch).length > 0) {
|
|
910
1014
|
const patched = patchInternalTask(normalizedId, linkagePatch);
|
|
1015
|
+
await waitForStoreWrites();
|
|
911
1016
|
if (patched) return this._normalizeTask(patched);
|
|
912
1017
|
}
|
|
1018
|
+
await waitForStoreWrites();
|
|
913
1019
|
return this._normalizeTask(updated);
|
|
914
1020
|
}
|
|
915
1021
|
|
|
@@ -1018,10 +1124,26 @@ class InternalAdapter {
|
|
|
1018
1124
|
baseBranch,
|
|
1019
1125
|
};
|
|
1020
1126
|
}
|
|
1127
|
+
const directLinkage = mergePrLinkageRecords(current?.prLinkage, current?.meta?.prLinkage, patch.prLinkage, patch.meta?.prLinkage, [normalizePrLinkageEntry(patch, patch)]);
|
|
1128
|
+
if (directLinkage.length > 0) {
|
|
1129
|
+
const primaryPrLinkage = directLinkage[0] || null;
|
|
1130
|
+
updates.prLinkage = directLinkage;
|
|
1131
|
+
updates.branchName = updates.branchName ?? primaryPrLinkage?.branchName ?? null;
|
|
1132
|
+
updates.prUrl = updates.prUrl ?? primaryPrLinkage?.prUrl ?? null;
|
|
1133
|
+
updates.prNumber = updates.prNumber ?? primaryPrLinkage?.prNumber ?? null;
|
|
1134
|
+
updates.meta = {
|
|
1135
|
+
...(updates.meta || current?.meta || {}),
|
|
1136
|
+
prLinkage: directLinkage,
|
|
1137
|
+
prLinkageSource: primaryPrLinkage?.source || updates.meta?.prLinkageSource || current?.meta?.prLinkageSource || null,
|
|
1138
|
+
prLinkageFreshness: primaryPrLinkage?.freshness || updates.meta?.prLinkageFreshness || current?.meta?.prLinkageFreshness || null,
|
|
1139
|
+
prLinkageUpdatedAt: primaryPrLinkage?.updatedAt || updates.meta?.prLinkageUpdatedAt || current?.meta?.prLinkageUpdatedAt || null,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1021
1142
|
const updated = patchInternalTask(normalizedId, updates);
|
|
1022
1143
|
if (!updated) {
|
|
1023
1144
|
throw new Error(`[kanban] internal task not found: ${normalizedId}`);
|
|
1024
1145
|
}
|
|
1146
|
+
await waitForStoreWrites();
|
|
1025
1147
|
return this._normalizeTask(updated);
|
|
1026
1148
|
}
|
|
1027
1149
|
|
|
@@ -1108,6 +1230,7 @@ class InternalAdapter {
|
|
|
1108
1230
|
if (!created) {
|
|
1109
1231
|
throw new Error("[kanban] internal task creation failed");
|
|
1110
1232
|
}
|
|
1233
|
+
await waitForStoreWrites();
|
|
1111
1234
|
return this._normalizeTask(created);
|
|
1112
1235
|
}
|
|
1113
1236
|
|
|
@@ -6198,3 +6321,4 @@ export async function unmarkTaskIgnored(taskId) {
|
|
|
6198
6321
|
);
|
|
6199
6322
|
return false;
|
|
6200
6323
|
}
|
|
6324
|
+
|
package/lib/repo-map.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { extname, join, resolve } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
const DEFAULT_FILE_LIMIT = 12;
|
|
5
5
|
const DEFAULT_MAX_SYMBOLS = 4;
|
|
6
|
+
const DEFAULT_TOPOLOGY_SUMMARY_LIMIT = 96;
|
|
6
7
|
const QUERY_STOP_WORDS = new Set([
|
|
7
8
|
"about", "after", "again", "agent", "along", "also", "architect", "before", "being", "bosun",
|
|
8
9
|
"build", "changes", "check", "code", "create", "debug", "editor", "ensure", "feature", "files",
|
|
@@ -35,6 +36,24 @@ function uniqueStrings(values) {
|
|
|
35
36
|
return [...new Set((Array.isArray(values) ? values : []).map((value) => String(value || "").trim()).filter(Boolean))];
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function truncateText(value, limit = DEFAULT_TOPOLOGY_SUMMARY_LIMIT) {
|
|
40
|
+
const text = String(value || "").trim();
|
|
41
|
+
const max = toPositiveInt(limit, DEFAULT_TOPOLOGY_SUMMARY_LIMIT);
|
|
42
|
+
if (!text || text.length <= max) return text;
|
|
43
|
+
if (max <= 3) return text.slice(0, max);
|
|
44
|
+
const ellipsis = "...";
|
|
45
|
+
const sliceLength = Math.max(1, max - ellipsis.length);
|
|
46
|
+
const nextWordBreak = text.indexOf(" ", sliceLength);
|
|
47
|
+
if (nextWordBreak > sliceLength && nextWordBreak - sliceLength <= 12) {
|
|
48
|
+
return `${text.slice(0, nextWordBreak).trimEnd()}${ellipsis}`;
|
|
49
|
+
}
|
|
50
|
+
const truncated = text.slice(0, sliceLength);
|
|
51
|
+
const lastWordBreak = truncated.lastIndexOf(" ");
|
|
52
|
+
const prefersWordBoundary = lastWordBreak >= Math.floor(sliceLength * 0.6);
|
|
53
|
+
const display = prefersWordBoundary ? truncated.slice(0, lastWordBreak) : truncated;
|
|
54
|
+
return `${display.trimEnd()}${ellipsis}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
function isTokenWordChar(char) {
|
|
39
58
|
if (!char) return false;
|
|
40
59
|
const code = char.charCodeAt(0);
|
|
@@ -132,6 +151,92 @@ export function formatRepoMap(repoMap, opts = {}) {
|
|
|
132
151
|
return lines.join("\n");
|
|
133
152
|
}
|
|
134
153
|
|
|
154
|
+
function inferRepoOwner(pathValue) {
|
|
155
|
+
const normalizedPath = String(pathValue || "").trim().replace(/\\/g, "/");
|
|
156
|
+
if (!normalizedPath) return "root";
|
|
157
|
+
const segments = normalizedPath.split("/");
|
|
158
|
+
if (segments.length === 1) return "root";
|
|
159
|
+
const [owner] = segments;
|
|
160
|
+
return owner || "root";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeAdjacencyStem(pathValue) {
|
|
164
|
+
return String(pathValue || "")
|
|
165
|
+
.trim()
|
|
166
|
+
.replace(/\\/g, "/")
|
|
167
|
+
.split("/")
|
|
168
|
+
.pop()?.toLowerCase()
|
|
169
|
+
.replace(/(\.node)?\.test\.[^.]+$/i, "")
|
|
170
|
+
.replace(/\.spec\.[^.]+$/i, "")
|
|
171
|
+
.replace(/\.runtime\.[^.]+$/i, "")
|
|
172
|
+
.replace(/\.[^.]+$/i, "")
|
|
173
|
+
|| "";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function scoreRepoAdjacency(left, right) {
|
|
177
|
+
if (!left || !right || left.path === right.path) return -1;
|
|
178
|
+
const leftPath = String(left.path || "").replace(/\\/g, "/");
|
|
179
|
+
const rightPath = String(right.path || "").replace(/\\/g, "/");
|
|
180
|
+
const leftDir = leftPath.includes("/") ? leftPath.split("/").slice(0, -1).join("/") : "";
|
|
181
|
+
const rightDir = rightPath.includes("/") ? rightPath.split("/").slice(0, -1).join("/") : "";
|
|
182
|
+
const leftOwner = inferRepoOwner(leftPath);
|
|
183
|
+
const rightOwner = inferRepoOwner(rightPath);
|
|
184
|
+
const leftStem = normalizeAdjacencyStem(leftPath);
|
|
185
|
+
const rightStem = normalizeAdjacencyStem(rightPath);
|
|
186
|
+
let score = 0;
|
|
187
|
+
if (leftDir && leftDir === rightDir) score += 60;
|
|
188
|
+
if (leftOwner && leftOwner === rightOwner) score += 25;
|
|
189
|
+
if (leftStem && leftStem === rightStem) score += 40;
|
|
190
|
+
if ((leftOwner === "tests" || rightOwner === "tests") && leftStem && leftStem === rightStem) score += 30;
|
|
191
|
+
return score;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function hasRepoMapContext(value = "") {
|
|
195
|
+
const text = String(value || "");
|
|
196
|
+
return text.includes("## Repo Topology") || text.includes("## Repo Map");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function formatRepoTopology(repoMap, opts = {}) {
|
|
200
|
+
const normalized = normalizeRepoMap(repoMap, opts);
|
|
201
|
+
if (!normalized) return "";
|
|
202
|
+
const files = Array.isArray(normalized.files) ? normalized.files : [];
|
|
203
|
+
const lines = [String(opts.title || "## Repo Topology")];
|
|
204
|
+
const summaryLimit = toPositiveInt(opts.repoMapSummaryLimit || opts.summaryLimit, DEFAULT_TOPOLOGY_SUMMARY_LIMIT);
|
|
205
|
+
if (normalized.root) lines.push(`- Root: ${normalized.root}`);
|
|
206
|
+
if (files.length === 0) return lines.join("\n");
|
|
207
|
+
|
|
208
|
+
const areaCounts = new Map();
|
|
209
|
+
for (const file of files) {
|
|
210
|
+
const owner = inferRepoOwner(file.path);
|
|
211
|
+
areaCounts.set(owner, (areaCounts.get(owner) || 0) + 1);
|
|
212
|
+
}
|
|
213
|
+
const areas = [...areaCounts.entries()]
|
|
214
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
215
|
+
.map(([owner, count]) => `${owner} (${count})`);
|
|
216
|
+
if (areas.length) lines.push(`- Areas: ${areas.join(", ")}`);
|
|
217
|
+
|
|
218
|
+
const adjacencyLimit = toPositiveInt(opts.repoMapAdjacencyLimit || opts.adjacencyLimit, 2);
|
|
219
|
+
for (const file of files) {
|
|
220
|
+
const owner = inferRepoOwner(file.path);
|
|
221
|
+
const summary = truncateText(file.summary, summaryLimit);
|
|
222
|
+
const adjacent = files
|
|
223
|
+
.map((candidate) => ({ path: candidate.path, score: scoreRepoAdjacency(file, candidate) }))
|
|
224
|
+
.filter((candidate) => candidate.score > 0)
|
|
225
|
+
.sort((left, right) => right.score - left.score || left.path.localeCompare(right.path))
|
|
226
|
+
.slice(0, adjacencyLimit)
|
|
227
|
+
.map((candidate) => candidate.path);
|
|
228
|
+
const parts = [file.path, `owner: ${owner}`];
|
|
229
|
+
if (summary) parts.push(summary);
|
|
230
|
+
if (adjacent.length) parts.push(`adjacent: ${adjacent.join(", ")}`);
|
|
231
|
+
lines.push(`- ${parts.join(" — ")}`);
|
|
232
|
+
}
|
|
233
|
+
return lines.join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function buildRepoTopologyContext(options = {}) {
|
|
237
|
+
return formatRepoTopology(buildRepoMap(options), options);
|
|
238
|
+
}
|
|
239
|
+
|
|
135
240
|
function resolveRootDir(options = {}) {
|
|
136
241
|
const explicit = String(options.rootDir || options.repoRoot || options.cwd || "").trim();
|
|
137
242
|
if (explicit) return explicit.replace(/\\/g, "/");
|
|
@@ -378,7 +483,9 @@ export function inferExecutionRole(options = {}, effectiveMode = "agent") {
|
|
|
378
483
|
|
|
379
484
|
export function buildArchitectEditorFrame(options = {}, effectiveMode = "agent") {
|
|
380
485
|
const executionRole = inferExecutionRole(options, effectiveMode);
|
|
381
|
-
const repoMapBlock =
|
|
486
|
+
const repoMapBlock = options.includeRepoMap === false
|
|
487
|
+
? ""
|
|
488
|
+
: formatRepoTopology(buildRepoMap(options), options);
|
|
382
489
|
const architectPlan = String(options.architectPlan || options.planSummary || "").trim();
|
|
383
490
|
const lines = ["## Architect/Editor Execution"];
|
|
384
491
|
|
|
@@ -386,14 +493,14 @@ export function buildArchitectEditorFrame(options = {}, effectiveMode = "agent")
|
|
|
386
493
|
lines.push(
|
|
387
494
|
"You are the architect phase.",
|
|
388
495
|
"Do not implement code changes in this phase.",
|
|
389
|
-
"Use the repo
|
|
496
|
+
"Use the repo topology to produce a compact structural plan that an editor can execute and validate.",
|
|
390
497
|
"Editor handoff: include ordered implementation steps, touched files, risks, and validation guidance.",
|
|
391
498
|
);
|
|
392
499
|
} else if (executionRole === "editor") {
|
|
393
500
|
lines.push(
|
|
394
501
|
"You are the editor phase.",
|
|
395
502
|
"Implement the approved plan with focused edits and verification.",
|
|
396
|
-
"Prefer the supplied repo
|
|
503
|
+
"Prefer the supplied repo topology over broad rediscovery unless validation reveals drift.",
|
|
397
504
|
);
|
|
398
505
|
if (architectPlan) {
|
|
399
506
|
lines.push("", "## Architect Plan", architectPlan);
|
|
@@ -409,3 +516,7 @@ export function buildArchitectEditorFrame(options = {}, effectiveMode = "agent")
|
|
|
409
516
|
return lines.join("\n");
|
|
410
517
|
}
|
|
411
518
|
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.4",
|
|
4
4
|
"description": "Bosun Autonomous Engineering — manages AI agent executors with failover, extremely powerful workflow builder, and a massive amount of included default workflow templates for autonomous engineering, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -383,7 +383,16 @@
|
|
|
383
383
|
"preact": "10.25.4",
|
|
384
384
|
"qrcode-terminal": "^0.12.0",
|
|
385
385
|
"vibe-kanban": "latest",
|
|
386
|
-
"ws": "^8.19.0"
|
|
386
|
+
"ws": "^8.19.0",
|
|
387
|
+
"@opentelemetry/api": "^1.9.0",
|
|
388
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.206.0",
|
|
389
|
+
"@opentelemetry/resources": "^2.1.0",
|
|
390
|
+
"@opentelemetry/sdk-metrics": "^2.1.0",
|
|
391
|
+
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
|
392
|
+
"@opentelemetry/semantic-conventions": "^1.37.0",
|
|
393
|
+
"figures": "^6.1.0",
|
|
394
|
+
"react": "^18.3.1",
|
|
395
|
+
"react-dom": "^18.3.1"
|
|
387
396
|
},
|
|
388
397
|
"devDependencies": {
|
|
389
398
|
"@emotion/react": "^11.14.0",
|
|
@@ -391,8 +400,6 @@
|
|
|
391
400
|
"@mui/material": "^5.18.0",
|
|
392
401
|
"@playwright/test": "^1.58.2",
|
|
393
402
|
"playwright": "^1.58.2",
|
|
394
|
-
"react": "^18.3.1",
|
|
395
|
-
"react-dom": "^18.3.1",
|
|
396
403
|
"vitest": "^4.0.18"
|
|
397
404
|
},
|
|
398
405
|
"engines": {
|
package/server/ui-server.mjs
CHANGED
|
@@ -11988,6 +11988,9 @@ async function listReplayableAgentRuns(options = {}) {
|
|
|
11988
11988
|
async function readReplayableAgentRun(attemptId) {
|
|
11989
11989
|
const normalizedAttemptId = String(attemptId || "").trim();
|
|
11990
11990
|
if (!normalizedAttemptId) return null;
|
|
11991
|
+
// Ensure attemptId cannot perform path traversal or escape the intended directory.
|
|
11992
|
+
// Only allow simple identifiers composed of letters, digits, underscore, and dash.
|
|
11993
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(normalizedAttemptId)) return null;
|
|
11991
11994
|
const filePath = resolve(resolveAgentWorkLogDir(), "agent-sessions", `${normalizedAttemptId}.jsonl`);
|
|
11992
11995
|
if (!existsSync(filePath)) return null;
|
|
11993
11996
|
const entries = await readJsonlTail(filePath, 20000, 25000000).catch(() => []);
|
package/task/task-archiver.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { existsSync } from "node:fs";
|
|
|
31
31
|
import { resolve, dirname } from "node:path";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
import { randomBytes } from "node:crypto";
|
|
34
|
+
import { normalizeTaskStorageRecord } from "./task-store.mjs";
|
|
34
35
|
|
|
35
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
36
37
|
|
|
@@ -173,7 +174,7 @@ export async function archiveTaskToFile(
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
const archiveEntry = {
|
|
176
|
-
task,
|
|
177
|
+
task: normalizeTaskStorageRecord(task),
|
|
177
178
|
attempt: attemptData,
|
|
178
179
|
archived_at: new Date().toISOString(),
|
|
179
180
|
archiver_version: 3,
|
|
@@ -470,7 +471,7 @@ export async function migrateLegacyArchives(archiveDir = ARCHIVE_DIR) {
|
|
|
470
471
|
for (const legacyFile of fileNames) {
|
|
471
472
|
try {
|
|
472
473
|
const raw = await readFile(resolve(archiveDir, legacyFile), "utf8");
|
|
473
|
-
const entry = JSON.parse(raw);
|
|
474
|
+
const entry = normalizeArchiveEntry(JSON.parse(raw));
|
|
474
475
|
const taskId = entry?.task?.id;
|
|
475
476
|
|
|
476
477
|
if (taskId && existingIds.has(taskId)) {
|
|
@@ -663,8 +664,9 @@ export async function loadArchivedTasks(options = {}) {
|
|
|
663
664
|
// Daily grouped file: array of entries
|
|
664
665
|
if (Array.isArray(data)) {
|
|
665
666
|
for (const entry of data) {
|
|
666
|
-
|
|
667
|
-
|
|
667
|
+
const normalizedEntry = normalizeArchiveEntry(entry);
|
|
668
|
+
if (matchesFilters(normalizedEntry, since, until, statusFilter)) {
|
|
669
|
+
archivedTasks.push(normalizedEntry);
|
|
668
670
|
}
|
|
669
671
|
}
|
|
670
672
|
continue;
|
|
@@ -672,8 +674,9 @@ export async function loadArchivedTasks(options = {}) {
|
|
|
672
674
|
|
|
673
675
|
// Legacy single-task file: object with { task, archived_at, ... }
|
|
674
676
|
if (data && typeof data === "object" && data.task) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
+
const normalizedEntry = normalizeArchiveEntry(data);
|
|
678
|
+
if (matchesFilters(normalizedEntry, since, until, statusFilter)) {
|
|
679
|
+
archivedTasks.push(normalizedEntry);
|
|
677
680
|
}
|
|
678
681
|
}
|
|
679
682
|
} catch {
|
|
@@ -701,6 +704,15 @@ function matchesFilters(entry, since, until, statusFilter) {
|
|
|
701
704
|
return true;
|
|
702
705
|
}
|
|
703
706
|
|
|
707
|
+
function normalizeArchiveEntry(entry) {
|
|
708
|
+
if (!entry || typeof entry !== "object") return null;
|
|
709
|
+
const normalized = { ...entry };
|
|
710
|
+
if (normalized.task && typeof normalized.task === "object") {
|
|
711
|
+
normalized.task = normalizeTaskStorageRecord(normalized.task);
|
|
712
|
+
}
|
|
713
|
+
return normalized;
|
|
714
|
+
}
|
|
715
|
+
|
|
704
716
|
/**
|
|
705
717
|
* Generate sprint review report from archived tasks.
|
|
706
718
|
* @param {object[]} archivedTasks
|
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
renameSync,
|
|
17
17
|
} from "node:fs";
|
|
18
18
|
import { resolveRepoRoot } from "../config/repo-root.mjs";
|
|
19
|
+
import {
|
|
20
|
+
getTaskAttachmentCanonicalKey,
|
|
21
|
+
normalizeTaskAttachmentRecord,
|
|
22
|
+
normalizeTaskAttachments,
|
|
23
|
+
} from "./task-store.mjs";
|
|
19
24
|
|
|
20
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
26
|
const TAG = "[task-attachments]";
|
|
@@ -70,17 +75,12 @@ function taskKey(taskId, backend) {
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
function attachmentKey(att) {
|
|
73
|
-
|
|
74
|
-
if (att.url) return `url:${att.url}`;
|
|
75
|
-
if (att.filePath) return `file:${att.filePath}`;
|
|
76
|
-
if (att.path) return `path:${att.path}`;
|
|
77
|
-
if (att.id) return `id:${att.id}`;
|
|
78
|
-
return `raw:${JSON.stringify(att)}`;
|
|
78
|
+
return getTaskAttachmentCanonicalKey(att);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function normalizeAttachment(att, backend) {
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
const normalized = normalizeTaskAttachmentRecord(att);
|
|
83
|
+
if (!normalized || typeof normalized !== "object") return null;
|
|
84
84
|
if (!normalized.id) normalized.id = randomUUID();
|
|
85
85
|
if (!normalized.createdAt) normalized.createdAt = nowIso();
|
|
86
86
|
if (!normalized.source) normalized.source = "upload";
|
|
@@ -133,6 +133,11 @@ export function loadStore() {
|
|
|
133
133
|
if (!_store || typeof _store !== "object") {
|
|
134
134
|
_store = defaultStore();
|
|
135
135
|
}
|
|
136
|
+
const tasks = _store.tasks && typeof _store.tasks === "object" ? _store.tasks : {};
|
|
137
|
+
for (const entry of Object.values(tasks)) {
|
|
138
|
+
if (!entry || typeof entry !== "object") continue;
|
|
139
|
+
entry.attachments = normalizeTaskAttachments(entry.attachments);
|
|
140
|
+
}
|
|
136
141
|
} catch (err) {
|
|
137
142
|
console.warn(`${TAG} failed to read store: ${err.message || err}`);
|
|
138
143
|
_store = defaultStore();
|
|
@@ -162,7 +167,7 @@ export function listTaskAttachments(taskId, backend = "internal") {
|
|
|
162
167
|
if (!key) return [];
|
|
163
168
|
const entry = store.tasks?.[key];
|
|
164
169
|
const attachments = Array.isArray(entry?.attachments) ? entry.attachments : [];
|
|
165
|
-
return attachments
|
|
170
|
+
return normalizeTaskAttachments(attachments);
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
export function addTaskAttachment(taskId, backend, attachment) {
|
|
@@ -184,4 +189,3 @@ export function addTaskAttachment(taskId, backend, attachment) {
|
|
|
184
189
|
saveStore();
|
|
185
190
|
return normalized;
|
|
186
191
|
}
|
|
187
|
-
|
package/task/task-cli.mjs
CHANGED
|
@@ -306,8 +306,7 @@ function isDebugModeEnabled(args = []) {
|
|
|
306
306
|
* Create a new task. Accepts a plain object with task fields.
|
|
307
307
|
* Returns the created task or throws on error.
|
|
308
308
|
*/
|
|
309
|
-
|
|
310
|
-
const store = await initStore();
|
|
309
|
+
function buildTaskInput(data, store) {
|
|
311
310
|
const normalizeKey = store.normalizeWorkspaceStorageKey || normalizeStoreScopeKey;
|
|
312
311
|
const normalizeKeys =
|
|
313
312
|
store.normalizeWorkspaceStorageKeys
|
|
@@ -388,6 +387,22 @@ export async function taskCreate(data) {
|
|
|
388
387
|
taskData.description = parts.join("\n");
|
|
389
388
|
}
|
|
390
389
|
|
|
390
|
+
return taskData;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function buildImportedTaskInput(data, store) {
|
|
394
|
+
const taskData = buildTaskInput(data, store);
|
|
395
|
+
return {
|
|
396
|
+
...data,
|
|
397
|
+
...taskData,
|
|
398
|
+
meta: taskData.meta,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function taskCreate(data) {
|
|
403
|
+
const store = await initStore();
|
|
404
|
+
const taskData = buildTaskInput(data, store);
|
|
405
|
+
|
|
391
406
|
const result = store.addTask(taskData);
|
|
392
407
|
if (!result) {
|
|
393
408
|
throw new Error(`Failed to create task — addTask returned null`);
|
|
@@ -1099,10 +1114,16 @@ export async function taskImport(source) {
|
|
|
1099
1114
|
let created = 0;
|
|
1100
1115
|
let failed = 0;
|
|
1101
1116
|
const errors = [];
|
|
1117
|
+
const store = await initStore();
|
|
1102
1118
|
|
|
1103
1119
|
for (const t of tasks) {
|
|
1104
1120
|
try {
|
|
1105
|
-
|
|
1121
|
+
const importedTask = buildImportedTaskInput(t, store);
|
|
1122
|
+
const result = store.addTask(importedTask);
|
|
1123
|
+
if (!result) {
|
|
1124
|
+
throw new Error("Failed to create task — addTask returned null");
|
|
1125
|
+
}
|
|
1126
|
+
await flushStoreWrites(store);
|
|
1106
1127
|
created++;
|
|
1107
1128
|
} catch (err) {
|
|
1108
1129
|
failed++;
|
|
@@ -1822,4 +1843,3 @@ if (process.argv[1] && resolve(process.argv[1]) === __filename) {
|
|
|
1822
1843
|
process.exit(1);
|
|
1823
1844
|
});
|
|
1824
1845
|
}
|
|
1825
|
-
|
package/task/task-executor.mjs
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
} from "./task-store.mjs";
|
|
67
67
|
import { createErrorDetector } from "../infra/error-detector.mjs";
|
|
68
68
|
import { getSessionTracker } from "../infra/session-tracker.mjs";
|
|
69
|
+
import { addSpanLink } from "../infra/tracing.mjs";
|
|
69
70
|
import {
|
|
70
71
|
getCompactDiffSummary,
|
|
71
72
|
getRecentCommits,
|
|
@@ -4365,6 +4366,24 @@ class TaskExecutor {
|
|
|
4365
4366
|
};
|
|
4366
4367
|
}
|
|
4367
4368
|
|
|
4369
|
+
getTuiStats() {
|
|
4370
|
+
const tokenTotals = Array.from(attemptTelemetry.values()).reduce((acc, telemetry) => {
|
|
4371
|
+
acc.tokensIn += Math.max(0, Number(telemetry?.prompt_tokens || 0) || 0);
|
|
4372
|
+
acc.tokensOut += Math.max(0, Number(telemetry?.completion_tokens || 0) || 0);
|
|
4373
|
+
acc.tokensTotal += Math.max(0, Number(telemetry?.total_tokens || 0) || 0);
|
|
4374
|
+
return acc;
|
|
4375
|
+
}, { tokensIn: 0, tokensOut: 0, tokensTotal: 0 });
|
|
4376
|
+
|
|
4377
|
+
return {
|
|
4378
|
+
activeAgents: this._activeSlots.size,
|
|
4379
|
+
maxAgents: this.maxParallel,
|
|
4380
|
+
tokensIn: tokenTotals.tokensIn,
|
|
4381
|
+
tokensOut: tokenTotals.tokensOut,
|
|
4382
|
+
tokensTotal: tokenTotals.tokensTotal || (tokenTotals.tokensIn + tokenTotals.tokensOut),
|
|
4383
|
+
rateLimits: {},
|
|
4384
|
+
};
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4368
4387
|
getBacklogReplenishmentConfig() {
|
|
4369
4388
|
return {
|
|
4370
4389
|
...this._backlogReplenishment,
|