bosun 0.28.3 → 0.29.0
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 +68 -0
- package/agent-prompts.mjs +12 -6
- package/agent-work-analyzer.mjs +39 -15
- package/cli.mjs +4 -1
- package/codex-config.mjs +7 -0
- package/monitor.mjs +40 -5
- package/package.json +1 -1
- package/primary-agent.mjs +5 -1
- package/setup.mjs +6 -0
- package/task-executor.mjs +125 -2
- package/ui/app.js +111 -155
- package/ui/components/command-palette.js +17 -139
- package/ui/components/diff-viewer.js +32 -32
- package/ui/components/forms.js +216 -98
- package/ui/components/session-list.js +40 -41
- package/ui/components/shared.js +86 -86
- package/ui/components/workspace-switcher.js +90 -94
- package/ui/index.html +91 -0
- package/ui/modules/settings-schema.js +7 -0
- package/ui/styles/base.css +3 -28
- package/ui/styles/components.css +339 -3194
- package/ui/styles/kanban.css +10 -16
- package/ui/styles/layout.css +22 -525
- package/ui/styles/sessions.css +27 -32
- package/ui/styles/variables.css +8 -8
- package/ui/styles/workspace-switcher.css +2 -4
- package/ui/styles.css +0 -225
- package/ui/tabs/agents.js +533 -504
- package/ui/tabs/chat.js +16 -16
- package/ui/tabs/control.js +108 -182
- package/ui/tabs/dashboard.js +113 -107
- package/ui/tabs/infra.js +76 -67
- package/ui/tabs/logs.js +71 -62
- package/ui/tabs/settings.js +233 -69
- package/ui/tabs/tasks.js +169 -179
- package/ui/tabs/telemetry.js +30 -30
- package/ui-server.mjs +487 -0
package/.env.example
CHANGED
|
@@ -303,6 +303,74 @@ TELEGRAM_MINIAPP_ENABLED=false
|
|
|
303
303
|
# Optional free-form constraints/scoping notes
|
|
304
304
|
# PROJECT_REQUIREMENTS_NOTES=
|
|
305
305
|
|
|
306
|
+
# ─── Branch Strategy ──────────────────────────────────────────────────
|
|
307
|
+
# How agents work on branches for each task:
|
|
308
|
+
#
|
|
309
|
+
# worktree (default) — Each task gets an isolated ve/<id>-<slug> sub-branch
|
|
310
|
+
# that PRs back into the module/base branch.
|
|
311
|
+
# Best for: independent, high-parallelism work.
|
|
312
|
+
#
|
|
313
|
+
# direct — Agents push directly onto the module branch
|
|
314
|
+
# (no ve/ sub-branch). All work is sequential on that
|
|
315
|
+
# branch. Conflicts are resolved in real time.
|
|
316
|
+
# Best for: fast iteration on a well-scoped module.
|
|
317
|
+
#
|
|
318
|
+
# TASK_BRANCH_MODE=worktree
|
|
319
|
+
#
|
|
320
|
+
# Auto-detect origin/<module> as base_branch from conventional commit task titles
|
|
321
|
+
# e.g. "[m] feat(veid): add verification" → base_branch = origin/veid
|
|
322
|
+
# TASK_BRANCH_AUTO_MODULE=true
|
|
323
|
+
#
|
|
324
|
+
# Prefix used when building module branch refs (default: origin/)
|
|
325
|
+
# MODULE_BRANCH_PREFIX=origin/
|
|
326
|
+
#
|
|
327
|
+
# Default upstream target branch (main branch) for upstream sync and PRs
|
|
328
|
+
# DEFAULT_TARGET_BRANCH=main
|
|
329
|
+
#
|
|
330
|
+
# Merge origin/main into module branches before each push (keeps branches in
|
|
331
|
+
# sync with upstream continuously). Conflicts abort the merge and are resolved
|
|
332
|
+
# by the next agent working on that branch. Default: true
|
|
333
|
+
# TASK_UPSTREAM_SYNC_MAIN=true
|
|
334
|
+
|
|
335
|
+
# ─── GitHub App (Bosun[botswain] Identity + Auth) ────────────────────────────
|
|
336
|
+
# App: https://github.com/apps/bosun-botswain (slug: bosun-botswain)
|
|
337
|
+
# Bot identity: bosun-botswain[bot] (appears as contributor on every agent commit)
|
|
338
|
+
#
|
|
339
|
+
# Numeric App ID (shown on the App settings page under "About"):
|
|
340
|
+
# BOSUN_GITHUB_APP_ID=2911413
|
|
341
|
+
#
|
|
342
|
+
# OAuth Client ID (from App settings → Client ID):
|
|
343
|
+
# BOSUN_GITHUB_CLIENT_ID=Iv23liZpVhGePGka9gcL
|
|
344
|
+
#
|
|
345
|
+
# OAuth Client Secret (only needed for callback-based OAuth, not for Device Flow):
|
|
346
|
+
# BOSUN_GITHUB_CLIENT_SECRET=
|
|
347
|
+
#
|
|
348
|
+
# Webhook secret (set this in App settings → Webhook, and keep in sync):
|
|
349
|
+
# BOSUN_GITHUB_WEBHOOK_SECRET=
|
|
350
|
+
#
|
|
351
|
+
# Path to the PEM private key downloaded from App settings → Generate a private key:
|
|
352
|
+
# BOSUN_GITHUB_PRIVATE_KEY_PATH=/path/to/bosun-botswain.pem
|
|
353
|
+
#
|
|
354
|
+
# ─── Authentication Method ───────────────────────────────────────────────
|
|
355
|
+
# RECOMMENDED: Device Flow (like VS Code / Roo Code — no public URL needed!)
|
|
356
|
+
# 1. Set BOSUN_GITHUB_CLIENT_ID above
|
|
357
|
+
# 2. Enable "Device Flow" in GitHub App settings (Settings → Optional features)
|
|
358
|
+
# 3. Go to Settings → GitHub in the Bosun UI and click "Sign in with GitHub"
|
|
359
|
+
# 4. That's it — no callback URL, no tunnel URL, no client secret needed
|
|
360
|
+
#
|
|
361
|
+
# ALTERNATIVE: OAuth Callback (requires a stable public URL)
|
|
362
|
+
# Set BOSUN_GITHUB_CLIENT_ID + BOSUN_GITHUB_CLIENT_SECRET
|
|
363
|
+
# Register callback URL in App settings:
|
|
364
|
+
# https://<your-bosun-public-url>/api/github/callback
|
|
365
|
+
#
|
|
366
|
+
# WEBHOOKS (optional — for real-time PR/issue sync):
|
|
367
|
+
# Register webhook URL in App settings:
|
|
368
|
+
# https://<your-bosun-public-url>/api/webhooks/github/app
|
|
369
|
+
# Webhooks require a stable public URL. Without them, Bosun polls instead.
|
|
370
|
+
#
|
|
371
|
+
# Leave BOSUN_GITHUB_APP_ID unset to disable co-author trailer injection.
|
|
372
|
+
# BOSUN_GITHUB_APP_ID=
|
|
373
|
+
|
|
306
374
|
# ─── Kanban Backend ──────────────────────────────────────────────────────────
|
|
307
375
|
# Task-board backend:
|
|
308
376
|
# internal - local task-store source of truth (recommended primary)
|
package/agent-prompts.mjs
CHANGED
|
@@ -173,11 +173,12 @@ Return exactly one fenced json block with this shape:
|
|
|
173
173
|
{
|
|
174
174
|
"tasks": [
|
|
175
175
|
{
|
|
176
|
-
"title": "[m]
|
|
176
|
+
"title": "[m] feat(veid): example task title",
|
|
177
177
|
"description": "Problem statement and scope",
|
|
178
178
|
"implementation_steps": ["step 1", "step 2"],
|
|
179
179
|
"acceptance_criteria": ["criterion 1", "criterion 2"],
|
|
180
|
-
"verification": ["test/check 1", "test/check 2"]
|
|
180
|
+
"verification": ["test/check 1", "test/check 2"],
|
|
181
|
+
"base_branch": "origin/veid"
|
|
181
182
|
}
|
|
182
183
|
]
|
|
183
184
|
}
|
|
@@ -187,6 +188,11 @@ Rules:
|
|
|
187
188
|
- Provide at least the requested task count unless blocked by duplicate safeguards.
|
|
188
189
|
- Keep titles unique and specific.
|
|
189
190
|
- Keep file overlap low across tasks to maximize parallel execution.
|
|
191
|
+
- **Module branch routing:** When the task title follows conventional commit format
|
|
192
|
+
\`feat(module):\` or \`fix(module):\`, set \`base_branch\` to \`origin/<module>\`.
|
|
193
|
+
This routes the task to the module's dedicated branch for parallel, isolated development.
|
|
194
|
+
Examples: \`feat(veid):\` → \`"base_branch": "origin/veid"\`, \`fix(market):\` → \`"base_branch": "origin/market"\`.
|
|
195
|
+
Omit \`base_branch\` for cross-cutting tasks that span multiple modules.
|
|
190
196
|
`,
|
|
191
197
|
monitorMonitor: `# Bosun-Monitor Agent
|
|
192
198
|
|
|
@@ -240,14 +246,14 @@ You are running as a **Bosun-managed task agent**. Environment variables
|
|
|
240
246
|
**After committing:**
|
|
241
247
|
- If a precommit hook auto-applies additional formatting changes, add those
|
|
242
248
|
to a follow-up commit before pushing.
|
|
243
|
-
- Merge any upstream changes from the base branch
|
|
244
|
-
\`git fetch origin && git merge origin/<base-branch> --no-edit\`
|
|
245
|
-
Resolve any conflicts that arise.
|
|
249
|
+
- Merge any upstream changes — BOTH from the base (module) branch AND from main:
|
|
250
|
+
\`git fetch origin && git merge origin/<base-branch> --no-edit && git merge origin/main --no-edit\`
|
|
251
|
+
Resolve any conflicts that arise before pushing.
|
|
246
252
|
- Push: \`git push --set-upstream origin {{BRANCH}}\`
|
|
247
253
|
- After a successful push, open a Pull Request:
|
|
248
254
|
\`gh pr create --title "{{TASK_TITLE}}" --body "Closes task {{TASK_ID}}"\`
|
|
249
255
|
- **Do NOT** run \`gh pr merge\` — the orchestrator handles merges after CI.
|
|
250
|
-
|
|
256
|
+
{{COAUTHOR_INSTRUCTION}}
|
|
251
257
|
**Do NOT:**
|
|
252
258
|
- Bypass pre-push hooks (\`git push --no-verify\` is forbidden).
|
|
253
259
|
- Use \`git add .\` — stage files individually.
|
package/agent-work-analyzer.mjs
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* - Cost anomaly detection (unusually expensive sessions)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { readFile, writeFile, appendFile, stat, watch } from "fs/promises";
|
|
16
|
-
import { createReadStream, existsSync } from "fs";
|
|
15
|
+
import { readFile, writeFile, appendFile, stat, watch, mkdir } from "fs/promises";
|
|
16
|
+
import { createReadStream, existsSync, mkdirSync } from "fs";
|
|
17
17
|
import { createInterface } from "readline";
|
|
18
18
|
import { resolve, dirname } from "path";
|
|
19
19
|
import { fileURLToPath } from "url";
|
|
@@ -73,30 +73,54 @@ export async function startAnalyzer() {
|
|
|
73
73
|
|
|
74
74
|
console.log("[agent-work-analyzer] Starting...");
|
|
75
75
|
|
|
76
|
-
// Ensure alerts log
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// Ensure parent directory and alerts log exist
|
|
77
|
+
try {
|
|
78
|
+
const alertsDir = dirname(ALERTS_LOG);
|
|
79
|
+
if (!existsSync(alertsDir)) {
|
|
80
|
+
await mkdir(alertsDir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
if (!existsSync(ALERTS_LOG)) {
|
|
83
|
+
await writeFile(ALERTS_LOG, "");
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.warn(`[agent-work-analyzer] Failed to init alerts log: ${err.message}`);
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
// Initial read of existing log
|
|
82
90
|
if (existsSync(AGENT_WORK_STREAM)) {
|
|
83
91
|
filePosition = await processLogFile(filePosition);
|
|
92
|
+
} else {
|
|
93
|
+
// Ensure the stream file exists so the watcher doesn't throw
|
|
94
|
+
try {
|
|
95
|
+
await writeFile(AGENT_WORK_STREAM, "");
|
|
96
|
+
} catch {
|
|
97
|
+
// May fail if another process creates it first — that's fine
|
|
98
|
+
}
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
// Watch for changes
|
|
101
|
+
// Watch for changes — retry loop handles the case where the file
|
|
102
|
+
// is deleted and recreated (e.g. log rotation).
|
|
87
103
|
console.log(`[agent-work-analyzer] Watching: ${AGENT_WORK_STREAM}`);
|
|
88
104
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
while (isRunning) {
|
|
106
|
+
try {
|
|
107
|
+
const watcher = watch(AGENT_WORK_STREAM, { persistent: true });
|
|
108
|
+
for await (const event of watcher) {
|
|
109
|
+
if (!isRunning) break;
|
|
110
|
+
if (event.eventType === "change") {
|
|
111
|
+
filePosition = await processLogFile(filePosition);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (!isRunning) break;
|
|
116
|
+
if (err.code === "ENOENT") {
|
|
117
|
+
// File was deleted — wait a bit and retry
|
|
118
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
119
|
+
continue;
|
|
95
120
|
}
|
|
96
|
-
}
|
|
97
|
-
} catch (err) {
|
|
98
|
-
if (err.code !== "ENOENT") {
|
|
99
121
|
console.error(`[agent-work-analyzer] Watcher error: ${err.message}`);
|
|
122
|
+
// Wait before retrying to avoid busy-loop
|
|
123
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
100
124
|
}
|
|
101
125
|
}
|
|
102
126
|
}
|
package/cli.mjs
CHANGED
|
@@ -979,7 +979,10 @@ async function main() {
|
|
|
979
979
|
const logDir = monitorPath ? resolve(dirname(monitorPath), "logs") : resolve(__dirname, "logs");
|
|
980
980
|
const daemonLog = existsSync(DAEMON_LOG) ? DAEMON_LOG : resolve(logDir, "daemon.log");
|
|
981
981
|
const monitorLog = resolve(logDir, "monitor.log");
|
|
982
|
-
|
|
982
|
+
// Prefer monitor.log — that's where the real activity goes.
|
|
983
|
+
// daemon.log only has the startup line; monitor.mjs intercepts
|
|
984
|
+
// all console output and writes to monitor.log.
|
|
985
|
+
const logFile = existsSync(monitorLog) ? monitorLog : daemonLog;
|
|
983
986
|
|
|
984
987
|
if (existsSync(logFile)) {
|
|
985
988
|
console.log(
|
package/codex-config.mjs
CHANGED
|
@@ -373,11 +373,18 @@ function normalizeWritableRoots(input, { repoRoot } = {}) {
|
|
|
373
373
|
if (repo) {
|
|
374
374
|
addRoot(repo);
|
|
375
375
|
addRoot(resolve(repo, ".git"));
|
|
376
|
+
// Worktree checkout paths (used by task-executor)
|
|
377
|
+
addRoot(resolve(repo, ".cache", "worktrees"));
|
|
378
|
+
// Cache directories for agent work logs, build artifacts, etc.
|
|
379
|
+
addRoot(resolve(repo, ".cache"));
|
|
376
380
|
const parent = dirname(repo);
|
|
377
381
|
if (parent && parent !== repo) addRoot(parent);
|
|
378
382
|
}
|
|
379
383
|
}
|
|
380
384
|
|
|
385
|
+
// /tmp is needed for sandbox temp files, pip installs, etc.
|
|
386
|
+
addRoot("/tmp");
|
|
387
|
+
|
|
381
388
|
return Array.from(roots);
|
|
382
389
|
}
|
|
383
390
|
|
package/monitor.mjs
CHANGED
|
@@ -312,7 +312,10 @@ function startAgentWorkAnalyzer() {
|
|
|
312
312
|
if (agentWorkAnalyzerActive) return;
|
|
313
313
|
if (process.env.AGENT_WORK_ANALYZER_ENABLED === "false") return;
|
|
314
314
|
try {
|
|
315
|
-
|
|
315
|
+
startAnalyzer().catch((err) => {
|
|
316
|
+
console.warn(`[monitor] agent-work analyzer async error: ${err.message}`);
|
|
317
|
+
agentWorkAnalyzerActive = false;
|
|
318
|
+
});
|
|
316
319
|
agentWorkAnalyzerActive = true;
|
|
317
320
|
console.log("[monitor] agent-work analyzer started");
|
|
318
321
|
} catch (err) {
|
|
@@ -1531,8 +1534,9 @@ async function handleMonitorFailure(reason, err) {
|
|
|
1531
1534
|
|
|
1532
1535
|
if (telegramToken && telegramChatId) {
|
|
1533
1536
|
try {
|
|
1537
|
+
const shortMsg = message.length > 200 ? message.slice(0, 200) + "…" : message;
|
|
1534
1538
|
await sendTelegramMessage(
|
|
1535
|
-
`⚠️ bosun exception (${reason})
|
|
1539
|
+
`⚠️ bosun exception (${reason}): ${shortMsg}\n\nAttempting recovery (count=${failureCount}).`,
|
|
1536
1540
|
);
|
|
1537
1541
|
} catch {
|
|
1538
1542
|
/* suppress Telegram errors during failure handling */
|
|
@@ -7551,6 +7555,27 @@ async function buildPlannerRuntimeContext(reason, details, numTasks) {
|
|
|
7551
7555
|
};
|
|
7552
7556
|
}
|
|
7553
7557
|
|
|
7558
|
+
/**
|
|
7559
|
+
* Extract a conventional-commit scope from a task title and return the
|
|
7560
|
+
* corresponding module branch ref (e.g. "origin/veid").
|
|
7561
|
+
* Respects TASK_BRANCH_AUTO_MODULE env var (default: true) and
|
|
7562
|
+
* MODULE_BRANCH_PREFIX (default: "origin/").
|
|
7563
|
+
*/
|
|
7564
|
+
function extractModuleBaseBranchFromTitle(title) {
|
|
7565
|
+
const enabled = (process.env.TASK_BRANCH_AUTO_MODULE ?? "true") !== "false";
|
|
7566
|
+
if (!enabled || !title) return null;
|
|
7567
|
+
const match = String(title).match(
|
|
7568
|
+
/(?:^\[[^\]]+\]\s*)?(?:feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)\(([^)]+)\)/i,
|
|
7569
|
+
);
|
|
7570
|
+
if (!match) return null;
|
|
7571
|
+
const scope = match[1].toLowerCase().trim();
|
|
7572
|
+
// Exclude generic scopes that don't map to real module branches
|
|
7573
|
+
const generic = new Set(["deps", "app", "sdk", "cli", "api"]);
|
|
7574
|
+
if (generic.has(scope)) return null;
|
|
7575
|
+
const prefix = (process.env.MODULE_BRANCH_PREFIX || "origin/").replace(/\/*$/, "/");
|
|
7576
|
+
return `${prefix}${scope}`;
|
|
7577
|
+
}
|
|
7578
|
+
|
|
7554
7579
|
function buildPlannerTaskDescription({
|
|
7555
7580
|
plannerPrompt,
|
|
7556
7581
|
reason,
|
|
@@ -7620,8 +7645,13 @@ function buildPlannerTaskDescription({
|
|
|
7620
7645
|
"4. Prioritize reliability and unblockers first when errors/review backlog is elevated.",
|
|
7621
7646
|
"5. Avoid duplicates with existing todo/inprogress/review tasks and open PRs.",
|
|
7622
7647
|
"6. Prefer task sets that can run in parallel with minimal file overlap.",
|
|
7623
|
-
"7.
|
|
7624
|
-
|
|
7648
|
+
"7. **Module branch routing (important):** When a task's title follows conventional commit format",
|
|
7649
|
+
" `feat(module):` or `fix(module):`, ALWAYS set `base_branch` to `origin/<module>` in the JSON.",
|
|
7650
|
+
" This enables parallel branch-per-module execution where all work for a module accumulates on",
|
|
7651
|
+
" its dedicated branch and integrates upstream changes continuously.",
|
|
7652
|
+
" Examples: `feat(veid):` → `origin/veid`, `fix(market):` → `origin/market`.",
|
|
7653
|
+
" Do NOT set base_branch for cross-cutting tasks that modify many modules.",
|
|
7654
|
+
"8. If a task should target a non-default epic/base branch for other reasons, include `base_branch` in the JSON task object.",].join("\n");
|
|
7625
7655
|
}
|
|
7626
7656
|
|
|
7627
7657
|
function normalizePlannerTitleForComparison(title) {
|
|
@@ -7798,7 +7828,12 @@ async function materializePlannerTasksToKanban(projectId, tasks) {
|
|
|
7798
7828
|
title: task.title,
|
|
7799
7829
|
description: task.description,
|
|
7800
7830
|
status: "todo",
|
|
7801
|
-
|
|
7831
|
+
// Honour explicit baseBranch from planner JSON, then fall back to
|
|
7832
|
+
// module auto-detection via conventional commit scope in the title.
|
|
7833
|
+
...(() => {
|
|
7834
|
+
const b = task.baseBranch || task.base_branch || extractModuleBaseBranchFromTitle(task.title);
|
|
7835
|
+
return b ? { baseBranch: b } : {};
|
|
7836
|
+
})(),
|
|
7802
7837
|
});
|
|
7803
7838
|
if (createdTask?.id) {
|
|
7804
7839
|
created.push({ id: createdTask.id, title: task.title });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
package/primary-agent.mjs
CHANGED
|
@@ -362,9 +362,13 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
|
|
|
362
362
|
);
|
|
363
363
|
|
|
364
364
|
if (result) {
|
|
365
|
+
// Extract human-readable text from structured responses
|
|
366
|
+
const text = typeof result === "string"
|
|
367
|
+
? result
|
|
368
|
+
: result.finalResponse || result.text || result.message || JSON.stringify(result);
|
|
365
369
|
tracker.recordEvent(sessionId, {
|
|
366
370
|
role: "assistant",
|
|
367
|
-
content:
|
|
371
|
+
content: text,
|
|
368
372
|
timestamp: new Date().toISOString(),
|
|
369
373
|
_sessionType: sessionType,
|
|
370
374
|
});
|
package/setup.mjs
CHANGED
|
@@ -1627,7 +1627,13 @@ function buildDefaultWritableRoots(repoRoot) {
|
|
|
1627
1627
|
if (parent && parent !== repo) roots.add(parent);
|
|
1628
1628
|
roots.add(repo);
|
|
1629
1629
|
roots.add(resolve(repo, ".git"));
|
|
1630
|
+
// Worktree checkout paths (used by task-executor)
|
|
1631
|
+
roots.add(resolve(repo, ".cache", "worktrees"));
|
|
1632
|
+
// Cache directories for agent work logs, build artifacts, etc.
|
|
1633
|
+
roots.add(resolve(repo, ".cache"));
|
|
1630
1634
|
}
|
|
1635
|
+
// /tmp needed for sandbox temp files, pip installs, etc.
|
|
1636
|
+
roots.add("/tmp");
|
|
1631
1637
|
return Array.from(roots).join(",");
|
|
1632
1638
|
}
|
|
1633
1639
|
|
package/task-executor.mjs
CHANGED
|
@@ -113,6 +113,35 @@ const CODEX_TASK_LABELS = (() => {
|
|
|
113
113
|
|
|
114
114
|
/** Watchdog interval: how often to check for stalled agent slots */
|
|
115
115
|
const WATCHDOG_INTERVAL_MS = 60_000; // 1 minute
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns the Co-authored-by trailer for bosun-botswain[bot], or empty string
|
|
119
|
+
* if the GitHub App ID is not configured. Used to attribute agent commits to
|
|
120
|
+
* the Bosun GitHub App so the bot shows up as a contributor.
|
|
121
|
+
*
|
|
122
|
+
* To enable: set BOSUN_GITHUB_APP_ID=<your-app-id> in .env
|
|
123
|
+
* App noreply email format: <id>+bosun-botswain[bot]@users.noreply.github.com
|
|
124
|
+
*/
|
|
125
|
+
function getBosunCoAuthorLine() {
|
|
126
|
+
const appId = String(process.env.BOSUN_GITHUB_APP_ID || "").trim();
|
|
127
|
+
if (!appId) return "";
|
|
128
|
+
return `Co-authored-by: bosun-botswain[bot] <${appId}+bosun-botswain[bot]@users.noreply.github.com>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns a prompt instruction block telling the agent to append the Bosun
|
|
133
|
+
* co-author trailer to every commit. Empty string when app ID not configured.
|
|
134
|
+
*/
|
|
135
|
+
function getBosunCoAuthorInstruction() {
|
|
136
|
+
const line = getBosunCoAuthorLine();
|
|
137
|
+
if (!line) return "";
|
|
138
|
+
return `\n**Attribution (required — do not omit):**
|
|
139
|
+
Every commit message MUST end with a blank line then this exact trailer:
|
|
140
|
+
\`\`\`
|
|
141
|
+
${line}
|
|
142
|
+
\`\`\`
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
116
145
|
/** Grace period after task timeout before watchdog force-kills the slot */
|
|
117
146
|
const WATCHDOG_GRACE_MS = 10 * 60_000; // 10 minutes — generous buffer, stream analysis handles real issues
|
|
118
147
|
/** Max age for in-progress tasks to auto-resume after monitor restart */
|
|
@@ -212,11 +241,30 @@ function normalizeBranchName(value) {
|
|
|
212
241
|
function extractScopeFromTitle(title) {
|
|
213
242
|
if (!title) return null;
|
|
214
243
|
const match = String(title).match(
|
|
215
|
-
/(?:^\[
|
|
244
|
+
/(?:^\[[^\]]+\]\s*)?(?:feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)\(([^)]+)\)/i,
|
|
216
245
|
);
|
|
217
246
|
return match ? match[1].toLowerCase().trim() : null;
|
|
218
247
|
}
|
|
219
248
|
|
|
249
|
+
/**
|
|
250
|
+
* If TASK_BRANCH_AUTO_MODULE=true (default), extract the conventional-commit
|
|
251
|
+
* scope from a task title and return the corresponding module branch ref.
|
|
252
|
+
*
|
|
253
|
+
* e.g. "[m] feat(veid): add verification" → "origin/veid"
|
|
254
|
+
* "[s] fix(market): resolve bid race" → "origin/market"
|
|
255
|
+
*/
|
|
256
|
+
function resolveModuleAutoBaseBranch(title) {
|
|
257
|
+
const enabled = (process.env.TASK_BRANCH_AUTO_MODULE ?? "true") !== "false";
|
|
258
|
+
if (!enabled) return null;
|
|
259
|
+
const scope = extractScopeFromTitle(title);
|
|
260
|
+
if (!scope) return null;
|
|
261
|
+
// Exclude generic scopes that don't map to real module branches
|
|
262
|
+
const generic = new Set(["deps", "app", "sdk", "cli", "api"]);
|
|
263
|
+
if (generic.has(scope)) return null;
|
|
264
|
+
const prefix = (process.env.MODULE_BRANCH_PREFIX || "origin/").replace(/\/*$/, "/");
|
|
265
|
+
return `${prefix}${scope}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
220
268
|
function collectTaskLabels(task) {
|
|
221
269
|
const labels = [];
|
|
222
270
|
if (!task) return labels;
|
|
@@ -350,6 +398,10 @@ function resolveTaskBaseBranch(task, branchRouting, defaultTargetBranch) {
|
|
|
350
398
|
const fromConfig = resolveUpstreamFromConfig(task, branchRouting);
|
|
351
399
|
if (fromConfig) return normalizeBranchName(fromConfig);
|
|
352
400
|
|
|
401
|
+
// Auto-detect module branch from conventional commit scope in title
|
|
402
|
+
const moduleAuto = resolveModuleAutoBaseBranch(task.title || task.name);
|
|
403
|
+
if (moduleAuto) return normalizeBranchName(moduleAuto);
|
|
404
|
+
|
|
353
405
|
return normalizeBranchName(defaultTargetBranch);
|
|
354
406
|
}
|
|
355
407
|
|
|
@@ -3285,12 +3337,25 @@ class TaskExecutor {
|
|
|
3285
3337
|
const requestedBranch = String(
|
|
3286
3338
|
options?.branch || options?.resumeBranch || "",
|
|
3287
3339
|
).trim();
|
|
3340
|
+
// Resolve base branch first so direct-mode can reuse it for the working branch
|
|
3341
|
+
const baseBranch = this._resolveTaskBaseBranch(task);
|
|
3342
|
+
// Branch name strategy:
|
|
3343
|
+
// TASK_BRANCH_MODE=direct → work directly on the module branch (no ve/ sub-branch),
|
|
3344
|
+
// enabling sequential in-place work with real-time conflict
|
|
3345
|
+
// resolution by the same team of agents.
|
|
3346
|
+
// TASK_BRANCH_MODE=worktree (default) → create an isolated ve/<id>-<slug> sub-branch
|
|
3347
|
+
// that PRs back into the module/base branch.
|
|
3348
|
+
const branchMode = (process.env.TASK_BRANCH_MODE || "worktree").trim().toLowerCase();
|
|
3349
|
+
const directModeBranch =
|
|
3350
|
+
branchMode === "direct" && baseBranch
|
|
3351
|
+
? normalizeBranchName(baseBranch)?.replace(/^origin\//, "") || null
|
|
3352
|
+
: null;
|
|
3288
3353
|
const branch =
|
|
3289
3354
|
requestedBranch ||
|
|
3290
3355
|
task.branchName ||
|
|
3291
3356
|
task.meta?.branch_name ||
|
|
3357
|
+
directModeBranch ||
|
|
3292
3358
|
`ve/${taskId.substring(0, 8)}-${slugify(taskTitle)}`;
|
|
3293
|
-
const baseBranch = this._resolveTaskBaseBranch(task);
|
|
3294
3359
|
const taskRepoContext = this._resolveTaskRepoContext(task);
|
|
3295
3360
|
const executionRepoRoot = taskRepoContext.repoRoot || this.repoRoot;
|
|
3296
3361
|
const executionRepoSlug = taskRepoContext.repoSlug || this.repoSlug;
|
|
@@ -4124,6 +4189,7 @@ class TaskExecutor {
|
|
|
4124
4189
|
REPO_ROOT: promptRepoRoot,
|
|
4125
4190
|
TASK_WORKSPACE: promptWorkspace,
|
|
4126
4191
|
TASK_REPOSITORY: promptRepository,
|
|
4192
|
+
COAUTHOR_INSTRUCTION: getBosunCoAuthorInstruction(),
|
|
4127
4193
|
},
|
|
4128
4194
|
fallbackPrompt,
|
|
4129
4195
|
);
|
|
@@ -4196,6 +4262,11 @@ class TaskExecutor {
|
|
|
4196
4262
|
`3. Re-run tests to verify`,
|
|
4197
4263
|
`4. Commit and push your fixes`,
|
|
4198
4264
|
``,
|
|
4265
|
+
...(getBosunCoAuthorLine() ? [
|
|
4266
|
+
`Attribution: append this trailer after each commit message body (blank line before it):`,
|
|
4267
|
+
getBosunCoAuthorLine(),
|
|
4268
|
+
``,
|
|
4269
|
+
] : []),
|
|
4199
4270
|
`Original task description:`,
|
|
4200
4271
|
task.description || "See task URL for details.",
|
|
4201
4272
|
].join("\n");
|
|
@@ -5101,6 +5172,58 @@ class TaskExecutor {
|
|
|
5101
5172
|
/* best-effort upstream rebase */
|
|
5102
5173
|
}
|
|
5103
5174
|
|
|
5175
|
+
// Additionally sync with origin/main (or DEFAULT_TARGET_BRANCH) so that
|
|
5176
|
+
// module branches continuously absorb upstream changes before each push.
|
|
5177
|
+
// This surfaces conflicts early — if a merge conflict occurs, we abort the
|
|
5178
|
+
// merge and push as-is; the NEXT agent working on this branch will encounter
|
|
5179
|
+
// the conflict immediately and resolve it inline (no separate conflict-fix task).
|
|
5180
|
+
try {
|
|
5181
|
+
const mainBranch =
|
|
5182
|
+
process.env.DEFAULT_TARGET_BRANCH ||
|
|
5183
|
+
process.env.VK_TARGET_BRANCH?.replace(/^origin\//, "") ||
|
|
5184
|
+
"main";
|
|
5185
|
+
const syncEnabled =
|
|
5186
|
+
(process.env.TASK_UPSTREAM_SYNC_MAIN ?? "true") !== "false";
|
|
5187
|
+
// Only sync main when base branch is NOT already main itself
|
|
5188
|
+
const isMainBase = baseInfo.branch === mainBranch || baseBranch === "main";
|
|
5189
|
+
if (syncEnabled && !isMainBase) {
|
|
5190
|
+
spawnSync("git", ["fetch", "origin", mainBranch, "--quiet"], {
|
|
5191
|
+
cwd: worktreePath,
|
|
5192
|
+
encoding: "utf8",
|
|
5193
|
+
timeout: 30_000,
|
|
5194
|
+
});
|
|
5195
|
+
const mainMergeResult = spawnSync(
|
|
5196
|
+
"git",
|
|
5197
|
+
["merge", `origin/${mainBranch}`, "--no-edit", "--no-ff", "-X", "ours"],
|
|
5198
|
+
{
|
|
5199
|
+
cwd: worktreePath,
|
|
5200
|
+
encoding: "utf8",
|
|
5201
|
+
timeout: 60_000,
|
|
5202
|
+
},
|
|
5203
|
+
);
|
|
5204
|
+
if (mainMergeResult.status !== 0) {
|
|
5205
|
+
// Conflicts or error — abort and continue with push as-is.
|
|
5206
|
+
// The unresolved divergence will be visible to the next agent.
|
|
5207
|
+
console.warn(
|
|
5208
|
+
`${TAG} upstream main merge (origin/${mainBranch}) had conflicts on ` +
|
|
5209
|
+
`${branch} — aborting merge, pushing branch as-is. ` +
|
|
5210
|
+
`Next agent on this branch will resolve conflicts.`,
|
|
5211
|
+
);
|
|
5212
|
+
spawnSync("git", ["merge", "--abort"], {
|
|
5213
|
+
cwd: worktreePath,
|
|
5214
|
+
encoding: "utf8",
|
|
5215
|
+
timeout: 10_000,
|
|
5216
|
+
});
|
|
5217
|
+
} else {
|
|
5218
|
+
console.log(
|
|
5219
|
+
`${TAG} synced ${branch} with origin/${mainBranch} before push.`,
|
|
5220
|
+
);
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
} catch {
|
|
5224
|
+
/* best-effort upstream main sync */
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5104
5227
|
const safety = evaluateBranchSafetyForPush(worktreePath, {
|
|
5105
5228
|
baseBranch,
|
|
5106
5229
|
remote: "origin",
|