bosun 0.41.2 → 0.41.3
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-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +6 -3
- 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 +28 -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/ui-server.mjs +1194 -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 +21 -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 +334 -80
- 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 +21 -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 +785 -140
- 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 +304 -52
- 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 +20 -9
- 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(
|
|
@@ -6867,8 +6871,8 @@ async function checkMergedPRsAndUpdateTasks() {
|
|
|
6867
6871
|
);
|
|
6868
6872
|
try {
|
|
6869
6873
|
setInternalTaskStatus(taskId, "done", "review-merge-reconcile");
|
|
6870
|
-
} catch {
|
|
6871
|
-
|
|
6874
|
+
} catch (internalErr) {
|
|
6875
|
+
console.warn(`[monitor] review reconcile: setInternalTaskStatus failed for ${taskId}: ${internalErr?.message?.slice(0, 200)}`);
|
|
6872
6876
|
}
|
|
6873
6877
|
try {
|
|
6874
6878
|
updateInternalTask(taskId, {
|
|
@@ -6879,20 +6883,23 @@ async function checkMergedPRsAndUpdateTasks() {
|
|
|
6879
6883
|
resolvedRepoSlug ||
|
|
6880
6884
|
undefined,
|
|
6881
6885
|
});
|
|
6882
|
-
} catch {
|
|
6883
|
-
|
|
6886
|
+
} catch (metaErr) {
|
|
6887
|
+
console.warn(`[monitor] review reconcile: updateInternalTask failed for ${taskId}: ${metaErr?.message?.slice(0, 200)}`);
|
|
6884
6888
|
}
|
|
6885
6889
|
try {
|
|
6886
6890
|
await updateTaskStatus(taskId, "done", {
|
|
6887
6891
|
source: "review-merge-reconcile",
|
|
6892
|
+
bypassWorkflowOwnership: true,
|
|
6888
6893
|
workflowData: {
|
|
6889
6894
|
prNumber,
|
|
6890
6895
|
prUrl,
|
|
6891
6896
|
repository: resolvedRepoSlug || task?.repository || null,
|
|
6892
6897
|
},
|
|
6893
6898
|
});
|
|
6894
|
-
} catch {
|
|
6895
|
-
|
|
6899
|
+
} catch (reconcileErr) {
|
|
6900
|
+
console.warn(
|
|
6901
|
+
`[monitor] review reconcile: failed to update kanban status for ${taskId}: ${reconcileErr?.message?.slice(0, 200)}`,
|
|
6902
|
+
);
|
|
6896
6903
|
}
|
|
6897
6904
|
summary.movedDone += 1;
|
|
6898
6905
|
}
|
|
@@ -8009,7 +8016,8 @@ async function triggerFlowPostReviewMerge(taskId, context = {}) {
|
|
|
8009
8016
|
const autoArgs = ["pr", "merge", String(prNumber)];
|
|
8010
8017
|
if (resolvedRepoSlug) autoArgs.push("--repo", resolvedRepoSlug);
|
|
8011
8018
|
autoArgs.push("--body", buildFlowGateMergeBody(taskTitle, id));
|
|
8012
|
-
|
|
8019
|
+
const mergeMethod = process.env.BOSUN_MERGE_METHOD || "merge";
|
|
8020
|
+
autoArgs.push("--auto", `--${mergeMethod}`);
|
|
8013
8021
|
|
|
8014
8022
|
const autoResult = spawnSync("gh", autoArgs, {
|
|
8015
8023
|
cwd: repoRoot,
|
|
@@ -8031,7 +8039,7 @@ async function triggerFlowPostReviewMerge(taskId, context = {}) {
|
|
|
8031
8039
|
const directArgs = ["pr", "merge", String(prNumber)];
|
|
8032
8040
|
if (resolvedRepoSlug) directArgs.push("--repo", resolvedRepoSlug);
|
|
8033
8041
|
directArgs.push("--body", buildFlowGateMergeBody(taskTitle, id));
|
|
8034
|
-
directArgs.push(
|
|
8042
|
+
directArgs.push(`--${mergeMethod}`);
|
|
8035
8043
|
const directResult = spawnSync("gh", directArgs, {
|
|
8036
8044
|
cwd: repoRoot,
|
|
8037
8045
|
encoding: "utf8",
|
|
@@ -14774,6 +14782,17 @@ async function syncDivergedWorktrees() {
|
|
|
14774
14782
|
continue;
|
|
14775
14783
|
}
|
|
14776
14784
|
|
|
14785
|
+
// Safety: refuse to push if HEAD now equals origin/main (would wipe PR changes)
|
|
14786
|
+
try {
|
|
14787
|
+
const headSha = execSync("git rev-parse HEAD", { cwd: wtPath, encoding: "utf8", timeout: 5_000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
14788
|
+
const mainSha = execSync("git rev-parse origin/main", { cwd: wtPath, encoding: "utf8", timeout: 5_000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
14789
|
+
if (headSha === mainSha) {
|
|
14790
|
+
console.warn(`[monitor:worktree-sync] ${branch} HEAD matches origin/main after rebase — aborting push to prevent PR wipe`);
|
|
14791
|
+
failed++;
|
|
14792
|
+
continue;
|
|
14793
|
+
}
|
|
14794
|
+
} catch { /* best-effort check */ }
|
|
14795
|
+
|
|
14777
14796
|
// Push with --force-with-lease (safe: we just fetched fresh remote refs)
|
|
14778
14797
|
try {
|
|
14779
14798
|
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.3",
|
|
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",
|