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
@@ -21,11 +21,14 @@ import {
21
21
  rmSync,
22
22
  statSync,
23
23
  readdirSync,
24
+ symlinkSync,
24
25
  } from "node:fs";
25
26
  import { readFile, writeFile, mkdir } from "node:fs/promises";
26
- import { resolve } from "node:path";
27
+ import { dirname, resolve } from "node:path";
27
28
  import { fileURLToPath } from "node:url";
29
+ import { loadConfig } from "../config/config.mjs";
28
30
  import { sanitizeGitEnv } from "../git/git-safety.mjs";
31
+ import { detectProjectStack } from "../workflow/project-detection.mjs";
29
32
 
30
33
  // ── Path Setup ──────────────────────────────────────────────────────────────
31
34
 
@@ -56,6 +59,18 @@ const GIT_ENV = {
56
59
  GIT_MERGE_AUTOEDIT: "no",
57
60
  GIT_TERMINAL_PROMPT: "0",
58
61
  };
62
+ const DEFAULT_WORKTREE_BOOTSTRAP = Object.freeze({
63
+ enabled: true,
64
+ linkSharedPaths: true,
65
+ commandTimeoutMs: 10 * 60 * 1000,
66
+ commandsByStack: Object.freeze({}),
67
+ sharedPathsByStack: Object.freeze({}),
68
+ });
69
+ const DEFAULT_SHARED_PATHS_BY_STACK = Object.freeze({
70
+ node: Object.freeze(["node_modules"]),
71
+ php: Object.freeze(["vendor"]),
72
+ ruby: Object.freeze(["vendor/bundle"]),
73
+ });
59
74
 
