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
@@ -0,0 +1,242 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import { homedir } from "node:os";
5
+
6
+ export function runWorkspaceHealthCheck(options = {}) {
7
+ const configDir = options.configDir || process.env.BOSUN_DIR || join(homedir(), "bosun");
8
+ const issues = { errors: [], warnings: [], infos: [] };
9
+ const workspaceResults = [];
10
+
11
+ // 1. Check if workspaces are configured
12
+ let workspaces = [];
13
+ try {
14
+ const configPath = join(configDir, "bosun.config.json");
15
+ if (existsSync(configPath)) {
16
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
17
+ workspaces = config.workspaces || [];
18
+ }
19
+ } catch (err) {
20
+ issues.errors.push({
21
+ code: "WS_CONFIG_READ_FAILED",
22
+ message: `Failed to read workspace config: ${err.message}`,
23
+ fix: "Check bosun.config.json is valid JSON",
24
+ });
25
+ }
26
+
27
+ if (workspaces.length === 0) {
28
+ issues.infos.push({
29
+ code: "WS_NONE_CONFIGURED",
30
+ message: "No workspaces configured — agents use developer repo directly.",
31
+ fix: "Run 'bosun --workspace-add <name>' to create a workspace for isolated agent execution.",
32
+ });
33
+ return { ok: true, workspaces: workspaceResults, issues };
34
+ }
35
+
36
+ // 2. Check each workspace
37
+ for (const ws of workspaces) {
38
+ const wsResult = {
39
+ id: ws.id || "unknown",
40
+ name: ws.name || ws.id || "unnamed",
41
+ path: ws.path || "",
42
+ repos: [],
43
+ ok: true,
44
+ };
45
+
46
+ // 2a. Workspace directory exists
47
+ const wsPath = ws.path || join(configDir, "workspaces", ws.id || ws.name);
48
+ if (!existsSync(wsPath)) {
49
+ issues.warnings.push({
50
+ code: "WS_DIR_MISSING",
51
+ message: `Workspace "${wsResult.name}" directory missing: ${wsPath}`,
52
+ fix: `Run 'bosun --setup' or mkdir -p "${wsPath}"`,
53
+ });
54
+ wsResult.ok = false;
55
+ }
56
+
57
+ // 2b. Check repos in workspace
58
+ for (const repo of ws.repos || []) {
59
+ const repoName = repo.name || repo.slug || "unknown";
60
+ const repoPath = join(wsPath, repoName);
61
+ const repoStatus = { name: repoName, path: repoPath, ok: true, issues: [] };
62
+
63
+ if (!existsSync(repoPath)) {
64
+ repoStatus.ok = false;
65
+ repoStatus.issues.push("directory missing");
66
+ issues.errors.push({
67
+ code: "WS_REPO_MISSING",
68
+ message: `Workspace repo "${repoName}" not found at ${repoPath}`,
69
+ fix: `Run 'bosun --workspace-add-repo <url>' or 'bosun --setup' to clone it`,
70
+ });
71
+ } else {
72
+ const gitPath = join(repoPath, ".git");
73
+ if (!existsSync(gitPath)) {
74
+ repoStatus.ok = false;
75
+ repoStatus.issues.push(".git missing");
76
+ issues.errors.push({
77
+ code: "WS_REPO_NO_GIT",
78
+ message: `Workspace repo "${repoName}" has no .git at ${repoPath}`,
79
+ fix: `Clone the repo: git clone <url> "${repoPath}"`,
80
+ });
81
+ } else {
82
+ // Check remote connectivity (quick)
83
+ try {
84
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
85
+ cwd: repoPath, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
86
+ }).trim();
87
+ repoStatus.branch = branch;
88
+ repoStatus.issues.push(`on branch: ${branch}`);
89
+ } catch {
90
+ repoStatus.issues.push("git status check failed");
91
+ }
92
+
93
+ // Check for uncommitted changes
94
+ try {
95
+ const status = execSync("git status --porcelain", {
96
+ cwd: repoPath, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
97
+ }).trim();
98
+ if (status) {
99
+ const lines = status.split("\n").length;
100
+ repoStatus.issues.push(`${lines} uncommitted change(s)`);
101
+ issues.infos.push({
102
+ code: "WS_REPO_DIRTY",
103
+ message: `Workspace repo "${repoName}" has ${lines} uncommitted change(s)`,
104
+ fix: null,
105
+ });
106
+ }
107
+ } catch { /* ignore */ }
108
+ }
109
+ }
110
+
111
+ wsResult.repos.push(repoStatus);
112
+ if (!repoStatus.ok) wsResult.ok = false;
113
+ }
114
+
115
+ workspaceResults.push(wsResult);
116
+ }
117
+
118
+ // 3. Check Codex sandbox writable_roots coverage
119
+ const codexConfigPath = join(homedir(), ".codex", "config.toml");
120
+ if (existsSync(codexConfigPath)) {
121
+ try {
122
+ const toml = readFileSync(codexConfigPath, "utf8");
123
+ const rootsMatch = toml.match(/writable_roots\s*=\s*\[([^\]]*)\]/);
124
+ if (rootsMatch) {
125
+ const roots = rootsMatch[1].split(",").map(r => r.trim().replace(/^"|"$/g, "")).filter(Boolean);
126
+ for (const ws of workspaces) {
127
+ const wsPath = ws.path || join(configDir, "workspaces", ws.id || ws.name);
128
+ for (const repo of ws.repos || []) {
129
+ const repoPath = join(wsPath, repo.name || repo.slug || "");
130
+ const gitPath = join(repoPath, ".git");
131
+ if (existsSync(gitPath) && !roots.some(r => gitPath.startsWith(r) || r === gitPath)) {
132
+ issues.warnings.push({
133
+ code: "WS_SANDBOX_MISSING_ROOT",
134
+ message: `Workspace repo .git not in Codex writable_roots: ${gitPath}`,
135
+ fix: `Run 'bosun --setup' to update Codex sandbox config, or add "${gitPath}" to writable_roots in ~/.codex/config.toml`,
136
+ });
137
+ }
138
+ }
139
+ }
140
+
141
+ // Check for phantom/relative writable roots
142
+ for (const root of roots) {
143
+ if (!root.startsWith("/")) {
144
+ issues.warnings.push({
145
+ code: "WS_SANDBOX_RELATIVE_ROOT",
146
+ message: `Relative path in Codex writable_roots: "${root}" — may resolve incorrectly`,
147
+ fix: `Remove "${root}" from writable_roots in ~/.codex/config.toml and run 'bosun --setup'`,
148
+ });
149
+ } else if (!existsSync(root) && root !== "/tmp") {
150
+ issues.infos.push({
151
+ code: "WS_SANDBOX_PHANTOM_ROOT",
152
+ message: `Codex writable_root path does not exist: ${root}`,
153
+ fix: null,
154
+ });
155
+ }
156
+ }
157
+ }
158
+ } catch { /* ignore parse errors */ }
159
+ }
160
+
161
+ // 4. Check BOSUN_AGENT_REPO_ROOT
162
+ const agentRoot = process.env.BOSUN_AGENT_REPO_ROOT || "";
163
+ if (agentRoot) {
164
+ if (!existsSync(agentRoot)) {
165
+ issues.warnings.push({
166
+ code: "WS_AGENT_ROOT_MISSING",
167
+ message: `BOSUN_AGENT_REPO_ROOT points to non-existent path: ${agentRoot}`,
168
+ fix: "Run 'bosun --setup' to bootstrap workspace repos",
169
+ });
170
+ } else if (!existsSync(join(agentRoot, ".git"))) {
171
+ issues.warnings.push({
172
+ code: "WS_AGENT_ROOT_NO_GIT",
173
+ message: `BOSUN_AGENT_REPO_ROOT has no .git: ${agentRoot}`,
174
+ fix: "Clone the repo at the workspace path or update BOSUN_AGENT_REPO_ROOT",
175
+ });
176
+ } else {
177
+ issues.infos.push({
178
+ code: "WS_AGENT_ROOT_OK",
179
+ message: `Agent repo root: ${agentRoot}`,
180
+ fix: null,
181
+ });
182
+ }
183
+ }
184
+
185
+ const hasErrors = issues.errors.length > 0;
186
+ return { ok: !hasErrors, workspaces: workspaceResults, issues };
187
+ }
188
+
189
+ /**
190
+ * Format workspace health report for CLI output.
191
+ * @param {{ ok: boolean, workspaces: Array, issues: object }} result
192
+ * @returns {string}
193
+ */
194
+ export function formatWorkspaceHealthReport(result) {
195
+ const lines = [];
196
+ lines.push("=== bosun workspace health ===");
197
+ lines.push(`Status: ${result.ok ? "HEALTHY" : "ISSUES FOUND"}`);
198
+ lines.push("");
199
+
200
+ if (result.workspaces.length === 0) {
201
+ lines.push(" No workspaces configured.");
202
+ lines.push("");
203
+ }
204
+
205
+ for (const ws of result.workspaces) {
206
+ const icon = ws.ok ? "✓" : "✗";
207
+ lines.push(` ${icon} ${ws.name} (${ws.id})`);
208
+ for (const repo of ws.repos) {
209
+ const rIcon = repo.ok ? "✓" : "✗";
210
+ const details = repo.issues.length > 0 ? ` — ${repo.issues.join(", ")}` : "";
211
+ lines.push(` ${rIcon} ${repo.name}${details}`);
212
+ }
213
+ }
214
+ lines.push("");
215
+
216
+ if (result.issues.errors.length > 0) {
217
+ lines.push("Errors:");
218
+ for (const e of result.issues.errors) {
219
+ lines.push(` ✗ ${e.message}`);
220
+ if (e.fix) lines.push(` fix: ${e.fix}`);
221
+ }
222
+ lines.push("");
223
+ }
224
+ if (result.issues.warnings.length > 0) {
225
+ lines.push("Warnings:");
226
+ for (const w of result.issues.warnings) {
227
+ lines.push(` :alert: ${w.message}`);
228
+ if (w.fix) lines.push(` fix: ${w.fix}`);
229
+ }
230
+ lines.push("");
231
+ }
232
+ if (result.issues.infos.length > 0) {
233
+ lines.push("Info:");
234
+ for (const i of result.issues.infos) {
235
+ lines.push(` :help: ${i.message}`);
236
+ }
237
+ lines.push("");
238
+ }
239
+
240
+ return lines.join("\n");
241
+ }
242
+
@@ -231,6 +231,21 @@ export function evaluateBranchSafetyForPush(worktreePath, opts = {}) {
231
231
  reasons.push(`HEAD tracks only ${headFiles}/${baseFiles} files vs ${remoteRef}`);
232
232
  }
