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.
Files changed (71) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-prompt-catalog.mjs +971 -0
  3. package/agent/agent-prompts.mjs +2 -970
  4. package/agent/agent-supervisor.mjs +6 -3
  5. package/agent/autofix-git.mjs +33 -0
  6. package/agent/autofix-prompts.mjs +151 -0
  7. package/agent/autofix.mjs +11 -175
  8. package/agent/bosun-skills.mjs +3 -2
  9. package/bosun.config.example.json +17 -0
  10. package/bosun.schema.json +87 -188
  11. package/cli.mjs +34 -1
  12. package/config/config-doctor.mjs +5 -250
  13. package/config/config-file-names.mjs +5 -0
  14. package/config/config.mjs +89 -493
  15. package/config/executor-config.mjs +493 -0
  16. package/config/repo-root.mjs +1 -2
  17. package/config/workspace-health.mjs +242 -0
  18. package/git/git-safety.mjs +15 -0
  19. package/github/github-oauth-portal.mjs +46 -0
  20. package/infra/library-manager-utils.mjs +22 -0
  21. package/infra/library-manager-well-known-sources.mjs +578 -0
  22. package/infra/library-manager.mjs +512 -1030
  23. package/infra/monitor.mjs +28 -9
  24. package/infra/session-tracker.mjs +10 -7
  25. package/kanban/kanban-adapter.mjs +17 -1
  26. package/lib/codebase-audit-manifests.mjs +117 -0
  27. package/lib/codebase-audit.mjs +18 -115
  28. package/package.json +18 -3
  29. package/server/ui-server.mjs +1194 -79
  30. package/shell/codex-config-file.mjs +178 -0
  31. package/shell/codex-config.mjs +538 -575
  32. package/task/task-cli.mjs +54 -3
  33. package/task/task-executor.mjs +143 -13
  34. package/task/task-store.mjs +409 -1
  35. package/telegram/telegram-bot.mjs +127 -0
  36. package/tools/apply-pr-suggestions.mjs +401 -0
  37. package/tools/syntax-check.mjs +21 -9
  38. package/ui/app.js +3 -14
  39. package/ui/components/kanban-board.js +227 -4
  40. package/ui/components/session-list.js +85 -5
  41. package/ui/demo-defaults.js +334 -80
  42. package/ui/demo.html +155 -0
  43. package/ui/modules/session-api.js +96 -0
  44. package/ui/modules/settings-schema.js +1 -2
  45. package/ui/modules/state.js +21 -3
  46. package/ui/setup.html +4 -5
  47. package/ui/styles/components.css +58 -4
  48. package/ui/tabs/agents.js +12 -15
  49. package/ui/tabs/control.js +1 -0
  50. package/ui/tabs/library.js +484 -22
  51. package/ui/tabs/manual-flows.js +105 -29
  52. package/ui/tabs/tasks.js +785 -140
  53. package/ui/tabs/telemetry.js +129 -11
  54. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  55. package/ui/tabs/workflows.js +293 -23
  56. package/voice/voice-tool-definitions.mjs +757 -0
  57. package/voice/voice-tools.mjs +34 -778
  58. package/workflow/manual-flow-audit.mjs +165 -0
  59. package/workflow/manual-flows.mjs +164 -259
  60. package/workflow/workflow-engine.mjs +147 -58
  61. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  62. package/workflow/workflow-nodes/transforms.mjs +612 -0
  63. package/workflow/workflow-nodes.mjs +304 -52
  64. package/workflow/workflow-templates.mjs +313 -191
  65. package/workflow-templates/_helpers.mjs +154 -0
  66. package/workflow-templates/agents.mjs +61 -4
  67. package/workflow-templates/code-quality.mjs +7 -7
  68. package/workflow-templates/github.mjs +20 -10
  69. package/workflow-templates/task-batch.mjs +20 -9
  70. package/workflow-templates/task-lifecycle.mjs +31 -6
  71. 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: ["template-task-lifecycle", "template-task-finalization-guard"],
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
- /* best-effort */
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
- /* best-effort */
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
- /* best-effort */
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
- autoArgs.push("--auto", "--squash");
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("--squash");
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
- id: event.id || `msg-${Date.now()}-${randomToken(6)}`,
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
- this.#markDirty(taskId);
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
+ }
@@ -7,7 +7,14 @@ import {
7
7
  statSync,
8
8
  writeFileSync,
9
9
  } from "node:fs";
10
- import { basename, dirname, extname, relative, resolve } from "node:path";
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
- function roleFromPath(relPath, content, category) {
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", buildSummary(file))),
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\(|require\(|importlib\.import_module/.test(segment)) {
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+|create|build|init|get|load)/.test(segment) || /sync\.Once|OnceLock|lazy_static!/.test(segment)) {
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 (/writeFile|appendFile|unlink|rmSync|mkdirSync|spawn\(|exec(?:File)?Sync|fork\(|process\.exit|std::fs::|std::process::Command|os\.WriteFile|os\.Remove|subprocess\./.test(segment)) {
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|getenv\(|std::env::var/.test(segment)) {
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, buildSummary(file))}`);
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.2",
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": "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",
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",