@bradygaster/squad-cli 0.8.5 → 0.8.17-preview
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/README.md +2 -2
- package/dist/cli/commands/copilot-bridge.d.ts +42 -0
- package/dist/cli/commands/copilot-bridge.d.ts.map +1 -0
- package/dist/cli/commands/copilot-bridge.js +191 -0
- package/dist/cli/commands/copilot-bridge.js.map +1 -0
- package/dist/cli/commands/import.js.map +1 -1
- package/dist/cli/commands/rc-tunnel.d.ts +30 -0
- package/dist/cli/commands/rc-tunnel.d.ts.map +1 -0
- package/dist/cli/commands/rc-tunnel.js +107 -0
- package/dist/cli/commands/rc-tunnel.js.map +1 -0
- package/dist/cli/commands/rc.d.ts +13 -0
- package/dist/cli/commands/rc.d.ts.map +1 -0
- package/dist/cli/commands/rc.js +270 -0
- package/dist/cli/commands/rc.js.map +1 -0
- package/dist/cli/commands/start.d.ts +18 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +219 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/upstream.js.map +1 -1
- package/dist/cli/commands/watch.d.ts +10 -0
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +181 -65
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/core/cast.d.ts +40 -0
- package/dist/cli/core/cast.d.ts.map +1 -0
- package/dist/cli/core/cast.js +442 -0
- package/dist/cli/core/cast.js.map +1 -0
- package/dist/cli/core/gh-cli.d.ts +25 -0
- package/dist/cli/core/gh-cli.d.ts.map +1 -1
- package/dist/cli/core/gh-cli.js +15 -1
- package/dist/cli/core/gh-cli.js.map +1 -1
- package/dist/cli/core/init.d.ts +9 -1
- package/dist/cli/core/init.d.ts.map +1 -1
- package/dist/cli/core/init.js +108 -13
- package/dist/cli/core/init.js.map +1 -1
- package/dist/cli/core/migrations.js.map +1 -1
- package/dist/cli/core/nap.d.ts +37 -0
- package/dist/cli/core/nap.d.ts.map +1 -0
- package/dist/cli/core/nap.js +528 -0
- package/dist/cli/core/nap.js.map +1 -0
- package/dist/cli/core/output.d.ts +5 -0
- package/dist/cli/core/output.d.ts.map +1 -1
- package/dist/cli/core/output.js +7 -0
- package/dist/cli/core/output.js.map +1 -1
- package/dist/cli/core/upgrade.d.ts +0 -1
- package/dist/cli/core/upgrade.d.ts.map +1 -1
- package/dist/cli/core/upgrade.js.map +1 -1
- package/dist/cli/core/version.js.map +1 -1
- package/dist/cli/shell/agent-status.d.ts +11 -0
- package/dist/cli/shell/agent-status.d.ts.map +1 -0
- package/dist/cli/shell/agent-status.js +26 -0
- package/dist/cli/shell/agent-status.js.map +1 -0
- package/dist/cli/shell/commands.d.ts +10 -0
- package/dist/cli/shell/commands.d.ts.map +1 -1
- package/dist/cli/shell/commands.js +143 -29
- package/dist/cli/shell/commands.js.map +1 -1
- package/dist/cli/shell/components/AgentPanel.d.ts +1 -4
- package/dist/cli/shell/components/AgentPanel.d.ts.map +1 -1
- package/dist/cli/shell/components/AgentPanel.js +88 -6
- package/dist/cli/shell/components/AgentPanel.js.map +1 -1
- package/dist/cli/shell/components/App.d.ts +11 -6
- package/dist/cli/shell/components/App.d.ts.map +1 -1
- package/dist/cli/shell/components/App.js +212 -35
- package/dist/cli/shell/components/App.js.map +1 -1
- package/dist/cli/shell/components/ErrorBoundary.d.ts +22 -0
- package/dist/cli/shell/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/cli/shell/components/ErrorBoundary.js +31 -0
- package/dist/cli/shell/components/ErrorBoundary.js.map +1 -0
- package/dist/cli/shell/components/InputPrompt.d.ts +3 -0
- package/dist/cli/shell/components/InputPrompt.d.ts.map +1 -1
- package/dist/cli/shell/components/InputPrompt.js +155 -13
- package/dist/cli/shell/components/InputPrompt.js.map +1 -1
- package/dist/cli/shell/components/MessageStream.d.ts +17 -4
- package/dist/cli/shell/components/MessageStream.d.ts.map +1 -1
- package/dist/cli/shell/components/MessageStream.js +215 -23
- package/dist/cli/shell/components/MessageStream.js.map +1 -1
- package/dist/cli/shell/components/Separator.d.ts +17 -0
- package/dist/cli/shell/components/Separator.d.ts.map +1 -0
- package/dist/cli/shell/components/Separator.js +10 -0
- package/dist/cli/shell/components/Separator.js.map +1 -0
- package/dist/cli/shell/components/ThinkingIndicator.d.ts +21 -0
- package/dist/cli/shell/components/ThinkingIndicator.d.ts.map +1 -0
- package/dist/cli/shell/components/ThinkingIndicator.js +102 -0
- package/dist/cli/shell/components/ThinkingIndicator.js.map +1 -0
- package/dist/cli/shell/components/index.d.ts +3 -0
- package/dist/cli/shell/components/index.d.ts.map +1 -1
- package/dist/cli/shell/components/index.js +2 -0
- package/dist/cli/shell/components/index.js.map +1 -1
- package/dist/cli/shell/coordinator.d.ts +10 -0
- package/dist/cli/shell/coordinator.d.ts.map +1 -1
- package/dist/cli/shell/coordinator.js +99 -4
- package/dist/cli/shell/coordinator.js.map +1 -1
- package/dist/cli/shell/error-messages.d.ts +21 -0
- package/dist/cli/shell/error-messages.d.ts.map +1 -0
- package/dist/cli/shell/error-messages.js +61 -0
- package/dist/cli/shell/error-messages.js.map +1 -0
- package/dist/cli/shell/index.d.ts +24 -3
- package/dist/cli/shell/index.d.ts.map +1 -1
- package/dist/cli/shell/index.js +943 -34
- package/dist/cli/shell/index.js.map +1 -1
- package/dist/cli/shell/lifecycle.d.ts +2 -0
- package/dist/cli/shell/lifecycle.d.ts.map +1 -1
- package/dist/cli/shell/lifecycle.js +59 -6
- package/dist/cli/shell/lifecycle.js.map +1 -1
- package/dist/cli/shell/memory.d.ts +6 -1
- package/dist/cli/shell/memory.d.ts.map +1 -1
- package/dist/cli/shell/memory.js +12 -1
- package/dist/cli/shell/memory.js.map +1 -1
- package/dist/cli/shell/router.d.ts +16 -0
- package/dist/cli/shell/router.d.ts.map +1 -1
- package/dist/cli/shell/router.js +27 -0
- package/dist/cli/shell/router.js.map +1 -1
- package/dist/cli/shell/session-store.d.ts +47 -0
- package/dist/cli/shell/session-store.d.ts.map +1 -0
- package/dist/cli/shell/session-store.js +125 -0
- package/dist/cli/shell/session-store.js.map +1 -0
- package/dist/cli/shell/sessions.d.ts +2 -0
- package/dist/cli/shell/sessions.d.ts.map +1 -1
- package/dist/cli/shell/sessions.js +19 -5
- package/dist/cli/shell/sessions.js.map +1 -1
- package/dist/cli/shell/shell-metrics.d.ts +34 -0
- package/dist/cli/shell/shell-metrics.d.ts.map +1 -0
- package/dist/cli/shell/shell-metrics.js +98 -0
- package/dist/cli/shell/shell-metrics.js.map +1 -0
- package/dist/cli/shell/spawn.d.ts.map +1 -1
- package/dist/cli/shell/spawn.js +20 -6
- package/dist/cli/shell/spawn.js.map +1 -1
- package/dist/cli/shell/terminal.d.ts +26 -0
- package/dist/cli/shell/terminal.d.ts.map +1 -1
- package/dist/cli/shell/terminal.js +65 -2
- package/dist/cli/shell/terminal.js.map +1 -1
- package/dist/cli/shell/theme-colors.d.ts +39 -0
- package/dist/cli/shell/theme-colors.d.ts.map +1 -0
- package/dist/cli/shell/theme-colors.js +39 -0
- package/dist/cli/shell/theme-colors.js.map +1 -0
- package/dist/cli/shell/types.d.ts +2 -0
- package/dist/cli/shell/types.d.ts.map +1 -1
- package/dist/cli/shell/useAnimation.d.ts +42 -0
- package/dist/cli/shell/useAnimation.d.ts.map +1 -0
- package/dist/cli/shell/useAnimation.js +139 -0
- package/dist/cli/shell/useAnimation.js.map +1 -0
- package/dist/cli-entry.d.ts +0 -7
- package/dist/cli-entry.d.ts.map +1 -1
- package/dist/cli-entry.js +701 -96
- package/dist/cli-entry.js.map +1 -1
- package/package.json +18 -2
- package/templates/orchestration-log.md +1 -1
- package/templates/package.json +3 -0
- package/templates/ralph-triage.js +543 -0
- package/templates/scribe-charter.md +1 -1
- package/templates/squad.agent.md +10 -10
- package/templates/workflows/squad-heartbeat.yml +52 -196
- package/templates/workflows/squad-insider-release.yml +1 -1
package/dist/cli/shell/index.js
CHANGED
|
@@ -5,47 +5,171 @@
|
|
|
5
5
|
* Manages CopilotSDK sessions and routes messages to agents/coordinator.
|
|
6
6
|
*/
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
|
+
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
8
10
|
import React from 'react';
|
|
9
11
|
import { render } from 'ink';
|
|
10
12
|
import { App } from './components/App.js';
|
|
13
|
+
import { ErrorBoundary } from './components/ErrorBoundary.js';
|
|
11
14
|
import { SessionRegistry } from './sessions.js';
|
|
12
15
|
import { ShellRenderer } from './render.js';
|
|
13
16
|
import { StreamBridge } from './stream-bridge.js';
|
|
14
|
-
import { ShellLifecycle } from './lifecycle.js';
|
|
17
|
+
import { ShellLifecycle, loadWelcomeData } from './lifecycle.js';
|
|
15
18
|
import { SquadClient } from '@bradygaster/squad-sdk/client';
|
|
16
|
-
import {
|
|
19
|
+
import { initSquadTelemetry, TIMEOUTS, StreamingPipeline, recordAgentSpawn, recordAgentDuration, recordAgentError, recordAgentDestroy, RuntimeEventBus } from '@bradygaster/squad-sdk';
|
|
20
|
+
import { enableShellMetrics, recordShellSessionDuration, recordAgentResponseLatency, recordShellError } from './shell-metrics.js';
|
|
21
|
+
import { buildCoordinatorPrompt, buildInitModePrompt, parseCoordinatorResponse, hasRosterEntries } from './coordinator.js';
|
|
17
22
|
import { loadAgentCharter, buildAgentPrompt } from './spawn.js';
|
|
23
|
+
import { createSession, saveSession, loadLatestSession } from './session-store.js';
|
|
24
|
+
import { parseDispatchTargets } from './router.js';
|
|
25
|
+
import { agentSessionGuidance, genericGuidance, formatGuidance } from './error-messages.js';
|
|
26
|
+
import { parseCastResponse, createTeam, formatCastSummary } from '../core/cast.js';
|
|
18
27
|
export { SessionRegistry } from './sessions.js';
|
|
19
28
|
export { StreamBridge } from './stream-bridge.js';
|
|
20
29
|
export { ShellRenderer } from './render.js';
|
|
21
30
|
export { ShellLifecycle } from './lifecycle.js';
|
|
22
31
|
export { spawnAgent, loadAgentCharter, buildAgentPrompt } from './spawn.js';
|
|
23
|
-
export { buildCoordinatorPrompt, parseCoordinatorResponse, formatConversationContext } from './coordinator.js';
|
|
24
|
-
export { parseInput } from './router.js';
|
|
32
|
+
export { buildCoordinatorPrompt, buildInitModePrompt, parseCoordinatorResponse, formatConversationContext, hasRosterEntries } from './coordinator.js';
|
|
33
|
+
export { parseInput, parseDispatchTargets } from './router.js';
|
|
25
34
|
export { executeCommand } from './commands.js';
|
|
26
35
|
export { MemoryManager, DEFAULT_LIMITS } from './memory.js';
|
|
27
36
|
export { detectTerminal, safeChar, boxChars } from './terminal.js';
|
|
28
37
|
export { createCompleter } from './autocomplete.js';
|
|
38
|
+
export { createSession, saveSession, loadLatestSession, listSessions, loadSessionById } from './session-store.js';
|
|
29
39
|
export { App } from './components/App.js';
|
|
40
|
+
export { ErrorBoundary } from './components/ErrorBoundary.js';
|
|
41
|
+
export { sdkDisconnectGuidance, teamConfigGuidance, agentSessionGuidance, genericGuidance, formatGuidance, } from './error-messages.js';
|
|
42
|
+
export { enableShellMetrics, recordShellSessionDuration, recordAgentResponseLatency, recordShellError, isShellTelemetryEnabled, _resetShellMetrics, } from './shell-metrics.js';
|
|
30
43
|
const require = createRequire(import.meta.url);
|
|
31
44
|
const pkg = require('../../../package.json');
|
|
45
|
+
/**
|
|
46
|
+
* Approve all permission requests. CLI runs locally with user trust,
|
|
47
|
+
* so no interactive confirmation is needed.
|
|
48
|
+
*/
|
|
49
|
+
const approveAllPermissions = () => ({ kind: 'approved' });
|
|
50
|
+
/** Debug logger — writes to stderr only when SQUAD_DEBUG=1. */
|
|
51
|
+
function debugLog(...args) {
|
|
52
|
+
if (process.env['SQUAD_DEBUG'] === '1') {
|
|
53
|
+
console.error('[SQUAD_DEBUG]', ...args);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Retry a send function when the response is empty (ghost response).
|
|
58
|
+
* Ghost responses occur when session.idle fires before assistant.message,
|
|
59
|
+
* causing sendAndWait() to return undefined or empty content.
|
|
60
|
+
*/
|
|
61
|
+
export async function withGhostRetry(sendFn, options = {}) {
|
|
62
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
63
|
+
const backoffMs = options.backoffMs ?? [1000, 2000, 4000];
|
|
64
|
+
const log = options.debugLog ?? (() => { });
|
|
65
|
+
const preview = options.promptPreview ?? '';
|
|
66
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
67
|
+
if (attempt > 0) {
|
|
68
|
+
log('ghost response detected', {
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
attempt,
|
|
71
|
+
promptPreview: preview.slice(0, 80),
|
|
72
|
+
});
|
|
73
|
+
options.onRetry?.(attempt, maxRetries);
|
|
74
|
+
const delay = backoffMs[attempt - 1] ?? backoffMs[backoffMs.length - 1] ?? 4000;
|
|
75
|
+
await new Promise(r => setTimeout(r, delay));
|
|
76
|
+
}
|
|
77
|
+
const result = await sendFn();
|
|
78
|
+
if (result)
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
log('ghost response: all retries exhausted', {
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
promptPreview: preview.slice(0, 80),
|
|
84
|
+
});
|
|
85
|
+
options.onExhausted?.(maxRetries);
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
32
88
|
export async function runShell() {
|
|
89
|
+
// Ink requires a TTY for raw mode input — bail out early when piped (#576)
|
|
90
|
+
if (!process.stdin.isTTY) {
|
|
91
|
+
console.error('✗ Squad shell requires an interactive terminal (TTY).');
|
|
92
|
+
console.error(' Piped or redirected stdin is not supported.');
|
|
93
|
+
console.error(" Tip: Run 'squad --preview' for non-interactive usage.");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
// Show immediate feedback — users need to see something within 100ms
|
|
97
|
+
console.error('◆ Loading Squad shell...');
|
|
98
|
+
// Configurable REPL timeout: SQUAD_REPL_TIMEOUT (seconds) > TIMEOUTS.SESSION_RESPONSE_MS (ms)
|
|
99
|
+
const replTimeoutMs = (() => {
|
|
100
|
+
const envSeconds = process.env['SQUAD_REPL_TIMEOUT'];
|
|
101
|
+
if (envSeconds) {
|
|
102
|
+
const parsed = parseInt(envSeconds, 10);
|
|
103
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
104
|
+
return parsed * 1000;
|
|
105
|
+
}
|
|
106
|
+
return TIMEOUTS.SESSION_RESPONSE_MS;
|
|
107
|
+
})();
|
|
108
|
+
debugLog('REPL timeout:', replTimeoutMs, 'ms');
|
|
109
|
+
const sessionStart = Date.now();
|
|
110
|
+
let messageCount = 0;
|
|
33
111
|
const registry = new SessionRegistry();
|
|
34
112
|
const renderer = new ShellRenderer();
|
|
35
113
|
const teamRoot = process.cwd();
|
|
114
|
+
// Session persistence — create or resume a previous session
|
|
115
|
+
// Skip resume on first run (no team.md or .first-run marker present)
|
|
116
|
+
const hasTeam = existsSync(join(teamRoot, '.squad', 'team.md'));
|
|
117
|
+
const isFirstRun = existsSync(join(teamRoot, '.squad', '.first-run'));
|
|
118
|
+
let persistedSession = createSession();
|
|
119
|
+
const recentSession = (hasTeam && !isFirstRun) ? loadLatestSession(teamRoot) : null;
|
|
120
|
+
if (recentSession) {
|
|
121
|
+
persistedSession = recentSession;
|
|
122
|
+
debugLog('resuming recent session', persistedSession.id);
|
|
123
|
+
}
|
|
124
|
+
// Initialize OpenTelemetry if endpoint is configured (e.g. Aspire dashboard)
|
|
125
|
+
const eventBus = new RuntimeEventBus();
|
|
126
|
+
const telemetry = initSquadTelemetry({ serviceName: 'squad-cli', mode: 'cli', eventBus });
|
|
127
|
+
if (telemetry.tracing || telemetry.metrics) {
|
|
128
|
+
debugLog('🔭 Telemetry active — exporting to ' + process.env['OTEL_EXPORTER_OTLP_ENDPOINT']);
|
|
129
|
+
}
|
|
130
|
+
// Streaming pipeline for token usage and response latency metrics
|
|
131
|
+
const streamingPipeline = new StreamingPipeline();
|
|
132
|
+
// Shell-level observability metrics (auto-enabled when OTel is configured)
|
|
133
|
+
const shellMetricsActive = enableShellMetrics();
|
|
134
|
+
if (shellMetricsActive) {
|
|
135
|
+
debugLog('shell observability metrics enabled');
|
|
136
|
+
}
|
|
36
137
|
// Initialize lifecycle — discover team agents
|
|
37
138
|
const lifecycle = new ShellLifecycle({ teamRoot, renderer, registry });
|
|
38
139
|
try {
|
|
39
140
|
await lifecycle.initialize();
|
|
40
141
|
}
|
|
41
|
-
catch {
|
|
142
|
+
catch (err) {
|
|
143
|
+
debugLog('lifecycle.initialize() failed:', err);
|
|
42
144
|
// Non-fatal: shell works without discovered agents
|
|
43
145
|
}
|
|
44
146
|
// Create SDK client (auto-connects on first session creation)
|
|
45
147
|
const client = new SquadClient({ cwd: teamRoot });
|
|
46
148
|
let shellApi;
|
|
149
|
+
let origAddMessage;
|
|
47
150
|
const agentSessions = new Map();
|
|
48
151
|
let coordinatorSession = null;
|
|
152
|
+
let activeInitSession = null;
|
|
153
|
+
let pendingCastConfirmation = null;
|
|
154
|
+
// Eager SDK warm-up — start coordinator session before user's first message
|
|
155
|
+
// This runs in background so UI renders immediately
|
|
156
|
+
(async () => {
|
|
157
|
+
try {
|
|
158
|
+
debugLog('eager warm-up: creating coordinator session');
|
|
159
|
+
const systemPrompt = buildCoordinatorPrompt({ teamRoot });
|
|
160
|
+
coordinatorSession = await client.createSession({
|
|
161
|
+
streaming: true,
|
|
162
|
+
systemMessage: { mode: 'append', content: systemPrompt },
|
|
163
|
+
workingDirectory: teamRoot,
|
|
164
|
+
onPermissionRequest: approveAllPermissions,
|
|
165
|
+
});
|
|
166
|
+
debugLog('eager warm-up: coordinator session ready');
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
debugLog('eager warm-up failed (non-fatal, will retry on first dispatch):', err);
|
|
170
|
+
// Non-fatal — first dispatch will create the session as before
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
49
173
|
const streamBuffers = new Map();
|
|
50
174
|
// StreamBridge wires streaming pipeline events into Ink component state.
|
|
51
175
|
const _bridge = new StreamBridge(registry, {
|
|
@@ -63,9 +187,13 @@ export async function runShell() {
|
|
|
63
187
|
shellApi?.refreshAgents();
|
|
64
188
|
},
|
|
65
189
|
onError: (agentName, error) => {
|
|
190
|
+
debugLog(`StreamBridge error for ${agentName}:`, error);
|
|
191
|
+
streamBuffers.delete(agentName);
|
|
192
|
+
const friendly = error.message.replace(/^Error:\s*/i, '');
|
|
193
|
+
const guidance = agentSessionGuidance(agentName, friendly);
|
|
66
194
|
shellApi?.addMessage({
|
|
67
195
|
role: 'system',
|
|
68
|
-
content:
|
|
196
|
+
content: formatGuidance(guidance),
|
|
69
197
|
timestamp: new Date(),
|
|
70
198
|
});
|
|
71
199
|
},
|
|
@@ -73,7 +201,9 @@ export async function runShell() {
|
|
|
73
201
|
/** Extract text delta from an SDK session event. */
|
|
74
202
|
function extractDelta(event) {
|
|
75
203
|
const val = event['deltaContent'] ?? event['delta'] ?? event['content'];
|
|
76
|
-
|
|
204
|
+
const result = typeof val === 'string' ? val : '';
|
|
205
|
+
debugLog('extractDelta', { type: event['type'], keys: Object.keys(event), hasDeltaContent: 'deltaContent' in event, result: result.slice(0, 80) });
|
|
206
|
+
return result;
|
|
77
207
|
}
|
|
78
208
|
/**
|
|
79
209
|
* Send a prompt and wait for the full streamed response.
|
|
@@ -82,10 +212,20 @@ export async function runShell() {
|
|
|
82
212
|
*/
|
|
83
213
|
async function awaitStreamedResponse(session, prompt) {
|
|
84
214
|
if (session.sendAndWait) {
|
|
85
|
-
|
|
215
|
+
debugLog('awaitStreamedResponse: using sendAndWait');
|
|
216
|
+
// ThinkingIndicator already shows elapsed time via its own timer;
|
|
217
|
+
// no need to override the current activity hint with generic text.
|
|
218
|
+
const result = await session.sendAndWait({ prompt }, replTimeoutMs);
|
|
219
|
+
debugLog('awaitStreamedResponse: sendAndWait returned', {
|
|
220
|
+
type: typeof result,
|
|
221
|
+
keys: result ? Object.keys(result) : [],
|
|
222
|
+
hasData: !!result?.['data'],
|
|
223
|
+
});
|
|
86
224
|
// Return full response content as fallback for when deltas weren't captured
|
|
87
225
|
const data = result?.['data'];
|
|
88
|
-
|
|
226
|
+
const content = typeof data?.['content'] === 'string' ? data['content'] : '';
|
|
227
|
+
debugLog('awaitStreamedResponse: fallback content length', content.length);
|
|
228
|
+
return content;
|
|
89
229
|
}
|
|
90
230
|
else {
|
|
91
231
|
const done = new Promise((resolve) => {
|
|
@@ -108,10 +248,53 @@ export async function runShell() {
|
|
|
108
248
|
return '';
|
|
109
249
|
}
|
|
110
250
|
}
|
|
111
|
-
/**
|
|
251
|
+
/** Convenience wrapper for withGhostRetry with shell UI integration. */
|
|
252
|
+
function ghostRetry(sendFn, promptPreview) {
|
|
253
|
+
return withGhostRetry(sendFn, {
|
|
254
|
+
debugLog,
|
|
255
|
+
promptPreview,
|
|
256
|
+
onRetry: (attempt, max) => {
|
|
257
|
+
const totalAttempts = max + 1; // max is retry count, +1 for initial attempt
|
|
258
|
+
const currentAttempt = attempt + 1; // attempt is retry number, +1 for total attempt number
|
|
259
|
+
shellApi?.addMessage({
|
|
260
|
+
role: 'system',
|
|
261
|
+
content: `⚠ Empty response detected. Retrying... (attempt ${currentAttempt}/${totalAttempts})`,
|
|
262
|
+
timestamp: new Date(),
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
onExhausted: (max) => {
|
|
266
|
+
const totalAttempts = max + 1;
|
|
267
|
+
shellApi?.addMessage({
|
|
268
|
+
role: 'system',
|
|
269
|
+
content: `❌ Agent did not respond after ${totalAttempts} attempts. Try again or run \`squad doctor\`.`,
|
|
270
|
+
timestamp: new Date(),
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Send a message to an agent session and stream the response.
|
|
277
|
+
*
|
|
278
|
+
* **Streaming architecture:**
|
|
279
|
+
* 1. Register `message_delta` listener BEFORE sending message (ensures we catch all deltas)
|
|
280
|
+
* 2. Call `awaitStreamedResponse` which uses `sendAndWait` (blocks until session idle)
|
|
281
|
+
* 3. Accumulate deltas into `accumulated` via the `onDelta` handler
|
|
282
|
+
* 4. Fallback to `sendAndWait` result if no deltas were captured (ghost response handling)
|
|
283
|
+
* 5. Remove listener in finally block to prevent memory leaks
|
|
284
|
+
*
|
|
285
|
+
* Both agent and coordinator dispatch use identical event wiring patterns.
|
|
286
|
+
*/
|
|
112
287
|
async function dispatchToAgent(agentName, message) {
|
|
288
|
+
debugLog('dispatchToAgent:', agentName, message.slice(0, 120));
|
|
289
|
+
const dispatchStartMs = Date.now();
|
|
290
|
+
let firstTokenRecorded = false;
|
|
291
|
+
let dispatchError = false;
|
|
113
292
|
let session = agentSessions.get(agentName);
|
|
114
293
|
if (!session) {
|
|
294
|
+
shellApi?.setActivityHint(`Connecting to ${agentName}...`);
|
|
295
|
+
shellApi?.setAgentActivity(agentName, 'connecting...');
|
|
296
|
+
// Give React a tick to render the connection hint before blocking on SDK
|
|
297
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
115
298
|
const charter = loadAgentCharter(agentName, teamRoot);
|
|
116
299
|
const systemPrompt = buildAgentPrompt(charter);
|
|
117
300
|
if (!registry.get(agentName)) {
|
|
@@ -122,32 +305,137 @@ export async function runShell() {
|
|
|
122
305
|
streaming: true,
|
|
123
306
|
systemMessage: { mode: 'append', content: systemPrompt },
|
|
124
307
|
workingDirectory: teamRoot,
|
|
308
|
+
onPermissionRequest: approveAllPermissions,
|
|
125
309
|
});
|
|
126
310
|
agentSessions.set(agentName, session);
|
|
127
311
|
}
|
|
312
|
+
// Record agent spawn metric
|
|
313
|
+
recordAgentSpawn(agentName, 'direct');
|
|
314
|
+
// Attach streaming pipeline for token/latency metrics
|
|
315
|
+
const sid = session.sessionId ?? `agent-${agentName}-${Date.now()}`;
|
|
316
|
+
if (!streamingPipeline.isAttached(sid))
|
|
317
|
+
streamingPipeline.attachToSession(sid);
|
|
318
|
+
streamingPipeline.markMessageStart(sid);
|
|
128
319
|
registry.updateStatus(agentName, 'streaming');
|
|
129
320
|
shellApi?.refreshAgents();
|
|
321
|
+
shellApi?.setActivityHint(`${agentName} is thinking...`);
|
|
322
|
+
shellApi?.setAgentActivity(agentName, 'thinking...');
|
|
130
323
|
let accumulated = '';
|
|
324
|
+
let deltaIndex = 0;
|
|
131
325
|
const onDelta = (event) => {
|
|
326
|
+
debugLog('agent onDelta fired', agentName, { eventType: event['type'] });
|
|
132
327
|
const delta = extractDelta(event);
|
|
133
328
|
if (!delta)
|
|
134
329
|
return;
|
|
330
|
+
if (!firstTokenRecorded) {
|
|
331
|
+
firstTokenRecorded = true;
|
|
332
|
+
recordAgentResponseLatency(agentName, Date.now() - dispatchStartMs, 'direct');
|
|
333
|
+
}
|
|
334
|
+
// Feed delta to streaming pipeline for TTFT/latency metrics
|
|
335
|
+
streamingPipeline.processEvent({
|
|
336
|
+
type: 'message_delta',
|
|
337
|
+
sessionId: sid,
|
|
338
|
+
agentName,
|
|
339
|
+
content: delta,
|
|
340
|
+
index: deltaIndex++,
|
|
341
|
+
timestamp: new Date(),
|
|
342
|
+
});
|
|
135
343
|
accumulated += delta;
|
|
136
344
|
shellApi?.setStreamingContent({ agentName, content: accumulated });
|
|
345
|
+
shellApi?.setActivityHint(undefined); // Clear hint once content is flowing
|
|
346
|
+
};
|
|
347
|
+
// Listen for usage events to record token metrics and capture model name
|
|
348
|
+
const onUsage = (event) => {
|
|
349
|
+
const inputTokens = typeof event['inputTokens'] === 'number' ? event['inputTokens'] : 0;
|
|
350
|
+
const outputTokens = typeof event['outputTokens'] === 'number' ? event['outputTokens'] : 0;
|
|
351
|
+
const model = typeof event['model'] === 'string' ? event['model'] : 'unknown';
|
|
352
|
+
const estimatedCost = typeof event['estimatedCost'] === 'number' ? event['estimatedCost'] : 0;
|
|
353
|
+
// Update model display in agent panel
|
|
354
|
+
registry.updateModel(agentName, model);
|
|
355
|
+
shellApi?.refreshAgents();
|
|
356
|
+
// Feed usage to streaming pipeline for token/duration metrics
|
|
357
|
+
streamingPipeline.processEvent({
|
|
358
|
+
type: 'usage',
|
|
359
|
+
sessionId: sid,
|
|
360
|
+
agentName,
|
|
361
|
+
model,
|
|
362
|
+
inputTokens,
|
|
363
|
+
outputTokens,
|
|
364
|
+
estimatedCost,
|
|
365
|
+
timestamp: new Date(),
|
|
366
|
+
});
|
|
137
367
|
};
|
|
138
368
|
session.on('message_delta', onDelta);
|
|
139
369
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
370
|
+
session.on('usage', onUsage);
|
|
371
|
+
}
|
|
372
|
+
catch { /* event may not exist */ }
|
|
373
|
+
// Listen for tool/activity events to show Copilot-style hints
|
|
374
|
+
const onToolCall = (event) => {
|
|
375
|
+
const toolName = event['toolName'] ?? event['name'] ?? event['tool'];
|
|
376
|
+
if (typeof toolName === 'string') {
|
|
377
|
+
const hintMap = {
|
|
378
|
+
'read_file': 'Reading file...',
|
|
379
|
+
'write_file': 'Writing file...',
|
|
380
|
+
'edit_file': 'Editing file...',
|
|
381
|
+
'run_command': 'Running command...',
|
|
382
|
+
'search': 'Searching codebase...',
|
|
383
|
+
'spawn_agent': `Spawning specialist...`,
|
|
384
|
+
'analyze': 'Analyzing dependencies...',
|
|
385
|
+
};
|
|
386
|
+
const hint = hintMap[toolName] ?? `Using ${toolName}...`;
|
|
387
|
+
shellApi?.setActivityHint(hint);
|
|
388
|
+
registry.updateActivityHint(agentName, hint.replace(/\.\.\.$/, ''));
|
|
389
|
+
shellApi?.setAgentActivity(agentName, hint.replace(/\.\.\.$/, '').toLowerCase());
|
|
390
|
+
shellApi?.refreshAgents();
|
|
143
391
|
}
|
|
392
|
+
};
|
|
393
|
+
try {
|
|
394
|
+
session.on('tool_call', onToolCall);
|
|
395
|
+
}
|
|
396
|
+
catch { /* event may not exist */ }
|
|
397
|
+
try {
|
|
398
|
+
accumulated = await ghostRetry(async () => {
|
|
399
|
+
accumulated = '';
|
|
400
|
+
deltaIndex = 0;
|
|
401
|
+
const fallback = await awaitStreamedResponse(session, message);
|
|
402
|
+
debugLog('agent dispatch:', agentName, 'accumulated length', accumulated.length, 'fallback length', fallback.length);
|
|
403
|
+
if (!accumulated && fallback)
|
|
404
|
+
accumulated = fallback;
|
|
405
|
+
return accumulated;
|
|
406
|
+
}, message);
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
dispatchError = true;
|
|
410
|
+
// Evict dead session so next attempt creates a fresh one
|
|
411
|
+
debugLog('dispatchToAgent: evicting dead session for', agentName, err);
|
|
412
|
+
recordShellError('agent_dispatch', agentName);
|
|
413
|
+
recordAgentError(agentName, 'dispatch_failure');
|
|
414
|
+
agentSessions.delete(agentName);
|
|
415
|
+
streamBuffers.delete(agentName);
|
|
416
|
+
throw err;
|
|
144
417
|
}
|
|
145
418
|
finally {
|
|
146
419
|
try {
|
|
147
420
|
session.off('message_delta', onDelta);
|
|
148
421
|
}
|
|
149
422
|
catch { /* session may not support off */ }
|
|
150
|
-
|
|
423
|
+
try {
|
|
424
|
+
session.off('usage', onUsage);
|
|
425
|
+
}
|
|
426
|
+
catch { /* ignore */ }
|
|
427
|
+
try {
|
|
428
|
+
session.off('tool_call', onToolCall);
|
|
429
|
+
}
|
|
430
|
+
catch { /* ignore */ }
|
|
431
|
+
// Record agent duration and destroy metrics
|
|
432
|
+
const durationMs = Date.now() - dispatchStartMs;
|
|
433
|
+
recordAgentDuration(agentName, durationMs, dispatchError ? 'error' : 'success');
|
|
434
|
+
recordAgentDestroy(agentName);
|
|
435
|
+
streamingPipeline.detachFromSession(sid);
|
|
436
|
+
shellApi?.clearAgentStream(agentName);
|
|
437
|
+
shellApi?.setActivityHint(undefined);
|
|
438
|
+
shellApi?.setAgentActivity(agentName, undefined);
|
|
151
439
|
if (accumulated) {
|
|
152
440
|
shellApi?.addMessage({
|
|
153
441
|
role: 'agent',
|
|
@@ -160,40 +448,243 @@ export async function runShell() {
|
|
|
160
448
|
shellApi?.refreshAgents();
|
|
161
449
|
}
|
|
162
450
|
}
|
|
163
|
-
/**
|
|
451
|
+
/**
|
|
452
|
+
* Send a message through the coordinator and route based on response.
|
|
453
|
+
*
|
|
454
|
+
* **Streaming architecture:**
|
|
455
|
+
* 1. Create coordinator session with `streaming: true` config
|
|
456
|
+
* 2. Register `message_delta` listener BEFORE sending message (ensures we catch all deltas)
|
|
457
|
+
* 3. Call `awaitStreamedResponse` which uses `sendAndWait` (blocks until session idle)
|
|
458
|
+
* 4. Accumulate deltas into `accumulated` via the `onDelta` handler
|
|
459
|
+
* 5. Fallback to `sendAndWait` result if no deltas were captured (ghost response handling)
|
|
460
|
+
* 6. Remove listener in finally block to prevent memory leaks
|
|
461
|
+
* 7. Parse accumulated response and route to agents or show direct answer
|
|
462
|
+
*
|
|
463
|
+
* Event wiring is identical to `dispatchToAgent` — both use the same `message_delta` pattern.
|
|
464
|
+
*/
|
|
465
|
+
/** Extract a meaningful activity description from coordinator text near an agent name mention. */
|
|
466
|
+
function extractAgentHint(text, agentName) {
|
|
467
|
+
const lower = text.toLowerCase();
|
|
468
|
+
const nameIdx = lower.lastIndexOf(agentName.toLowerCase());
|
|
469
|
+
if (nameIdx === -1)
|
|
470
|
+
return 'working...';
|
|
471
|
+
const afterName = text.slice(nameIdx + agentName.length, nameIdx + agentName.length + 120);
|
|
472
|
+
const patterns = [
|
|
473
|
+
/^\s*(?:is|will|should|can)\s+(\w[\w\s,'-]{3,50}?)(?:[.\n;]|$)/i,
|
|
474
|
+
/^\s*[:\-→—]+\s*(\w[\w\s,'-]{3,50}?)(?:[.\n;]|$)/i,
|
|
475
|
+
/^\s+(?:to|for)\s+(\w[\w\s,'-]{3,50}?)(?:[.\n;]|$)/i,
|
|
476
|
+
];
|
|
477
|
+
for (const pattern of patterns) {
|
|
478
|
+
const match = afterName.match(pattern);
|
|
479
|
+
if (match?.[1]) {
|
|
480
|
+
let hint = match[1].trim().replace(/[.…,;:\-]+$/, '').trim();
|
|
481
|
+
if (hint.length > 45)
|
|
482
|
+
hint = hint.slice(0, 42) + '...';
|
|
483
|
+
return hint.charAt(0).toUpperCase() + hint.slice(1);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return 'working...';
|
|
487
|
+
}
|
|
164
488
|
async function dispatchToCoordinator(message) {
|
|
489
|
+
debugLog('dispatchToCoordinator: sending message', message.slice(0, 120));
|
|
490
|
+
const coordStartMs = Date.now();
|
|
491
|
+
let coordFirstToken = false;
|
|
492
|
+
let coordError = false;
|
|
165
493
|
if (!coordinatorSession) {
|
|
494
|
+
shellApi?.setActivityHint('Connecting to SDK...');
|
|
495
|
+
// Give React a tick to render the connection hint before blocking on SDK
|
|
496
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
166
497
|
const systemPrompt = buildCoordinatorPrompt({ teamRoot });
|
|
167
498
|
coordinatorSession = await client.createSession({
|
|
168
499
|
streaming: true,
|
|
169
500
|
systemMessage: { mode: 'append', content: systemPrompt },
|
|
170
501
|
workingDirectory: teamRoot,
|
|
502
|
+
onPermissionRequest: approveAllPermissions,
|
|
503
|
+
});
|
|
504
|
+
debugLog('coordinator session created:', {
|
|
505
|
+
sessionId: coordinatorSession.sessionId,
|
|
506
|
+
hasOn: typeof coordinatorSession.on === 'function',
|
|
507
|
+
hasSendAndWait: typeof coordinatorSession.sendAndWait === 'function',
|
|
171
508
|
});
|
|
172
509
|
}
|
|
510
|
+
shellApi?.setActivityHint('Coordinator is thinking...');
|
|
511
|
+
// Record coordinator spawn metric
|
|
512
|
+
recordAgentSpawn('coordinator', 'coordinator');
|
|
513
|
+
const coordSid = coordinatorSession.sessionId ?? `coordinator-${Date.now()}`;
|
|
514
|
+
if (!streamingPipeline.isAttached(coordSid))
|
|
515
|
+
streamingPipeline.attachToSession(coordSid);
|
|
516
|
+
streamingPipeline.markMessageStart(coordSid);
|
|
517
|
+
// Build a set of known agent names for detecting mentions in coordinator text
|
|
518
|
+
const knownAgentNames = registry.getAll().map(a => a.name.toLowerCase());
|
|
173
519
|
let accumulated = '';
|
|
520
|
+
let coordDeltaIndex = 0;
|
|
174
521
|
const onDelta = (event) => {
|
|
522
|
+
debugLog('coordinator onDelta fired', { eventType: event['type'] });
|
|
175
523
|
const delta = extractDelta(event);
|
|
176
524
|
if (!delta)
|
|
177
525
|
return;
|
|
526
|
+
if (!coordFirstToken) {
|
|
527
|
+
coordFirstToken = true;
|
|
528
|
+
recordAgentResponseLatency('coordinator', Date.now() - coordStartMs, 'coordinator');
|
|
529
|
+
}
|
|
530
|
+
// Feed delta to streaming pipeline for TTFT/latency metrics
|
|
531
|
+
streamingPipeline.processEvent({
|
|
532
|
+
type: 'message_delta',
|
|
533
|
+
sessionId: coordSid,
|
|
534
|
+
agentName: 'coordinator',
|
|
535
|
+
content: delta,
|
|
536
|
+
index: coordDeltaIndex++,
|
|
537
|
+
timestamp: new Date(),
|
|
538
|
+
});
|
|
178
539
|
accumulated += delta;
|
|
179
|
-
|
|
540
|
+
// Don't push coordinator routing text to streamingContent — it's internal
|
|
541
|
+
// routing instructions, not user-facing content. Keeping streamingContent
|
|
542
|
+
// empty lets the ThinkingIndicator stay visible with the "Routing..." hint.
|
|
543
|
+
// Parse streaming text for agent name mentions → update AgentPanel
|
|
544
|
+
for (const name of knownAgentNames) {
|
|
545
|
+
if (delta.toLowerCase().includes(name)) {
|
|
546
|
+
const displayName = registry.get(name)?.name ?? name;
|
|
547
|
+
registry.updateStatus(name, 'working');
|
|
548
|
+
// Extract task description from accumulated coordinator text
|
|
549
|
+
const hint = extractAgentHint(accumulated, name);
|
|
550
|
+
registry.updateActivityHint(name, hint);
|
|
551
|
+
shellApi?.setActivityHint(`${displayName} — ${hint}`);
|
|
552
|
+
shellApi?.setAgentActivity(name, hint);
|
|
553
|
+
shellApi?.refreshAgents();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
180
556
|
};
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
557
|
+
// Listen for usage events to record token metrics
|
|
558
|
+
const onCoordUsage = (event) => {
|
|
559
|
+
const inputTokens = typeof event['inputTokens'] === 'number' ? event['inputTokens'] : 0;
|
|
560
|
+
const outputTokens = typeof event['outputTokens'] === 'number' ? event['outputTokens'] : 0;
|
|
561
|
+
const model = typeof event['model'] === 'string' ? event['model'] : 'unknown';
|
|
562
|
+
const estimatedCost = typeof event['estimatedCost'] === 'number' ? event['estimatedCost'] : 0;
|
|
563
|
+
streamingPipeline.processEvent({
|
|
564
|
+
type: 'usage',
|
|
565
|
+
sessionId: coordSid,
|
|
566
|
+
agentName: 'coordinator',
|
|
567
|
+
model,
|
|
568
|
+
inputTokens,
|
|
569
|
+
outputTokens,
|
|
570
|
+
estimatedCost,
|
|
571
|
+
timestamp: new Date(),
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
// Listen for tool/activity events (same pattern as dispatchToAgent)
|
|
575
|
+
const onToolCall = (event) => {
|
|
576
|
+
const toolName = event['toolName'] ?? event['name'] ?? event['tool'];
|
|
577
|
+
if (typeof toolName === 'string') {
|
|
578
|
+
const hintMap = {
|
|
579
|
+
'read_file': 'Reading file...',
|
|
580
|
+
'write_file': 'Writing file...',
|
|
581
|
+
'edit_file': 'Editing file...',
|
|
582
|
+
'run_command': 'Running command...',
|
|
583
|
+
'search': 'Searching codebase...',
|
|
584
|
+
'spawn_agent': 'Spawning agent...',
|
|
585
|
+
'task': 'Dispatching to agent...',
|
|
586
|
+
'analyze': 'Analyzing dependencies...',
|
|
587
|
+
};
|
|
588
|
+
// Try to extract agent name from task description (e.g., "🔧 Morpheus: Building effects")
|
|
589
|
+
const desc = typeof event['description'] === 'string' ? event['description'] : '';
|
|
590
|
+
const agentMatch = desc.match(/^\S*\s*(\w+):/);
|
|
591
|
+
const matchedAgent = agentMatch?.[1]?.toLowerCase();
|
|
592
|
+
if (matchedAgent && knownAgentNames.includes(matchedAgent)) {
|
|
593
|
+
registry.updateStatus(matchedAgent, 'working');
|
|
594
|
+
const taskSummary = desc.replace(/^\S*\s*\w+:\s*/, '').slice(0, 60);
|
|
595
|
+
registry.updateActivityHint(matchedAgent, taskSummary || 'working...');
|
|
596
|
+
shellApi?.setActivityHint(`${registry.get(matchedAgent)?.name ?? matchedAgent} — ${taskSummary || 'working'}...`);
|
|
597
|
+
shellApi?.setAgentActivity(matchedAgent, taskSummary || 'working...');
|
|
598
|
+
shellApi?.refreshAgents();
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
const hint = hintMap[toolName] ?? `Using ${toolName}...`;
|
|
602
|
+
shellApi?.setActivityHint(hint);
|
|
603
|
+
}
|
|
186
604
|
}
|
|
605
|
+
};
|
|
606
|
+
const activeCoordSession = coordinatorSession;
|
|
607
|
+
// Wire event listeners BEFORE sending the message to ensure we catch all events
|
|
608
|
+
activeCoordSession.on('message_delta', onDelta);
|
|
609
|
+
try {
|
|
610
|
+
activeCoordSession.on('usage', onCoordUsage);
|
|
611
|
+
}
|
|
612
|
+
catch { /* event may not exist */ }
|
|
613
|
+
try {
|
|
614
|
+
activeCoordSession.on('tool_call', onToolCall);
|
|
615
|
+
}
|
|
616
|
+
catch { /* event may not exist */ }
|
|
617
|
+
debugLog('coordinator message_delta + usage + tool_call listeners registered');
|
|
618
|
+
try {
|
|
619
|
+
accumulated = await ghostRetry(async () => {
|
|
620
|
+
accumulated = '';
|
|
621
|
+
coordDeltaIndex = 0;
|
|
622
|
+
debugLog('coordinator: starting awaitStreamedResponse');
|
|
623
|
+
const fallback = await awaitStreamedResponse(activeCoordSession, message);
|
|
624
|
+
debugLog('coordinator dispatch: accumulated length', accumulated.length, 'fallback length', fallback.length);
|
|
625
|
+
if (!accumulated && fallback) {
|
|
626
|
+
debugLog('coordinator: using sendAndWait fallback content');
|
|
627
|
+
accumulated = fallback;
|
|
628
|
+
}
|
|
629
|
+
return accumulated;
|
|
630
|
+
}, message);
|
|
631
|
+
debugLog('coordinator: final accumulated length', accumulated.length);
|
|
632
|
+
}
|
|
633
|
+
catch (err) {
|
|
634
|
+
coordError = true;
|
|
635
|
+
// Evict dead coordinator session so next attempt creates a fresh one
|
|
636
|
+
debugLog('dispatchToCoordinator: evicting dead coordinator session', err);
|
|
637
|
+
recordShellError('coordinator_dispatch');
|
|
638
|
+
recordAgentError('coordinator', 'dispatch_failure');
|
|
639
|
+
coordinatorSession = null;
|
|
640
|
+
streamBuffers.delete('coordinator');
|
|
641
|
+
throw err;
|
|
187
642
|
}
|
|
188
643
|
finally {
|
|
189
644
|
try {
|
|
190
|
-
|
|
645
|
+
activeCoordSession.off('message_delta', onDelta);
|
|
646
|
+
debugLog('coordinator message_delta listener removed');
|
|
191
647
|
}
|
|
192
648
|
catch { /* session may not support off */ }
|
|
193
|
-
|
|
649
|
+
try {
|
|
650
|
+
activeCoordSession.off('usage', onCoordUsage);
|
|
651
|
+
}
|
|
652
|
+
catch { /* ignore */ }
|
|
653
|
+
try {
|
|
654
|
+
activeCoordSession.off('tool_call', onToolCall);
|
|
655
|
+
}
|
|
656
|
+
catch { /* ignore */ }
|
|
657
|
+
// Record coordinator duration and destroy metrics
|
|
658
|
+
const coordDurationMs = Date.now() - coordStartMs;
|
|
659
|
+
recordAgentDuration('coordinator', coordDurationMs, coordError ? 'error' : 'success');
|
|
660
|
+
recordAgentDestroy('coordinator');
|
|
661
|
+
streamingPipeline.detachFromSession(coordSid);
|
|
662
|
+
shellApi?.clearAgentStream('coordinator');
|
|
663
|
+
// Reset any agents that were marked working during coordinator dispatch
|
|
664
|
+
for (const name of knownAgentNames) {
|
|
665
|
+
const agent = registry.get(name);
|
|
666
|
+
if (agent && (agent.status === 'working' || agent.status === 'streaming')) {
|
|
667
|
+
registry.updateStatus(name, 'idle');
|
|
668
|
+
shellApi?.setAgentActivity(name, undefined);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// Re-sync registry from team.md for any new agents added by coordinator
|
|
672
|
+
const freshRoster = loadWelcomeData(teamRoot);
|
|
673
|
+
if (freshRoster) {
|
|
674
|
+
for (const agent of freshRoster.agents) {
|
|
675
|
+
const lname = agent.name.toLowerCase();
|
|
676
|
+
if (!registry.get(lname)) {
|
|
677
|
+
registry.register(agent.name, agent.role);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
shellApi?.refreshWelcome();
|
|
682
|
+
shellApi?.refreshAgents();
|
|
194
683
|
}
|
|
195
684
|
// Parse routing decision from coordinator response
|
|
685
|
+
debugLog('coordinator accumulated (first 200 chars)', accumulated.slice(0, 200));
|
|
196
686
|
const decision = parseCoordinatorResponse(accumulated);
|
|
687
|
+
debugLog('coordinator decision', { type: decision.type, hasRoutes: !!(decision.routes?.length), hasDirectAnswer: !!decision.directAnswer });
|
|
197
688
|
if (decision.type === 'route' && decision.routes?.length) {
|
|
198
689
|
for (const route of decision.routes) {
|
|
199
690
|
shellApi?.addMessage({
|
|
@@ -225,42 +716,435 @@ export async function runShell() {
|
|
|
225
716
|
});
|
|
226
717
|
}
|
|
227
718
|
}
|
|
719
|
+
/** Cancel all active operations (called on Ctrl+C during processing). */
|
|
720
|
+
async function handleCancel() {
|
|
721
|
+
debugLog('handleCancel: aborting active sessions');
|
|
722
|
+
// Abort init session if active
|
|
723
|
+
if (activeInitSession) {
|
|
724
|
+
try {
|
|
725
|
+
await activeInitSession.abort?.();
|
|
726
|
+
debugLog('aborted init session');
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
debugLog('abort init failed:', err);
|
|
730
|
+
}
|
|
731
|
+
activeInitSession = null;
|
|
732
|
+
}
|
|
733
|
+
// Clear pending cast confirmation
|
|
734
|
+
pendingCastConfirmation = null;
|
|
735
|
+
// Abort coordinator session
|
|
736
|
+
if (coordinatorSession) {
|
|
737
|
+
try {
|
|
738
|
+
await coordinatorSession.abort?.();
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
debugLog('abort coordinator failed:', err);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// Abort all agent sessions
|
|
745
|
+
for (const [name, session] of agentSessions) {
|
|
746
|
+
try {
|
|
747
|
+
await session.abort?.();
|
|
748
|
+
debugLog(`aborted session: ${name}`);
|
|
749
|
+
}
|
|
750
|
+
catch (err) {
|
|
751
|
+
debugLog(`abort ${name} failed:`, err);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// Clear streaming state
|
|
755
|
+
streamBuffers.clear();
|
|
756
|
+
shellApi?.setStreamingContent(null);
|
|
757
|
+
shellApi?.setActivityHint(undefined);
|
|
758
|
+
shellApi?.addMessage({
|
|
759
|
+
role: 'system',
|
|
760
|
+
content: 'Operation cancelled.',
|
|
761
|
+
timestamp: new Date(),
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Init Mode — cast a team when the roster is empty.
|
|
766
|
+
* Creates a temporary coordinator session with Init Mode instructions,
|
|
767
|
+
* sends the user's message, parses the team proposal, creates files,
|
|
768
|
+
* and then re-dispatches the original message to the now-populated team.
|
|
769
|
+
*/
|
|
770
|
+
async function handleInitCast(parsed, skipConfirmation) {
|
|
771
|
+
debugLog('handleInitCast: entering Init Mode');
|
|
772
|
+
shellApi?.setProcessing(true);
|
|
773
|
+
// Check for a stored init prompt (from `squad init "prompt"`)
|
|
774
|
+
const initPromptFile = join(teamRoot, '.squad', '.init-prompt');
|
|
775
|
+
let castPrompt = parsed.raw;
|
|
776
|
+
if (existsSync(initPromptFile)) {
|
|
777
|
+
const storedPrompt = readFileSync(initPromptFile, 'utf-8').trim();
|
|
778
|
+
if (storedPrompt) {
|
|
779
|
+
debugLog('handleInitCast: using stored init prompt', storedPrompt.slice(0, 100));
|
|
780
|
+
castPrompt = storedPrompt;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
shellApi?.addMessage({
|
|
784
|
+
role: 'system',
|
|
785
|
+
content: '🏗️ No team yet — casting one based on your project...',
|
|
786
|
+
timestamp: new Date(),
|
|
787
|
+
});
|
|
788
|
+
shellApi?.setActivityHint('Casting your team...');
|
|
789
|
+
// Create a temporary Init Mode coordinator session
|
|
790
|
+
let initSession = null;
|
|
791
|
+
try {
|
|
792
|
+
const initSysPrompt = buildInitModePrompt({ teamRoot });
|
|
793
|
+
initSession = await client.createSession({
|
|
794
|
+
streaming: true,
|
|
795
|
+
systemMessage: { mode: 'append', content: initSysPrompt },
|
|
796
|
+
workingDirectory: teamRoot,
|
|
797
|
+
onPermissionRequest: approveAllPermissions,
|
|
798
|
+
});
|
|
799
|
+
activeInitSession = initSession;
|
|
800
|
+
debugLog('handleInitCast: init session created');
|
|
801
|
+
// Send the prompt and collect the response
|
|
802
|
+
let accumulated = '';
|
|
803
|
+
const onDelta = (event) => {
|
|
804
|
+
const delta = extractDelta(event);
|
|
805
|
+
if (delta)
|
|
806
|
+
accumulated += delta;
|
|
807
|
+
};
|
|
808
|
+
initSession.on('message_delta', onDelta);
|
|
809
|
+
try {
|
|
810
|
+
accumulated = await ghostRetry(async () => {
|
|
811
|
+
accumulated = '';
|
|
812
|
+
const fallback = await awaitStreamedResponse(initSession, castPrompt);
|
|
813
|
+
if (!accumulated && fallback)
|
|
814
|
+
accumulated = fallback;
|
|
815
|
+
return accumulated;
|
|
816
|
+
}, castPrompt);
|
|
817
|
+
}
|
|
818
|
+
finally {
|
|
819
|
+
try {
|
|
820
|
+
initSession.off('message_delta', onDelta);
|
|
821
|
+
}
|
|
822
|
+
catch { /* ignore */ }
|
|
823
|
+
}
|
|
824
|
+
debugLog('handleInitCast: response length', accumulated.length);
|
|
825
|
+
debugLog('handleInitCast: response preview', accumulated.slice(0, 500));
|
|
826
|
+
// Parse the team proposal
|
|
827
|
+
const proposal = parseCastResponse(accumulated);
|
|
828
|
+
if (!proposal) {
|
|
829
|
+
debugLog('handleInitCast: failed to parse team from response');
|
|
830
|
+
debugLog('handleInitCast: full response:', accumulated);
|
|
831
|
+
shellApi?.addMessage({
|
|
832
|
+
role: 'system',
|
|
833
|
+
content: [
|
|
834
|
+
'⚠ Could not parse a team proposal from the model response.',
|
|
835
|
+
'',
|
|
836
|
+
'Try again, or run: squad init "describe your project"',
|
|
837
|
+
].join('\n'),
|
|
838
|
+
timestamp: new Date(),
|
|
839
|
+
});
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
// Show the proposed team
|
|
843
|
+
shellApi?.addMessage({
|
|
844
|
+
role: 'agent',
|
|
845
|
+
agentName: 'coordinator',
|
|
846
|
+
content: `Team proposed:\n\n${formatCastSummary(proposal)}\n\nUniverse: ${proposal.universe}`,
|
|
847
|
+
timestamp: new Date(),
|
|
848
|
+
});
|
|
849
|
+
// Close the init session — it's no longer needed after parsing the proposal
|
|
850
|
+
try {
|
|
851
|
+
await initSession.close?.();
|
|
852
|
+
}
|
|
853
|
+
catch { /* ignore */ }
|
|
854
|
+
initSession = null;
|
|
855
|
+
activeInitSession = null;
|
|
856
|
+
// P2: Cast confirmation — require user approval for freeform REPL casts
|
|
857
|
+
if (!skipConfirmation) {
|
|
858
|
+
shellApi?.addMessage({
|
|
859
|
+
role: 'system',
|
|
860
|
+
content: 'Look good? Type **y** to confirm or **n** to cancel.',
|
|
861
|
+
timestamp: new Date(),
|
|
862
|
+
});
|
|
863
|
+
pendingCastConfirmation = { proposal, parsed };
|
|
864
|
+
shellApi?.setActivityHint(undefined);
|
|
865
|
+
shellApi?.setProcessing(false);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
// Auto-confirmed path (auto-cast or /init command) — create team immediately
|
|
869
|
+
await finalizeCast(proposal, parsed);
|
|
870
|
+
}
|
|
871
|
+
catch (err) {
|
|
872
|
+
debugLog('handleInitCast error:', err);
|
|
873
|
+
recordShellError('init_cast', err instanceof Error ? err.constructor.name : 'unknown');
|
|
874
|
+
shellApi?.addMessage({
|
|
875
|
+
role: 'system',
|
|
876
|
+
content: `⚠ Team casting failed: ${err instanceof Error ? err.message : String(err)}\nTry again or edit .squad/team.md directly.`,
|
|
877
|
+
timestamp: new Date(),
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
finally {
|
|
881
|
+
if (initSession) {
|
|
882
|
+
try {
|
|
883
|
+
await initSession.close?.();
|
|
884
|
+
}
|
|
885
|
+
catch { /* ignore */ }
|
|
886
|
+
}
|
|
887
|
+
activeInitSession = null;
|
|
888
|
+
shellApi?.setActivityHint(undefined);
|
|
889
|
+
shellApi?.setProcessing(false);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Finalize a confirmed cast — create team files, register agents, re-dispatch.
|
|
894
|
+
* Shared by the auto-confirmed path and the pending-confirmation accept path.
|
|
895
|
+
*/
|
|
896
|
+
async function finalizeCast(proposal, parsed) {
|
|
897
|
+
shellApi?.setActivityHint('Creating team files...');
|
|
898
|
+
const result = await createTeam(teamRoot, proposal);
|
|
899
|
+
debugLog('finalizeCast: team created', {
|
|
900
|
+
members: result.membersCreated.length,
|
|
901
|
+
files: result.filesCreated.length,
|
|
902
|
+
});
|
|
903
|
+
shellApi?.addMessage({
|
|
904
|
+
role: 'system',
|
|
905
|
+
content: `✅ Team hired! ${result.membersCreated.length} members created.`,
|
|
906
|
+
timestamp: new Date(),
|
|
907
|
+
});
|
|
908
|
+
// Clean up stored init prompt (it's been consumed)
|
|
909
|
+
const initPromptFile = join(teamRoot, '.squad', '.init-prompt');
|
|
910
|
+
if (existsSync(initPromptFile)) {
|
|
911
|
+
try {
|
|
912
|
+
unlinkSync(initPromptFile);
|
|
913
|
+
}
|
|
914
|
+
catch { /* ignore */ }
|
|
915
|
+
}
|
|
916
|
+
// Invalidate the old coordinator session so the next dispatch builds one
|
|
917
|
+
// with the real team roster
|
|
918
|
+
if (coordinatorSession) {
|
|
919
|
+
try {
|
|
920
|
+
await coordinatorSession.abort?.();
|
|
921
|
+
}
|
|
922
|
+
catch { /* ignore */ }
|
|
923
|
+
coordinatorSession = null;
|
|
924
|
+
streamBuffers.delete('coordinator');
|
|
925
|
+
}
|
|
926
|
+
// Register the new agents in the session registry
|
|
927
|
+
for (const member of proposal.members) {
|
|
928
|
+
const roleName = member.role || 'Agent';
|
|
929
|
+
registry.register(member.name, roleName);
|
|
930
|
+
}
|
|
931
|
+
// Refresh the header box to show new team roster
|
|
932
|
+
shellApi?.refreshWelcome();
|
|
933
|
+
shellApi?.setActivityHint('Routing your message to the team...');
|
|
934
|
+
// Re-dispatch the original message — now with a populated roster
|
|
935
|
+
shellApi?.addMessage({
|
|
936
|
+
role: 'system',
|
|
937
|
+
content: '📌 Routing your message to the team now...',
|
|
938
|
+
timestamp: new Date(),
|
|
939
|
+
});
|
|
940
|
+
await dispatchToCoordinator(parsed.content ?? parsed.raw);
|
|
941
|
+
shellApi?.setActivityHint(undefined);
|
|
942
|
+
}
|
|
228
943
|
/** Handle dispatching parsed input to agents or coordinator. */
|
|
229
944
|
async function handleDispatch(parsed) {
|
|
945
|
+
// P2: Handle pending cast confirmation before any other dispatch
|
|
946
|
+
if (pendingCastConfirmation) {
|
|
947
|
+
const input = parsed.raw.trim().toLowerCase();
|
|
948
|
+
const { proposal, parsed: originalParsed } = pendingCastConfirmation;
|
|
949
|
+
pendingCastConfirmation = null;
|
|
950
|
+
if (input === 'y' || input === 'yes') {
|
|
951
|
+
try {
|
|
952
|
+
await finalizeCast(proposal, originalParsed);
|
|
953
|
+
}
|
|
954
|
+
catch (err) {
|
|
955
|
+
debugLog('finalizeCast error:', err);
|
|
956
|
+
recordShellError('init_cast', err instanceof Error ? err.constructor.name : 'unknown');
|
|
957
|
+
shellApi?.addMessage({
|
|
958
|
+
role: 'system',
|
|
959
|
+
content: `⚠ Team casting failed: ${err instanceof Error ? err.message : String(err)}\nTry again or edit .squad/team.md directly.`,
|
|
960
|
+
timestamp: new Date(),
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
shellApi?.addMessage({
|
|
966
|
+
role: 'system',
|
|
967
|
+
content: 'Cast cancelled. Describe what you\'re building to try again.',
|
|
968
|
+
timestamp: new Date(),
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
// Guard: require a Squad team before processing work requests
|
|
974
|
+
const teamFile = join(teamRoot, '.squad', 'team.md');
|
|
975
|
+
if (!existsSync(teamFile)) {
|
|
976
|
+
shellApi?.addMessage({
|
|
977
|
+
role: 'system',
|
|
978
|
+
content: '\u26A0 No Squad team found. Run /init to create your team first.',
|
|
979
|
+
timestamp: new Date(),
|
|
980
|
+
});
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
// Check if roster is actually populated — if not, enter Init Mode (cast a team)
|
|
984
|
+
const teamContent = readFileSync(teamFile, 'utf-8');
|
|
985
|
+
if (!hasRosterEntries(teamContent)) {
|
|
986
|
+
await handleInitCast(parsed, parsed.skipCastConfirmation);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
messageCount++;
|
|
230
990
|
try {
|
|
231
|
-
|
|
991
|
+
// Check for multiple @agent mentions for parallel dispatch
|
|
992
|
+
const knownAgents = registry.getAll().map(s => s.name);
|
|
993
|
+
const targets = parseDispatchTargets(parsed.raw, knownAgents);
|
|
994
|
+
if (targets.agents.length > 1) {
|
|
995
|
+
debugLog('handleDispatch: multi-agent dispatch detected', {
|
|
996
|
+
agents: targets.agents,
|
|
997
|
+
contentPreview: targets.content.slice(0, 80),
|
|
998
|
+
});
|
|
999
|
+
for (const agent of targets.agents) {
|
|
1000
|
+
shellApi?.addMessage({
|
|
1001
|
+
role: 'system',
|
|
1002
|
+
content: `📌 Dispatching to ${agent} (parallel)`,
|
|
1003
|
+
timestamp: new Date(),
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
const results = await Promise.allSettled(targets.agents.map(agent => dispatchToAgent(agent, targets.content || parsed.raw)));
|
|
1007
|
+
for (let i = 0; i < results.length; i++) {
|
|
1008
|
+
const r = results[i];
|
|
1009
|
+
if (r.status === 'rejected') {
|
|
1010
|
+
debugLog('handleDispatch: parallel agent failed', targets.agents[i], r.reason);
|
|
1011
|
+
shellApi?.addMessage({
|
|
1012
|
+
role: 'system',
|
|
1013
|
+
content: `⚠ ${targets.agents[i]} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`,
|
|
1014
|
+
timestamp: new Date(),
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
else if (parsed.type === 'direct_agent' && parsed.agentName) {
|
|
1020
|
+
debugLog('handleDispatch: single agent dispatch', { agent: parsed.agentName });
|
|
232
1021
|
await dispatchToAgent(parsed.agentName, parsed.content ?? parsed.raw);
|
|
233
1022
|
}
|
|
234
1023
|
else if (parsed.type === 'coordinator') {
|
|
1024
|
+
debugLog('handleDispatch: routing through coordinator');
|
|
235
1025
|
await dispatchToCoordinator(parsed.content ?? parsed.raw);
|
|
236
1026
|
}
|
|
237
1027
|
}
|
|
238
1028
|
catch (err) {
|
|
1029
|
+
debugLog('handleDispatch error:', err);
|
|
1030
|
+
recordShellError('dispatch', err instanceof Error ? err.constructor.name : 'unknown');
|
|
239
1031
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1032
|
+
const friendly = errorMsg.replace(/^Error:\s*/i, '');
|
|
1033
|
+
// Only show raw error detail when SQUAD_DEBUG=1; otherwise keep it generic
|
|
1034
|
+
const detail = process.env['SQUAD_DEBUG'] === '1' ? friendly : 'Something went wrong processing your message.';
|
|
240
1035
|
if (shellApi) {
|
|
1036
|
+
const guidance = genericGuidance(detail);
|
|
241
1037
|
shellApi.addMessage({
|
|
242
1038
|
role: 'system',
|
|
243
|
-
content:
|
|
1039
|
+
content: formatGuidance(guidance),
|
|
244
1040
|
timestamp: new Date(),
|
|
245
1041
|
});
|
|
246
1042
|
}
|
|
247
1043
|
}
|
|
248
1044
|
}
|
|
249
|
-
|
|
1045
|
+
/** Auto-save session when messages change. */
|
|
1046
|
+
let shellMessages = [];
|
|
1047
|
+
function autoSave() {
|
|
1048
|
+
persistedSession.messages = shellMessages;
|
|
1049
|
+
try {
|
|
1050
|
+
saveSession(teamRoot, persistedSession);
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
debugLog('autoSave failed:', err);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/** Callback for /resume command — replaces current messages with restored session. */
|
|
1057
|
+
function onRestoreSession(session) {
|
|
1058
|
+
persistedSession = session;
|
|
1059
|
+
// Clear old messages and terminal to prevent content bleed-through
|
|
1060
|
+
shellApi?.clearMessages();
|
|
1061
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
1062
|
+
// Use unwrapped addMessage to avoid per-message autoSave and duplicate pushes
|
|
1063
|
+
for (const msg of session.messages) {
|
|
1064
|
+
origAddMessage?.(msg);
|
|
1065
|
+
}
|
|
1066
|
+
shellMessages = [...session.messages];
|
|
1067
|
+
autoSave();
|
|
1068
|
+
}
|
|
1069
|
+
// Clear terminal and scrollback — prevents old scaffold output from
|
|
1070
|
+
// bleeding through above the header box in extended sessions.
|
|
1071
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
1072
|
+
const { waitUntilExit } = render(React.createElement(ErrorBoundary, null, React.createElement(App, {
|
|
250
1073
|
registry,
|
|
251
1074
|
renderer,
|
|
252
1075
|
teamRoot,
|
|
253
1076
|
version: pkg.version,
|
|
254
|
-
onReady: (api) => {
|
|
1077
|
+
onReady: (api) => {
|
|
1078
|
+
// Wrap addMessage to auto-save on every message
|
|
1079
|
+
const origAdd = api.addMessage;
|
|
1080
|
+
origAddMessage = origAdd;
|
|
1081
|
+
api.addMessage = (msg) => {
|
|
1082
|
+
origAdd(msg);
|
|
1083
|
+
shellMessages.push(msg);
|
|
1084
|
+
autoSave();
|
|
1085
|
+
};
|
|
1086
|
+
shellApi = api;
|
|
1087
|
+
// Restore messages from resumed session
|
|
1088
|
+
if (recentSession && recentSession.messages.length > 0) {
|
|
1089
|
+
for (const msg of recentSession.messages) {
|
|
1090
|
+
origAdd(msg);
|
|
1091
|
+
}
|
|
1092
|
+
shellMessages = [...recentSession.messages];
|
|
1093
|
+
origAdd({
|
|
1094
|
+
role: 'system',
|
|
1095
|
+
content: `✓ Resumed session ${recentSession.id.slice(0, 8)} (${recentSession.messages.length} messages)`,
|
|
1096
|
+
timestamp: new Date(),
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
// Bug fix #3: Clean up orphan .init-prompt if team already exists
|
|
1100
|
+
const initPromptPath = join(teamRoot, '.squad', '.init-prompt');
|
|
1101
|
+
const teamFilePath = join(teamRoot, '.squad', 'team.md');
|
|
1102
|
+
if (existsSync(teamFilePath)) {
|
|
1103
|
+
const tc = readFileSync(teamFilePath, 'utf-8');
|
|
1104
|
+
if (hasRosterEntries(tc) && existsSync(initPromptPath)) {
|
|
1105
|
+
debugLog('Cleaning up orphan .init-prompt (team already exists)');
|
|
1106
|
+
try {
|
|
1107
|
+
unlinkSync(initPromptPath);
|
|
1108
|
+
}
|
|
1109
|
+
catch { /* ignore */ }
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
// Bug fix #1: Auto-cast after shellApi is guaranteed to be set (no race condition)
|
|
1113
|
+
if (existsSync(initPromptPath) && existsSync(teamFilePath)) {
|
|
1114
|
+
const tc = readFileSync(teamFilePath, 'utf-8');
|
|
1115
|
+
if (!hasRosterEntries(tc)) {
|
|
1116
|
+
const storedPrompt = readFileSync(initPromptPath, 'utf-8').trim();
|
|
1117
|
+
if (storedPrompt) {
|
|
1118
|
+
debugLog('Auto-cast: .init-prompt found with empty roster, triggering cast');
|
|
1119
|
+
// Trigger cast after Ink settles, but now shellApi is guaranteed to be set
|
|
1120
|
+
setTimeout(() => {
|
|
1121
|
+
handleInitCast({ type: 'coordinator', raw: storedPrompt, content: storedPrompt }, true).catch(err => {
|
|
1122
|
+
debugLog('Auto-cast error:', err);
|
|
1123
|
+
});
|
|
1124
|
+
}, 100);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
255
1129
|
onDispatch: handleDispatch,
|
|
256
|
-
|
|
1130
|
+
onCancel: handleCancel,
|
|
1131
|
+
onRestoreSession,
|
|
1132
|
+
})), { exitOnCtrlC: false, patchConsole: false });
|
|
1133
|
+
// Clear the loading message now that Ink is rendering
|
|
1134
|
+
process.stderr.write('\r\x1b[K');
|
|
257
1135
|
await waitUntilExit();
|
|
1136
|
+
// Record shell session duration before cleanup
|
|
1137
|
+
recordShellSessionDuration(Date.now() - sessionStart);
|
|
1138
|
+
// Final session save before cleanup
|
|
1139
|
+
autoSave();
|
|
258
1140
|
// Cleanup: close all sessions and disconnect
|
|
259
|
-
for (const [, session] of agentSessions) {
|
|
1141
|
+
for (const [name, session] of agentSessions) {
|
|
260
1142
|
try {
|
|
261
1143
|
await session.close();
|
|
262
1144
|
}
|
|
263
|
-
catch {
|
|
1145
|
+
catch (err) {
|
|
1146
|
+
debugLog(`Failed to close session for ${name}:`, err);
|
|
1147
|
+
}
|
|
264
1148
|
}
|
|
265
1149
|
// coordinatorSession is assigned inside dispatchToCoordinator closure;
|
|
266
1150
|
// TS control flow can't see the mutation, so assert the type.
|
|
@@ -269,16 +1153,41 @@ export async function runShell() {
|
|
|
269
1153
|
try {
|
|
270
1154
|
await coordSession.close();
|
|
271
1155
|
}
|
|
272
|
-
catch {
|
|
1156
|
+
catch (err) {
|
|
1157
|
+
debugLog('Failed to close coordinator session:', err);
|
|
1158
|
+
}
|
|
273
1159
|
}
|
|
274
1160
|
try {
|
|
275
1161
|
await client.disconnect();
|
|
276
1162
|
}
|
|
277
|
-
catch {
|
|
1163
|
+
catch (err) {
|
|
1164
|
+
debugLog('Failed to disconnect client:', err);
|
|
1165
|
+
}
|
|
278
1166
|
try {
|
|
279
1167
|
await lifecycle.shutdown();
|
|
280
1168
|
}
|
|
281
|
-
catch {
|
|
282
|
-
|
|
1169
|
+
catch (err) {
|
|
1170
|
+
debugLog('Failed to shutdown lifecycle:', err);
|
|
1171
|
+
}
|
|
1172
|
+
try {
|
|
1173
|
+
await telemetry.shutdown();
|
|
1174
|
+
}
|
|
1175
|
+
catch (err) {
|
|
1176
|
+
debugLog('Failed to shutdown telemetry:', err);
|
|
1177
|
+
}
|
|
1178
|
+
// NO_COLOR-aware exit message with session summary
|
|
1179
|
+
const nc = process.env['NO_COLOR'] != null && process.env['NO_COLOR'] !== '';
|
|
1180
|
+
const prefix = nc ? '-- ' : '\x1b[36m--\x1b[0m ';
|
|
1181
|
+
if (messageCount > 0) {
|
|
1182
|
+
const elapsedMs = Date.now() - sessionStart;
|
|
1183
|
+
const mins = Math.round(elapsedMs / 60000);
|
|
1184
|
+
const durationStr = mins >= 1 ? `${mins} min` : '<1 min';
|
|
1185
|
+
const agentNames = [...agentSessions.keys()];
|
|
1186
|
+
const agentStr = agentNames.length > 0 ? ` with ${agentNames.join(', ')}.` : '';
|
|
1187
|
+
console.log(`${prefix}Squad out. ${durationStr}${agentStr} ${messageCount} message${messageCount === 1 ? '' : 's'}.`);
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
console.log(`${prefix}Squad out.`);
|
|
1191
|
+
}
|
|
283
1192
|
}
|
|
284
1193
|
//# sourceMappingURL=index.js.map
|