bosun 0.42.0 → 0.42.2

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 (63) hide show
  1. package/.env.example +12 -0
  2. package/README.md +2 -0
  3. package/agent/agent-pool.mjs +34 -1
  4. package/agent/agent-work-report.mjs +89 -3
  5. package/agent/analyze-agent-work-helpers.mjs +14 -0
  6. package/agent/analyze-agent-work.mjs +23 -3
  7. package/agent/primary-agent.mjs +23 -1
  8. package/bosun-tui.mjs +4 -3
  9. package/bosun.schema.json +1 -1
  10. package/config/config.mjs +58 -0
  11. package/config/workspace-health.mjs +36 -6
  12. package/git/diff-stats.mjs +550 -124
  13. package/github/github-app-auth.mjs +9 -5
  14. package/infra/maintenance.mjs +13 -6
  15. package/infra/monitor.mjs +398 -10
  16. package/infra/runtime-accumulator.mjs +9 -1
  17. package/infra/session-tracker.mjs +163 -1
  18. package/infra/tui-bridge.mjs +415 -0
  19. package/infra/worktree-recovery-state.mjs +159 -0
  20. package/kanban/kanban-adapter.mjs +41 -8
  21. package/lib/repo-map.mjs +411 -0
  22. package/package.json +140 -137
  23. package/server/ui-server.mjs +953 -59
  24. package/shell/codex-config.mjs +34 -8
  25. package/task/task-cli.mjs +93 -19
  26. package/task/task-executor.mjs +397 -8
  27. package/task/task-store.mjs +194 -1
  28. package/telegram/telegram-bot.mjs +267 -18
  29. package/tools/vitest-runner.mjs +108 -0
  30. package/tui/app.mjs +252 -148
  31. package/tui/components/status-header.mjs +88 -131
  32. package/tui/lib/ws-bridge.mjs +125 -35
  33. package/tui/screens/agents-screen-helpers.mjs +219 -0
  34. package/tui/screens/agents.mjs +287 -270
  35. package/tui/screens/status.mjs +51 -189
  36. package/tui/screens/tasks.mjs +41 -253
  37. package/ui/app.js +52 -23
  38. package/ui/components/chat-view.js +263 -84
  39. package/ui/components/diff-viewer.js +324 -140
  40. package/ui/components/kanban-board.js +13 -9
  41. package/ui/components/session-list.js +111 -41
  42. package/ui/demo-defaults.js +481 -59
  43. package/ui/demo.html +32 -0
  44. package/ui/modules/session-api.js +320 -5
  45. package/ui/modules/stream-timeline.js +356 -0
  46. package/ui/modules/telegram.js +5 -2
  47. package/ui/modules/worktree-recovery.js +85 -0
  48. package/ui/styles.css +44 -0
  49. package/ui/tabs/chat.js +19 -4
  50. package/ui/tabs/dashboard.js +22 -0
  51. package/ui/tabs/infra.js +25 -0
  52. package/ui/tabs/tasks.js +119 -11
  53. package/voice/voice-auth-manager.mjs +10 -5
  54. package/workflow/workflow-engine.mjs +179 -1
  55. package/workflow/workflow-nodes.mjs +872 -16
  56. package/workflow/workflow-templates.mjs +4 -0
  57. package/workflow-templates/github.mjs +2 -1
  58. package/workflow-templates/planning.mjs +2 -1
  59. package/workflow-templates/sub-workflows.mjs +10 -0
  60. package/workflow-templates/task-batch.mjs +9 -8
  61. package/workflow-templates/task-execution.mjs +30 -12
  62. package/workflow-templates/task-lifecycle.mjs +59 -4
  63. package/workspace/shared-knowledge.mjs +409 -155
@@ -1329,10 +1329,40 @@ function buildModelProviderSection(providerName, config = {}) {
1329
1329
  return lines.join("\n");
1330
1330
  }
1331
1331
 
