bosun 0.35.4 → 0.36.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/agent-prompts.mjs +6 -7
- package/agent-supervisor.mjs +3 -3
- package/autofix.mjs +1 -1
- package/bosun-skills.mjs +6 -14
- package/bosun.schema.json +67 -0
- package/claude-shell.mjs +19 -2
- package/codex-shell.mjs +50 -16
- package/config.mjs +3 -0
- package/copilot-shell.mjs +16 -3
- package/opencode-shell.mjs +14 -1
- package/package.json +1 -1
- package/session-tracker.mjs +135 -0
- package/setup.mjs +4 -4
- package/task-executor.mjs +50 -119
- package/telegram-bot.mjs +90 -0
- package/ui/components/chat-view.js +50 -22
- package/ui/components/session-list.js +9 -1
- package/ui/demo.html +22 -22
- package/ui/modules/audio-visualizer.js +156 -0
- package/ui/modules/settings-schema.js +14 -0
- package/ui/modules/voice-client.js +451 -0
- package/ui/modules/voice-fallback.js +208 -0
- package/ui/modules/voice-overlay.js +341 -0
- package/ui/styles/components.css +43 -386
- package/ui/styles/kanban.css +91 -17
- package/ui/styles/layout.css +37 -8
- package/ui/tabs/agents.js +1 -1
- package/ui/tabs/chat.js +31 -2
- package/ui/tabs/tasks.js +86 -266
- package/ui/tabs/workflows.js +36 -5
- package/ui-server.mjs +99 -5
- package/ve-kanban.ps1 +11 -11
- package/ve-orchestrator.ps1 +30 -63
- package/workflow-engine.mjs +31 -7
- package/workflow-nodes.mjs +25 -26
- package/workflow-templates/agents.mjs +18 -17
- package/workflow-templates/ci-cd.mjs +9 -3
- package/workflow-templates/github.mjs +9 -5
- package/workflow-templates/planning.mjs +21 -12
- package/workflow-templates/reliability.mjs +26 -14
- package/workflow-templates/security.mjs +53 -46
- package/workspace-manager.mjs +88 -13
package/agent-prompts.mjs
CHANGED
|
@@ -437,7 +437,7 @@ You are the always-on reliability guardian for bosun in devmode.
|
|
|
437
437
|
## Constraints
|
|
438
438
|
|
|
439
439
|
- Operate only in devmode.
|
|
440
|
-
- Do not commit/push/
|
|
440
|
+
- Do not commit/push/initiate PR lifecycle changes in this context.
|
|
441
441
|
- Apply focused fixes, run focused validation, and keep monitoring.
|
|
442
442
|
`,
|
|
443
443
|
taskExecutor: `# {{TASK_ID}} — {{TASK_TITLE}}
|
|
@@ -495,7 +495,7 @@ These patterns have caused real production crashes. Treat them as hard rules:
|
|
|
495
495
|
add config overrides that bypass safety checks. If a feature is behind a flag,
|
|
496
496
|
respect it.
|
|
497
497
|
|
|
498
|
-
## Bosun Task Agent — Git &
|
|
498
|
+
## Bosun Task Agent — Git & Bosun Lifecycle Workflow
|
|
499
499
|
|
|
500
500
|
You are running as a **Bosun-managed task agent**. Environment variables
|
|
501
501
|
\`BOSUN_TASK_TITLE\`, \`BOSUN_BRANCH_NAME\`, \`BOSUN_TASK_ID\`, and their
|
|
@@ -512,18 +512,17 @@ You are running as a **Bosun-managed task agent**. Environment variables
|
|
|
512
512
|
\`git fetch origin && git merge origin/<base-branch> --no-edit && git merge origin/main --no-edit\`
|
|
513
513
|
Resolve any conflicts that arise before pushing.
|
|
514
514
|
- Push: \`git push --set-upstream origin {{BRANCH}}\`
|
|
515
|
-
- After a successful push,
|
|
516
|
-
|
|
517
|
-
- **Do NOT** run \`gh pr merge\` — the orchestrator handles merges after CI.
|
|
515
|
+
- After a successful push, hand off PR lifecycle to Bosun management.
|
|
516
|
+
- Do not run direct PR commands.
|
|
518
517
|
{{COAUTHOR_INSTRUCTION}}
|
|
519
518
|
**Do NOT:**
|
|
520
519
|
- Bypass pre-push hooks (\`git push --no-verify\` is forbidden).
|
|
521
520
|
- Use \`git add .\` — stage files individually.
|
|
522
|
-
- Wait for user confirmation before pushing or
|
|
521
|
+
- Wait for user confirmation before pushing or handing off lifecycle state.
|
|
523
522
|
|
|
524
523
|
## Agent Status Endpoint
|
|
525
524
|
- URL: http://127.0.0.1:{{ENDPOINT_PORT}}/api/tasks/{{TASK_ID}}
|
|
526
|
-
- POST /status {"status":"inreview"} after
|
|
525
|
+
- POST /status {"status":"inreview"} after push + Bosun lifecycle handoff readiness
|
|
527
526
|
- POST /heartbeat {} while running
|
|
528
527
|
- POST /error {"error":"..."} on fatal failure
|
|
529
528
|
- POST /complete {"hasCommits":true} when done
|
package/agent-supervisor.mjs
CHANGED
|
@@ -192,9 +192,9 @@ const RECOVERY_PROMPTS = {
|
|
|
192
192
|
`If push fails due to pre-push hooks, fix the issues and push again.`,
|
|
193
193
|
|
|
194
194
|
[SITUATION.PR_NOT_CREATED]: (ctx) =>
|
|
195
|
-
`You pushed commits for "${ctx.taskTitle}" but no PR
|
|
196
|
-
`
|
|
197
|
-
`
|
|
195
|
+
`You pushed commits for "${ctx.taskTitle}" but no PR is visible yet.\n` +
|
|
196
|
+
`Direct PR commands are disabled. Confirm the branch is pushed, then mark this run as ready for Bosun-managed PR lifecycle handoff.\n` +
|
|
197
|
+
`Do not run direct PR-create commands.`,
|
|
198
198
|
|
|
199
199
|
[SITUATION.TOOL_LOOP]: (ctx) =>
|
|
200
200
|
`You've been repeating the same tools (${ctx.loopedTools || "unknown"}) without progress. ` +
|
package/autofix.mjs
CHANGED
|
@@ -1270,7 +1270,7 @@ ${messagesCtx}
|
|
|
1270
1270
|
3. Identify why it loops (missing break/continue/return, no state change between iterations, etc.)
|
|
1271
1271
|
4. Fix the loop by adding proper exit conditions, error handling, or state tracking
|
|
1272
1272
|
5. Common loop-causing patterns in this codebase:
|
|
1273
|
-
-
|
|
1273
|
+
- PR lifecycle handoff repeatedly retried with no diff between branch and base
|
|
1274
1274
|
- API calls returning the same error repeatedly with no backoff or give-up logic
|
|
1275
1275
|
- Status not updated after failure → next cycle tries the same thing
|
|
1276
1276
|
- Missing \`continue\` or state change in foreach loops over tracked attempts
|
package/bosun-skills.mjs
CHANGED
|
@@ -114,7 +114,7 @@ Your worktree path is provided via \`BOSUN_WORKTREE_PATH\`. Stay inside it.
|
|
|
114
114
|
scope: "global",
|
|
115
115
|
content: `# Skill: Pull Request Workflow
|
|
116
116
|
|
|
117
|
-
## Standard
|
|
117
|
+
## Standard Bosun Lifecycle Flow
|
|
118
118
|
|
|
119
119
|
After committing all changes on your task branch:
|
|
120
120
|
|
|
@@ -127,11 +127,11 @@ git merge origin/main --no-edit 2>/dev/null || true
|
|
|
127
127
|
# Resolve any conflicts, commit, then push
|
|
128
128
|
git push --set-upstream origin <branch-name>
|
|
129
129
|
|
|
130
|
-
#
|
|
131
|
-
|
|
130
|
+
# Hand off PR lifecycle to Bosun manager (no direct PR-create command)
|
|
131
|
+
echo "PR lifecycle handoff ready for <branch-name>"
|
|
132
132
|
\`\`\`
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
Bosun manages PR lifecycle (create/update/merge) after handoff.
|
|
135
135
|
|
|
136
136
|
## PR Description Template
|
|
137
137
|
|
|
@@ -158,17 +158,9 @@ Bosun installs pre-push hooks that run build + test validation.
|
|
|
158
158
|
If the hook runs \`npm test\` or \`dotnet test\` and fails:
|
|
159
159
|
1. Read the test output carefully.
|
|
160
160
|
2. Fix the root cause (not just suppress the error).
|
|
161
|
-
3. If the failure is in an unrelated existing test, note it in the
|
|
161
|
+
3. If the failure is in an unrelated existing test, note it in the lifecycle handoff context
|
|
162
162
|
and run a targeted test to confirm your changes don't regress it.
|
|
163
163
|
|
|
164
|
-
## Draft PRs
|
|
165
|
-
|
|
166
|
-
Use \`--draft\` when the implementation is complete but CI is long-running:
|
|
167
|
-
\`\`\`bash
|
|
168
|
-
gh pr create --draft --title "..." --body "..."
|
|
169
|
-
\`\`\`
|
|
170
|
-
Convert to ready when CI passes: \`gh pr ready <number>\`
|
|
171
|
-
|
|
172
164
|
## Reviewing CI Status
|
|
173
165
|
|
|
174
166
|
\`\`\`bash
|
|
@@ -427,7 +419,7 @@ Your branch was created from that base — not from \`main\` directly.
|
|
|
427
419
|
Merge order on completion:
|
|
428
420
|
1. Merge upstream base branch changes into your branch (keeps drift low).
|
|
429
421
|
2. Merge main (catches global changes like dep bumps).
|
|
430
|
-
3. Push and
|
|
422
|
+
3. Push and hand off lifecycle targeting the base branch.
|
|
431
423
|
|
|
432
424
|
The orchestrator then merges the base branch into main after CI.
|
|
433
425
|
|
package/bosun.schema.json
CHANGED
|
@@ -40,6 +40,73 @@
|
|
|
40
40
|
"type": "string",
|
|
41
41
|
"enum": ["codex-sdk", "copilot-sdk", "claude-sdk", "opencode-sdk"]
|
|
42
42
|
},
|
|
43
|
+
"voice": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"description": "Voice assistant configuration for real-time voice interaction",
|
|
46
|
+
"additionalProperties": false,
|
|
47
|
+
"properties": {
|
|
48
|
+
"enabled": {
|
|
49
|
+
"type": "boolean",
|
|
50
|
+
"default": true,
|
|
51
|
+
"description": "Enable voice assistant features"
|
|
52
|
+
},
|
|
53
|
+
"provider": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"enum": ["openai", "azure", "fallback", "auto"],
|
|
56
|
+
"default": "auto",
|
|
57
|
+
"description": "Voice provider: openai (direct API), azure (Azure OpenAI), fallback (browser STT/TTS), auto (detect from env)"
|
|
58
|
+
},
|
|
59
|
+
"model": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"default": "gpt-4o-realtime-preview-2024-12-17",
|
|
62
|
+
"description": "Realtime API model name"
|
|
63
|
+
},
|
|
64
|
+
"openaiApiKey": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "OpenAI API key for Realtime API (overrides OPENAI_API_KEY env)"
|
|
67
|
+
},
|
|
68
|
+
"azureApiKey": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "Azure OpenAI API key for Realtime API"
|
|
71
|
+
},
|
|
72
|
+
"azureEndpoint": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"description": "Azure OpenAI endpoint URL"
|
|
75
|
+
},
|
|
76
|
+
"azureDeployment": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"default": "gpt-4o-realtime-preview",
|
|
79
|
+
"description": "Azure OpenAI deployment name"
|
|
80
|
+
},
|
|
81
|
+
"voiceId": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"enum": ["alloy", "ash", "ballad", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer", "verse"],
|
|
84
|
+
"default": "alloy",
|
|
85
|
+
"description": "Voice ID for TTS output"
|
|
86
|
+
},
|
|
87
|
+
"turnDetection": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"enum": ["server_vad", "semantic_vad", "none"],
|
|
90
|
+
"default": "server_vad",
|
|
91
|
+
"description": "Turn detection mode for voice activity detection"
|
|
92
|
+
},
|
|
93
|
+
"instructions": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"description": "Custom system instructions for the voice assistant"
|
|
96
|
+
},
|
|
97
|
+
"fallbackMode": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"enum": ["browser", "disabled"],
|
|
100
|
+
"default": "browser",
|
|
101
|
+
"description": "Fallback when Realtime API unavailable: browser (Web Speech API) or disabled"
|
|
102
|
+
},
|
|
103
|
+
"delegateExecutor": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"enum": ["codex-sdk", "copilot-sdk", "claude-sdk", "opencode-sdk"],
|
|
106
|
+
"description": "Which executor to use for delegate_to_agent calls. Defaults to primaryAgent."
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
43
110
|
"vkSpawnEnabled": { "type": "boolean" },
|
|
44
111
|
"kanban": {
|
|
45
112
|
"type": "object",
|
package/claude-shell.mjs
CHANGED
|
@@ -444,7 +444,23 @@ function extractTaskHeading(msg) {
|
|
|
444
444
|
return heading || 'Execute Task';
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
function buildPrompt(userMessage, statusData) {
|
|
447
|
+
function buildPrompt(userMessage, statusData, { mode = null } = {}) {
|
|
448
|
+
// ── Mode detection ────────────────────────────────────────────────────
|
|
449
|
+
// "ask" mode should be lightweight — no heavy executor framing that
|
|
450
|
+
// instructs the agent to run commands and read files.
|
|
451
|
+
const isAskMode =
|
|
452
|
+
mode === "ask" || /^\[MODE:\s*ask\]/i.test(userMessage);
|
|
453
|
+
|
|
454
|
+
if (isAskMode) {
|
|
455
|
+
// Ask mode — pass through without executor framing. The [MODE: ask]
|
|
456
|
+
// prefix from primary-agent already tells the model to be brief.
|
|
457
|
+
if (statusData) {
|
|
458
|
+
const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
|
|
459
|
+
return `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n${userMessage}`;
|
|
460
|
+
}
|
|
461
|
+
return userMessage;
|
|
462
|
+
}
|
|
463
|
+
|
|
448
464
|
const title = extractTaskHeading(userMessage);
|
|
449
465
|
if (!statusData) {
|
|
450
466
|
return `# ${title}\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output.`;
|
|
@@ -474,6 +490,7 @@ export async function execClaudePrompt(userMessage, options = {}) {
|
|
|
474
490
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
475
491
|
sendRawEvents = false,
|
|
476
492
|
abortController = null,
|
|
493
|
+
mode = null,
|
|
477
494
|
} = options;
|
|
478
495
|
|
|
479
496
|
if (activeTurn && !options._holdActiveTurn) {
|
|
@@ -544,7 +561,7 @@ export async function execClaudePrompt(userMessage, options = {}) {
|
|
|
544
561
|
try {
|
|
545
562
|
const queue = createMessageQueue();
|
|
546
563
|
activeQueue = queue;
|
|
547
|
-
queue.push(makeUserMessage(buildPrompt(userMessage, statusData)));
|
|
564
|
+
queue.push(makeUserMessage(buildPrompt(userMessage, statusData, { mode })));
|
|
548
565
|
|
|
549
566
|
const optionsPayload = buildOptions();
|
|
550
567
|
|
package/codex-shell.mjs
CHANGED
|
@@ -79,6 +79,7 @@ let activeThreadId = null; // Thread ID for resume
|
|
|
79
79
|
let activeTurn = null; // Whether a turn is in-flight
|
|
80
80
|
let turnCount = 0; // Number of turns in this thread
|
|
81
81
|
let currentSessionId = null; // Active session identifier
|
|
82
|
+
let threadNeedsPriming = false; // True when a fresh thread needs the system prompt on next turn
|
|
82
83
|
let codexRuntimeCaps = {
|
|
83
84
|
hasSteeringApi: false,
|
|
84
85
|
steeringMethod: null,
|
|
@@ -333,22 +334,20 @@ async function getThread() {
|
|
|
333
334
|
activeThreadId = null;
|
|
334
335
|
}
|
|
335
336
|
|
|
336
|
-
// Start a new thread
|
|
337
|
+
// Start a new thread — defer the system prompt to the first user message so
|
|
338
|
+
// the priming turn is STREAMED (runStreamed) instead of blocking (run).
|
|
339
|
+
// This eliminates the 2-5 minute silent delay the chat UI suffered because
|
|
340
|
+
// the old `thread.run(SYSTEM_PROMPT)` call produced zero streaming events.
|
|
337
341
|
activeThread = codexInstance.startThread(THREAD_OPTIONS);
|
|
338
342
|
detectThreadCapabilities(activeThread);
|
|
343
|
+
threadNeedsPriming = true;
|
|
339
344
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
await
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
await saveState();
|
|
347
|
-
console.log(`[codex-shell] new thread started: ${activeThreadId}`);
|
|
348
|
-
}
|
|
349
|
-
} catch (err) {
|
|
350
|
-
console.warn(`[codex-shell] prime turn failed: ${err.message}`);
|
|
351
|
-
// Thread is still usable even if prime fails
|
|
345
|
+
if (activeThread.id) {
|
|
346
|
+
activeThreadId = activeThread.id;
|
|
347
|
+
await saveState();
|
|
348
|
+
console.log(`[codex-shell] new thread started: ${activeThreadId} (priming deferred to first user turn)`);
|
|
349
|
+
} else {
|
|
350
|
+
console.log("[codex-shell] new thread started (priming deferred to first user turn)");
|
|
352
351
|
}
|
|
353
352
|
|
|
354
353
|
return activeThread;
|
|
@@ -519,6 +518,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
|
|
|
519
518
|
abortController = null,
|
|
520
519
|
persistent = false,
|
|
521
520
|
sessionId = null,
|
|
521
|
+
mode = null,
|
|
522
522
|
} = options;
|
|
523
523
|
|
|
524
524
|
agentSdk = resolveAgentSdkConfig({ reload: true });
|
|
@@ -560,23 +560,56 @@ export async function execCodexPrompt(userMessage, options = {}) {
|
|
|
560
560
|
}
|
|
561
561
|
// else: persistent && same session && under limit → reuse activeThread
|
|
562
562
|
|
|
563
|
+
// ── Mode detection ───────────────────────────────────────────────────
|
|
564
|
+
// "ask" mode should be lightweight — no heavy executor framing that
|
|
565
|
+
// instructs the agent to run commands and read files. The mode is
|
|
566
|
+
// either passed explicitly or detected from the MODE prefix that
|
|
567
|
+
// primary-agent.mjs prepends.
|
|
568
|
+
const isAskMode =
|
|
569
|
+
mode === "ask" || /^\[MODE:\s*ask\]/i.test(userMessage);
|
|
570
|
+
|
|
563
571
|
// Build the user prompt with optional status context (built once, reused across retries)
|
|
564
572
|
let prompt = userMessage;
|
|
565
|
-
if (statusData) {
|
|
573
|
+
if (statusData && !isAskMode) {
|
|
566
574
|
const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
|
|
567
575
|
prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output.`;
|
|
576
|
+
} else if (isAskMode) {
|
|
577
|
+
// Ask mode — pass through without executor framing. The mode
|
|
578
|
+
// prefix from primary-agent already tells the model to be brief.
|
|
579
|
+
prompt = userMessage;
|
|
568
580
|
} else {
|
|
569
581
|
prompt = `${userMessage}\n\n\n# YOUR TASK — EXECUTE NOW\n\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output & complete the user's request E2E.`;
|
|
570
582
|
}
|
|
571
583
|
// Sanitize & size-guard once — prevents invalid_request_error from oversized
|
|
572
584
|
// bodies (BytePositionInLine > 80 000) or unescaped control characters.
|
|
573
|
-
|
|
585
|
+
let safePrompt = sanitizeAndTruncatePrompt(prompt);
|
|
574
586
|
|
|
575
587
|
let threadResetDone = false;
|
|
576
588
|
|
|
577
589
|
for (let attempt = 0; attempt < MAX_STREAM_RETRIES; attempt += 1) {
|
|
578
590
|
const thread = await getThread();
|
|
579
591
|
|
|
592
|
+
// If the thread is freshly created (or was just reset in a recovery path),
|
|
593
|
+
// prepend the system prompt so the agent gets its identity/context on the
|
|
594
|
+
// FIRST streamed turn. Previously this was done via a blocking
|
|
595
|
+
// `thread.run(SYSTEM_PROMPT)` call inside `getThread()`, which produced
|
|
596
|
+
// zero streaming events and caused the chat UI to appear frozen for
|
|
597
|
+
// 2-5+ minutes. Checking threadNeedsPriming INSIDE the retry loop
|
|
598
|
+
// ensures a freshly-reset thread still receives the primer.
|
|
599
|
+
let attemptPrompt = safePrompt;
|
|
600
|
+
if (threadNeedsPriming) {
|
|
601
|
+
// Ask mode gets a lightweight primer — no heavy executor directives
|
|
602
|
+
// that contradict the "don't use tools" instruction.
|
|
603
|
+
const primer = isAskMode
|
|
604
|
+
? "You are a helpful AI assistant deployed inside the bosun orchestrator. " +
|
|
605
|
+
"Answer the user's questions concisely. Only use tools when explicitly asked to."
|
|
606
|
+
: SYSTEM_PROMPT;
|
|
607
|
+
attemptPrompt = sanitizeAndTruncatePrompt(
|
|
608
|
+
primer + "\n\n---\n\n" + prompt,
|
|
609
|
+
);
|
|
610
|
+
threadNeedsPriming = false;
|
|
611
|
+
}
|
|
612
|
+
|
|
580
613
|
// Each attempt gets a fresh AbortController tied to the same timeout budget.
|
|
581
614
|
// We intentionally do NOT share the same controller across retries: if the
|
|
582
615
|
// first attempt times out the signal is already aborted and the retry would
|
|
@@ -587,7 +620,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
|
|
|
587
620
|
|
|
588
621
|
try {
|
|
589
622
|
// Use runStreamed for real-time event streaming
|
|
590
|
-
const streamedTurn = await thread.runStreamed(
|
|
623
|
+
const streamedTurn = await thread.runStreamed(attemptPrompt, {
|
|
591
624
|
signal: controller.signal,
|
|
592
625
|
});
|
|
593
626
|
|
|
@@ -776,6 +809,7 @@ export async function resetThread() {
|
|
|
776
809
|
turnCount = 0;
|
|
777
810
|
activeTurn = null;
|
|
778
811
|
currentSessionId = null;
|
|
812
|
+
threadNeedsPriming = false;
|
|
779
813
|
await saveState();
|
|
780
814
|
console.log("[codex-shell] thread reset");
|
|
781
815
|
}
|
package/config.mjs
CHANGED
|
@@ -2201,6 +2201,9 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
2201
2201
|
jira,
|
|
2202
2202
|
projectRequirements,
|
|
2203
2203
|
|
|
2204
|
+
// Voice assistant
|
|
2205
|
+
voice: Object.freeze(configData.voice || {}),
|
|
2206
|
+
|
|
2204
2207
|
// Merge Strategy
|
|
2205
2208
|
codexAnalyzeMergeStrategy:
|
|
2206
2209
|
codexEnabled &&
|
package/copilot-shell.mjs
CHANGED
|
@@ -713,6 +713,7 @@ export async function execCopilotPrompt(userMessage, options = {}) {
|
|
|
713
713
|
sendRawEvents = false,
|
|
714
714
|
abortController = null,
|
|
715
715
|
persistent = false,
|
|
716
|
+
mode = null,
|
|
716
717
|
} = options;
|
|
717
718
|
|
|
718
719
|
if (activeTurn && !options._holdActiveTurn) {
|
|
@@ -790,13 +791,25 @@ export async function execCopilotPrompt(userMessage, options = {}) {
|
|
|
790
791
|
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
791
792
|
}
|
|
792
793
|
|
|
794
|
+
// ── Mode detection ───────────────────────────────────────────────────
|
|
795
|
+
const isAskMode =
|
|
796
|
+
mode === "ask" || /^\[MODE:\s*ask\]/i.test(userMessage);
|
|
797
|
+
|
|
793
798
|
// Build prompt with optional orchestrator status
|
|
794
799
|
let prompt = userMessage;
|
|
795
|
-
if (
|
|
800
|
+
if (isAskMode) {
|
|
801
|
+
// Ask mode — pass through without executor framing
|
|
802
|
+
if (statusData) {
|
|
803
|
+
const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
|
|
804
|
+
prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n${userMessage}`;
|
|
805
|
+
} else {
|
|
806
|
+
prompt = userMessage;
|
|
807
|
+
}
|
|
808
|
+
} else if (statusData) {
|
|
796
809
|
const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
|
|
797
|
-
prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output & complete the user's request E2E
|
|
810
|
+
prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output & complete the user's request E2E.`;
|
|
798
811
|
} else {
|
|
799
|
-
|
|
812
|
+
prompt = `${userMessage}\n\n\n# YOUR TASK — EXECUTE NOW\n\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output & complete the user's request E2E.`;
|
|
800
813
|
}
|
|
801
814
|
|
|
802
815
|
const sendFn = session.sendAndWait || session.send;
|
package/opencode-shell.mjs
CHANGED
|
@@ -478,6 +478,7 @@ export async function execOpencodePrompt(userMessage, options = {}) {
|
|
|
478
478
|
sessionId = null,
|
|
479
479
|
sendRawEvents = false,
|
|
480
480
|
abortController = null,
|
|
481
|
+
mode = null,
|
|
481
482
|
} = options;
|
|
482
483
|
|
|
483
484
|
// Re-read config in case it changed hot
|
|
@@ -539,9 +540,21 @@ export async function execOpencodePrompt(userMessage, options = {}) {
|
|
|
539
540
|
};
|
|
540
541
|
}
|
|
541
542
|
|
|
543
|
+
// ── Mode detection ───────────────────────────────────────────────────
|
|
544
|
+
const isAskMode =
|
|
545
|
+
mode === "ask" || /^\[MODE:\s*ask\]/i.test(userMessage);
|
|
546
|
+
|
|
542
547
|
// Build enriched prompt
|
|
543
548
|
let prompt = userMessage;
|
|
544
|
-
if (
|
|
549
|
+
if (isAskMode) {
|
|
550
|
+
// Ask mode — pass through without executor framing
|
|
551
|
+
if (statusData) {
|
|
552
|
+
const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
|
|
553
|
+
prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n${userMessage}`;
|
|
554
|
+
} else {
|
|
555
|
+
prompt = userMessage;
|
|
556
|
+
}
|
|
557
|
+
} else if (statusData) {
|
|
545
558
|
const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
|
|
546
559
|
prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task.`;
|
|
547
560
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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/session-tracker.mjs
CHANGED
|
@@ -928,6 +928,90 @@ ${items.join("\n")}` : "todo updated";
|
|
|
928
928
|
timestamp: ts,
|
|
929
929
|
};
|
|
930
930
|
}
|
|
931
|
+
|
|
932
|
+
// ── Additional item.started subtypes ──────────────────────────────
|
|
933
|
+
// Emit lifecycle events so the streaming module keeps the
|
|
934
|
+
// "thinking / executing" indicator alive and the chat UI shows
|
|
935
|
+
// real-time progress instead of going silent for minutes.
|
|
936
|
+
if (itemType === "agent_message") {
|
|
937
|
+
return {
|
|
938
|
+
type: "system",
|
|
939
|
+
content: "Agent is composing a response…",
|
|
940
|
+
timestamp: ts,
|
|
941
|
+
meta: { lifecycle: "started", itemType },
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (itemType === "function_call") {
|
|
946
|
+
const name = toText(item.name || "").trim();
|
|
947
|
+
return {
|
|
948
|
+
type: "tool_call",
|
|
949
|
+
content: name ? `${name}(…)` : "(tool call starting)",
|
|
950
|
+
timestamp: ts,
|
|
951
|
+
meta: { toolName: name || "function_call", lifecycle: "started" },
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (itemType === "mcp_tool_call") {
|
|
956
|
+
const server = toText(item.server || "").trim();
|
|
957
|
+
const tool = toText(item.tool || "").trim();
|
|
958
|
+
return {
|
|
959
|
+
type: "tool_call",
|
|
960
|
+
content: `MCP [${server || "?"}]: ${tool || "(starting)"}`,
|
|
961
|
+
timestamp: ts,
|
|
962
|
+
meta: { toolName: tool || "mcp_tool_call", lifecycle: "started" },
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (itemType === "web_search") {
|
|
967
|
+
const query = toText(item.query || "").trim();
|
|
968
|
+
return {
|
|
969
|
+
type: "system",
|
|
970
|
+
content: query ? `Searching: ${query}` : "Web search…",
|
|
971
|
+
timestamp: ts,
|
|
972
|
+
meta: { lifecycle: "started", itemType },
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (itemType === "file_change") {
|
|
977
|
+
return {
|
|
978
|
+
type: "system",
|
|
979
|
+
content: "Editing files…",
|
|
980
|
+
timestamp: ts,
|
|
981
|
+
meta: { lifecycle: "started", itemType },
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (itemType === "todo_list") {
|
|
986
|
+
return {
|
|
987
|
+
type: "system",
|
|
988
|
+
content: "Updating plan…",
|
|
989
|
+
timestamp: ts,
|
|
990
|
+
meta: { lifecycle: "started", itemType },
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// ── Turn lifecycle events ──────────────────────────────────────────
|
|
996
|
+
// Without these, the streaming module sees no events between the last
|
|
997
|
+
// item.completed and the response finishing, causing the indicator
|
|
998
|
+
// to flip between "thinking" and "idle".
|
|
999
|
+
if (event.type === "turn.completed") {
|
|
1000
|
+
return {
|
|
1001
|
+
type: "system",
|
|
1002
|
+
content: "Turn completed",
|
|
1003
|
+
timestamp: ts,
|
|
1004
|
+
meta: { lifecycle: "turn_completed" },
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (event.type === "turn.failed") {
|
|
1009
|
+
const detail = toText(event.error?.message || "unknown error");
|
|
1010
|
+
return {
|
|
1011
|
+
type: "error",
|
|
1012
|
+
content: `Turn failed: ${detail}`.slice(0, MAX_MESSAGE_CHARS),
|
|
1013
|
+
timestamp: ts,
|
|
1014
|
+
};
|
|
931
1015
|
}
|
|
932
1016
|
|
|
933
1017
|
if (event.type === "assistant.message" && event.data?.content) {
|
|
@@ -999,6 +1083,56 @@ ${items.join("\n")}` : "todo updated";
|
|
|
999
1083
|
};
|
|
1000
1084
|
}
|
|
1001
1085
|
|
|
1086
|
+
// ── Voice events ──
|
|
1087
|
+
if (event.type === "voice.start") {
|
|
1088
|
+
return {
|
|
1089
|
+
type: "system",
|
|
1090
|
+
content: `Voice session started (provider: ${event.provider || "unknown"}, tier: ${event.tier || "?"})`,
|
|
1091
|
+
timestamp: ts,
|
|
1092
|
+
meta: { voiceEvent: "start", provider: event.provider, tier: event.tier },
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
if (event.type === "voice.end") {
|
|
1096
|
+
return {
|
|
1097
|
+
type: "system",
|
|
1098
|
+
content: `Voice session ended (duration: ${event.duration || 0}s)`,
|
|
1099
|
+
timestamp: ts,
|
|
1100
|
+
meta: { voiceEvent: "end", duration: event.duration },
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
if (event.type === "voice.transcript") {
|
|
1104
|
+
return {
|
|
1105
|
+
type: "user",
|
|
1106
|
+
content: (event.text || event.transcript || "").slice(0, MAX_MESSAGE_CHARS),
|
|
1107
|
+
timestamp: ts,
|
|
1108
|
+
meta: { voiceEvent: "transcript" },
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
if (event.type === "voice.response") {
|
|
1112
|
+
return {
|
|
1113
|
+
type: "agent_message",
|
|
1114
|
+
content: (event.text || event.response || "").slice(0, MAX_MESSAGE_CHARS),
|
|
1115
|
+
timestamp: ts,
|
|
1116
|
+
meta: { voiceEvent: "response" },
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
if (event.type === "voice.tool_call") {
|
|
1120
|
+
return {
|
|
1121
|
+
type: "tool_call",
|
|
1122
|
+
content: `voice:${event.name || "tool"}(${(event.arguments || "").slice(0, 500)})`,
|
|
1123
|
+
timestamp: ts,
|
|
1124
|
+
meta: { voiceEvent: "tool_call", toolName: event.name },
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
if (event.type === "voice.delegate") {
|
|
1128
|
+
return {
|
|
1129
|
+
type: "system",
|
|
1130
|
+
content: `Voice delegated to ${event.executor || "agent"}: ${(event.message || "").slice(0, 500)}`,
|
|
1131
|
+
timestamp: ts,
|
|
1132
|
+
meta: { voiceEvent: "delegate", executor: event.executor },
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1002
1136
|
return null;
|
|
1003
1137
|
}
|
|
1004
1138
|
|
|
@@ -1017,6 +1151,7 @@ ${items.join("\n")}` : "todo updated";
|
|
|
1017
1151
|
case "system": return "SYS";
|
|
1018
1152
|
case "user": return "USER";
|
|
1019
1153
|
case "assistant": return "ASSISTANT";
|
|
1154
|
+
case "voice": return "VOICE";
|
|
1020
1155
|
default: return type.toUpperCase();
|
|
1021
1156
|
}
|
|
1022
1157
|
}
|
package/setup.mjs
CHANGED
|
@@ -2155,7 +2155,8 @@ Before finishing a task — create a commit using conventional commits and push.
|
|
|
2155
2155
|
### PR Creation
|
|
2156
2156
|
|
|
2157
2157
|
After committing:
|
|
2158
|
-
-
|
|
2158
|
+
- Push your branch updates
|
|
2159
|
+
- Hand off PR lifecycle to Bosun management (do not run direct PR-create commands)
|
|
2159
2160
|
- Ensure pre-push hooks pass
|
|
2160
2161
|
- Fix any lint or test errors encountered
|
|
2161
2162
|
|
|
@@ -2300,13 +2301,12 @@ set -euo pipefail
|
|
|
2300
2301
|
|
|
2301
2302
|
echo "Cleaning up workspace for ${config.projectName}..."
|
|
2302
2303
|
|
|
2303
|
-
#
|
|
2304
|
+
# Hand off PR lifecycle if branch has commits
|
|
2304
2305
|
BRANCH=$(git branch --show-current 2>/dev/null || true)
|
|
2305
2306
|
if [ -n "$BRANCH" ] && [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
|
|
2306
2307
|
COMMITS=$(git log main.."$BRANCH" --oneline 2>/dev/null | wc -l || echo 0)
|
|
2307
2308
|
if [ "$COMMITS" -gt 0 ]; then
|
|
2308
|
-
echo "Branch $BRANCH has $COMMITS commit(s) —
|
|
2309
|
-
gh pr create --fill 2>/dev/null || echo "PR creation skipped"
|
|
2309
|
+
echo "Branch $BRANCH has $COMMITS commit(s) — PR lifecycle will be managed by Bosun."
|
|
2310
2310
|
fi
|
|
2311
2311
|
fi
|
|
2312
2312
|
|