233
233
 
234
+ // Zero-diff guard: refuse to push if HEAD is identical to base (would wipe PR)
235
+ try {
236
+ const headRes = spawnSync("git", ["rev-parse", "HEAD"], {
237
+ cwd: worktreePath, encoding: "utf8", timeout: 5_000, stdio: ["pipe", "pipe", "pipe"],
238
+ });
239
+ const baseRes = spawnSync("git", ["rev-parse", remoteRef], {
240
+ cwd: worktreePath, encoding: "utf8", timeout: 5_000, stdio: ["pipe", "pipe", "pipe"],
241
+ });
242
+ const headSha = headRes.stdout?.trim();
243
+ const baseSha = baseRes.stdout?.trim();
244
+ if (headSha && baseSha && headSha === baseSha) {
245
+ reasons.push(`HEAD (${headSha.slice(0, 8)}) is identical to ${remoteRef} — push would create zero-diff PR`);
246
+ }
247
+ } catch { /* best-effort */ }
248
+
234
249
  const deletedToInserted =
235
250
  diff.inserted > 0 ? diff.deleted / diff.inserted : diff.deleted > 0 ? Infinity : 0;
236
251
  const manyFilesChanged = diff.files >= Math.max(2_000, Math.floor(baseFiles * 0.5));
@@ -700,6 +700,13 @@ function handleWebhookEvent(eventType, payload) {
700
700
  });