1332
+ /**
1333
+ * Codex CLI built-in provider IDs that cannot be used in [model_providers.*].
1334
+ * Declaring these in config.toml causes a fatal "reserved built-in provider"
1335
+ * error on Codex CLI >=0.x (March 2026+).
1336
+ */
1337
+ const CODEX_RESERVED_PROVIDER_IDS = new Set(["openai"]);
1338
+
1339
+ /**
1340
+ * Migrate legacy [model_providers.openai] sections that Bosun previously
1341
+ * generated. Newer Codex CLI versions reject this ID as a reserved built-in.
1342
+ * We rename it to "openai-direct" so existing timeout / retry settings
1343
+ * are preserved without triggering the error.
1344
+ */
1345
+ function migrateReservedProviderIds(toml) {
1346
+ const migrated = [];
1347
+ for (const reserved of CODEX_RESERVED_PROVIDER_IDS) {
1348
+ const header = `[model_providers.${reserved}]`;
1349
+ if (toml.includes(header)) {
1350
+ const replacement = `[model_providers.${reserved}-direct]`;
1351
+ toml = toml.replace(header, replacement);
1352
+ migrated.push({ from: reserved, to: `${reserved}-direct` });
1353
+ }
1354
+ }
1355
+ return { toml, migrated };
1356
+ }
1357
+
1332
1358
  function ensureModelProviderSectionsFromEnv(toml, env = process.env) {
1333
1359
  const added = [];
1334
1360
  const { env: resolvedEnv, active } = resolveCodexProfileRuntime(env);
1335
1361
 
1362
+ // Migrate any legacy reserved provider IDs before adding new sections
1363
+ const migration = migrateReservedProviderIds(toml);
1364
+ toml = migration.toml;
1365
+
1336
1366
  const activeProvider = String(active?.provider || "").toLowerCase();
1337
1367
  const activeBaseUrl =
1338
1368
  active?.baseUrl ||
@@ -1363,15 +1393,11 @@ function ensureModelProviderSectionsFromEnv(toml, env = process.env) {
1363
1393
  }
1364
1394
  }
1365
1395
 
1366
- if (!hasModelProviderSection(toml, "openai")) {
1367
- toml += buildModelProviderSection("openai", {
1368
- name: "OpenAI",
1369
- envKey: "OPENAI_API_KEY",
1370
- });
1371
- added.push("openai");
1372
- }
1396
+ // NOTE: Do NOT add [model_providers.openai] — it is a Codex built-in.
1397
+ // The built-in already handles OPENAI_API_KEY. Declaring it causes:
1398
+ // "model_providers contains reserved built-in provider IDs: openai"
1373
1399
 
1374
- return { toml, added };
1400
+ return { toml, added, migrated: migration.migrated };
1375
1401
  }
1376
1402
 
1377
1403
  /**
package/task/task-cli.mjs CHANGED
@@ -25,11 +25,15 @@
25
25
  * taskStats() — Programmatic stats
26
26
  */
27
27
 
28
- import { resolve, dirname } from "node:path";
28
+ import { resolve, dirname, isAbsolute } from "node:path";
29
29
  import { homedir } from "node:os";
30
30
  import { fileURLToPath } from "node:url";
31
31
  import { readFileSync, existsSync, statSync } from "node:fs";
32
32
  import { randomUUID } from "node:crypto";
33
+ import {
34
+ normalizeWorkspaceStorageKey,
35
+ normalizeWorkspaceStorageKeys,
36
+ } from "./task-store.mjs";
33
37
  import { getTaskLifetimeTotals } from "../infra/runtime-accumulator.mjs";
34
38
 
35
39
  const __filename = fileURLToPath(import.meta.url);
@@ -56,6 +60,20 @@ function normalizeStorePath(pathLike) {
56
60
  : resolvedPath;
57
61
  }
58
62
 
