bosun 0.41.8 → 0.41.10

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 (58) hide show
  1. package/.env.example +1 -1
  2. package/README.md +23 -1
  3. package/agent/agent-event-bus.mjs +31 -2
  4. package/agent/agent-pool.mjs +275 -40
  5. package/agent/agent-prompts.mjs +9 -1
  6. package/agent/agent-supervisor.mjs +22 -0
  7. package/agent/autofix.mjs +1 -1
  8. package/agent/primary-agent.mjs +115 -5
  9. package/cli.mjs +3 -2
  10. package/config/config.mjs +47 -33
  11. package/config/context-shredding-config.mjs +1 -1
  12. package/config/repo-root.mjs +41 -33
  13. package/desktop/main.mjs +350 -25
  14. package/desktop/preload.cjs +8 -0
  15. package/desktop/preload.mjs +19 -0
  16. package/entrypoint.mjs +332 -0
  17. package/git/sdk-conflict-resolver.mjs +1 -1
  18. package/infra/health-status.mjs +72 -0
  19. package/infra/library-manager.mjs +58 -1
  20. package/infra/maintenance.mjs +1 -2
  21. package/infra/monitor.mjs +26 -8
  22. package/infra/session-tracker.mjs +30 -3
  23. package/package.json +12 -4
  24. package/server/bosun-mcp-server.mjs +1004 -0
  25. package/server/setup-web-server.mjs +288 -259
  26. package/server/ui-server.mjs +1323 -26
  27. package/shell/claude-shell.mjs +14 -1
  28. package/shell/codex-config.mjs +1 -1
  29. package/shell/codex-model-profiles.mjs +170 -30
  30. package/shell/codex-shell.mjs +63 -18
  31. package/shell/opencode-providers.mjs +20 -8
  32. package/task/task-executor.mjs +28 -0
  33. package/task/task-store.mjs +13 -4
  34. package/telegram/telegram-sentinel.mjs +54 -3
  35. package/tools/list-todos.mjs +7 -1
  36. package/ui/app.js +3 -2
  37. package/ui/components/agent-selector.js +127 -0
  38. package/ui/components/session-list.js +15 -10
  39. package/ui/demo-defaults.js +334 -336
  40. package/ui/modules/router.js +2 -0
  41. package/ui/modules/state.js +13 -5
  42. package/ui/tabs/chat.js +3 -0
  43. package/ui/tabs/library.js +284 -52
  44. package/ui/tabs/tasks.js +5 -13
  45. package/ui/tabs/workflows.js +766 -3
  46. package/workflow/workflow-engine.mjs +246 -5
  47. package/workflow/workflow-nodes/definitions.mjs +37 -0
  48. package/workflow/workflow-nodes.mjs +1014 -184
  49. package/workflow/workflow-templates.mjs +0 -5
  50. package/workflow-templates/_helpers.mjs +253 -0
  51. package/workflow-templates/agents.mjs +199 -226
  52. package/workflow-templates/github.mjs +106 -16
  53. package/workflow-templates/sub-workflows.mjs +233 -0
  54. package/workflow-templates/task-execution.mjs +125 -471
  55. package/workflow-templates/task-lifecycle.mjs +11 -48
  56. package/workspace/command-diagnostics.mjs +460 -0
  57. package/workspace/context-cache.mjs +396 -28
  58. package/workspace/worktree-manager.mjs +1 -1
@@ -11,6 +11,7 @@ import { ensureRepoConfigs, printRepoConfigSummary } from "../config/repo-config
11
11
  import { resolveRepoRoot } from "../config/repo-root.mjs";
12
12
  import { getAgentToolConfig, getEffectiveTools } from "./agent-tool-config.mjs";
13
13
  import { getSessionTracker } from "../infra/session-tracker.mjs";
14
+ import { getEntry, getEntryContent, resolveAgentProfileLibraryMetadata } from "../infra/library-manager.mjs";
14
15
  import { execPooledPrompt } from "./agent-pool.mjs";