701
701
  break;
702
702
 
703
+ case "pull_request_review":
704
+ webhookEvents.emit("github:pull_request_review", { action, payload });
705
+ if (action === "submitted") {
706
+ maybeAutoApplySuggestions(payload);
707
+ }
708
+ break;
709
+
703
710
  case "pull_request":
704
711
  if (action === "opened" || action === "synchronize") {
705
712
  webhookEvents.emit("github:pull_request", { action, payload });
@@ -746,6 +753,45 @@ function processBosunCommand(body, payload) {
746
753
  }
747
754
  }
748
755
 
756
+ // ── Auto-apply Copilot review suggestions ─────────────────────────────────────
757
+
758
+ const AUTO_APPLY_AUTHORS = new Set([
759
+ "copilot[bot]",
760
+ "copilot-pull-request-reviewer[bot]",
761
+ "Copilot",
762
+ "github-actions[bot]",
763
+ ]);
764
+
765
+ /**
766
+ * When a review is submitted by an auto-apply author (e.g. copilot[bot]),
767
+ * automatically batch-apply any code suggestions in the review comments.
768
+ * Controlled by BOSUN_AUTO_APPLY_SUGGESTIONS env (default: "true").
769
+ */
770
+ async function maybeAutoApplySuggestions(payload) {
771
+ if (process.env.BOSUN_AUTO_APPLY_SUGGESTIONS === "false") return;
772
+ const reviewer = payload?.review?.user?.login;
773
+ if (!reviewer || !AUTO_APPLY_AUTHORS.has(reviewer)) return;
774
+
775
+ const prNumber = payload?.pull_request?.number;
776
+ const repoFullName = payload?.repository?.full_name;
777
+ if (!prNumber || !repoFullName) return;
778
+
779
+ const [owner, repo] = repoFullName.split("/");
780
+ console.log(`[webhook] Auto-applying ${reviewer} suggestions on PR #${prNumber}`);
781
+
782
+ try {
783
+ const { applyPrSuggestions } = await import("../tools/apply-pr-suggestions.mjs");
784
+ const result = await applyPrSuggestions({ owner, repo, prNumber });
785
+ if (result.commitSha) {
786
+ console.log(`[webhook] Applied ${result.applied} suggestion(s) → ${result.commitSha.slice(0, 8)}`);
787
+ } else {
788
+ console.log(`[webhook] No suggestions to apply: ${result.message}`);
789
+ }
790
+ } catch (err) {
791
+ console.error(`[webhook] Failed to auto-apply suggestions on PR #${prNumber}:`, err.message);
792
+ }
793
+ }
794
+
749
795
  async function handleApiStatus(req, res) {
750
796
  const state = loadOAuthState();
751
797
  const appId = getAppId();
@@ -0,0 +1,22 @@
1
+ export function nowISO() {
2
+ return new Date().toISOString();
3
+ }
4
+
5
+ export function toStringArray(input) {
6
+ if (!Array.isArray(input)) return [];
7
+ return input.map((item) => String(item || '').trim()).filter(Boolean);
8
+ }
9
+
10
+ export function uniqueStrings(values = []) {
11
+ const out = [];
12
+ const seen = new Set();
13
+ for (const raw of values) {
14
+ const value = String(raw || '').trim();
15
+ if (!value) continue;
16
+ const key = value.toLowerCase();
17
+ if (seen.has(key)) continue;
18
+ seen.add(key);
19
+ out.push(value);
20
+ }
21
+ return out;
22
+ }