63
+ function rethrowKeyCollision(err) {
64
+ if (err?.code === "TASK_STORE_KEY_COLLISION") {
65
+ throw err;
66
+ }
67
+ }
68
+
69
+ function normalizeStoreScopeKey(value) {
70
+ return normalizeWorkspaceStorageKey(value);
71
+ }
72
+
73
+ function assertNoStoreKeyCollisions(values, kind) {
74
+ normalizeWorkspaceStorageKeys(values, { kind });
75
+ }
76
+
59
77
  function ensureStore() {
60
78
  if (_storeReady) return;
61
79
  // Import is sync-cached after first call
@@ -115,16 +133,42 @@ function resolveKanbanStorePath() {
115
133
  if (existsSync(configPath)) {
116
134
  const cfg = JSON.parse(readFileSync(configPath, "utf8"));
117
135
  const workspacesDir = cfg.workspacesDir || resolve(bosunHome, "workspaces");
118
- const activeWs = cfg.activeWorkspace;
136
+ const activeWs = String(cfg?.activeWorkspace || "").trim();
137
+ const workspaceEntries = Array.isArray(cfg?.workspaces) ? cfg.workspaces : [];
138
+ assertNoStoreKeyCollisions(
139
+ workspaceEntries.map((entry) => entry?.id),
140
+ "bosun.config.workspaces",
141
+ );
119
142
  if (activeWs && workspacesDir) {
120
- const ws = (cfg.workspaces || []).find((w) => w.id === activeWs);
121
- const primaryRepoName =
122
- ws?.activeRepo ||
123
- (cfg.repos || []).find((r) => r.primary)?.name;
143
+ const activeWorkspaceKey = normalizeStoreScopeKey(activeWs);
144
+ const ws =
145
+ workspaceEntries.find(
146
+ (entry) => normalizeStoreScopeKey(entry?.id) === activeWorkspaceKey,
147
+ ) || null;
148
+ const repos = Array.isArray(ws?.repos) ? ws.repos : [];
149
+ assertNoStoreKeyCollisions(
150
+ repos.map((repo) => repo?.slug || repo?.name),
151
+ "bosun.config.repos",
152
+ );
153
+ const activeRepoKey = normalizeStoreScopeKey(ws?.activeRepo);
154
+ const selectedRepo =
155
+ (activeRepoKey
156
+ ? repos.find(
157
+ (repo) =>
158
+ normalizeStoreScopeKey(repo?.slug || repo?.name)
159
+ === activeRepoKey,
160
+ )
161
+ : null) ||
162
+ repos.find((repo) => repo?.primary) ||
163
+ null;
164
+ const fallbackRepoName = (cfg.repos || []).find((r) => r.primary)?.name;
165
+ const primaryRepoName = normalizeStoreScopeKey(
166
+ selectedRepo?.name || selectedRepo?.slug || fallbackRepoName,
167
+ );
124
168
  if (primaryRepoName) {
125
169
  const wsStorePath = resolve(
126
170
  workspacesDir,
127
- activeWs,
171
+ activeWorkspaceKey,
128
172
  primaryRepoName,
129
173
  ".bosun",
130
174
  ".cache",
@@ -140,7 +184,8 @@ function resolveKanbanStorePath() {
140
184
  }
141
185
  }
142
186
  }
143
- } catch {
187
+ } catch (err) {
188
+ rethrowKeyCollision(err);
144
189
  // fall through to legacy CWD-based resolution
145
190
  }
146
191
 
@@ -155,31 +200,46 @@ function resolveActiveWorkspaceDefaults() {
155
200
  const configPath = resolve(bosunHome, "bosun.config.json");
156
201
  if (!existsSync(configPath)) return { workspace: "", repository: "" };
157
202
  const cfg = JSON.parse(readFileSync(configPath, "utf8"));
158
- const activeWsId = String(cfg?.activeWorkspace || "").trim();
203
+ const activeWsId = normalizeStoreScopeKey(cfg?.activeWorkspace);
159
204
  const workspaces = Array.isArray(cfg?.workspaces) ? cfg.workspaces : [];
205
+ assertNoStoreKeyCollisions(
206
+ workspaces.map((entry) => entry?.id),
207
+ "bosun.config.workspaces",
208
+ );
160
209
  const workspace =
161
210
  (activeWsId
162
- ? workspaces.find((entry) => String(entry?.id || "").trim() === activeWsId)
211
+ ? workspaces.find(
212
+ (entry) => normalizeStoreScopeKey(entry?.id) === activeWsId,
213
+ )
163
214
  : null) ||
164
215
  workspaces[0] ||
165
216
  null;
166
217
  const repos = Array.isArray(workspace?.repos) ? workspace.repos : [];
167
- const activeRepoName = String(workspace?.activeRepo || "").trim();
218
+ assertNoStoreKeyCollisions(
219
+ repos.map((repo) => repo?.slug || repo?.name),
220
+ "bosun.config.repos",
221
+ );
222
+ const activeRepoName = normalizeStoreScopeKey(workspace?.activeRepo);
168
223
  const selectedRepo =
169
224
  (activeRepoName
170
- ? repos.find((repo) => String(repo?.name || "").trim() === activeRepoName)
225
+ ? repos.find(
226
+ (repo) =>
227
+ normalizeStoreScopeKey(repo?.slug || repo?.name)
228
+ === activeRepoName,
229
+ )
171
230
  : null) ||
172
231
  repos.find((repo) => repo?.primary) ||
173
232
  repos[0] ||
174
233
  null;
175
- const repository = String(
234
+ const repository = normalizeStoreScopeKey(
176
235
  selectedRepo?.slug || selectedRepo?.name || "",
177
- ).trim();
236
+ );
178
237
  return {
179
- workspace: String(workspace?.id || activeWsId || "").trim(),
238
+ workspace: normalizeStoreScopeKey(workspace?.id || activeWsId || ""),
180
239
  repository,
181
240
  };
182
- } catch {
241
+ } catch (err) {
242
+ rethrowKeyCollision(err);
183
243
  return { workspace: "", repository: "" };
184
244
  }
185
245
  }
@@ -248,6 +308,10 @@ function isDebugModeEnabled(args = []) {
248
308
  */
249
309
  export async function taskCreate(data) {
250
310
  const store = await initStore();
311
+ const normalizeKey = store.normalizeWorkspaceStorageKey || normalizeStoreScopeKey;
312
+ const normalizeKeys =
313
+ store.normalizeWorkspaceStorageKeys
314
+ || ((values, options = {}) => normalizeWorkspaceStorageKeys(values, options));
251
315
  const id = data.id || randomUUID();
252
316
  const parsedCandidateCount = Number(data?.candidateCount);
253
317
  const candidateCount = Number.isFinite(parsedCandidateCount)
@@ -268,6 +332,16 @@ export async function taskCreate(data) {
268
332
  inputMeta.execution = executionMeta;
269
333
  }
270
334
  const defaults = resolveActiveWorkspaceDefaults();
335
+ const rawWorkspace = data.workspace || defaults.workspace || process.cwd();
336
+ const workspaceKey = normalizeKey(rawWorkspace);
337
+ const workspaceValue = typeof rawWorkspace === "string" && isAbsolute(rawWorkspace)
338
+ ? rawWorkspace
339
+ : (workspaceKey || null);
340
+ const repositoryKey = normalizeKey(data.repository || defaults.repository || "");
341
+ const repositoryKeys = normalizeKeys(
342
+ [repositoryKey, ...(Array.isArray(data.repositories) ? data.repositories : [])],
343
+ { kind: `task-cli:create:${id}:repositories` },
344
+ );
271
345
  const taskData = {
272
346
  id,
273
347
  title: data.title,
@@ -277,9 +351,9 @@ export async function taskCreate(data) {
277
351
  priority: data.priority || "medium",
278
352
  tags: normalizeTags(data.tags),
279
353
  baseBranch: data.baseBranch || data.base_branch || "main",
280
- workspace: data.workspace || defaults.workspace || process.cwd(),
281
- repository: data.repository || defaults.repository || "",
282
- repositories: data.repositories || [],
354
+ workspace: workspaceValue,
355
+ repository: repositoryKey || null,
356
+ repositories: repositoryKeys,
283
357
  candidateCount: candidateCount && candidateCount > 1 ? candidateCount : undefined,
284
358
  meta: inputMeta,
285
359
  };