15
16
  import {
16
17
  execCodexPrompt,
@@ -290,6 +291,108 @@ function buildPrimaryToolCapabilityContract(options = {}) {
290
291
  ].join("\n");
291
292
  }
292
293
 
294
+ function toStringArray(value) {
295
+ if (!Array.isArray(value)) return [];
296
+ return value.map((item) => String(item || "").trim()).filter(Boolean);
297
+ }
298
+
299
+ function resolveSelectedAgentProfileContext(rootDir, agentProfileId) {
300
+ const id = String(agentProfileId || "").trim();
301
+ if (!id) return null;
302
+ const entry = getEntry(rootDir, id);
303
+ if (!entry || entry.type !== "agent") return null;
304
+ const profile = getEntryContent(rootDir, entry);
305
+ if (!profile || typeof profile !== "object") return null;
306
+
307
+ const metadata = resolveAgentProfileLibraryMetadata(entry, profile);
308
+ const promptEntry = profile?.promptOverride ? getEntry(rootDir, profile.promptOverride) : null;
309
+ const promptContent = promptEntry ? getEntryContent(rootDir, promptEntry) : null;
310
+ const skills = toStringArray(profile?.skills)
311
+ .map((skillId) => {
312
+ const skillEntry = getEntry(rootDir, skillId);
313
+ if (!skillEntry || skillEntry.type !== "skill") return null;
314
+ return {
315
+ id: skillEntry.id,
316
+ name: skillEntry.name || skillEntry.id,
317
+ content: String(getEntryContent(rootDir, skillEntry) || "").trim(),
318
+ };
319
+ })
320
+ .filter(Boolean);
321
+
322
+ return {
323
+ id: entry.id,
324
+ name: entry.name || entry.id,
325
+ description: entry.description || "",
326
+ profile,
327
+ metadata,
328
+ promptOverride: promptEntry
329
+ ? {
330
+ id: promptEntry.id,
331
+ name: promptEntry.name || promptEntry.id,
332
+ content: typeof promptContent === "string"
333
+ ? promptContent.trim()
334
+ : String(promptContent || "").trim(),
335
+ }
336
+ : null,
337
+ skills,
338
+ };
339
+ }
340
+
341
+ function buildPrimaryAgentProfileContract(options = {}) {
342
+ let rootDir = "";
343
+ try {
344
+ rootDir = String(options.cwd || resolveRepoRoot() || process.cwd()).trim();
345
+ } catch {
346
+ rootDir = String(options.cwd || process.cwd()).trim();
347
+ }
348
+ const selected = resolveSelectedAgentProfileContext(rootDir, options.agentProfileId);
349
+ if (!selected) return { block: "", preferredMode: "", preferredModel: "" };
350
+
351
+ const profileInstructions = String(
352
+ selected.profile?.instructions
353
+ || selected.profile?.manualInstructions
354
+ || selected.profile?.voiceInstructions
355
+ || "",
356
+ ).trim();
357
+ const summary = {
358
+ id: selected.id,
359
+ name: selected.name,
360
+ description: selected.description,
361
+ agentCategory: selected.metadata.agentCategory,
362
+ interactiveMode: selected.metadata.interactiveMode,
363
+ interactiveLabel: selected.metadata.interactiveLabel,
364
+ sdk: String(selected.profile?.sdk || "").trim() || null,
365
+ model: String(selected.profile?.model || "").trim() || null,
366
+ showInChatDropdown: selected.metadata.showInChatDropdown,
367
+ skillIds: selected.skills.map((skill) => skill.id),
368
+ };
369
+ const lines = [
370
+ "## Selected Agent Profile",
371
+ "Apply this profile consistently unless the user explicitly overrides it.",
372
+ "```json",
373
+ JSON.stringify(summary, null, 2),
374
+ "```",
375
+ ];
376
+ if (profileInstructions) {
377
+ lines.push("## Profile Instructions", profileInstructions);
378
+ }
379
+ if (selected.promptOverride?.content) {
380
+ lines.push(`## Prompt Override: ${selected.promptOverride.name}`, selected.promptOverride.content);
381
+ }
382
+ if (selected.skills.length > 0) {
383
+ lines.push("## Profile Skills");
384
+ for (const skill of selected.skills) {
385
+ if (!skill.content) continue;
386
+ lines.push(`### ${skill.name}`, skill.content);
387
+ }
388
+ }
389
+ return {
390
+ block: lines.join("\n\n"),
391
+ preferredMode: String(selected.metadata.interactiveMode || "").trim(),
392
+ preferredModel: String(selected.profile?.model || "").trim(),
393
+ };
394
+ }
395
+
293
396
  const ADAPTERS = {
294
397
  "codex-sdk": {
295
398
  name: "codex-sdk",
@@ -933,13 +1036,18 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
933
1036
  if (!initialized) {
934
1037
  await initPrimaryAgent();
935
1038
  }
1039
+ const selectedProfile = buildPrimaryAgentProfileContract(options);
936
1040
  const sessionId =
937
1041
  (options && options.sessionId ? String(options.sessionId) : "") ||
938
1042
  `primary-${activeAdapter.name}`;
939
1043
  const sessionType =
940
1044
  (options && options.sessionType ? String(options.sessionType) : "") ||
941
1045
  "primary";
942
- const effectiveMode = normalizeAgentMode(options.mode || agentMode, agentMode);
1046
+ const effectiveMode = normalizeAgentMode(
1047
+ options.mode || selectedProfile.preferredMode || agentMode,
1048
+ agentMode,
1049
+ );
1050
+ const effectiveModel = options.model || selectedProfile.preferredModel || undefined;
943
1051
  const modePolicy = getModeExecPolicy(effectiveMode);
944
1052
  const timeoutMs = options.timeoutMs || modePolicy?.timeoutMs || PRIMARY_EXEC_TIMEOUT_MS;
945
1053
  const maxFailoverAttempts = Number.isInteger(options.maxFailoverAttempts)
@@ -955,7 +1063,9 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
955
1063
  ? appendAttachmentsToPrompt(userMessage, attachments).message
956
1064
  : userMessage;
957
1065
  const toolContract = buildPrimaryToolCapabilityContract(options);
958
- const messageWithToolContract = `${toolContract}\n\n${messageWithAttachments}`;
1066
+ const messageWithToolContract = [selectedProfile.block, toolContract, messageWithAttachments]
1067
+ .filter(Boolean)
1068
+ .join("\n\n");
959
1069
  const framedMessage = modePrefix ? modePrefix + messageWithToolContract : messageWithToolContract;
960
1070
 
961
1071
  // Record user message (original, without mode prefix)
@@ -974,7 +1084,7 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
974
1084
  onEvent: options.onEvent,
975
1085
  abortController: options.abortController,
976
1086
  cwd: options.cwd,
977
- model: options.model,
1087
+ model: effectiveModel,
978
1088
  sdk: mapAdapterToPoolSdk(activeAdapter.name),
979
1089
  sessionType,
980
1090
  });
