bosun 0.28.2 → 0.28.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 +68 -0
- package/README.md +1 -1
- 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 +83 -24
- package/package.json +2 -1
- package/preflight.mjs +3 -1
- package/primary-agent.mjs +5 -1
- package/pwsh-runtime.mjs +62 -0
- package/setup.mjs +70 -3
- package/task-executor.mjs +125 -2
- package/telegram-bot.mjs +45 -8
- package/ui/app.js +2 -16
- package/ui/components/workspace-switcher.js +25 -32
- package/ui/modules/settings-schema.js +7 -0
- package/ui/styles/base.css +3 -28
- package/ui/styles/components.css +309 -73
- package/ui/styles/kanban.css +10 -16
- package/ui/styles/layout.css +81 -101
- package/ui/styles/sessions.css +27 -32
- package/ui/styles/variables.css +8 -8
- package/ui/styles/workspace-switcher.css +2 -4
- package/ui/tabs/control.js +40 -71
- package/ui/tabs/settings.js +207 -0
- package/ui/tabs/tasks.js +116 -129
- package/ui-server.mjs +487 -0
- package/workspace-manager.mjs +57 -11
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/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Bosun is a production-grade supervisor for AI coding agents. It routes tasks acr
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<img src="site/social-
|
|
13
|
+
<img src="site/social-card.png" alt="bosun — AI agent supervisor" width="100%" />
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
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
|
@@ -157,6 +157,7 @@ import { WorkspaceMonitor } from "./workspace-monitor.mjs";
|
|
|
157
157
|
import { VkLogStream } from "./vk-log-stream.mjs";
|
|
158
158
|
import { VKErrorResolver } from "./vk-error-resolver.mjs";
|
|
159
159
|
import { createAnomalyDetector } from "./anomaly-detector.mjs";
|
|
160
|
+
import { resolvePwshRuntime } from "./pwsh-runtime.mjs";
|
|
160
161
|
import {
|
|
161
162
|
getWorktreeManager,
|
|
162
163
|
acquireWorktree,
|
|
@@ -311,7 +312,10 @@ function startAgentWorkAnalyzer() {
|
|
|
311
312
|
if (agentWorkAnalyzerActive) return;
|
|
312
313
|
if (process.env.AGENT_WORK_ANALYZER_ENABLED === "false") return;
|
|
313
314
|
try {
|
|
314
|
-
|
|
315
|
+
startAnalyzer().catch((err) => {
|
|
316
|
+
console.warn(`[monitor] agent-work analyzer async error: ${err.message}`);
|
|
317
|
+
agentWorkAnalyzerActive = false;
|
|
318
|
+
});
|
|
315
319
|
agentWorkAnalyzerActive = true;
|
|
316
320
|
console.log("[monitor] agent-work analyzer started");
|
|
317
321
|
} catch (err) {
|
|
@@ -1530,8 +1534,9 @@ async function handleMonitorFailure(reason, err) {
|
|
|
1530
1534
|
|
|
1531
1535
|
if (telegramToken && telegramChatId) {
|
|
1532
1536
|
try {
|
|
1537
|
+
const shortMsg = message.length > 200 ? message.slice(0, 200) + "…" : message;
|
|
1533
1538
|
await sendTelegramMessage(
|
|
1534
|
-
`⚠️ bosun exception (${reason})
|
|
1539
|
+
`⚠️ bosun exception (${reason}): ${shortMsg}\n\nAttempting recovery (count=${failureCount}).`,
|
|
1535
1540
|
);
|
|
1536
1541
|
} catch {
|
|
1537
1542
|
/* suppress Telegram errors during failure handling */
|
|
@@ -7550,17 +7555,47 @@ async function buildPlannerRuntimeContext(reason, details, numTasks) {
|
|
|
7550
7555
|
};
|
|
7551
7556
|
}
|
|
7552
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
|
+
|
|
7553
7579
|
function buildPlannerTaskDescription({
|
|
7554
7580
|
plannerPrompt,
|
|
7555
7581
|
reason,
|
|
7556
7582
|
numTasks,
|
|
7557
7583
|
runtimeContext,
|
|
7584
|
+
userPrompt,
|
|
7558
7585
|
}) {
|
|
7559
7586
|
return [
|
|
7560
7587
|
"## Task Planner — Auto-created by bosun",
|
|
7561
7588
|
"",
|
|
7562
7589
|
`**Trigger reason:** ${reason || "manual"}`,
|
|
7563
7590
|
`**Requested task count:** ${numTasks}`,
|
|
7591
|
+
...(userPrompt
|
|
7592
|
+
? [
|
|
7593
|
+
"",
|
|
7594
|
+
"### User Planning Prompt",
|
|
7595
|
+
"",
|
|
7596
|
+
userPrompt,
|
|
7597
|
+
]
|
|
7598
|
+
: []),
|
|
7564
7599
|
"",
|
|
7565
7600
|
"### Planner Prompt (Injected by bosun)",
|
|
7566
7601
|
"",
|
|
@@ -7610,8 +7645,13 @@ function buildPlannerTaskDescription({
|
|
|
7610
7645
|
"4. Prioritize reliability and unblockers first when errors/review backlog is elevated.",
|
|
7611
7646
|
"5. Avoid duplicates with existing todo/inprogress/review tasks and open PRs.",
|
|
7612
7647
|
"6. Prefer task sets that can run in parallel with minimal file overlap.",
|
|
7613
|
-
"7.
|
|
7614
|
-
|
|
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");
|
|
7615
7655
|
}
|
|
7616
7656
|
|
|
7617
7657
|
function normalizePlannerTitleForComparison(title) {
|
|
@@ -7788,7 +7828,12 @@ async function materializePlannerTasksToKanban(projectId, tasks) {
|
|
|
7788
7828
|
title: task.title,
|
|
7789
7829
|
description: task.description,
|
|
7790
7830
|
status: "todo",
|
|
7791
|
-
|
|
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
|
+
})(),
|
|
7792
7837
|
});
|
|
7793
7838
|
if (createdTask?.id) {
|
|
7794
7839
|
created.push({ id: createdTask.id, title: task.title });
|
|
@@ -8689,6 +8734,7 @@ async function triggerTaskPlanner(
|
|
|
8689
8734
|
details,
|
|
8690
8735
|
{
|
|
8691
8736
|
taskCount,
|
|
8737
|
+
userPrompt,
|
|
8692
8738
|
notify = true,
|
|
8693
8739
|
preferredMode,
|
|
8694
8740
|
allowCodexWhenDisabled = false,
|
|
@@ -8724,6 +8770,7 @@ async function triggerTaskPlanner(
|
|
|
8724
8770
|
try {
|
|
8725
8771
|
result = await triggerTaskPlannerViaKanban(reason, details, {
|
|
8726
8772
|
taskCount,
|
|
8773
|
+
userPrompt,
|
|
8727
8774
|
notify,
|
|
8728
8775
|
});
|
|
8729
8776
|
} catch (kanbanErr) {
|
|
@@ -8753,6 +8800,7 @@ async function triggerTaskPlanner(
|
|
|
8753
8800
|
}
|
|
8754
8801
|
result = await triggerTaskPlannerViaCodex(reason, details, {
|
|
8755
8802
|
taskCount,
|
|
8803
|
+
userPrompt,
|
|
8756
8804
|
notify,
|
|
8757
8805
|
allowWhenDisabled: allowCodexWhenDisabled,
|
|
8758
8806
|
});
|
|
@@ -8761,6 +8809,7 @@ async function triggerTaskPlanner(
|
|
|
8761
8809
|
try {
|
|
8762
8810
|
result = await triggerTaskPlannerViaCodex(reason, details, {
|
|
8763
8811
|
taskCount,
|
|
8812
|
+
userPrompt,
|
|
8764
8813
|
notify,
|
|
8765
8814
|
allowWhenDisabled: allowCodexWhenDisabled,
|
|
8766
8815
|
});
|
|
@@ -8784,6 +8833,7 @@ async function triggerTaskPlanner(
|
|
|
8784
8833
|
|
|
8785
8834
|
result = await triggerTaskPlannerViaKanban(reason, details, {
|
|
8786
8835
|
taskCount,
|
|
8836
|
+
userPrompt,
|
|
8787
8837
|
notify,
|
|
8788
8838
|
});
|
|
8789
8839
|
}
|
|
@@ -8818,7 +8868,7 @@ async function triggerTaskPlanner(
|
|
|
8818
8868
|
async function triggerTaskPlannerViaKanban(
|
|
8819
8869
|
reason,
|
|
8820
8870
|
details,
|
|
8821
|
-
{ taskCount, notify = true } = {},
|
|
8871
|
+
{ taskCount, userPrompt, notify = true } = {},
|
|
8822
8872
|
) {
|
|
8823
8873
|
const defaultPlannerTaskCount = Number(
|
|
8824
8874
|
process.env.TASK_PLANNER_DEFAULT_COUNT || "30",
|
|
@@ -8844,12 +8894,15 @@ async function triggerTaskPlannerViaKanban(
|
|
|
8844
8894
|
);
|
|
8845
8895
|
}
|
|
8846
8896
|
|
|
8847
|
-
const desiredTitle =
|
|
8897
|
+
const desiredTitle = userPrompt
|
|
8898
|
+
? `[${plannerTaskSizeLabel}] Plan next tasks (${reason || "backlog-empty"}) — ${userPrompt.slice(0, 60)}${userPrompt.length > 60 ? "…" : ""}`
|
|
8899
|
+
: `[${plannerTaskSizeLabel}] Plan next tasks (${reason || "backlog-empty"})`;
|
|
8848
8900
|
const desiredDescription = buildPlannerTaskDescription({
|
|
8849
8901
|
plannerPrompt,
|
|
8850
8902
|
reason,
|
|
8851
8903
|
numTasks,
|
|
8852
8904
|
runtimeContext,
|
|
8905
|
+
userPrompt,
|
|
8853
8906
|
});
|
|
8854
8907
|
|
|
8855
8908
|
// Check for existing planner tasks to avoid duplicates
|
|
@@ -8941,7 +8994,7 @@ async function triggerTaskPlannerViaKanban(
|
|
|
8941
8994
|
async function triggerTaskPlannerViaCodex(
|
|
8942
8995
|
reason,
|
|
8943
8996
|
details,
|
|
8944
|
-
{ taskCount, notify = true, allowWhenDisabled = false } = {},
|
|
8997
|
+
{ taskCount, userPrompt, notify = true, allowWhenDisabled = false } = {},
|
|
8945
8998
|
) {
|
|
8946
8999
|
if (!codexEnabled && !allowWhenDisabled) {
|
|
8947
9000
|
throw new Error(
|
|
@@ -8973,6 +9026,17 @@ async function triggerTaskPlannerViaCodex(
|
|
|
8973
9026
|
"## Execution Context",
|
|
8974
9027
|
`- Trigger reason: ${reason || "manual"}`,
|
|
8975
9028
|
`- Requested task count: ${numTasks}`,
|
|
9029
|
+
...(userPrompt
|
|
9030
|
+
? [
|
|
9031
|
+
"",
|
|
9032
|
+
"## User Planning Prompt",
|
|
9033
|
+
"",
|
|
9034
|
+
userPrompt,
|
|
9035
|
+
"",
|
|
9036
|
+
"Incorporate the above user prompt into any relevant planning decisions.",
|
|
9037
|
+
]
|
|
9038
|
+
: []),
|
|
9039
|
+
"",
|
|
8976
9040
|
"Context JSON:",
|
|
8977
9041
|
"```json",
|
|
8978
9042
|
safeJsonBlock(runtimeContext),
|
|
@@ -10590,21 +10654,16 @@ async function startProcess() {
|
|
|
10590
10654
|
let orchestratorArgs = [...scriptArgs];
|
|
10591
10655
|
|
|
10592
10656
|
if (scriptLower.endsWith(".ps1")) {
|
|
10593
|
-
const
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
const pwshLabel = configuredPwsh
|
|
10604
|
-
? `PWSH_PATH (${configuredPwsh})`
|
|
10605
|
-
: bundledPwshExists
|
|
10606
|
-
? `bundled pwsh (${bundledPwsh})`
|
|
10607
|
-
: "pwsh on PATH";
|
|
10657
|
+
const pwshRuntime = resolvePwshRuntime({ preferBundled: true });
|
|
10658
|
+
if (!pwshRuntime.exists) {
|
|
10659
|
+
const pwshLabel =
|
|
10660
|
+
pwshRuntime.source === "env"
|
|
10661
|
+
? `PWSH_PATH (${pwshRuntime.command})`
|
|
10662
|
+
: pwshRuntime.source === "bundled"
|
|
10663
|
+
? `bundled pwsh (${pwshRuntime.command})`
|
|
10664
|
+
: pwshRuntime.source === "powershell"
|
|
10665
|
+
? `powershell on PATH`
|
|
10666
|
+
: "pwsh on PATH";
|
|
10608
10667
|
const pauseMs = Math.max(orchestratorPauseMs, 60_000);
|
|
10609
10668
|
const pauseMin = Math.max(1, Math.round(pauseMs / 60_000));
|
|
10610
10669
|
monitorSafeModeUntil = Math.max(monitorSafeModeUntil, Date.now() + pauseMs);
|
|
@@ -10622,7 +10681,7 @@ async function startProcess() {
|
|
|
10622
10681
|
setTimeout(startProcess, pauseMs);
|
|
10623
10682
|
return;
|
|
10624
10683
|
}
|
|
10625
|
-
orchestratorCmd =
|
|
10684
|
+
orchestratorCmd = pwshRuntime.command;
|
|
10626
10685
|
orchestratorArgs = ["-File", scriptPath, ...scriptArgs];
|
|
10627
10686
|
} else if (scriptLower.endsWith(".sh")) {
|
|
10628
10687
|
const shellCmd =
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.4",
|
|
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",
|
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
"sdk-conflict-resolver.mjs",
|
|
153
153
|
"session-tracker.mjs",
|
|
154
154
|
"setup.mjs",
|
|
155
|
+
"pwsh-runtime.mjs",
|
|
155
156
|
"shared-knowledge.mjs",
|
|
156
157
|
"shared-state-manager.mjs",
|
|
157
158
|
"shared-workspace-cli.mjs",
|
package/preflight.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { resolvePwshRuntime } from "./pwsh-runtime.mjs";
|
|
4
5
|
|
|
5
6
|
const isWindows = process.platform === "win32";
|
|
6
7
|
const MIN_FREE_GB = Number(process.env.BOSUN_MIN_FREE_GB || "10");
|
|
@@ -179,6 +180,7 @@ function checkToolchain() {
|
|
|
179
180
|
"node",
|
|
180
181
|
shellMode ? "shell" : "pwsh",
|
|
181
182
|
]);
|
|
183
|
+
const pwshRuntime = resolvePwshRuntime({ preferBundled: true });
|
|
182
184
|
|
|
183
185
|
const tools = [
|
|
184
186
|
checkToolVersion(
|
|
@@ -213,7 +215,7 @@ function checkToolchain() {
|
|
|
213
215
|
),
|
|
214
216
|
checkToolVersion(
|
|
215
217
|
"pwsh",
|
|
216
|
-
|
|
218
|
+
pwshRuntime.command,
|
|
217
219
|
["-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()"],
|
|
218
220
|
"Install PowerShell 7+ (pwsh) and ensure it is on PATH.",
|
|
219
221
|
),
|
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/pwsh-runtime.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const BUNDLED_PWSH_PATH = resolve(__dirname, ".cache", "bosun", "pwsh", "pwsh");
|
|
8
|
+
|
|
9
|
+
function commandExists(cmd) {
|
|
10
|
+
try {
|
|
11
|
+
execSync(`${process.platform === "win32" ? "where" : "which"} ${cmd}`, {
|
|
12
|
+
stdio: "ignore",
|
|
13
|
+
});
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPathLike(value) {
|
|
21
|
+
return value.includes("/") || value.includes("\\");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolvePwshRuntime({ preferBundled = true } = {}) {
|
|
25
|
+
const configured = String(process.env.PWSH_PATH || "").trim();
|
|
26
|
+
if (configured) {
|
|
27
|
+
if (isPathLike(configured)) {
|
|
28
|
+
if (existsSync(configured)) {
|
|
29
|
+
return { command: configured, source: "env", exists: true };
|
|
30
|
+
}
|
|
31
|
+
return { command: configured, source: "env", exists: false };
|
|
32
|
+
}
|
|
33
|
+
if (commandExists(configured)) {
|
|
34
|
+
return { command: configured, source: "env", exists: true };
|
|
35
|
+
}
|
|
36
|
+
return { command: configured, source: "env", exists: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (preferBundled && existsSync(BUNDLED_PWSH_PATH)) {
|
|
40
|
+
return { command: BUNDLED_PWSH_PATH, source: "bundled", exists: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (commandExists("pwsh")) {
|
|
44
|
+
return { command: "pwsh", source: "path", exists: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (process.platform === "win32" && commandExists("powershell")) {
|
|
48
|
+
return { command: "powershell", source: "powershell", exists: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { command: "pwsh", source: "missing", exists: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolvePwshCommand(options = {}) {
|
|
55
|
+
return resolvePwshRuntime(options).command;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function hasPwshRuntime(options = {}) {
|
|
59
|
+
return resolvePwshRuntime(options).exists;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { BUNDLED_PWSH_PATH };
|