bosun 0.41.0 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
15
15
|
import { resolve, dirname } from "node:path";
|
|
16
|
+
import { randomBytes } from "node:crypto";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
17
18
|
import { buildSessionInsights } from "../lib/session-insights.mjs";
|
|
18
19
|
import { isTestRuntime } from "./test-runtime.mjs";
|
|
20
|
+
import { addCompletedSession } from "./runtime-accumulator.mjs";
|
|
19
21
|
|
|
20
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
23
|
const SESSIONS_DIR = resolve(__dirname, "..", "logs", "sessions");
|
|
@@ -37,6 +39,15 @@ const MAX_MESSAGE_CHARS = 100_000;
|
|
|
37
39
|
|
|
38
40
|
/** Maximum total sessions to keep in memory. */
|
|
39
41
|
const MAX_SESSIONS = 100;
|
|
42
|
+
const TERMINAL_SESSION_STATUSES = new Set(["completed", "failed", "idle", "archived"]);
|
|
43
|
+
|
|
44
|
+
function isTerminalSessionStatus(status) {
|
|
45
|
+
return TERMINAL_SESSION_STATUSES.has(String(status || "").trim().toLowerCase());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function randomToken(length = 8) {
|
|
49
|
+
return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
|
|
50
|
+
}
|
|
40
51
|
|
|
41
52
|
function resolveSessionTrackerPersistDir(options = {}) {
|
|
42
53
|
if (options.persistDir !== undefined) {
|
|
@@ -182,6 +193,7 @@ export class SessionTracker {
|
|
|
182
193
|
taskId,
|
|
183
194
|
taskTitle,
|
|
184
195
|
id: taskId,
|
|
196
|
+
sessionKey: `${taskId}:${Date.now()}:${randomToken(8)}`,
|
|
185
197
|
type: "task",
|
|
186
198
|
maxMessages: this.#maxMessages,
|
|
187
199
|
startedAt: Date.now(),
|
|
@@ -192,6 +204,7 @@ export class SessionTracker {
|
|
|
192
204
|
totalEvents: 0,
|
|
193
205
|
turnCount: 0,
|
|
194
206
|
status: "active",
|
|
207
|
+
accumulatedAt: null,
|
|
195
208
|
lastActivityAt: Date.now(),
|
|
196
209
|
metadata: {},
|
|
197
210
|
insights: buildSessionInsights({ messages: [] }),
|
|
@@ -255,7 +268,7 @@ export class SessionTracker {
|
|
|
255
268
|
// Direct message format (role/content)
|
|
256
269
|
if (event && event.role && event.content !== undefined) {
|
|
257
270
|
const msg = {
|
|
258
|
-
|
|
271
|
+
id: event.id || `msg-${Date.now()}-${randomToken(6)}`,
|
|
259
272
|
type: event.type || undefined,
|
|
260
273
|
role: event.role,
|
|
261
274
|
content: String(event.content).slice(0, MAX_MESSAGE_CHARS),
|
|
@@ -312,25 +325,8 @@ export class SessionTracker {
|
|
|
312
325
|
session.endedAt = Date.now();
|
|
313
326
|
session.status = status;
|
|
314
327
|
this.#refreshDerivedState(session);
|
|
328
|
+
this.#accumulateCompletedSession(session, taskId);
|
|
315
329
|
this.#markDirty(taskId);
|
|
316
|
-
|
|
317
|
-
// Lazy import to avoid top-level await issues
|
|
318
|
-
import("./runtime-accumulator.mjs")
|
|
319
|
-
.then((module) => {
|
|
320
|
-
if (module.addCompletedSession) {
|
|
321
|
-
module.addCompletedSession({
|
|
322
|
-
id: taskId,
|
|
323
|
-
taskId: taskId,
|
|
324
|
-
taskTitle: session.taskTitle,
|
|
325
|
-
executor: session.executor,
|
|
326
|
-
model: session.model,
|
|
327
|
-
startedAt: session.startedAt,
|
|
328
|
-
endedAt: session.endedAt,
|
|
329
|
-
status: status,
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
})
|
|
333
|
-
.catch(() => {});
|
|
334
330
|
}
|
|
335
331
|
|
|
336
332
|
/**
|
|
@@ -494,6 +490,8 @@ export class SessionTracker {
|
|
|
494
490
|
* @param {string} taskId
|
|
495
491
|
*/
|
|
496
492
|
removeSession(taskId) {
|
|
493
|
+
const session = this.#sessions.get(taskId);
|
|
494
|
+
this.#accumulateCompletedSession(session, taskId);
|
|
497
495
|
this.#sessions.delete(taskId);
|
|
498
496
|
this.#dirty.delete(taskId);
|
|
499
497
|
// Remove persisted session file if it exists
|
|
@@ -525,7 +523,7 @@ export class SessionTracker {
|
|
|
525
523
|
* Create a new session with explicit options.
|
|
526
524
|
* @param {{ id: string, type?: string, taskId?: string, metadata?: Object }} opts
|
|
527
525
|
*/
|
|
528
|
-
createSession({ id, type = "manual", taskId, metadata = {}, maxMessages }) {
|
|
526
|
+
createSession({ id, type = "manual", taskId, metadata = {}, maxMessages, sessionKey }) {
|
|
529
527
|
// Evict oldest non-active sessions if at capacity
|
|
530
528
|
if (this.#sessions.size >= MAX_SESSIONS && !this.#sessions.has(id)) {
|
|
531
529
|
this.#evictOldest();
|
|
@@ -542,6 +540,9 @@ export class SessionTracker {
|
|
|
542
540
|
id,
|
|
543
541
|
taskId: taskId || id,
|
|
544
542
|
taskTitle: metadata.title || id,
|
|
543
|
+
sessionKey:
|
|
544
|
+
String(sessionKey || "").trim() ||
|
|
545
|
+
`${taskId || id}:${Date.now()}:${randomToken(8)}`,
|
|
545
546
|
type,
|
|
546
547
|
status: "active",
|
|
547
548
|
createdAt: now,
|
|
@@ -552,6 +553,7 @@ export class SessionTracker {
|
|
|
552
553
|
totalEvents: 0,
|
|
553
554
|
turnCount: 0,
|
|
554
555
|
lastActivityAt: Date.now(),
|
|
556
|
+
accumulatedAt: null,
|
|
555
557
|
metadata,
|
|
556
558
|
maxMessages: resolvedMax,
|
|
557
559
|
insights: buildSessionInsights({ messages: [] }),
|
|
@@ -620,10 +622,11 @@ export class SessionTracker {
|
|
|
620
622
|
const session = this.#sessions.get(sessionId);
|
|
621
623
|
if (!session) return;
|
|
622
624
|
session.status = status;
|
|
623
|
-
if (status === "completed" || status === "archived") {
|
|
625
|
+
if (status === "completed" || status === "archived" || status === "failed" || status === "idle") {
|
|
624
626
|
session.endedAt = Date.now();
|
|
625
627
|
}
|
|
626
628
|
this.#refreshDerivedState(session);
|
|
629
|
+
this.#accumulateCompletedSession(session, sessionId);
|
|
627
630
|
this.#markDirty(sessionId);
|
|
628
631
|
}
|
|
629
632
|
|
|
@@ -696,7 +699,7 @@ export class SessionTracker {
|
|
|
696
699
|
return { ok: false, error: "Only user messages can be edited" };
|
|
697
700
|
}
|
|
698
701
|
|
|
699
|
-
target.id = target.id || `msg-${Date.now()}-${
|
|
702
|
+
target.id = target.id || `msg-${Date.now()}-${randomToken(6)}`;
|
|
700
703
|
target.content = nextContent.slice(0, MAX_MESSAGE_CHARS);
|
|
701
704
|
target.edited = true;
|
|
702
705
|
target.editedAt = new Date().toISOString();
|
|
@@ -816,6 +819,7 @@ export class SessionTracker {
|
|
|
816
819
|
const type = event._sessionType || "task";
|
|
817
820
|
this.createSession({
|
|
818
821
|
id: taskId,
|
|
822
|
+
sessionKey: `${taskId}:${Date.now()}:${randomToken(8)}`,
|
|
819
823
|
type,
|
|
820
824
|
taskId,
|
|
821
825
|
metadata: { autoCreated: true },
|
|
@@ -837,7 +841,8 @@ export class SessionTracker {
|
|
|
837
841
|
return (a[1].lastActivityAt || a[1].startedAt) - (b[1].lastActivityAt || b[1].startedAt);
|
|
838
842
|
});
|
|
839
843
|
const toEvict = sorted.slice(0, evictCount);
|
|
840
|
-
for (const [id] of toEvict) {
|
|
844
|
+
for (const [id, session] of toEvict) {
|
|
845
|
+
this.#accumulateCompletedSession(session, id);
|
|
841
846
|
this.#sessions.delete(id);
|
|
842
847
|
}
|
|
843
848
|
}
|
|
@@ -857,6 +862,7 @@ export class SessionTracker {
|
|
|
857
862
|
session.status = "completed";
|
|
858
863
|
session.endedAt = now;
|
|
859
864
|
this.#refreshDerivedState(session);
|
|
865
|
+
this.#accumulateCompletedSession(session, id);
|
|
860
866
|
this.#markDirty(id);
|
|
861
867
|
reaped++;
|
|
862
868
|
}
|
|
@@ -880,6 +886,43 @@ export class SessionTracker {
|
|
|
880
886
|
}
|
|
881
887
|
}
|
|
882
888
|
|
|
889
|
+
#accumulateCompletedSession(session, fallbackTaskId = "") {
|
|
890
|
+
if (!session || session.accumulatedAt) return false;
|
|
891
|
+
if (!isTerminalSessionStatus(session.status)) return false;
|
|
892
|
+
const taskId = String(session.taskId || session.id || fallbackTaskId || "").trim();
|
|
893
|
+
if (!taskId) return false;
|
|
894
|
+
|
|
895
|
+
const now = Date.now();
|
|
896
|
+
const endedAt = Number.isFinite(Number(session.endedAt)) && Number(session.endedAt) > 0
|
|
897
|
+
? Number(session.endedAt)
|
|
898
|
+
: now;
|
|
899
|
+
const startedAt = Number.isFinite(Number(session.startedAt))
|
|
900
|
+
? Number(session.startedAt)
|
|
901
|
+
: endedAt;
|
|
902
|
+
const tokenUsage = session.insights?.tokenUsage || null;
|
|
903
|
+
|
|
904
|
+
addCompletedSession({
|
|
905
|
+
id: session.id || taskId,
|
|
906
|
+
sessionId: session.id || taskId,
|
|
907
|
+
sessionKey: session.sessionKey || `${taskId}:${startedAt}:${endedAt}`,
|
|
908
|
+
taskId,
|
|
909
|
+
taskTitle: session.taskTitle,
|
|
910
|
+
executor: session.executor,
|
|
911
|
+
model: session.model,
|
|
912
|
+
startedAt,
|
|
913
|
+
endedAt,
|
|
914
|
+
durationMs: Math.max(0, endedAt - startedAt),
|
|
915
|
+
tokenCount: tokenUsage?.totalTokens || 0,
|
|
916
|
+
inputTokens: tokenUsage?.inputTokens || 0,
|
|
917
|
+
outputTokens: tokenUsage?.outputTokens || 0,
|
|
918
|
+
tokenUsage,
|
|
919
|
+
insights: session.insights || null,
|
|
920
|
+
status: String(session.status || "completed"),
|
|
921
|
+
});
|
|
922
|
+
session.accumulatedAt = new Date().toISOString();
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
|
|
883
926
|
#refreshDerivedState(session) {
|
|
884
927
|
if (!session) return;
|
|
885
928
|
try {
|
|
@@ -917,10 +960,14 @@ export class SessionTracker {
|
|
|
917
960
|
taskId: session.taskId,
|
|
918
961
|
title: session.taskTitle || session.title || null,
|
|
919
962
|
taskTitle: session.taskTitle || null,
|
|
963
|
+
sessionKey: session.sessionKey || null,
|
|
920
964
|
type: session.type || "task",
|
|
921
965
|
status: session.status,
|
|
922
966
|
createdAt: session.createdAt || new Date(session.startedAt).toISOString(),
|
|
923
967
|
lastActiveAt: session.lastActiveAt || new Date(session.lastActivityAt).toISOString(),
|
|
968
|
+
startedAt: session.startedAt || null,
|
|
969
|
+
endedAt: session.endedAt || null,
|
|
970
|
+
accumulatedAt: session.accumulatedAt || null,
|
|
924
971
|
turnCount: session.turnCount || 0,
|
|
925
972
|
messages: session.messages || [],
|
|
926
973
|
metadata: session.metadata || {},
|
|
@@ -987,20 +1034,30 @@ export class SessionTracker {
|
|
|
987
1034
|
this.#sessions.set(id, {
|
|
988
1035
|
id,
|
|
989
1036
|
taskId: data.taskId || id,
|
|
990
|
-
taskTitle: data.metadata?.title || id,
|
|
1037
|
+
taskTitle: data.taskTitle || data.title || data.metadata?.title || id,
|
|
1038
|
+
sessionKey:
|
|
1039
|
+
String(data.sessionKey || "").trim() ||
|
|
1040
|
+
`${data.taskId || id}:${data.startedAt || Date.now()}:${endedAt || data.startedAt || Date.now()}`,
|
|
991
1041
|
type: data.type || "task",
|
|
992
1042
|
status,
|
|
993
1043
|
createdAt: data.createdAt || new Date().toISOString(),
|
|
994
1044
|
lastActiveAt: data.lastActiveAt || new Date().toISOString(),
|
|
995
|
-
startedAt: data.createdAt ? new Date(data.createdAt).getTime() : Date.now(),
|
|
1045
|
+
startedAt: data.startedAt || (data.createdAt ? new Date(data.createdAt).getTime() : Date.now()),
|
|
996
1046
|
endedAt,
|
|
997
1047
|
messages: data.messages || [],
|
|
998
1048
|
totalEvents: (data.messages || []).length,
|
|
999
1049
|
turnCount: data.turnCount || 0,
|
|
1050
|
+
accumulatedAt: data.accumulatedAt || null,
|
|
1000
1051
|
lastActivityAt: lastActive || Date.now(),
|
|
1001
1052
|
metadata: data.metadata || {},
|
|
1002
1053
|
insights: data.insights || buildSessionInsights({ messages: data.messages || [] }),
|
|
1003
1054
|
});
|
|
1055
|
+
const restored = this.#sessions.get(id);
|
|
1056
|
+
if (restored && isTerminalSessionStatus(restored.status) && !restored.accumulatedAt) {
|
|
1057
|
+
if (this.#accumulateCompletedSession(restored, id)) {
|
|
1058
|
+
this.#markDirty(id);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1004
1061
|
}
|
|
1005
1062
|
} catch {
|
|
1006
1063
|
// Directory read failed — proceed without disk data
|
package/lib/codebase-audit.mjs
CHANGED
|
@@ -23,8 +23,6 @@ const SOURCE_TYPES = new Map([
|
|
|
23
23
|
|
|
24
24
|
const SUMMARY_MARKERS = ["CLAUDE:SUMMARY", "BOSUN:SUMMARY"];
|
|
25
25
|
const WARN_MARKERS = ["CLAUDE:WARN", "BOSUN:WARN"];
|
|
26
|
-
const SUMMARY_LINE_RE = /^\s*(?:\/\/|#)\s*(?:CLAUDE|BOSUN):SUMMARY\b/i;
|
|
27
|
-
const WARN_LINE_RE = /^\s*(?:\/\/|#)\s*(?:CLAUDE|BOSUN):WARN\b/i;
|
|
28
26
|
const GENERATED_PATTERNS = [
|
|
29
27
|
/^\.git(?:\/|$)/,
|
|
30
28
|
/^node_modules(?:\/|$)/,
|
|
@@ -151,6 +149,101 @@ function getSourceType(pathValue) {
|
|
|
151
149
|
return SOURCE_TYPES.get(extname(pathValue).toLowerCase()) || null;
|
|
152
150
|
}
|
|
153
151
|
|
|
152
|
+
function parseImportSpecifiers(content, language) {
|
|
153
|
+
if (language !== "javascript" && language !== "typescript") return [];
|
|
154
|
+
const specs = new Set();
|
|
155
|
+
const importRegex = /import\s+(?:[^"'`]+?\s+from\s+)?["'`]([^"'`]+)["'`]/g;
|
|
156
|
+
const dynamicImportRegex = /import\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
|
|
157
|
+
const requireRegex = /require\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
|
|
158
|
+
for (const pattern of [importRegex, dynamicImportRegex, requireRegex]) {
|
|
159
|
+
let match;
|
|
160
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
161
|
+
if (match[1]) specs.add(match[1]);
|
|
162
|
+
}
|
|
163
|
+
pattern.lastIndex = 0;
|
|
164
|
+
}
|
|
165
|
+
return [...specs];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveLocalImportPath(repoRoot, fromFile, specifier) {
|
|
169
|
+
if (!specifier || !specifier.startsWith(".")) return "";
|
|
170
|
+
const fromDir = dirname(fromFile);
|
|
171
|
+
const absoluteBase = resolve(fromDir, specifier);
|
|
172
|
+
const candidates = [
|
|
173
|
+
absoluteBase,
|
|
174
|
+
`${absoluteBase}.js`,
|
|
175
|
+
`${absoluteBase}.mjs`,
|
|
176
|
+
`${absoluteBase}.cjs`,
|
|
177
|
+
`${absoluteBase}.ts`,
|
|
178
|
+
`${absoluteBase}.tsx`,
|
|
179
|
+
`${absoluteBase}.jsx`,
|
|
180
|
+
resolve(absoluteBase, "index.js"),
|
|
181
|
+
resolve(absoluteBase, "index.mjs"),
|
|
182
|
+
resolve(absoluteBase, "index.cjs"),
|
|
183
|
+
resolve(absoluteBase, "index.ts"),
|
|
184
|
+
resolve(absoluteBase, "index.tsx"),
|
|
185
|
+
resolve(absoluteBase, "index.jsx"),
|
|
186
|
+
];
|
|
187
|
+
for (const candidate of candidates) {
|
|
188
|
+
const info = safeStat(candidate);
|
|
189
|
+
if (info?.isFile()) return toPosix(relative(repoRoot, candidate));
|
|
190
|
+
}
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function findCycleMembers(edgesByFile) {
|
|
195
|
+
const states = new Map();
|
|
196
|
+
const stack = [];
|
|
197
|
+
const inCycles = new Set();
|
|
198
|
+
|
|
199
|
+
function visit(node) {
|
|
200
|
+
const state = states.get(node) || 0;
|
|
201
|
+
if (state === 2) return;
|
|
202
|
+
if (state === 1) {
|
|
203
|
+
const cycleStart = stack.lastIndexOf(node);
|
|
204
|
+
if (cycleStart >= 0) {
|
|
205
|
+
for (let index = cycleStart; index < stack.length; index += 1) inCycles.add(stack[index]);
|
|
206
|
+
inCycles.add(node);
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
states.set(node, 1);
|
|
211
|
+
stack.push(node);
|
|
212
|
+
for (const dep of edgesByFile.get(node) || []) visit(dep);
|
|
213
|
+
stack.pop();
|
|
214
|
+
states.set(node, 2);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const node of edgesByFile.keys()) visit(node);
|
|
218
|
+
return inCycles;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function appendCircularDependencyWarnings(files, repoRoot, warningKinds) {
|
|
222
|
+
const sourceSet = new Set(files.map((file) => file.path));
|
|
223
|
+
const edgesByFile = new Map();
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
if (file.language !== "javascript" && file.language !== "typescript") continue;
|
|
226
|
+
const deps = [];
|
|
227
|
+
for (const specifier of file.importSpecifiers || []) {
|
|
228
|
+
const resolved = resolveLocalImportPath(repoRoot, file.absolutePath, specifier);
|
|
229
|
+
if (resolved && sourceSet.has(resolved)) deps.push(resolved);
|
|
230
|
+
}
|
|
231
|
+
edgesByFile.set(file.path, deps);
|
|
232
|
+
}
|
|
233
|
+
const cycleMembers = findCycleMembers(edgesByFile);
|
|
234
|
+
for (const file of files) {
|
|
235
|
+
if (!cycleMembers.has(file.path)) continue;
|
|
236
|
+
if (file.warnings.some((warning) => warning.kind === "circular-deps")) continue;
|
|
237
|
+
file.warnings.push({
|
|
238
|
+
kind: "circular-deps",
|
|
239
|
+
text: "Module participates in circular dependency chains; avoid reordering imports or eager top-level side effects.",
|
|
240
|
+
functionName: "__module__",
|
|
241
|
+
lineIndex: file.firstFunctionLine,
|
|
242
|
+
});
|
|
243
|
+
warningKinds["circular-deps"] = (warningKinds["circular-deps"] || 0) + 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
154
247
|
function detectCategory(relPath) {
|
|
155
248
|
if (/(^|\/)(tests?|__tests__|fixtures?|sandbox)(\/|$)|\.(test|spec)\./i.test(relPath)) return "test";
|
|
156
249
|
if (/(^|\/)(config|configs)(\/|$)|(^|\/)(AGENTS|CLAUDE)\.md$/i.test(relPath)) return "config";
|
|
@@ -159,15 +252,20 @@ function detectCategory(relPath) {
|
|
|
159
252
|
return "core";
|
|
160
253
|
}
|
|
161
254
|
|
|
162
|
-
function
|
|
163
|
-
const
|
|
164
|
-
|
|
255
|
+
function isAnnotationLine(line, markers) {
|
|
256
|
+
const trimmed = String(line || "").trim();
|
|
257
|
+
if (!trimmed.startsWith("//") && !trimmed.startsWith("#")) return false;
|
|
258
|
+
return markers.some((marker) => new RegExp(`\\b${marker}\\b`).test(trimmed));
|
|
165
259
|
}
|
|
166
260
|
|
|
167
|
-
function extractAnnotationLines(content) {
|
|
261
|
+
function extractAnnotationLines(content, markers = SUMMARY_MARKERS) {
|
|
168
262
|
const lines = content.split(/\r?\n/);
|
|
169
|
-
|
|
170
|
-
|
|
263
|
+
return lines.filter((line) => isAnnotationLine(line, markers));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function collectAnnotations(content) {
|
|
267
|
+
const summaryLine = extractAnnotationLines(content, SUMMARY_MARKERS)[0] || "";
|
|
268
|
+
const warnLines = extractAnnotationLines(content, WARN_MARKERS);
|
|
171
269
|
return { summaryLine, warnLines };
|
|
172
270
|
}
|
|
173
271
|
|
|
@@ -210,7 +308,8 @@ export function scanRepository(rootDir, options = {}) {
|
|
|
210
308
|
if (isGeneratedPath(relPath)) return;
|
|
211
309
|
|
|
212
310
|
const content = readText(absolutePath);
|
|
213
|
-
const
|
|
311
|
+
const contentLines = content.split(/\r?\n/);
|
|
312
|
+
const annotations = collectAnnotations(content);
|
|
214
313
|
const functionMatches = findFunctionMatches(content, sourceType.language);
|
|
215
314
|
const warnings = analyzeFileWarnings(content, sourceType.language, functionMatches);
|
|
216
315
|
for (const warning of warnings) {
|
|
@@ -223,12 +322,14 @@ export function scanRepository(rootDir, options = {}) {
|
|
|
223
322
|
language: sourceType.language,
|
|
224
323
|
extension: extname(absolutePath).toLowerCase(),
|
|
225
324
|
comment: sourceType.comment,
|
|
226
|
-
lines: content === "" ? 0 :
|
|
325
|
+
lines: content === "" ? 0 : contentLines.length,
|
|
227
326
|
category: detectCategory(relPath),
|
|
228
|
-
hasSummary:
|
|
229
|
-
hasWarn:
|
|
327
|
+
hasSummary: annotations.summaryLine !== "",
|
|
328
|
+
hasWarn: annotations.warnLines.length > 0,
|
|
230
329
|
summaryLine: annotations.summaryLine,
|
|
231
330
|
warnLines: annotations.warnLines,
|
|
331
|
+
importSpecifiers: parseImportSpecifiers(content, sourceType.language),
|
|
332
|
+
firstFunctionLine: functionMatches[0]?.lineIndex ?? findInsertionIndex(contentLines),
|
|
232
333
|
warnings,
|
|
233
334
|
});
|
|
234
335
|
}
|
|
@@ -261,6 +362,7 @@ export function scanRepository(rootDir, options = {}) {
|
|
|
261
362
|
const relPath = toPosix(relative(repoRoot, targetDir));
|
|
262
363
|
if (getSourceType(targetDir) && !isGeneratedPath(relPath)) addFile(targetDir);
|
|
263
364
|
}
|
|
365
|
+
appendCircularDependencyWarnings(files, repoRoot, warningKinds);
|
|
264
366
|
|
|
265
367
|
const result = {
|
|
266
368
|
rootDir: repoRoot,
|
|
@@ -432,7 +534,7 @@ function hasNearbyWarn(lines, lineIndex) {
|
|
|
432
534
|
const start = Math.max(0, lineIndex - 2);
|
|
433
535
|
const end = Math.min(lines.length - 1, lineIndex + 1);
|
|
434
536
|
for (let index = start; index <= end; index += 1) {
|
|
435
|
-
if (
|
|
537
|
+
if (isAnnotationLine(lines[index], WARN_MARKERS)) return true;
|
|
436
538
|
}
|
|
437
539
|
return false;
|
|
438
540
|
}
|
|
@@ -447,8 +549,9 @@ export function generateWarnings(rootDir, options = {}) {
|
|
|
447
549
|
const inserts = [];
|
|
448
550
|
for (const warning of file.warnings) {
|
|
449
551
|
if (hasNearbyWarn(lines, warning.lineIndex)) continue;
|
|
552
|
+
const preferredIndex = Number.isInteger(warning.lineIndex) ? warning.lineIndex : findInsertionIndex(lines);
|
|
450
553
|
inserts.push({
|
|
451
|
-
index: warning.functionName ?
|
|
554
|
+
index: warning.functionName ? preferredIndex : findInsertionIndex(lines),
|
|
452
555
|
text: buildCommentLine(file.comment, "CLAUDE:WARN", warning.text),
|
|
453
556
|
});
|
|
454
557
|
}
|
|
@@ -618,7 +721,8 @@ function findStaleWarnings(content, language) {
|
|
|
618
721
|
const matcher = patterns[language];
|
|
619
722
|
if (!matcher) return stale;
|
|
620
723
|
for (let index = 0; index < lines.length; index += 1) {
|
|
621
|
-
if (!
|
|
724
|
+
if (!isAnnotationLine(lines[index], WARN_MARKERS)) continue;
|
|
725
|
+
if (/circular dependency/i.test(lines[index])) continue;
|
|
622
726
|
const window = lines.slice(index + 1, index + 5).join("\n");
|
|
623
727
|
if (!matcher.test(window)) stale.push(index + 1);
|
|
624
728
|
}
|
|
@@ -666,9 +770,21 @@ export function runConformity(rootDir, options = {}) {
|
|
|
666
770
|
|
|
667
771
|
export function migrateAnnotations(rootDir, options = {}) {
|
|
668
772
|
const scan = scanRepository(rootDir, options);
|
|
773
|
+
const migrateLegacyAnnotationMarkers = (content) =>
|
|
774
|
+
content
|
|
775
|
+
.replace(/BOSUN:SUMMARY/g, "CLAUDE:SUMMARY")
|
|
776
|
+
.replace(/BOSUN:WARN/g, "CLAUDE:WARN")
|
|
777
|
+
.replace(
|
|
778
|
+
/^(\s*(?:\/\/|#)\s*)(?:LEGACY:)?SUMMARY\s*[:\-]\s*/gim,
|
|
779
|
+
"$1CLAUDE:SUMMARY ",
|
|
780
|
+
)
|
|
781
|
+
.replace(
|
|
782
|
+
/^(\s*(?:\/\/|#)\s*)(?:LEGACY:)?WARN(?:ING)?\s*[:\-]\s*/gim,
|
|
783
|
+
"$1CLAUDE:WARN ",
|
|
784
|
+
);
|
|
669
785
|
const changed = updateFiles(
|
|
670
|
-
scan.files
|
|
671
|
-
(file, content) => content
|
|
786
|
+
scan.files,
|
|
787
|
+
(file, content) => migrateLegacyAnnotationMarkers(content),
|
|
672
788
|
options,
|
|
673
789
|
);
|
|
674
790
|
return {
|
|
@@ -804,4 +920,3 @@ export async function runAuditCli(argv, io = {}) {
|
|
|
804
920
|
const shouldFail = command === "conformity" || Boolean(flags.ci);
|
|
805
921
|
return { exitCode: shouldFail && result.ok === false ? 1 : 0, result };
|
|
806
922
|
}
|
|
807
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.2",
|
|
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",
|
|
@@ -71,10 +71,17 @@
|
|
|
71
71
|
"./container-runner": "./infra/container-runner.mjs",
|
|
72
72
|
"./compat": "./compat.mjs",
|
|
73
73
|
"./task-cli": "./task/task-cli.mjs",
|
|
74
|
+
"./task-pipeline": "./task/pipeline.mjs",
|
|
75
|
+
"./task-msg-hub": "./task/msg-hub.mjs",
|
|
76
|
+
"./workflow-cli": "./workflow/workflow-cli.mjs",
|
|
77
|
+
"./pipeline-workflows": "./workflow/pipeline-workflows.mjs",
|
|
74
78
|
"./github-auth-manager": "./github/github-auth-manager.mjs",
|
|
75
79
|
"./git-commit-helpers": "./git/git-commit-helpers.mjs",
|
|
76
80
|
"./opencode-shell": "./shell/opencode-shell.mjs",
|
|
77
|
-
"./context-indexer": "./workspace/context-indexer.mjs"
|
|
81
|
+
"./context-indexer": "./workspace/context-indexer.mjs",
|
|
82
|
+
"./msg-hub": "./workflow/msg-hub.mjs",
|
|
83
|
+
"./declarative-workflows": "./workflow/declarative-workflows.mjs",
|
|
84
|
+
"./pipeline": "./workflow/pipeline.mjs"
|
|
78
85
|
},
|
|
79
86
|
"bin": {
|
|
80
87
|
"bosun": "cli.mjs",
|
|
@@ -108,6 +115,8 @@
|
|
|
108
115
|
"test:vitest": "node --max-old-space-size=4096 node_modules/vitest/vitest.mjs run --config vitest.config.mjs",
|
|
109
116
|
"test:node": "node --import ./tests/node-test-bootstrap.mjs --test tests/*.node.test.mjs",
|
|
110
117
|
"test:all": "npm run test:vitest && npm run test:node",
|
|
118
|
+
"test:e2e": "npx playwright test server/playwright-ui-e2e.mjs",
|
|
119
|
+
"test:e2e:all": "npx playwright test server/playwright-ui-e2e.mjs server/playwright-ui-smoke.mjs",
|
|
111
120
|
"test:voice-provider-smoke": "vitest run --config vitest.config.mjs tests/voice-provider-smoke.test.mjs",
|
|
112
121
|
"check:native-call-parity": "vitest run --config vitest.config.mjs tests/voice-provider-smoke.test.mjs tests/native-call-parity-checklist.test.mjs",
|
|
113
122
|
"test:watch": "vitest",
|
|
@@ -147,6 +156,7 @@
|
|
|
147
156
|
"agent/agent-custom-tools.mjs",
|
|
148
157
|
"agent/agent-endpoint.mjs",
|
|
149
158
|
"agent/agent-event-bus.mjs",
|
|
159
|
+
"agent/retry-queue.mjs",
|
|
150
160
|
"agent/agent-pool.mjs",
|
|
151
161
|
"agent/agent-prompts.mjs",
|
|
152
162
|
"agent/agent-sdk.mjs",
|
|
@@ -200,7 +210,8 @@
|
|
|
200
210
|
"infra/library-manager.mjs",
|
|
201
211
|
"infra/maintenance.mjs",
|
|
202
212
|
"workflow/manual-flows.mjs",
|
|
203
|
-
"workflow
|
|
213
|
+
"workflow/pipeline-workflows.mjs",
|
|
214
|
+
"workflow/workflow-cli.mjs",
|
|
204
215
|
"workflow/mcp-discovery-proxy.mjs",
|
|
205
216
|
"workflow/mcp-workflow-adapter.mjs",
|
|
206
217
|
"workflow/mcp-registry.mjs",
|
|
@@ -242,7 +253,10 @@
|
|
|
242
253
|
"task/task-claims.mjs",
|
|
243
254
|
"task/task-context.mjs",
|
|
244
255
|
"task/task-attachments.mjs",
|
|
256
|
+
"task/pipeline.mjs",
|
|
257
|
+
"task/msg-hub.mjs",
|
|
245
258
|
"task/task-executor.mjs",
|
|
259
|
+
"task/task-executor-pipeline.mjs",
|
|
246
260
|
"task/task-store.mjs",
|
|
247
261
|
"telegram/telegram-bot.mjs",
|
|
248
262
|
"server/ui-server.mjs",
|
|
@@ -290,6 +304,7 @@
|
|
|
290
304
|
"workflow/workflow-engine.mjs",
|
|
291
305
|
"workflow/workflow-migration.mjs",
|
|
292
306
|
"workflow/workflow-nodes.mjs",
|
|
307
|
+
"workflow/workflow-nodes/custom-loader.mjs",
|
|
293
308
|
"workflow/project-detection.mjs",
|
|
294
309
|
"workflow/workflow-templates.mjs",
|
|
295
310
|
"workflow/workflow-contract.mjs",
|
|
@@ -308,11 +323,15 @@
|
|
|
308
323
|
"workflow-templates/task-execution.mjs",
|
|
309
324
|
"workflow-templates/task-lifecycle.mjs",
|
|
310
325
|
"workflow-templates/issue-continuation.mjs",
|
|
326
|
+
"workflow-templates/continuation-loop.mjs",
|
|
327
|
+
"workflow-templates/code-quality.mjs",
|
|
311
328
|
"workflow-templates/bosun-native.mjs",
|
|
312
329
|
"bosun-tui.mjs",
|
|
313
330
|
"tui/",
|
|
314
331
|
"tools/",
|
|
315
|
-
"ui/vendor/"
|
|
332
|
+
"ui/vendor/",
|
|
333
|
+
"workflow/msg-hub.mjs",
|
|
334
|
+
"workflow/declarative-workflows.mjs"
|
|
316
335
|
],
|
|
317
336
|
"dependencies": {
|
|
318
337
|
"@anthropic-ai/claude-agent-sdk": "latest",
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
resolveWorkflowTemplateIds,
|
|
30
30
|
normalizeTemplateOverridesById,
|
|
31
31
|
} from "../workflow/workflow-templates.mjs";
|
|
32
|
+
import { discoverTelegramChats } from "../telegram/get-telegram-chat-id.mjs";
|
|
32
33
|
|
|
33
34
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
35
|
|
|
@@ -1981,6 +1982,20 @@ function handleValidate(body) {
|
|
|
1981
1982
|
return { ok: true, valid: Object.keys(errors).length === 0, errors };
|
|
1982
1983
|
}
|
|
1983
1984
|
|
|
1985
|
+
async function handleTelegramChatIdLookup(body) {
|
|
1986
|
+
const token = String(body?.token || "").trim();
|
|
1987
|
+
if (!token) {
|
|
1988
|
+
return { ok: false, status: 400, error: "TELEGRAM_BOT_TOKEN is required" };
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
try {
|
|
1992
|
+
const { chats, message } = await discoverTelegramChats(token);
|
|
1993
|
+
return { ok: true, status: 200, chats, message };
|
|
1994
|
+
} catch (err) {
|
|
1995
|
+
return { ok: false, status: 500, error: err.message || String(err) };
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1984
1999
|
function handleApply(body) {
|
|
1985
2000
|
try {
|
|
1986
2001
|
const { env = {}, configJson = {} } = body || {};
|
|
@@ -2579,6 +2594,15 @@ async function handleRequest(req, res) {
|
|
|
2579
2594
|
}
|
|
2580
2595
|
jsonResponse(res, 200, handleValidate(await readBody(req)));
|
|
2581
2596
|
return;
|
|
2597
|
+
case "telegram-chat-id": {
|
|
2598
|
+
if (req.method !== "POST") {
|
|
2599
|
+
jsonResponse(res, 405, { ok: false, error: "POST required" });
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const result = await handleTelegramChatIdLookup(await readBody(req));
|
|
2603
|
+
jsonResponse(res, result.status, result);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2582
2606
|
case "apply":
|
|
2583
2607
|
if (req.method !== "POST") {
|
|
2584
2608
|
jsonResponse(res, 405, { ok: false, error: "POST required" });
|
|
@@ -2984,6 +3008,7 @@ export async function startSetupServer(options = {}) {
|
|
|
2984
3008
|
export {
|
|
2985
3009
|
applyTelegramMiniAppSetupEnv,
|
|
2986
3010
|
applyNonBlockingSetupEnvDefaults,
|
|
3011
|
+
handleTelegramChatIdLookup,
|
|
2987
3012
|
normalizeWorkflowTemplateOverrides,
|
|
2988
3013
|
normalizeTelegramUiPort,
|
|
2989
3014
|
normalizeRepoConfigEntry,
|