bosun 0.41.2 → 0.41.4
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 +1 -1
- package/agent/agent-pool.mjs +9 -2
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +119 -6
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +35 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/setup-web-server.mjs +58 -5
- package/server/ui-server.mjs +1394 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +28 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +338 -84
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +43 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +848 -141
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +358 -63
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +44 -11
- package/workflow-templates/task-lifecycle.mjs +31 -6
- 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
|
-
|
|
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}`);
|