60
75
  /**
61
76
  * Guard against git config corruption caused by worktree operations.
@@ -127,6 +142,217 @@ function sanitizeBranchName(branch) {
127
142
  return safe.slice(0, 60); // Windows MAX_PATH is 260, worktree base path ~60, leaves ~140 for this + git overhead
128
143
  }
129
144
 
145
+ function normalizeStringList(value) {
146
+ const source = Array.isArray(value) ? value : [value];
147
+ const values = [];
148
+ for (const entry of source) {
149
+ const normalized = String(entry || "").trim();
150
+ if (!normalized || values.includes(normalized)) continue;
151
+ values.push(normalized);
152
+ }
153
+ return values;
154
+ }
155
+
156
+ function freezePlainObject(value) {
157
+ return Object.freeze({ ...(value && typeof value === "object" ? value : {}) });
158
+ }
159
+
160
+ function withIsolatedEnv(callback) {
161
+ const originalEnv = { ...process.env };
162
+ try {
163
+ return callback();
164
+ } finally {
165
+ // Remove any keys that were added during callback execution.
166
+ for (const key of Object.keys(process.env)) {
167
+ if (!(key in originalEnv)) {
168
+ delete process.env[key];
169
+ }
170
+ }
171
+ // Restore original keys and their values, including any that were deleted.
172
+ for (const [key, value] of Object.entries(originalEnv)) {
173
+ process.env[key] = value;
174
+ }
175
+ }
176
+ }
177
+
178
+ function readWorktreeBootstrapConfig(repoRoot) {
179
+ try {
180
+ const config = withIsolatedEnv(() =>
181
+ loadConfig(["node", "bosun", "--repo-root", repoRoot]),
182
+ );
183
+ if (config?.worktreeBootstrap && typeof config.worktreeBootstrap === "object") {
184
+ return config.worktreeBootstrap;
185
+ }
186
+ } catch (error) {
187
+ console.warn(
188
+ `${TAG} failed to load worktree bootstrap config: ${error?.message || error}`,
189
+ );
190
+ }
191
+ return DEFAULT_WORKTREE_BOOTSTRAP;
192
+ }
193
+
194
+ function resolveWorktreeSharedPaths(policy, stackId) {
195
+ const override = policy?.sharedPathsByStack?.[stackId];
196
+ if (Array.isArray(override) && override.length > 0) return override;
197
+ return DEFAULT_SHARED_PATHS_BY_STACK[stackId] || [];
198
+ }
199
+
200
+ function resolveDefaultBootstrapCommand(stack, worktreePath) {
201
+ const packageManager = String(stack?.packageManager || "").trim().toLowerCase();
202
+ switch (stack?.id) {
203
+ case "node":
204
+ if (packageManager === "pnpm") return "pnpm install";
205
+ if (packageManager === "yarn") return "yarn install";
206
+ if (packageManager === "bun") return "bun install";
207
+ return "npm install";
208
+ case "python":
209
+ if (packageManager === "poetry") return "poetry install --no-interaction";
210
+ if (packageManager === "uv") return "uv sync";
211
+ if (packageManager === "pipenv") return "pipenv install --dev";
212
+ if (packageManager === "pdm") return "pdm install";
213
+ return existsSync(resolve(worktreePath, "requirements.txt"))
214
+ ? "python -m pip install -r requirements.txt"
215
+ : "python -m pip install -e .";
216
+ case "go":
217
+ return "go mod download";
218
+ case "rust":
219
+ return "cargo fetch";
220
+ case "java":
221
+ if (packageManager === "gradle") {
222
+ if (process.platform === "win32" && existsSync(resolve(worktreePath, "gradlew.bat"))) {
223
+ return "gradlew.bat dependencies";
224
+ }
225
+ return existsSync(resolve(worktreePath, "gradlew"))
226
+ ? "./gradlew dependencies"
227
+ : "gradle dependencies";
228
+ }
229
+ return "mvn -q -DskipTests dependency:go-offline";
230
+ case "dotnet":
231
+ return "dotnet restore";
232
+ case "ruby":
233
+ return "bundle install";
234
+ case "php":
235
+ return "composer install";
236
+ default:
237
+ return "";
238
+ }
239
+ }
240
+
241
+ function buildBootstrapPlan(worktreePath, policy, detection, repoRoot) {
242
+ const sharedPaths = [];
243
+ const commands = [];
244
+ for (const stack of detection?.stacks || []) {
245
+ const stackSharedPaths = policy?.linkSharedPaths
246
+ ? resolveWorktreeSharedPaths(policy, stack.id)
247
+ : [];
248
+ for (const relativePath of stackSharedPaths) {
249
+ if (!sharedPaths.includes(relativePath)) sharedPaths.push(relativePath);
250
+ }
251
+ const overrideCommands = normalizeStringList(policy?.commandsByStack?.[stack.id]);
252
+ const stackCommands = overrideCommands.length > 0
253
+ ? overrideCommands
254
+ : normalizeStringList(resolveDefaultBootstrapCommand(stack, worktreePath));
255
+ const hasReadySharedPathsInWorktree =
256
+ stackSharedPaths.length > 0 &&
257
+ stackSharedPaths.every((relativePath) =>
258
+ existsSync(resolve(worktreePath, relativePath)),
259
+ );
260
+ let willLinkSharedPathsFromRepoRoot = false;
261
+ if (!hasReadySharedPathsInWorktree && policy?.linkSharedPaths && repoRoot) {
262
+ willLinkSharedPathsFromRepoRoot =
263
+ stackSharedPaths.length > 0 &&
264
+ stackSharedPaths.every((relativePath) => {
265
+ const sourcePath = resolve(repoRoot, relativePath);
266
+ const targetPath = resolve(worktreePath, relativePath);
267
+ return existsSync(sourcePath) && !existsSync(targetPath);
268
+ });
269
+ }
270
+ const hasReadySharedPaths =
271
+ hasReadySharedPathsInWorktree || willLinkSharedPathsFromRepoRoot;
272
+ if (hasReadySharedPaths) continue;
273
+ for (const command of stackCommands) {
274
+ if (!commands.includes(command)) commands.push(command);
275
+ }
276
+ }
277
+ return {
278
+ sharedPaths,
279
+ commands,
280
+ stacks: (detection?.stacks || []).map((stack) => stack.id),
281
+ };
282
+ }
283
+
284
+ function ensureWorktreeSharedPath(repoRoot, worktreePath, relativePath) {
285
+ const sourcePath = resolve(repoRoot, relativePath);
286
+ const targetPath = resolve(worktreePath, relativePath);
287
+ if (!existsSync(sourcePath) || existsSync(targetPath)) {
288
+ return false;
289
+ }
290
+
291
+ try {
292
+ mkdirSync(dirname(targetPath), { recursive: true });
293
+ let linkType = process.platform === "win32" ? "junction" : "dir";
294
+ try {
295
+ const sourceStats = statSync(sourcePath);
296
+ linkType = sourceStats.isDirectory()
297
+ ? process.platform === "win32" ? "junction" : "dir"
298
+ : "file";
299
+ } catch {
300
+ // In tests or partial checkouts the path may be mocked/exist logically without a stat-able inode.
301
+ }
302
+ symlinkSync(
303
+ sourcePath,
304
+ targetPath,
305
+ linkType,
306
+ );
307
+ return true;
308
+ } catch (error) {
309
+ console.warn(
310
+ `${TAG} failed to link ${relativePath} into worktree ${worktreePath}: ${error?.message || error}`,
311
+ );
312
+ return false;
313
+ }
314
+ }
315
+
316
+ function ensureWorktreeSharedPaths(repoRoot, worktreePath, relativePaths = []) {
317
+ const linkedPaths = [];
318
+ for (const relativePath of relativePaths) {
319
+ if (ensureWorktreeSharedPath(repoRoot, worktreePath, relativePath)) {
320
+ linkedPaths.push(relativePath);
321
+ }
322
+ }
323
+ return linkedPaths;
324
+ }
325
+
326
+ function executeWorktreeBootstrapCommand(command, worktreePath, timeoutMs) {
327
+ const result = spawnSync(command, {
328
+ cwd: worktreePath,
329
+ encoding: "utf8",
330
+ timeout: timeoutMs,
331
+ windowsHide: true,
332
+ env: process.env,
333
+ stdio: ["ignore", "pipe", "pipe"],
334
+ shell: true,
335
+ });
336
+ if (result.status === 0) return true;
337
+ const stderr = String(result.stderr || result.stdout || "").trim();
338
+ console.warn(
339
+ `${TAG} bootstrap command failed in ${worktreePath}: ${command}${stderr ? ` :: ${stderr}` : ""}`,
340
+ );
341
+ return false;
342
+ }
343
+
344
+ function buildBootstrapSignature(plan) {
345
+ return JSON.stringify({
346
+ stacks: plan.stacks || [],
347
+ sharedPaths: plan.sharedPaths || [],
348
+ commands: plan.commands || [],
349
+ });
350
+ }
351
+
352
+ function ensureWorktreeNodeModules(repoRoot, worktreePath) {
353
+ ensureWorktreeSharedPath(repoRoot, worktreePath, "node_modules");
354
+ }
355
+
130
356
  /**
131
357
  * Build the env object for all git subprocess calls.
132
358
  * @returns {NodeJS.ProcessEnv}
@@ -308,6 +534,7 @@ class WorktreeManager {
308
534
  /** @type {Map<string, WorktreeRecord>} keyed by taskKey (or auto-generated key) */
