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.
- package/.env.example +12 -0
- package/README.md +2 -0
- package/agent/agent-pool.mjs +34 -1
- package/agent/agent-work-report.mjs +89 -3
- package/agent/analyze-agent-work-helpers.mjs +14 -0
- package/agent/analyze-agent-work.mjs +23 -3
- package/agent/primary-agent.mjs +23 -1
- package/bosun-tui.mjs +4 -3
- package/bosun.schema.json +1 -1
- package/config/config.mjs +58 -0
- package/config/workspace-health.mjs +36 -6
- package/git/diff-stats.mjs +550 -124
- package/github/github-app-auth.mjs +9 -5
- package/infra/maintenance.mjs +13 -6
- package/infra/monitor.mjs +398 -10
- package/infra/runtime-accumulator.mjs +9 -1
- package/infra/session-tracker.mjs +163 -1
- package/infra/tui-bridge.mjs +415 -0
- package/infra/worktree-recovery-state.mjs +159 -0
- package/kanban/kanban-adapter.mjs +41 -8
- package/lib/repo-map.mjs +411 -0
- package/package.json +140 -137
- package/server/ui-server.mjs +953 -59
- package/shell/codex-config.mjs +34 -8
- package/task/task-cli.mjs +93 -19
- package/task/task-executor.mjs +397 -8
- package/task/task-store.mjs +194 -1
- package/telegram/telegram-bot.mjs +267 -18
- package/tools/vitest-runner.mjs +108 -0
- package/tui/app.mjs +252 -148
- package/tui/components/status-header.mjs +88 -131
- package/tui/lib/ws-bridge.mjs +125 -35
- package/tui/screens/agents-screen-helpers.mjs +219 -0
- package/tui/screens/agents.mjs +287 -270
- package/tui/screens/status.mjs +51 -189
- package/tui/screens/tasks.mjs +41 -253
- package/ui/app.js +52 -23
- package/ui/components/chat-view.js +263 -84
- package/ui/components/diff-viewer.js +324 -140
- package/ui/components/kanban-board.js +13 -9
- package/ui/components/session-list.js +111 -41
- package/ui/demo-defaults.js +481 -59
- package/ui/demo.html +32 -0
- package/ui/modules/session-api.js +320 -5
- package/ui/modules/stream-timeline.js +356 -0
- package/ui/modules/telegram.js +5 -2
- package/ui/modules/worktree-recovery.js +85 -0
- package/ui/styles.css +44 -0
- package/ui/tabs/chat.js +19 -4
- package/ui/tabs/dashboard.js +22 -0
- package/ui/tabs/infra.js +25 -0
- package/ui/tabs/tasks.js +119 -11
- package/voice/voice-auth-manager.mjs +10 -5
- package/workflow/workflow-engine.mjs +179 -1
- package/workflow/workflow-nodes.mjs +872 -16
- package/workflow/workflow-templates.mjs +4 -0
- package/workflow-templates/github.mjs +2 -1
- package/workflow-templates/planning.mjs +2 -1
- package/workflow-templates/sub-workflows.mjs +10 -0
- package/workflow-templates/task-batch.mjs +9 -8
- package/workflow-templates/task-execution.mjs +30 -12
- package/workflow-templates/task-lifecycle.mjs +59 -4
- package/workspace/shared-knowledge.mjs +409 -155
package/shell/codex-config.mjs
CHANGED
|
@@ -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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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.
|
|
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
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
234
|
+
const repository = normalizeStoreScopeKey(
|
|
176
235
|
selectedRepo?.slug || selectedRepo?.name || "",
|
|
177
|
-
)
|
|
236
|
+
);
|
|
178
237
|
return {
|
|
179
|
-
workspace:
|
|
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:
|
|
281
|
-
repository:
|
|
282
|
-
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
|
};
|