@@ -1064,7 +1174,7 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
1064
1174
  }
1065
1175
  }
1066
1176
  const result = await withTimeout(
1067
- adapter.exec(framedMessage, { ...options, sessionId, abortController: timeoutAbort }),
1177
+ adapter.exec(framedMessage, { ...options, sessionId, model: effectiveModel, abortController: timeoutAbort }),
1068
1178
  timeoutMs,
1069
1179
  `${adapterName}.exec`,
1070
1180
  timeoutAbort,
@@ -1133,7 +1243,7 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
1133
1243
  }
1134
1244
  }
1135
1245
  const retryResult = await withTimeout(
1136
- adapter.exec(framedMessage, { ...options, sessionId, abortController: timeoutAbort }),
1246
+ adapter.exec(framedMessage, { ...options, sessionId, model: effectiveModel, abortController: timeoutAbort }),
1137
1247
  timeoutMs,
1138
1248
  `${adapterName}.exec.retry`,
1139
1249
  timeoutAbort,
package/cli.mjs CHANGED
@@ -79,6 +79,7 @@ function showHelp() {
79
79
  COMMANDS
80
80
  workflow list List declarative pipeline workflows
81
81
  workflow run <name> Run a declarative pipeline workflow
82
+ audit <command> Run codebase annotation audit tools (scan|generate|warn|manifest|index|trim|conformity|migrate)
82
83
  --setup Launch the web-based setup wizard (default)
83
84
  --setup-terminal Run the legacy terminal setup wizard
84
85
  --where Show the resolved bosun config directory
@@ -1368,8 +1369,8 @@ async function main() {
1368
1369
  const { runAuditCli } = await import("./lib/codebase-audit.mjs");
1369
1370
  const commandStartIndex = auditCommandIndex >= 0 ? auditCommandIndex : auditFlagIndex;
1370
1371
  const auditArgs = args.slice(commandStartIndex + 1);
1371
- await runAuditCli(auditArgs);
1372
- process.exit(0);
1372
+ const { exitCode } = await runAuditCli(auditArgs);
1373
+ process.exit(exitCode);
1373
1374
  }
1374
1375
 
1375
1376
  if (args[0] === "node:create" || (args[0] === "node" && args[1] === "create")) {
package/config/config.mjs CHANGED
@@ -711,9 +711,15 @@ function detectRepoSlug(repoRoot = "") {
711
711
  const direct = tryResolve(process.cwd());
712
712
  if (direct) return direct;
713
713
 
714
- // Fall back to detected repo root if provided (or detectable)
715
- const root = repoRoot || detectRepoRoot();
716
- if (root) {
714
+ // Fall back to detected repo root if provided
715
+ if (repoRoot) {
716
+ const viaRoot = tryResolve(repoRoot);
717
+ if (viaRoot) return viaRoot;
718
+ }
719
+
720
+ // Last resort — use cached detectRepoRoot (avoids redundant subprocess calls)
721
+ const root = detectRepoRoot();
722
+ if (root && root !== process.cwd()) {
717
723
  const viaRoot = tryResolve(root);
718
724
  if (viaRoot) return viaRoot;
719
725
  }
@@ -721,7 +727,16 @@ function detectRepoSlug(repoRoot = "") {
721
727
  return null;
722
728
  }
723
729
 
730
+ let _detectRepoRootCache = null;
731
+
724
732
  function detectRepoRoot() {
733
+ if (_detectRepoRootCache) return _detectRepoRootCache;
734
+ const result = _detectRepoRootUncached();
735
+ _detectRepoRootCache = result;
736
+ return result;
737
+ }
738
+
739
+ function _detectRepoRootUncached() {
725
740
  const gitExecOptions = {
726
741
  encoding: "utf8",
727
742
  stdio: ["pipe", "pipe", "ignore"],
@@ -734,7 +749,30 @@ function detectRepoRoot() {
734
749
  if (existsSync(envRoot)) return envRoot;
735
750
  }
736
751
 
737
- // 2. Try git from cwd
752
+ // 2. Check bosun config for workspace repos FIRST — this is the primary
753
+ // source of truth and works regardless of whether cwd is inside a git repo.
754
+ const configDirs = getConfigSearchDirs();
755
+ let fallbackRepo = null;
756
+ for (const cfgName of CONFIG_FILES) {
757
+ for (const configDir of configDirs) {
758
+ const cfgPath = resolve(configDir, cfgName);
759
+ if (!existsSync(cfgPath)) continue;
760
+ try {
761
+ const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
762
+ const repoPaths = collectRepoPathsFromConfig(cfg, configDir);
763
+ for (const repoPath of repoPaths) {
764
+ if (!repoPath || !existsSync(repoPath)) continue;
765
+ if (existsSync(resolve(repoPath, ".git"))) return repoPath;
766
+ fallbackRepo ??= repoPath;
767
+ }
768
+ } catch {
769
+ /* invalid config */
770
+ }
771
+ }
772
+ }
773
+ if (fallbackRepo) return fallbackRepo;
774
+
775
+ // 3. Try git from cwd
738
776
  try {
739
777
  const gitRoot = execSync("git rev-parse --show-toplevel", {
740
778
  ...gitExecOptions,
@@ -744,7 +782,7 @@ function detectRepoRoot() {
744
782
  // not in a git repo from cwd
745
783
  }
746
784
 
747
- // 3. Bosun package directory may be inside a repo (common: scripts/bosun/ within a project)
785
+ // 4. Bosun package directory may be inside a repo (common: scripts/bosun/ within a project)
748
786
  try {
749
787
  const gitRoot = execSync("git rev-parse --show-toplevel", {
750
788
  cwd: __dirname,
@@ -755,7 +793,7 @@ function detectRepoRoot() {
755
793
  // bosun installed standalone, not in a repo
756
794
  }
757
795
 
758
- // 4. Module root detection — when bosun is installed as a standalone npm package,
796
+ // 5. Module root detection — when bosun is installed as a standalone npm package,
759
797
  // use the module root directory as a stable base for config resolution.
760
798
  const moduleRoot = detectBosunModuleRoot();
761
799
  if (moduleRoot && moduleRoot !== process.cwd()) {
@@ -770,33 +808,9 @@ function detectRepoRoot() {
770
808
  }
771
809
  }
772
810
 
773
- // 5. Check bosun config for workspace repos
774
- const configDirs = getConfigSearchDirs();
775
- let fallbackRepo = null;
776
- for (const cfgName of CONFIG_FILES) {
777
- for (const configDir of configDirs) {
778
- const cfgPath = resolve(configDir, cfgName);
779
- if (!existsSync(cfgPath)) continue;
780
- try {
781
- const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
782
- const repoPaths = collectRepoPathsFromConfig(cfg, configDir);
783
- for (const repoPath of repoPaths) {
784
- if (!repoPath || !existsSync(repoPath)) continue;
785
- if (existsSync(resolve(repoPath, ".git"))) return repoPath;
786
- fallbackRepo ??= repoPath;
787
- }
788
- } catch {
789
- /* invalid config */
790
- }
791
- }
792
- }
793
- if (fallbackRepo) return fallbackRepo;
794
-
795
- // 6. Final fallback — warn and return cwd.
796
- // git repo (e.g. when the daemon spawns with cwd=homedir), but returning
797
- // null would crash downstream callers like resolve(repoRoot). The warning
798
- // helps diagnose "not a git repository" errors from child processes.
799
- console.warn("[config] detectRepoRoot: no git repository found — falling back to cwd:", process.cwd());
811
+ // 6. Final fallback — return cwd silently. The config system resolves
812
+ // repos from BOSUN_HOME workspaces, so a missing git repo in cwd is
813
+ // expected for globally-installed usage and daemon spawns.
800
814
  return process.cwd();
801
815
  }
802
816
 
@@ -147,7 +147,7 @@ export const DEFAULT_SHREDDING_CONFIG = Object.freeze({
147
147
  userMsgFullTurns: 1, // only the current turn keeps the full user prompt
148
148
 
149
149
  // ── Live command/tool compaction ─────────────────────────────
150
- liveToolCompactionEnabled: false,
150
+ liveToolCompactionEnabled: true,
151
151
  liveToolCompactionMode: "auto",
152
152
  liveToolCompactionMinChars: 4000,
153
153
  liveToolCompactionTargetChars: 1800,
@@ -98,40 +98,9 @@ export function resolveRepoRoot(options = {}) {
98
98
 
99
99
  const cwd = options.cwd || process.cwd();
100
100
 
101
- // Try git from cwd
102
- try {
103
- const gitRoot = execSync("git rev-parse --show-toplevel", {
104
- encoding: "utf8",
105
- cwd,
106
- stdio: ["ignore", "pipe", "ignore"],
107
- }).trim();
108
- if (gitRoot) return gitRoot;
109
- } catch {
110
- // ignore - fall back
111
- }
112
-
113
- // Try git from the bosun package directory (may be inside a repo)
114
- try {
115
- const gitRoot = execSync("git rev-parse --show-toplevel", {
116
- encoding: "utf8",
117
- cwd: __dirname,
118
- stdio: ["ignore", "pipe", "ignore"],
119
- }).trim();
120
- if (gitRoot) return gitRoot;
121
- } catch {
122
- // bosun installed standalone
123
- }
124
-
125
- // Check if __dirname itself is the bosun module root (installed as npm package)
126
- const moduleRoot = detectBosunModuleRoot();
127
- if (moduleRoot && moduleRoot !== cwd && existsSync(resolve(moduleRoot, "package.json"))) {
128
- // When bosun is installed globally/locally outside a git repo, use its directory
129
- // as a reference point if no git root was found above.
130
- // Only use it if it's a valid directory on disk (don't return the fallback as a repo root).
131
- }
132
-
133
- // Check bosun config for workspace repos
101
+ // Check bosun config for workspace repos FIRST — works regardless of cwd
134
102
  const configDirs = [...getConfigSearchDirs(), __dirname];
103
+ if (process.env.BOSUN_HOME) configDirs.unshift(resolve(process.env.BOSUN_HOME));
135
104
  let fallbackRepo = null;
136
105
  for (const cfgName of CONFIG_FILES) {
137
106
  for (const dir of configDirs) {
@@ -139,6 +108,21 @@ export function resolveRepoRoot(options = {}) {
139
108
  if (!existsSync(cfgPath)) continue;
140
109
  try {
141
110
  const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
111
+ // Check workspace repos (higher priority — explicit workspace configuration)
112
+ const workspaces = Array.isArray(cfg.workspaces) ? cfg.workspaces : [];
113
+ for (const ws of workspaces) {
114
+ const wsRepos = ws?.repos || ws?.repositories || [];
115
+ if (!Array.isArray(wsRepos)) continue;
116
+ const wsId = ws?.id || "default";
117
+ for (const repo of wsRepos) {
118
+ const name = typeof repo === "string" ? repo.split("/").pop() : (repo?.name || repo?.id || "");
119
+ if (!name) continue;
120
+ const repoPath = resolve(dir, "workspaces", wsId, name);
121
+ if (existsSync(resolve(repoPath, ".git"))) return repoPath;
122
+ if (existsSync(repoPath)) fallbackRepo ??= repoPath;
123
+ }
124
+ }
125
+ // Check flat repositories
142
126
  const repos = cfg.repositories || cfg.repos || [];
143
127
  if (Array.isArray(repos) && repos.length > 0) {
144
128
  const primary = repos.find((r) => r.primary) || repos[0];
@@ -155,6 +139,30 @@ export function resolveRepoRoot(options = {}) {
155
139
  }
156
140
  if (fallbackRepo) return fallbackRepo;
157
141
 
142
+ // Try git from cwd
143
+ try {
144
+ const gitRoot = execSync("git rev-parse --show-toplevel", {
145
+ encoding: "utf8",
146
+ cwd,
147
+ stdio: ["ignore", "pipe", "ignore"],
148
+ }).trim();
149
+ if (gitRoot) return gitRoot;
150
+ } catch {
151
+ // ignore - fall back
152
+ }
153
+
154
+ // Try git from the bosun package directory (may be inside a repo)
155
+ try {
156
+ const gitRoot = execSync("git rev-parse --show-toplevel", {
157
+ encoding: "utf8",
158
+ cwd: __dirname,
159
+ stdio: ["ignore", "pipe", "ignore"],
160
+ }).trim();
161
+ if (gitRoot) return gitRoot;
162
+ } catch {
163
+ // bosun installed standalone
164
+ }
165
+
158
166
  return resolve(cwd);
159
167
  }
160
168