309
535
  this.registry = new Map();
310
536
  this._loaded = false;
537
+ this._worktreeBootstrapConfig = null;
311
538
  }
312
539
 
313
540
  // ── Registry Persistence ────────────────────────────────────────────────
@@ -359,6 +586,44 @@ class WorktreeManager {
359
586
  }
360
587
  }
361
588
 
589
+ getWorktreeBootstrapConfig() {
590
+ if (!this._worktreeBootstrapConfig) {
591
+ this._worktreeBootstrapConfig = readWorktreeBootstrapConfig(this.repoRoot);
592
+ }
593
+ return this._worktreeBootstrapConfig;
594
+ }
595
+
596
+ bootstrapWorktree(worktreePath, record = null) {
597
+ const policy = this.getWorktreeBootstrapConfig();
598
+ if (!policy?.enabled) return;
599
+ const detection = detectProjectStack(worktreePath);
600
+ if (!detection?.primary) return;
601
+
602
+ const plan = buildBootstrapPlan(worktreePath, policy, detection);
603
+ ensureWorktreeSharedPaths(this.repoRoot, worktreePath, plan.sharedPaths);
604
+
605
+ const signature = buildBootstrapSignature(plan);
606
+ if (record?.bootstrapState?.signature === signature) {
607
+ return;
608
+ }
609
+
610
+ let bootstrapSucceeded = true;
611
+ for (const command of plan.commands) {
612
+ if (!executeWorktreeBootstrapCommand(command, worktreePath, policy.commandTimeoutMs)) {
613
+ bootstrapSucceeded = false;
614
+ break;
615
+ }
616
+ }
617
+ if (!bootstrapSucceeded || !record) return;
618
+
619
+ record.bootstrapState = freezePlainObject({
620
+ signature,
621
+ completedAt: new Date().toISOString(),
622
+ stacks: plan.stacks,
623
+ commands: plan.commands,
624
+ });
625
+ }
626
+
362
627
  // ── Core Operations ─────────────────────────────────────────────────────
