bosun 0.41.2 → 0.41.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 +1 -1
- package/agent/agent-pool.mjs +9 -2
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +119 -6
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +35 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/setup-web-server.mjs +58 -5
- package/server/ui-server.mjs +1394 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +28 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +338 -84
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +43 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +848 -141
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +358 -63
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +44 -11
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
package/infra/monitor.mjs
CHANGED
|
@@ -659,7 +659,11 @@ async function ensureWorkflowAutomationEngine() {
|
|
|
659
659
|
if (typeof workflowTemplates?.reconcileInstalledTemplates === "function") {
|
|
660
660
|
const reconcile = workflowTemplates.reconcileInstalledTemplates(engine, {
|
|
661
661
|
autoUpdateUnmodified: true,
|
|
662
|
-
forceUpdateTemplateIds: [
|
|
662
|
+
forceUpdateTemplateIds: [
|
|
663
|
+
"template-task-lifecycle",
|
|
664
|
+
"template-task-finalization-guard",
|
|
665
|
+
"template-agent-session-monitor",
|
|
666
|
+
],
|
|
663
667
|
});
|
|
664
668
|
if (Number(reconcile?.autoUpdated || 0) > 0) {
|
|
665
669
|
console.log(
|
|
@@ -720,6 +724,13 @@ async function ensureWorkflowAutomationEngine() {
|
|
|
720
724
|
);
|
|
721
725
|
}
|
|
722
726
|
}
|
|
727
|
+
|
|
728
|
+
// Resume runs paused by a previous monitor shutdown after services are wired.
|
|
729
|
+
if (typeof engine.resumeInterruptedRuns === "function") {
|
|
730
|
+
engine.resumeInterruptedRuns().catch((err) => {
|
|
731
|
+
console.warn(`[workflows] Failed to resume interrupted runs: ${err?.message || err}`);
|
|
732
|
+
});
|
|
733
|
+
}
|
|
723
734
|
workflowAutomationInitDone = true;
|
|
724
735
|
return engine;
|
|
725
736
|
} catch (err) {
|
|
@@ -6867,8 +6878,8 @@ async function checkMergedPRsAndUpdateTasks() {
|
|
|
6867
6878
|
);
|
|
6868
6879
|
try {
|
|
6869
6880
|
setInternalTaskStatus(taskId, "done", "review-merge-reconcile");
|
|
6870
|
-
} catch {
|
|
6871
|
-
|
|
6881
|
+
} catch (internalErr) {
|
|
6882
|
+
console.warn(`[monitor] review reconcile: setInternalTaskStatus failed for ${taskId}: ${internalErr?.message?.slice(0, 200)}`);
|
|
6872
6883
|
}
|
|
6873
6884
|
try {
|
|
6874
6885
|
updateInternalTask(taskId, {
|
|
@@ -6879,20 +6890,23 @@ async function checkMergedPRsAndUpdateTasks() {
|
|
|
6879
6890
|
resolvedRepoSlug ||
|
|
6880
6891
|
undefined,
|
|
6881
6892
|
});
|
|
6882
|
-
} catch {
|
|
6883
|
-
|
|
6893
|
+
} catch (metaErr) {
|
|
6894
|
+
console.warn(`[monitor] review reconcile: updateInternalTask failed for ${taskId}: ${metaErr?.message?.slice(0, 200)}`);
|
|
6884
6895
|
}
|
|
6885
6896
|
try {
|
|
6886
6897
|
await updateTaskStatus(taskId, "done", {
|
|
6887
6898
|
source: "review-merge-reconcile",
|
|
6899
|
+
bypassWorkflowOwnership: true,
|
|
6888
6900
|
workflowData: {
|
|
6889
6901
|
prNumber,
|
|
6890
6902
|
prUrl,
|
|
6891
6903
|
repository: resolvedRepoSlug || task?.repository || null,
|
|
6892
6904
|
},
|
|
6893
6905
|
});
|
|
6894
|
-
} catch {
|
|
6895
|
-
|
|
6906
|
+
} catch (reconcileErr) {
|
|
6907
|
+
console.warn(
|
|
6908
|
+
`[monitor] review reconcile: failed to update kanban status for ${taskId}: ${reconcileErr?.message?.slice(0, 200)}`,
|
|
6909
|
+
);
|
|
6896
6910
|
}
|
|
6897
6911
|
summary.movedDone += 1;
|
|
6898
6912
|
}
|
|
@@ -8009,7 +8023,8 @@ async function triggerFlowPostReviewMerge(taskId, context = {}) {
|
|
|
8009
8023
|
const autoArgs = ["pr", "merge", String(prNumber)];
|
|
8010
8024
|
if (resolvedRepoSlug) autoArgs.push("--repo", resolvedRepoSlug);
|
|
8011
8025
|
autoArgs.push("--body", buildFlowGateMergeBody(taskTitle, id));
|
|
8012
|
-
|
|
8026
|
+
const mergeMethod = process.env.BOSUN_MERGE_METHOD || "merge";
|
|
8027
|
+
autoArgs.push("--auto", `--${mergeMethod}`);
|
|
8013
8028
|
|
|
8014
8029
|
const autoResult = spawnSync("gh", autoArgs, {
|
|
8015
8030
|
cwd: repoRoot,
|
|
@@ -8031,7 +8046,7 @@ async function triggerFlowPostReviewMerge(taskId, context = {}) {
|
|
|
8031
8046
|
const directArgs = ["pr", "merge", String(prNumber)];
|
|
8032
8047
|
if (resolvedRepoSlug) directArgs.push("--repo", resolvedRepoSlug);
|
|
8033
8048
|
directArgs.push("--body", buildFlowGateMergeBody(taskTitle, id));
|
|
8034
|
-
directArgs.push(
|
|
8049
|
+
directArgs.push(`--${mergeMethod}`);
|
|
8035
8050
|
const directResult = spawnSync("gh", directArgs, {
|
|
8036
8051
|
cwd: repoRoot,
|
|
8037
8052
|
encoding: "utf8",
|
|
@@ -14774,6 +14789,17 @@ async function syncDivergedWorktrees() {
|
|
|
14774
14789
|
continue;
|
|
14775
14790
|
}
|
|
14776
14791
|
|
|
14792
|
+
// Safety: refuse to push if HEAD now equals origin/main (would wipe PR changes)
|
|
14793
|
+
try {
|
|
14794
|
+
const headSha = execSync("git rev-parse HEAD", { cwd: wtPath, encoding: "utf8", timeout: 5_000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
14795
|
+
const mainSha = execSync("git rev-parse origin/main", { cwd: wtPath, encoding: "utf8", timeout: 5_000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
14796
|
+
if (headSha === mainSha) {
|
|
14797
|
+
console.warn(`[monitor:worktree-sync] ${branch} HEAD matches origin/main after rebase — aborting push to prevent PR wipe`);
|
|
14798
|
+
failed++;
|
|
14799
|
+
continue;
|
|
14800
|
+
}
|
|
14801
|
+
} catch { /* best-effort check */ }
|
|
14802
|
+
|
|
14777
14803
|
// Push with --force-with-lease (safe: we just fetched fresh remote refs)
|
|
14778
14804
|
try {
|
|
14779
14805
|
execSync(`git push --force-with-lease --set-upstream origin HEAD`, {
|
|
@@ -240,16 +240,18 @@ export class SessionTracker {
|
|
|
240
240
|
if (!session) return;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
session.totalEvents++;
|
|
244
|
-
session.lastActivityAt = Date.now();
|
|
245
|
-
session.lastActiveAt = new Date().toISOString();
|
|
246
|
-
|
|
247
243
|
const maxMessages =
|
|
248
244
|
session.maxMessages === null || session.maxMessages === undefined
|
|
249
245
|
? this.#maxMessages
|
|
250
246
|
: session.maxMessages;
|
|
247
|
+
const markActivity = () => {
|
|
248
|
+
session.totalEvents++;
|
|
249
|
+
session.lastActivityAt = Date.now();
|
|
250
|
+
session.lastActiveAt = new Date().toISOString();
|
|
251
|
+
};
|
|
251
252
|
|
|
252
253
|
if (typeof event === "string" && event.trim()) {
|
|
254
|
+
markActivity();
|
|
253
255
|
const msg = {
|
|
254
256
|
type: "system",
|
|
255
257
|
content: event.trim().slice(0, MAX_MESSAGE_CHARS),
|
|
@@ -267,8 +269,9 @@ export class SessionTracker {
|
|
|
267
269
|
|
|
268
270
|
// Direct message format (role/content)
|
|
269
271
|
if (event && event.role && event.content !== undefined) {
|
|
272
|
+
markActivity();
|
|
270
273
|
const msg = {
|
|
271
|
-
|
|
274
|
+
id: event.id || `msg-${Date.now()}-${randomToken(6)}`,
|
|
272
275
|
type: event.type || undefined,
|
|
273
276
|
role: event.role,
|
|
274
277
|
content: String(event.content).slice(0, MAX_MESSAGE_CHARS),
|
|
@@ -299,10 +302,10 @@ export class SessionTracker {
|
|
|
299
302
|
|
|
300
303
|
const msg = this.#normalizeEvent(event);
|
|
301
304
|
if (!msg) {
|
|
302
|
-
|
|
303
|
-
return; // Skip uninteresting events — still update timestamp
|
|
305
|
+
return; // Ignore low-signal events that should not mask idle/stalled sessions
|
|
304
306
|
}
|
|
305
307
|
|
|
308
|
+
markActivity();
|
|
306
309
|
// Push to ring buffer (keep only last N)
|
|
307
310
|
session.messages.push(msg);
|
|
308
311
|
if (Number.isFinite(maxMessages) && maxMessages > 0) {
|
|
@@ -912,6 +912,15 @@ class InternalAdapter {
|
|
|
912
912
|
if (typeof patch.workspace === "string") updates.workspace = patch.workspace;
|
|
913
913
|
if (typeof patch.repository === "string") updates.repository = patch.repository;
|
|
914
914
|
if (Array.isArray(patch.repositories)) updates.repositories = patch.repositories;
|
|
915
|
+
if (hasOwnField(patch, "workflowRuns")) updates.workflowRuns = patch.workflowRuns;
|
|
916
|
+
if (hasOwnField(patch, "workflowHistory")) updates.workflowHistory = patch.workflowHistory;
|
|
917
|
+
if (hasOwnField(patch, "workflows")) updates.workflows = patch.workflows;
|
|
918
|
+
if (hasOwnField(patch, "cooldownUntil")) {
|
|
919
|
+
updates.cooldownUntil = normalizeTaskStringField(patch.cooldownUntil);
|
|
920
|
+
}
|
|
921
|
+
if (hasOwnField(patch, "blockedReason")) {
|
|
922
|
+
updates.blockedReason = normalizeTaskStringField(patch.blockedReason);
|
|
923
|
+
}
|
|
915
924
|
if (typeof patch.branchName === "string") {
|
|
916
925
|
updates.branchName = patch.branchName.trim() || null;
|
|
917
926
|
}
|
|
@@ -950,6 +959,7 @@ class InternalAdapter {
|
|
|
950
959
|
}
|
|
951
960
|
}
|
|
952
961
|
const current = getInternalTask(normalizedId);
|
|
962
|
+
const replaceMeta = patch.replaceMeta === true;
|
|
953
963
|
if (baseBranch) {
|
|
954
964
|
updates.baseBranch = baseBranch;
|
|
955
965
|
}
|
|
@@ -967,7 +977,7 @@ class InternalAdapter {
|
|
|
967
977
|
if (hasOwnField(patch, "dueDate") || dueDate) updates.dueDate = dueDate;
|
|
968
978
|
if (patch.meta && typeof patch.meta === "object") {
|
|
969
979
|
updates.meta = {
|
|
970
|
-
...(current?.meta || {}),
|
|
980
|
+
...(replaceMeta ? {} : (current?.meta || {})),
|
|
971
981
|
...patch.meta,
|
|
972
982
|
...((assigneeProvided || assignee || assignees.length > 0)
|
|
973
983
|
? {
|
|
@@ -1015,6 +1025,9 @@ class InternalAdapter {
|
|
|
1015
1025
|
taskData.parentTaskId ?? taskData.meta?.parentTaskId,
|
|
1016
1026
|
);
|
|
1017
1027
|
const dueDate = normalizeTaskStringField(taskData.dueDate ?? taskData.meta?.dueDate);
|
|
1028
|
+
const blockedReason = normalizeTaskStringField(
|
|
1029
|
+
taskData.blockedReason ?? taskData.meta?.blockedReason,
|
|
1030
|
+
);
|
|
1018
1031
|
const created = addInternalTask({
|
|
1019
1032
|
id,
|
|
1020
1033
|
title: taskData.title || "Untitled task",
|
|
@@ -1026,6 +1039,7 @@ class InternalAdapter {
|
|
|
1026
1039
|
storyPoints,
|
|
1027
1040
|
parentTaskId,
|
|
1028
1041
|
dueDate,
|
|
1042
|
+
blockedReason,
|
|
1029
1043
|
priority: taskData.priority || null,
|
|
1030
1044
|
tags,
|
|
1031
1045
|
draft,
|
|
@@ -1059,6 +1073,7 @@ class InternalAdapter {
|
|
|
1059
1073
|
...(storyPoints != null ? { storyPoints } : {}),
|
|
1060
1074
|
...(parentTaskId ? { parentTaskId } : {}),
|
|
1061
1075
|
...(dueDate ? { dueDate } : {}),
|
|
1076
|
+
...(blockedReason ? { blockedReason } : {}),
|
|
1062
1077
|
...(taskData.workspace ? { workspace: taskData.workspace } : {}),
|
|
1063
1078
|
...(taskData.repository || taskData.repo
|
|
1064
1079
|
? { repository: taskData.repository || taskData.repo }
|
|
@@ -6149,3 +6164,4 @@ export async function unmarkTaskIgnored(taskId) {
|
|
|
6149
6164
|
);
|
|
6150
6165
|
return false;
|
|
6151
6166
|
}
|
|
6167
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { basename, dirname, extname, relative, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
function human(value) {
|
|
4
|
+
return String(value || "")
|
|
5
|
+
.replace(/[-_]+/g, " ")
|
|
6
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
7
|
+
.replace(/\s+/g, " ")
|
|
8
|
+
.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toPosix(pathValue) {
|
|
12
|
+
return String(pathValue || "").replace(/\\/g, "/");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function roleFromPath(relPath, content, category) {
|
|
16
|
+
const base = basename(relPath, extname(relPath)).toLowerCase();
|
|
17
|
+
if (category === "test") return "Covers regression and behavior checks";
|
|
18
|
+
if (base === "cli") return "Routes the command-line entrypoint and subcommands";
|
|
19
|
+
if (base.includes("audit")) return "Implements codebase annotation auditing and reporting";
|
|
20
|
+
if (base.includes("config")) return "Loads and normalizes configuration state";
|
|
21
|
+
if (base.includes("server")) return "Hosts request handling and server lifecycle logic";
|
|
22
|
+
if (base.includes("hook")) return "Applies hook validation and workflow guardrails";
|
|
23
|
+
if (base.includes("index")) return "Exports the directory entrypoint and shared surface area";
|
|
24
|
+
if (base.includes("manager")) return "Coordinates lifecycle management and shared state";
|
|
25
|
+
if (base.includes("store")) return "Persists and retrieves shared state";
|
|
26
|
+
if (base.includes("parser")) return "Parses structured input into internal state";
|
|
27
|
+
if (base.includes("logger")) return "Formats and emits logging output";
|
|
28
|
+
if (/process\.argv|args\s*=\s*process\.argv|main\(/.test(content)) return "Runs command handling and process orchestration";
|
|
29
|
+
if (/createServer|express\(|fastify\(|http\.createServer/.test(content)) return "Serves HTTP-facing runtime behavior";
|
|
30
|
+
if (/readFileSync|writeFileSync|readdirSync|statSync|fs\./.test(content)) return "Performs filesystem discovery and state updates";
|
|
31
|
+
const dirName = human(dirname(relPath).split("/").filter(Boolean).pop() || "repository");
|
|
32
|
+
const baseName = human(base || "module");
|
|
33
|
+
return `Owns ${baseName} logic for ${dirName}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildSummary(file, readText) {
|
|
37
|
+
const content = readText(file.absolutePath);
|
|
38
|
+
const prefix = roleFromPath(file.path, content, file.category);
|
|
39
|
+
const hints = [];
|
|
40
|
+
if (/export\s+(async\s+)?function|module\.exports|export\s+const|pub\s+fn|^func\s+/m.test(content)) hints.push("its public API");
|
|
41
|
+
if (/readFileSync|writeFileSync|mkdirSync|unlinkSync|execFileSync|spawn\(/.test(content)) hints.push("file or process side effects");
|
|
42
|
+
if (/describe\(|it\(|test\(/.test(content)) hints.push("test coverage");
|
|
43
|
+
const detail = hints.length > 0 ? `, including ${hints.slice(0, 2).join(" and ")}` : "";
|
|
44
|
+
return `${prefix}${detail}.`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function summaryFromLine(line, fallback) {
|
|
48
|
+
if (!line) return fallback;
|
|
49
|
+
return line.replace(/^\s*(?:\/\/|#)\s*(?:CLAUDE|BOSUN):SUMMARY\s*/i, "").trim() || fallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function upsertManagedBlock(existing, block) {
|
|
53
|
+
const begin = "<!-- bosun-audit:begin -->";
|
|
54
|
+
const end = "<!-- bosun-audit:end -->";
|
|
55
|
+
if (!existing) return `${block}\n`;
|
|
56
|
+
const start = existing.indexOf(begin);
|
|
57
|
+
if (start !== -1) {
|
|
58
|
+
const finish = existing.indexOf(end, start + begin.length);
|
|
59
|
+
if (finish !== -1) {
|
|
60
|
+
const updated = `${existing.slice(0, start)}${block}${existing.slice(finish + end.length)}`;
|
|
61
|
+
return `${updated.trimEnd()}\n`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return `${existing.trimEnd()}\n\n${block}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildClaudeManifest(dirPath, entries, repoRoot, summarizeFile) {
|
|
68
|
+
const relDir = toPosix(relative(repoRoot, dirPath)) || ".";
|
|
69
|
+
const lines = [
|
|
70
|
+
"<!-- bosun-audit:begin -->",
|
|
71
|
+
"# CLAUDE.md",
|
|
72
|
+
"",
|
|
73
|
+
"## Protocol",
|
|
74
|
+
`- Start with \`grep -R \"CLAUDE:SUMMARY\" ${relDir === "." ? "." : relDir}\`.`,
|
|
75
|
+
"- Read files with warnings before editing adjacent code.",
|
|
76
|
+
"- Treat this file as a fast map, not exhaustive prose.",
|
|
77
|
+
"",
|
|
78
|
+
"## Files",
|
|
79
|
+
];
|
|
80
|
+
for (const entry of entries.slice(0, 12)) {
|
|
81
|
+
const relFile = toPosix(relative(dirPath, resolve(repoRoot, entry.path))) || basename(entry.path);
|
|
82
|
+
lines.push(`- \`${relFile}\` - ${summaryFromLine(entry.summaryLine, summarizeFile(entry))}`);
|
|
83
|
+
}
|
|
84
|
+
if (entries.length > 12) lines.push(`- Remaining files: ${entries.length - 12} (see \`INDEX.map\`).`);
|
|
85
|
+
lines.push("", "## Validation", "- Run \`bosun audit conformity\` after documentation-only updates.", "- Regenerate with \`bosun audit manifest\` when file responsibilities change.", "<!-- bosun-audit:end -->");
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildAgentsManifest(dirPath, entries, repoRoot, summarizeFile) {
|
|
90
|
+
const relDir = toPosix(relative(repoRoot, dirPath)) || ".";
|
|
91
|
+
const scope = relDir === "." ? "repository root" : relDir;
|
|
92
|
+
const lines = [
|
|
93
|
+
"<!-- bosun-audit:begin -->",
|
|
94
|
+
"# AGENTS.md",
|
|
95
|
+
"",
|
|
96
|
+
"## Scope",
|
|
97
|
+
`Audit-managed quick guide for \`${scope}\`. Keep edits documentation-only unless deeper instructions say otherwise.`,
|
|
98
|
+
"",
|
|
99
|
+
"## Start Files",
|
|
100
|
+
];
|
|
101
|
+
for (const entry of entries.slice(0, 8)) {
|
|
102
|
+
lines.push(`- \`${basename(entry.path)}\` - ${summaryFromLine(entry.summaryLine, summarizeFile(entry))}`);
|
|
103
|
+
}
|
|
104
|
+
lines.push(
|
|
105
|
+
"",
|
|
106
|
+
"## Workflow",
|
|
107
|
+
"- Read \`CLAUDE.md\` before broad file discovery.",
|
|
108
|
+
"- Prefer \`grep CLAUDE:SUMMARY\` over opening whole directories.",
|
|
109
|
+
"- Re-run \`bosun audit index\` after annotation updates.",
|
|
110
|
+
"",
|
|
111
|
+
"## Validation",
|
|
112
|
+
"- \`bosun audit conformity\`",
|
|
113
|
+
"- \`bosun audit trim\` when manifests drift or grow stale",
|
|
114
|
+
"<!-- bosun-audit:end -->",
|
|
115
|
+
);
|
|
116
|
+
return lines.join("\n");
|
|
117
|
+
}
|
package/lib/codebase-audit.mjs
CHANGED
|
@@ -7,7 +7,14 @@ import {
|
|
|
7
7
|
statSync,
|
|
8
8
|
writeFileSync,
|
|
9
9
|
} from "node:fs";
|
|
10
|
-
import {
|
|
10
|
+
import { dirname, extname, relative, resolve } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
buildAgentsManifest,
|
|
13
|
+
buildClaudeManifest,
|
|
14
|
+
buildSummary,
|
|
15
|
+
summaryFromLine,
|
|
16
|
+
upsertManagedBlock,
|
|
17
|
+
} from "./codebase-audit-manifests.mjs";
|
|
11
18
|
|
|
12
19
|
const SOURCE_TYPES = new Map([
|
|
13
20
|
[".js", { language: "javascript", comment: "//" }],
|
|
@@ -77,14 +84,6 @@ function parseCliArgs(argv) {
|
|
|
77
84
|
return { positionals, flags };
|
|
78
85
|
}
|
|
79
86
|
|
|
80
|
-
function human(value) {
|
|
81
|
-
return String(value || "")
|
|
82
|
-
.replace(/[-_]+/g, " ")
|
|
83
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
84
|
-
.replace(/\s+/g, " ")
|
|
85
|
-
.trim();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
87
|
function capitalize(value) {
|
|
89
88
|
return value ? value.charAt(0).toUpperCase() + value.slice(1) : value;
|
|
90
89
|
}
|
|
@@ -381,37 +380,7 @@ export function scanRepository(rootDir, options = {}) {
|
|
|
381
380
|
return result;
|
|
382
381
|
}
|
|
383
382
|
|
|
384
|
-
|
|
385
|
-
const base = basename(relPath, extname(relPath)).toLowerCase();
|
|
386
|
-
if (category === "test") return "Covers regression and behavior checks";
|
|
387
|
-
if (base === "cli") return "Routes the command-line entrypoint and subcommands";
|
|
388
|
-
if (base.includes("audit")) return "Implements codebase annotation auditing and reporting";
|
|
389
|
-
if (base.includes("config")) return "Loads and normalizes configuration state";
|
|
390
|
-
if (base.includes("server")) return "Hosts request handling and server lifecycle logic";
|
|
391
|
-
if (base.includes("hook")) return "Applies hook validation and workflow guardrails";
|
|
392
|
-
if (base.includes("index")) return "Exports the directory entrypoint and shared surface area";
|
|
393
|
-
if (base.includes("manager")) return "Coordinates lifecycle management and shared state";
|
|
394
|
-
if (base.includes("store")) return "Persists and retrieves shared state";
|
|
395
|
-
if (base.includes("parser")) return "Parses structured input into internal state";
|
|
396
|
-
if (base.includes("logger")) return "Formats and emits logging output";
|
|
397
|
-
if (/process\.argv|args\s*=\s*process\.argv|main\(/.test(content)) return "Runs command handling and process orchestration";
|
|
398
|
-
if (/createServer|express\(|fastify\(|http\.createServer/.test(content)) return "Serves HTTP-facing runtime behavior";
|
|
399
|
-
if (/readFileSync|writeFileSync|readdirSync|statSync|fs\./.test(content)) return "Performs filesystem discovery and state updates";
|
|
400
|
-
const dirName = human(dirname(relPath).split("/").filter(Boolean).pop() || "repository");
|
|
401
|
-
const baseName = human(base || "module");
|
|
402
|
-
return `Owns ${baseName} logic for ${dirName}`;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function buildSummary(file) {
|
|
406
|
-
const content = readText(file.absolutePath);
|
|
407
|
-
const prefix = roleFromPath(file.path, content, file.category);
|
|
408
|
-
const hints = [];
|
|
409
|
-
if (/export\s+(async\s+)?function|module\.exports|export\s+const|pub\s+fn|^func\s+/m.test(content)) hints.push("its public API");
|
|
410
|
-
if (/readFileSync|writeFileSync|mkdirSync|unlinkSync|execFileSync|spawn\(/.test(content)) hints.push("file or process side effects");
|
|
411
|
-
if (/describe\(|it\(|test\(/.test(content)) hints.push("test coverage");
|
|
412
|
-
const detail = hints.length > 0 ? `, including ${hints.slice(0, 2).join(" and ")}` : "";
|
|
413
|
-
return `${prefix}${detail}.`;
|
|
414
|
-
}
|
|
383
|
+
const summarizeFile = (file) => buildSummary(file, readText);
|
|
415
384
|
|
|
416
385
|
function buildCommentLine(commentPrefix, marker, text) {
|
|
417
386
|
return `${commentPrefix} ${marker} ${text}`.trimEnd();
|
|
@@ -447,7 +416,7 @@ export function generateSummaries(rootDir, options = {}) {
|
|
|
447
416
|
const scan = scanRepository(rootDir, options);
|
|
448
417
|
const changed = updateFiles(
|
|
449
418
|
scan.files.filter((file) => !file.hasSummary),
|
|
450
|
-
(file, content) => insertHeaderAnnotation(content, buildCommentLine(file.comment, "CLAUDE:SUMMARY",
|
|
419
|
+
(file, content) => insertHeaderAnnotation(content, buildCommentLine(file.comment, "CLAUDE:SUMMARY", summarizeFile(file))),
|
|
451
420
|
options,
|
|
452
421
|
);
|
|
453
422
|
return {
|
|
@@ -499,16 +468,16 @@ function findFunctionMatches(content, language) {
|
|
|
499
468
|
|
|
500
469
|
function analyzeFunctionWarnings(segment) {
|
|
501
470
|
const warnings = [];
|
|
502
|
-
if (/await\s+import\(
|
|
471
|
+
if (/await\s+import\(|\brequire\(|importlib\.import_module/.test(segment)) {
|
|
503
472
|
warnings.push({ kind: "lazy-init", text: "Lazily resolves dependencies at runtime; preserve initialization order and cache behavior." });
|
|
504
473
|
}
|
|
505
|
-
if (/if\s*\((?:!|typeof\s+)[^)]+\)\s*\{?[\s\S]{0,180}?=\s*(?:await\s+)?(?:new\s
|
|
474
|
+
if (/if\s*\((?:!|typeof\s+)[^)]+\)\s*\{?[\s\S]{0,180}?=\s*(?:await\s+)?(?:new\s+|\b(?:create|build|init|get|load)\b)/.test(segment) || /sync\.Once|OnceLock|lazy_static!/.test(segment)) {
|
|
506
475
|
warnings.push({ kind: "singleton", text: "Initializes shared state on demand; changing call order can duplicate or corrupt cached state." });
|
|
507
476
|
}
|
|
508
|
-
if (
|
|
477
|
+
if (/\b(?:writeFile|appendFile|unlink|rmSync|mkdirSync)|spawn\(|exec(?:File)?Sync|fork\(|process\.exit|std::fs::|std::process::Command|os\.WriteFile|os\.Remove|subprocess\./.test(segment)) {
|
|
509
478
|
warnings.push({ kind: "side-effects", text: "Performs filesystem or process side effects; audit callers before reordering, retrying, or parallelizing." });
|
|
510
479
|
}
|
|
511
|
-
if (/process\.env|os\.environ
|
|
480
|
+
if (/(?:process\.env|os\.environ|\bgetenv\(|std::env::var)/.test(segment)) {
|
|
512
481
|
warnings.push({ kind: "env", text: "Depends on ambient environment state; validate required variables before changing execution flow." });
|
|
513
482
|
}
|
|
514
483
|
return warnings;
|
|
@@ -574,73 +543,6 @@ export function generateWarnings(rootDir, options = {}) {
|
|
|
574
543
|
};
|
|
575
544
|
}
|
|
576
545
|
|
|
577
|
-
function summaryFromLine(line, fallback) {
|
|
578
|
-
if (!line) return fallback;
|
|
579
|
-
return line.replace(/^\s*(?:\/\/|#)\s*(?:CLAUDE|BOSUN):SUMMARY\s*/i, "").trim() || fallback;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function upsertManagedBlock(existing, block) {
|
|
583
|
-
const begin = "<!-- bosun-audit:begin -->";
|
|
584
|
-
const end = "<!-- bosun-audit:end -->";
|
|
585
|
-
if (!existing) return `${block}\n`;
|
|
586
|
-
if (existing.includes(begin) && existing.includes(end)) {
|
|
587
|
-
return `${existing.replace(new RegExp(`${begin}[\\s\\S]*?${end}`), block).replace(/\s+$/, "")}\n`;
|
|
588
|
-
}
|
|
589
|
-
return `${existing.replace(/\s+$/, "")}\n\n${block}\n`;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function buildClaudeManifest(dirPath, entries, repoRoot) {
|
|
593
|
-
const relDir = toPosix(relative(repoRoot, dirPath)) || ".";
|
|
594
|
-
const lines = [
|
|
595
|
-
"<!-- bosun-audit:begin -->",
|
|
596
|
-
"# CLAUDE.md",
|
|
597
|
-
"",
|
|
598
|
-
"## Protocol",
|
|
599
|
-
`- Start with \`grep -R \"CLAUDE:SUMMARY\" ${relDir === "." ? "." : relDir}\`.`,
|
|
600
|
-
"- Read files with warnings before editing adjacent code.",
|
|
601
|
-
"- Treat this file as a fast map, not exhaustive prose.",
|
|
602
|
-
"",
|
|
603
|
-
"## Files",
|
|
604
|
-
];
|
|
605
|
-
for (const entry of entries.slice(0, 12)) {
|
|
606
|
-
const relFile = toPosix(relative(dirPath, resolve(repoRoot, entry.path))) || basename(entry.path);
|
|
607
|
-
lines.push(`- \`${relFile}\` - ${summaryFromLine(entry.summaryLine, buildSummary(entry))}`);
|
|
608
|
-
}
|
|
609
|
-
if (entries.length > 12) lines.push(`- Remaining files: ${entries.length - 12} (see \`INDEX.map\`).`);
|
|
610
|
-
lines.push("", "## Validation", "- Run \`bosun audit conformity\` after documentation-only updates.", "- Regenerate with \`bosun audit manifest\` when file responsibilities change.", "<!-- bosun-audit:end -->");
|
|
611
|
-
return lines.join("\n");
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function buildAgentsManifest(dirPath, entries, repoRoot) {
|
|
615
|
-
const relDir = toPosix(relative(repoRoot, dirPath)) || ".";
|
|
616
|
-
const scope = relDir === "." ? "repository root" : relDir;
|
|
617
|
-
const lines = [
|
|
618
|
-
"<!-- bosun-audit:begin -->",
|
|
619
|
-
"# AGENTS.md",
|
|
620
|
-
"",
|
|
621
|
-
"## Scope",
|
|
622
|
-
`Audit-managed quick guide for \`${scope}\`. Keep edits documentation-only unless deeper instructions say otherwise.`,
|
|
623
|
-
"",
|
|
624
|
-
"## Start Files",
|
|
625
|
-
];
|
|
626
|
-
for (const entry of entries.slice(0, 8)) {
|
|
627
|
-
lines.push(`- \`${basename(entry.path)}\` - ${summaryFromLine(entry.summaryLine, buildSummary(entry))}`);
|
|
628
|
-
}
|
|
629
|
-
lines.push(
|
|
630
|
-
"",
|
|
631
|
-
"## Workflow",
|
|
632
|
-
"- Read \`CLAUDE.md\` before broad file discovery.",
|
|
633
|
-
"- Prefer \`grep CLAUDE:SUMMARY\` over opening whole directories.",
|
|
634
|
-
"- Re-run \`bosun audit index\` after annotation updates.",
|
|
635
|
-
"",
|
|
636
|
-
"## Validation",
|
|
637
|
-
"- \`bosun audit conformity\`",
|
|
638
|
-
"- \`bosun audit trim\` when manifests drift or grow stale",
|
|
639
|
-
"<!-- bosun-audit:end -->",
|
|
640
|
-
);
|
|
641
|
-
return lines.join("\n");
|
|
642
|
-
}
|
|
643
|
-
|
|
644
546
|
export function generateManifests(rootDir, options = {}) {
|
|
645
547
|
const scan = scanRepository(rootDir, options);
|
|
646
548
|
const byDirectory = new Map();
|
|
@@ -657,8 +559,8 @@ export function generateManifests(rootDir, options = {}) {
|
|
|
657
559
|
const claudePath = resolve(dirPath, "CLAUDE.md");
|
|
658
560
|
const agentsPath = resolve(dirPath, "AGENTS.md");
|
|
659
561
|
if (!options.dryRun) {
|
|
660
|
-
writeFileSync(claudePath, upsertManagedBlock(existsSync(claudePath) ? readText(claudePath) : "", buildClaudeManifest(dirPath, entries, rootDir)), "utf8");
|
|
661
|
-
writeFileSync(agentsPath, upsertManagedBlock(existsSync(agentsPath) ? readText(agentsPath) : "", buildAgentsManifest(dirPath, entries, rootDir)), "utf8");
|
|
562
|
+
writeFileSync(claudePath, upsertManagedBlock(existsSync(claudePath) ? readText(claudePath) : "", buildClaudeManifest(dirPath, entries, rootDir, summarizeFile)), "utf8");
|
|
563
|
+
writeFileSync(agentsPath, upsertManagedBlock(existsSync(agentsPath) ? readText(agentsPath) : "", buildAgentsManifest(dirPath, entries, rootDir, summarizeFile)), "utf8");
|
|
662
564
|
}
|
|
663
565
|
changed.push(toPosix(relative(rootDir, claudePath)));
|
|
664
566
|
changed.push(toPosix(relative(rootDir, agentsPath)));
|
|
@@ -680,7 +582,7 @@ export function buildIndexMap(rootDir, options = {}) {
|
|
|
680
582
|
const indexPath = resolve(rootDir, "INDEX.map");
|
|
681
583
|
const lines = ["# INDEX.map", ""];
|
|
682
584
|
for (const file of scan.files) {
|
|
683
|
-
lines.push(`${file.path} => ${summaryFromLine(file.summaryLine,
|
|
585
|
+
lines.push(`${file.path} => ${summaryFromLine(file.summaryLine, summarizeFile(file))}`);
|
|
684
586
|
}
|
|
685
587
|
if (!options.dryRun) writeFileSync(indexPath, `${lines.join("\n")}\n`, "utf8");
|
|
686
588
|
return {
|
|
@@ -920,3 +822,4 @@ export async function runAuditCli(argv, io = {}) {
|
|
|
920
822
|
const shouldFail = command === "conformity" || Boolean(flags.ci);
|
|
921
823
|
return { exitCode: shouldFail && result.ok === false ? 1 : 0, result };
|
|
922
824
|
}
|
|
825
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.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",
|
|
@@ -115,8 +115,8 @@
|
|
|
115
115
|
"test:vitest": "node --max-old-space-size=4096 node_modules/vitest/vitest.mjs run --config vitest.config.mjs",
|
|
116
116
|
"test:node": "node --import ./tests/node-test-bootstrap.mjs --test tests/*.node.test.mjs",
|
|
117
117
|
"test:all": "npm run test:vitest && npm run test:node",
|
|
118
|
-
"test:e2e": "
|
|
119
|
-
"test:e2e:all": "
|
|
118
|
+
"test:e2e": "node --import ./tests/node-test-bootstrap.mjs --test tests/portal-ui-smoke.node.test.mjs",
|
|
119
|
+
"test:e2e:all": "node --import ./tests/node-test-bootstrap.mjs --test tests/portal-ui-smoke.node.test.mjs",
|
|
120
120
|
"test:voice-provider-smoke": "vitest run --config vitest.config.mjs tests/voice-provider-smoke.test.mjs",
|
|
121
121
|
"check:native-call-parity": "vitest run --config vitest.config.mjs tests/voice-provider-smoke.test.mjs tests/native-call-parity-checklist.test.mjs",
|
|
122
122
|
"test:watch": "vitest",
|
|
@@ -158,17 +158,21 @@
|
|
|
158
158
|
"agent/agent-event-bus.mjs",
|
|
159
159
|
"agent/retry-queue.mjs",
|
|
160
160
|
"agent/agent-pool.mjs",
|
|
161
|
+
"agent/agent-prompt-catalog.mjs",
|
|
161
162
|
"agent/agent-prompts.mjs",
|
|
162
163
|
"agent/agent-sdk.mjs",
|
|
163
164
|
"agent/agent-work-report.mjs",
|
|
164
165
|
"agent/analyze-agent-work-helpers.mjs",
|
|
165
166
|
"agent/analyze-agent-work.mjs",
|
|
167
|
+
"agent/autofix-git.mjs",
|
|
166
168
|
"infra/anomaly-detector.mjs",
|
|
167
169
|
"agent/autofix.mjs",
|
|
170
|
+
"agent/autofix-prompts.mjs",
|
|
168
171
|
"shell/claude-shell.mjs",
|
|
169
172
|
"cli.mjs",
|
|
170
173
|
"compat.mjs",
|
|
171
174
|
"shell/codex-config.mjs",
|
|
175
|
+
"shell/codex-config-file.mjs",
|
|
172
176
|
"bosun.config.example.json",
|
|
173
177
|
"bosun.schema.json",
|
|
174
178
|
"agent/bosun-skills.mjs",
|
|
@@ -176,7 +180,10 @@
|
|
|
176
180
|
"shell/codex-model-profiles.mjs",
|
|
177
181
|
"shell/codex-shell.mjs",
|
|
178
182
|
"config/config.mjs",
|
|
183
|
+
"config/config-file-names.mjs",
|
|
179
184
|
"config/config-doctor.mjs",
|
|
185
|
+
"config/executor-config.mjs",
|
|
186
|
+
"config/workspace-health.mjs",
|
|
180
187
|
"git/conflict-resolver.mjs",
|
|
181
188
|
"workspace/context-cache.mjs",
|
|
182
189
|
"workspace/context-indexer.mjs",
|
|
@@ -204,12 +211,17 @@
|
|
|
204
211
|
"git/git-editor-fix.mjs",
|
|
205
212
|
"git/git-safety.mjs",
|
|
206
213
|
"kanban/kanban-adapter.mjs",
|
|
214
|
+
"lib/codebase-audit-manifests.mjs",
|
|
207
215
|
"lib/logger.mjs",
|
|
208
216
|
"lib/codebase-audit.mjs",
|
|
217
|
+
"lib/codebase-audit-warnings.mjs",
|
|
209
218
|
"lib/session-insights.mjs",
|
|
210
219
|
"infra/library-manager.mjs",
|
|
220
|
+
"infra/library-manager-utils.mjs",
|
|
221
|
+
"infra/library-manager-well-known-sources.mjs",
|
|
211
222
|
"infra/maintenance.mjs",
|
|
212
223
|
"workflow/manual-flows.mjs",
|
|
224
|
+
"workflow/manual-flow-audit.mjs",
|
|
213
225
|
"workflow/pipeline-workflows.mjs",
|
|
214
226
|
"workflow/workflow-cli.mjs",
|
|
215
227
|
"workflow/mcp-discovery-proxy.mjs",
|
|
@@ -280,6 +292,7 @@
|
|
|
280
292
|
"voice/voice-agents-sdk.mjs",
|
|
281
293
|
"voice/voice-relay.mjs",
|
|
282
294
|
"voice/voice-tools.mjs",
|
|
295
|
+
"voice/voice-tool-definitions.mjs",
|
|
283
296
|
"workspace/workspace-manager.mjs",
|
|
284
297
|
"workspace/workspace-monitor.mjs",
|
|
285
298
|
"workspace-reaper.mjs",
|
|
@@ -304,6 +317,8 @@
|
|
|
304
317
|
"workflow/workflow-engine.mjs",
|
|
305
318
|
"workflow/workflow-migration.mjs",
|
|
306
319
|
"workflow/workflow-nodes.mjs",
|
|
320
|
+
"workflow/workflow-nodes/transforms.mjs",
|
|
321
|
+
"workflow/workflow-nodes/definitions.mjs",
|
|
307
322
|
"workflow/workflow-nodes/custom-loader.mjs",
|
|
308
323
|
"workflow/project-detection.mjs",
|
|
309
324
|
"workflow/workflow-templates.mjs",
|