363
628
 
364
629
  /**
@@ -378,6 +643,7 @@ class WorktreeManager {
378
643
  // 1. Check if a worktree already exists for this branch
379
644
  const existingPath = this.findWorktreeForBranch(normalizedBranch);
380
645
  if (existingPath) {
646
+ let recordForBootstrap = null;
381
647
  // Update registry with the (possibly new) taskKey
382
648
  const existingKey = this._findKeyByPath(existingPath);
383
649
  if (existingKey && existingKey !== taskKey) {
@@ -389,10 +655,11 @@ class WorktreeManager {
389
655
  record.lastUsedAt = Date.now();
390
656
  record.owner = opts.owner ?? record.owner;
391
657
  this.registry.set(taskKey, record);
658
+ recordForBootstrap = record;
392
659
  }
393
660
  } else if (!existingKey) {
394
661
  // Not tracked — register it now
395
- this.registry.set(taskKey, {
662
+ const record = {
396
663
  path: existingPath,
397
664
  normalizedBranch,
398
665
  taskKey,
@@ -400,14 +667,19 @@ class WorktreeManager {
400
667
  lastUsedAt: Date.now(),
401
668
  status: "active",
402
669
  owner: opts.owner ?? "manual",
403
- });
670
+ };
671
+ this.registry.set(taskKey, record);
672
+ recordForBootstrap = record;
404
673
  } else {
405
674
  // Same key — just update timestamp
406
675
  const record = this.registry.get(taskKey);
407
676
  if (record) {
408
677
  record.lastUsedAt = Date.now();
678
+ recordForBootstrap = record;
409
679
  }
410
680
  }
681
+ ensureWorktreeNodeModules(this.repoRoot, existingPath);
682
+ this.bootstrapWorktree(existingPath, recordForBootstrap);
411
683
  await this.saveRegistry();
412
684
  return { path: existingPath, created: false, existing: true };
413
685
  }
@@ -547,6 +819,7 @@ class WorktreeManager {
547
819
  // Some git versions on Windows set core.bare=true on the main repo
548
820
  // when adding worktrees, which conflicts with core.worktree and breaks git.
549
821
  fixGitConfigCorruption(this.repoRoot);
822
+ ensureWorktreeNodeModules(this.repoRoot, worktreePath);
550
823
 
551
824
  // 3. Register the new worktree
552
825
  /** @type {WorktreeRecord} */
@@ -560,6 +833,7 @@ class WorktreeManager {
560
833
  owner: opts.owner ?? "manual",
561
834
  };
562
835
  this.registry.set(taskKey, record);
836
+ this.bootstrapWorktree(worktreePath, record);
563
837
  await this.saveRegistry();
564
838
 
565
839
  console.log(`${TAG} Created worktree for ${branch} at ${worktreePath}`);