@kendoo.agentdesk/agentdesk 0.26.0 → 0.28.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/CHANGELOG.md +32 -1
- package/bin/agentdesk.mjs +35 -45
- package/cli/agents.mjs +4 -256
- package/cli/bootstrap.mjs +40 -59
- package/cli/config.mjs +29 -4
- package/cli/daemon.mjs +148 -66
- package/cli/dotenv.mjs +96 -13
- package/cli/engine/agents/index.mjs +151 -0
- package/cli/engine/claude-auth.mjs +72 -0
- package/cli/engine/env.mjs +56 -0
- package/cli/engine/events.mjs +214 -0
- package/cli/engine/hooks.mjs +112 -0
- package/cli/engine/phases/EXECUTION.md +45 -0
- package/cli/engine/phases/INTAKE.md +34 -0
- package/cli/engine/phases/PLAN.md +26 -0
- package/cli/engine/phases/REVIEW.md +21 -0
- package/cli/engine/phases/SOLO.md +115 -0
- package/cli/engine/phases/SUMMARY.md +23 -0
- package/cli/engine/prompts.mjs +181 -0
- package/cli/engine/query.mjs +63 -0
- package/cli/engine/schemas.mjs +180 -0
- package/cli/engine/session.mjs +285 -0
- package/cli/engine/spawn.mjs +83 -0
- package/cli/engine/tracker/github.md +19 -0
- package/cli/engine/tracker/jira.md +23 -0
- package/cli/engine/tracker/linear.md +24 -0
- package/cli/engine/verdict.mjs +83 -0
- package/cli/init.mjs +295 -149
- package/cli/login.mjs +52 -6
- package/cli/phase-loop.mjs +78 -0
- package/cli/proc.mjs +131 -0
- package/cli/project-key.mjs +56 -0
- package/cli/projects.mjs +41 -6
- package/cli/prompt.mjs +9 -503
- package/cli/prompts.mjs +20 -1
- package/cli/security-check.mjs +1 -1
- package/cli/session-isolation.mjs +65 -9
- package/cli/session-sandbox.mjs +13 -1
- package/cli/setup-helpers.mjs +83 -36
- package/cli/team.mjs +41 -34
- package/cli/tracker-check.mjs +12 -2
- package/cli/tracker-project.mjs +93 -0
- package/cli/update-check.mjs +62 -0
- package/package.json +12 -3
- package/cli/orchestrator.mjs +0 -461
- package/cli/stream-parser.mjs +0 -216
- package/prompts/phased.md +0 -549
- package/prompts/team.md +0 -505
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kendoo.agentdesk/agentdesk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
11
|
"cli/",
|
|
12
|
-
"prompts/",
|
|
13
12
|
"README.md",
|
|
14
13
|
"CHANGELOG.md"
|
|
15
14
|
],
|
|
@@ -22,11 +21,15 @@
|
|
|
22
21
|
"server": "node server/index.mjs",
|
|
23
22
|
"build": "vite build",
|
|
24
23
|
"preview": "vite preview",
|
|
25
|
-
"test": "node --test tests/server.test.mjs tests/agents.test.mjs tests/homepage.test.mjs tests/sessionUtils.test.mjs tests/tracker-url.test.mjs tests/session-preflight.test.mjs tests/access.test.mjs tests/project-ownership.test.mjs tests/random-hex.test.mjs",
|
|
24
|
+
"test": "node --test tests/server.test.mjs tests/agents.test.mjs tests/homepage.test.mjs tests/sessionUtils.test.mjs tests/tracker-url.test.mjs tests/session-preflight.test.mjs tests/access.test.mjs tests/project-ownership.test.mjs tests/random-hex.test.mjs tests/projects-registry.test.mjs tests/phase-loop.test.mjs tests/proc.test.mjs tests/crypto.test.mjs tests/dotenv.test.mjs tests/update-check.test.mjs tests/setup-helpers.test.mjs tests/project-key.test.mjs tests/tracker-project.test.mjs tests/tracker-check.test.mjs tests/config.test.mjs tests/engine-env.test.mjs tests/engine-events.test.mjs tests/engine-verdict.test.mjs tests/engine-hooks.test.mjs tests/engine-agents.test.mjs tests/session-isolation.test.mjs tests/engine-session.test.mjs tests/engine-prompts.test.mjs tests/engine-schemas.test.mjs tests/engine-claude-auth.test.mjs",
|
|
25
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-include='cli/**' --test-coverage-include='server/**' --test-coverage-lines=60 --test-coverage-branches=62 tests/*.test.mjs",
|
|
26
|
+
"lint": "eslint .",
|
|
27
|
+
"lint:fix": "eslint . --fix",
|
|
26
28
|
"lint:changelog": "node scripts/lint-changelog.mjs",
|
|
27
29
|
"prepublishOnly": "node scripts/lint-changelog.mjs"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
32
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.266",
|
|
30
33
|
"@inquirer/prompts": "^7.10.1",
|
|
31
34
|
"@radix-ui/react-avatar": "^1.1.11",
|
|
32
35
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
@@ -51,9 +54,15 @@
|
|
|
51
54
|
"ws": "^8.18.0"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
57
|
+
"@eslint/js": "^9.39.5",
|
|
54
58
|
"@vitejs/plugin-react": "^4.5.2",
|
|
55
59
|
"autoprefixer": "^10.4.21",
|
|
56
60
|
"concurrently": "^9.2.0",
|
|
61
|
+
"eslint": "^9.39.5",
|
|
62
|
+
"eslint-plugin-import": "^2.32.0",
|
|
63
|
+
"eslint-plugin-react": "^7.37.5",
|
|
64
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
65
|
+
"globals": "^17.12.0",
|
|
57
66
|
"lucide-react": "^0.577.0",
|
|
58
67
|
"postcss": "^8.5.4",
|
|
59
68
|
"react": "^19.1.0",
|
package/cli/orchestrator.mjs
DELETED
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
// Orchestrator — runs a single Claude process with all agents as personas
|
|
2
|
-
|
|
3
|
-
import { spawn, execSync } from "child_process";
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
5
|
-
import { createInterface } from "readline";
|
|
6
|
-
import { join, dirname } from "path";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
|
-
import { buildPrompt, buildSoloPrompt, buildPhasedPrompt } from "./prompt.mjs";
|
|
9
|
-
import { createStreamParser } from "./stream-parser.mjs";
|
|
10
|
-
import { createScratchHome } from "./session-sandbox.mjs";
|
|
11
|
-
import { wrapIsolatedSpawn, probeIsolation } from "./session-isolation.mjs";
|
|
12
|
-
import { buildTrackerUrl } from "./tracker-url.mjs";
|
|
13
|
-
import { resolveGitHubCreds, assertPushable, PreflightError } from "./session-preflight.mjs";
|
|
14
|
-
import { loadDotEnv } from "./dotenv.mjs";
|
|
15
|
-
|
|
16
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")).version;
|
|
18
|
-
|
|
19
|
-
// Fetch decrypted tracker credentials from server
|
|
20
|
-
async function fetchTrackerCredentials(projectName, apiKey, serverUrl) {
|
|
21
|
-
if (!apiKey || !serverUrl || !projectName) return {};
|
|
22
|
-
try {
|
|
23
|
-
const res = await fetch(`${serverUrl}/api/projects/${projectName}/settings/credentials`, {
|
|
24
|
-
headers: { "x-api-key": apiKey },
|
|
25
|
-
signal: AbortSignal.timeout(5000),
|
|
26
|
-
});
|
|
27
|
-
if (res.ok) return await res.json();
|
|
28
|
-
} catch {}
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function timestamp() {
|
|
33
|
-
const d = new Date();
|
|
34
|
-
return [d.getHours(), d.getMinutes(), d.getSeconds()]
|
|
35
|
-
.map(n => String(n).padStart(2, "0")).join(":");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Phase-specific fallback when the user hasn't set a model override.
|
|
39
|
-
// REVIEW and SUMMARY default to haiku (fast + cheap — no heavy reasoning needed).
|
|
40
|
-
// INTAKE/PLAN/EXECUTION default to Claude Code's default (sonnet).
|
|
41
|
-
function defaultForPhase(phase) {
|
|
42
|
-
if (phase === "REVIEW" || phase === "SUMMARY") return "haiku";
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Resolve --model args for a given phase based on project settings.
|
|
47
|
-
// phaseModels is { INTAKE?, PLAN?, EXECUTION?, REVIEW?, SUMMARY? } with values "opus"|"sonnet"|"haiku"|"default".
|
|
48
|
-
// Returns [] when no override (so Claude Code picks its default).
|
|
49
|
-
function modelArgsForPhase(phase, phaseModels) {
|
|
50
|
-
let choice = phaseModels?.[phase];
|
|
51
|
-
if (!choice || choice === "default") choice = defaultForPhase(phase);
|
|
52
|
-
if (!choice) return [];
|
|
53
|
-
return ["--model", choice];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Display label for a phase's model.
|
|
57
|
-
function modelLabelForPhase(phase, phaseModels) {
|
|
58
|
-
const override = phaseModels?.[phase];
|
|
59
|
-
if (override && override !== "default") return override;
|
|
60
|
-
return defaultForPhase(phase) || "default";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function runOrchestrator({
|
|
64
|
-
taskId, taskLink, description, createTask, tracker, config,
|
|
65
|
-
project, team, teamSections, sessionUrl, cwd,
|
|
66
|
-
onEvent, apiKey, serverUrl, soloAgent, childStrategy, sessionId,
|
|
67
|
-
}) {
|
|
68
|
-
const trackerCreds = await fetchTrackerCredentials(project?.name, apiKey, serverUrl);
|
|
69
|
-
|
|
70
|
-
// Resolve creds from server + project .env BEFORE building the scratch
|
|
71
|
-
// HOME, so the sandbox helper writes the right token (or we fail fast
|
|
72
|
-
// when none is available — see assertPushable below).
|
|
73
|
-
const resolvedToken = resolveGitHubCreds({ cwd, trackerCreds }).GITHUB_TOKEN;
|
|
74
|
-
const resolvedCreds = { ...trackerCreds, GITHUB_TOKEN: resolvedToken || trackerCreds.GITHUB_TOKEN };
|
|
75
|
-
|
|
76
|
-
const startTime = Date.now();
|
|
77
|
-
let totalInputTokens = 0;
|
|
78
|
-
let totalOutputTokens = 0;
|
|
79
|
-
let totalSteps = 0;
|
|
80
|
-
|
|
81
|
-
let lastPhase = null;
|
|
82
|
-
|
|
83
|
-
function emit(event) {
|
|
84
|
-
if (event.type === "phase:change") lastPhase = event.phase;
|
|
85
|
-
onEvent?.({ ...event, timestamp: timestamp() });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const fullPrompt = soloAgent
|
|
89
|
-
? buildSoloPrompt({ agentName: soloAgent, taskId, description, tracker, config, project, sessionUrl, childStrategy, cwd })
|
|
90
|
-
: buildPrompt({ taskId, taskLink, description, createTask, tracker, config, project, teamSections, sessionUrl, cwd });
|
|
91
|
-
|
|
92
|
-
emit({
|
|
93
|
-
type: "session:start",
|
|
94
|
-
taskId, taskLink,
|
|
95
|
-
title: description || taskId,
|
|
96
|
-
project: project?.name || null,
|
|
97
|
-
sessionNumber: 1,
|
|
98
|
-
agents: soloAgent ? [soloAgent] : teamSections.names,
|
|
99
|
-
cliVersion: CLI_VERSION,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Preflight: must run AFTER session:start (so the UI has a session to
|
|
103
|
-
// bind the error to) but BEFORE we touch the scratch sandbox or spawn
|
|
104
|
-
// the child. Surfaces missing creds / SSH-only remotes as a clean
|
|
105
|
-
// session error instead of letting the agent silently fail to push.
|
|
106
|
-
try {
|
|
107
|
-
assertPushable({ cwd, creds: resolvedCreds, projectName: project?.name });
|
|
108
|
-
} catch (err) {
|
|
109
|
-
if (err instanceof PreflightError) {
|
|
110
|
-
emit({ type: "session:error", code: err.code, message: err.message });
|
|
111
|
-
emit({ type: "session:end", duration: `${((Date.now() - startTime) / 1000).toFixed(1)}s`, steps: 0, inputTokens: 0, outputTokens: 0, status: "error" });
|
|
112
|
-
return { duration: "0s", steps: 0, inputTokens: 0, outputTokens: 0, handoff: false, error: err.message };
|
|
113
|
-
}
|
|
114
|
-
throw err;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Build a per-session scratch HOME that isolates THIS project's identity
|
|
118
|
-
// from the user's global git/gh/ssh state and from other projects'
|
|
119
|
-
// credentials. Cleanup runs at session end regardless of outcome.
|
|
120
|
-
const sandbox = createScratchHome({
|
|
121
|
-
projectId: project?.name,
|
|
122
|
-
sessionId,
|
|
123
|
-
creds: resolvedCreds,
|
|
124
|
-
commitIdentity: {
|
|
125
|
-
name: config?.identityBadge || "AgentDesk",
|
|
126
|
-
email: resolvedCreds.JIRA_EMAIL || `agentdesk@local`,
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
const env = { ...process.env, ...loadDotEnv(cwd), ...sandbox.env };
|
|
130
|
-
|
|
131
|
-
const initialPhase = soloAgent ? "EXECUTION" : "INTAKE";
|
|
132
|
-
// Non-phased single run spans all phases in one process — use EXECUTION's model
|
|
133
|
-
// as the representative choice (matches where the heavy lifting happens).
|
|
134
|
-
const representativeModel = modelLabelForPhase("EXECUTION", config?.phaseModels);
|
|
135
|
-
emit({ type: "phase:change", phase: initialPhase, model: representativeModel });
|
|
136
|
-
|
|
137
|
-
const modelArgs = modelArgsForPhase("EXECUTION", config?.phaseModels);
|
|
138
|
-
const phaseModelsResolved = config?.phaseModels && Object.keys(config.phaseModels).length
|
|
139
|
-
? JSON.stringify(config.phaseModels)
|
|
140
|
-
: "(none — using Claude Code defaults)";
|
|
141
|
-
console.error(`[agentdesk] phaseModels: ${phaseModelsResolved}`);
|
|
142
|
-
console.error(`[agentdesk] EXECUTION model: ${modelArgs.length ? modelArgs[1] : "default (no --model flag)"}`);
|
|
143
|
-
const wrapped = wrapIsolatedSpawn({
|
|
144
|
-
cmd: "claude",
|
|
145
|
-
args: ["-p", fullPrompt, ...modelArgs, "--allowedTools", "Bash,Read,Edit,Write,Glob,Grep", "--verbose", "--output-format", "stream-json"],
|
|
146
|
-
cwd,
|
|
147
|
-
scratchHome: sandbox.home,
|
|
148
|
-
sessionId,
|
|
149
|
-
});
|
|
150
|
-
if (wrapped.isolation.kind !== "none") {
|
|
151
|
-
console.error(`[agentdesk] session isolation: ${wrapped.isolation.kind}`);
|
|
152
|
-
} else if (wrapped.isolation.reason) {
|
|
153
|
-
console.error(`[agentdesk] hard isolation unavailable (${wrapped.isolation.reason}) — scoped-env only`);
|
|
154
|
-
}
|
|
155
|
-
const child = spawn(
|
|
156
|
-
wrapped.cmd,
|
|
157
|
-
wrapped.args,
|
|
158
|
-
{ stdio: ["pipe", "pipe", "inherit"], shell: false, env, cwd }
|
|
159
|
-
);
|
|
160
|
-
child.stdin.end();
|
|
161
|
-
|
|
162
|
-
const { parseLine } = createStreamParser({
|
|
163
|
-
teamNames: soloAgent ? [soloAgent] : teamSections.names,
|
|
164
|
-
callbacks: {
|
|
165
|
-
onPhaseChange({ phase }) { emit({ type: "phase:change", phase, model: representativeModel }); },
|
|
166
|
-
onModel({ model }) { emit({ type: "session:model", model }); },
|
|
167
|
-
onAgentMessage({ agent, tag, message }) { emit({ type: "agent:message", agent, tag, message }); },
|
|
168
|
-
onToolUse({ agent, tool, description }) { totalSteps++; emit({ type: "tool:use", agent, tool, description }); },
|
|
169
|
-
onToolResult({ success, summary }) { emit({ type: "tool:result", success, summary }); },
|
|
170
|
-
onSessionUpdate({ taskId: newTaskId, title }) {
|
|
171
|
-
if (newTaskId) {
|
|
172
|
-
const update = { type: "session:update", taskId: newTaskId };
|
|
173
|
-
const taskLink = buildTrackerUrl({ tracker, config, taskId: newTaskId });
|
|
174
|
-
if (taskLink) update.taskLink = taskLink;
|
|
175
|
-
emit(update);
|
|
176
|
-
}
|
|
177
|
-
if (title) emit({ type: "session:update", title });
|
|
178
|
-
},
|
|
179
|
-
onSessionEnd({ duration, steps, inputTokens, outputTokens }) {
|
|
180
|
-
totalInputTokens = inputTokens;
|
|
181
|
-
totalOutputTokens = outputTokens;
|
|
182
|
-
totalSteps = steps;
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const rl = createInterface({ input: child.stdout });
|
|
188
|
-
for await (const line of rl) {
|
|
189
|
-
parseLine(line);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const exitCode = await new Promise(resolve => child.on("close", resolve));
|
|
193
|
-
|
|
194
|
-
const duration = `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
|
|
195
|
-
|
|
196
|
-
// Detect limit/crash — non-zero exit without a clean session end
|
|
197
|
-
const isHandoff = exitCode !== 0;
|
|
198
|
-
|
|
199
|
-
if (isHandoff) {
|
|
200
|
-
// Write mechanical resume snapshot
|
|
201
|
-
let branch = "";
|
|
202
|
-
let diffStat = "";
|
|
203
|
-
try { branch = execSync("git branch --show-current", { cwd, encoding: "utf-8" }).trim(); } catch {}
|
|
204
|
-
try { diffStat = execSync("git diff --stat HEAD", { cwd, encoding: "utf-8" }).trim(); } catch {}
|
|
205
|
-
|
|
206
|
-
const resumePath = join(cwd, ".agentdesk-resume.md");
|
|
207
|
-
const resumeContent = [
|
|
208
|
-
`# AgentDesk Resume — ${taskId}`,
|
|
209
|
-
``,
|
|
210
|
-
`Session: ${sessionUrl}`,
|
|
211
|
-
`Date: ${new Date().toISOString()}`,
|
|
212
|
-
`Phase: ${lastPhase || "UNKNOWN"}`,
|
|
213
|
-
`Duration: ${duration}`,
|
|
214
|
-
`Steps: ${totalSteps}`,
|
|
215
|
-
`Exit code: ${exitCode}`,
|
|
216
|
-
``,
|
|
217
|
-
`## Branch`,
|
|
218
|
-
branch || "(no branch)",
|
|
219
|
-
``,
|
|
220
|
-
`## Uncommitted changes`,
|
|
221
|
-
diffStat || "(none)",
|
|
222
|
-
``,
|
|
223
|
-
`## Notes`,
|
|
224
|
-
`Session ended unexpectedly (likely Claude rate/context limit).`,
|
|
225
|
-
`Resume with: agentdesk team ${taskId}`,
|
|
226
|
-
``,
|
|
227
|
-
].join("\n");
|
|
228
|
-
try { writeFileSync(resumePath, resumeContent); } catch {}
|
|
229
|
-
|
|
230
|
-
emit({ type: "session:end", duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, status: "handoff" });
|
|
231
|
-
} else {
|
|
232
|
-
// Clean exit — remove stale resume file if present
|
|
233
|
-
const resumePath = join(cwd, ".agentdesk-resume.md");
|
|
234
|
-
try { if (existsSync(resumePath)) unlinkSync(resumePath); } catch {}
|
|
235
|
-
|
|
236
|
-
emit({ type: "session:end", duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens });
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
sandbox.cleanup();
|
|
240
|
-
|
|
241
|
-
return { duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, handoff: isHandoff };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// --- Phased orchestrator: runs 3 sequential Claude processes ---
|
|
245
|
-
|
|
246
|
-
async function runSinglePhase({ prompt, cwd, env, teamNames, emit, modelArgs = [], scratchHome, sessionId, tracker, config }) {
|
|
247
|
-
const wrapped = wrapIsolatedSpawn({
|
|
248
|
-
cmd: "claude",
|
|
249
|
-
args: ["-p", prompt, ...modelArgs, "--allowedTools", "Bash,Read,Edit,Write,Glob,Grep", "--verbose", "--output-format", "stream-json"],
|
|
250
|
-
cwd,
|
|
251
|
-
scratchHome,
|
|
252
|
-
sessionId,
|
|
253
|
-
});
|
|
254
|
-
const child = spawn(
|
|
255
|
-
wrapped.cmd,
|
|
256
|
-
wrapped.args,
|
|
257
|
-
{ stdio: ["pipe", "pipe", "inherit"], shell: false, env, cwd }
|
|
258
|
-
);
|
|
259
|
-
child.stdin.end();
|
|
260
|
-
|
|
261
|
-
let inputTokens = 0, outputTokens = 0, steps = 0, lastPhase = null;
|
|
262
|
-
|
|
263
|
-
const { parseLine } = createStreamParser({
|
|
264
|
-
teamNames,
|
|
265
|
-
callbacks: {
|
|
266
|
-
onPhaseChange({ phase }) { lastPhase = phase; },
|
|
267
|
-
onModel({ model }) { emit({ type: "session:model", model }); },
|
|
268
|
-
onAgentMessage({ agent, tag, message }) { emit({ type: "agent:message", agent, tag, message }); },
|
|
269
|
-
onToolUse({ agent, tool, description }) { steps++; emit({ type: "tool:use", agent, tool, description }); },
|
|
270
|
-
onToolResult({ success, summary }) { emit({ type: "tool:result", success, summary }); },
|
|
271
|
-
onSessionUpdate({ taskId: newTaskId, title }) {
|
|
272
|
-
if (newTaskId) {
|
|
273
|
-
const update = { type: "session:update", taskId: newTaskId };
|
|
274
|
-
const taskLink = buildTrackerUrl({ tracker, config, taskId: newTaskId });
|
|
275
|
-
if (taskLink) update.taskLink = taskLink;
|
|
276
|
-
emit(update);
|
|
277
|
-
}
|
|
278
|
-
if (title) emit({ type: "session:update", title });
|
|
279
|
-
},
|
|
280
|
-
onSessionEnd({ inputTokens: iT, outputTokens: oT, steps: s }) {
|
|
281
|
-
inputTokens = iT; outputTokens = oT; steps = s;
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
const rl = createInterface({ input: child.stdout });
|
|
287
|
-
for await (const line of rl) {
|
|
288
|
-
parseLine(line);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const exitCode = await new Promise(resolve => child.on("close", resolve));
|
|
292
|
-
return { exitCode, inputTokens, outputTokens, steps, child };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export async function runPhasedOrchestrator({
|
|
296
|
-
taskId, taskLink, description, createTask, tracker, config,
|
|
297
|
-
project, team, teamSections, sessionUrl, cwd,
|
|
298
|
-
onEvent, apiKey, serverUrl, onChild, sessionId,
|
|
299
|
-
}) {
|
|
300
|
-
const trackerCreds = await fetchTrackerCredentials(project?.name, apiKey, serverUrl);
|
|
301
|
-
|
|
302
|
-
// Resolve creds (server + project .env) before any scratch HOME setup
|
|
303
|
-
// so preflight can fail fast and the sandbox helper bakes the right
|
|
304
|
-
// token. Server creds win over .env when both are present.
|
|
305
|
-
const resolvedToken = resolveGitHubCreds({ cwd, trackerCreds }).GITHUB_TOKEN;
|
|
306
|
-
const resolvedCreds = { ...trackerCreds, GITHUB_TOKEN: resolvedToken || trackerCreds.GITHUB_TOKEN };
|
|
307
|
-
|
|
308
|
-
const startTime = Date.now();
|
|
309
|
-
const teamNames = teamSections.names;
|
|
310
|
-
|
|
311
|
-
function timestamp() {
|
|
312
|
-
const d = new Date();
|
|
313
|
-
return [d.getHours(), d.getMinutes(), d.getSeconds()].map(n => String(n).padStart(2, "0")).join(":");
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function emit(event) {
|
|
317
|
-
onEvent?.({ ...event, timestamp: timestamp() });
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Emit session start once
|
|
321
|
-
emit({
|
|
322
|
-
type: "session:start",
|
|
323
|
-
taskId, taskLink,
|
|
324
|
-
title: description || taskId,
|
|
325
|
-
project: project?.name || null,
|
|
326
|
-
sessionNumber: 1,
|
|
327
|
-
agents: teamNames,
|
|
328
|
-
cliVersion: CLI_VERSION,
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// Preflight: AFTER session:start so the UI can bind the error, BEFORE
|
|
332
|
-
// touching the scratch sandbox or spawning a child. Same contract as
|
|
333
|
-
// runOrchestrator above.
|
|
334
|
-
try {
|
|
335
|
-
assertPushable({ cwd, creds: resolvedCreds, projectName: project?.name });
|
|
336
|
-
} catch (err) {
|
|
337
|
-
if (err instanceof PreflightError) {
|
|
338
|
-
emit({ type: "session:error", code: err.code, message: err.message });
|
|
339
|
-
emit({ type: "session:end", duration: `${((Date.now() - startTime) / 1000).toFixed(1)}s`, steps: 0, inputTokens: 0, outputTokens: 0, status: "error" });
|
|
340
|
-
return { duration: "0s", steps: 0, inputTokens: 0, outputTokens: 0, handoff: false, error: err.message };
|
|
341
|
-
}
|
|
342
|
-
throw err;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const sandbox = createScratchHome({
|
|
346
|
-
projectId: project?.name,
|
|
347
|
-
sessionId,
|
|
348
|
-
creds: resolvedCreds,
|
|
349
|
-
commitIdentity: {
|
|
350
|
-
name: config?.identityBadge || "AgentDesk",
|
|
351
|
-
email: resolvedCreds.JIRA_EMAIL || `agentdesk@local`,
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
const env = { ...process.env, ...loadDotEnv(cwd), ...sandbox.env };
|
|
355
|
-
|
|
356
|
-
const probe = probeIsolation();
|
|
357
|
-
if (probe.kind !== "none") {
|
|
358
|
-
console.error(`[agentdesk] session isolation: ${probe.kind}`);
|
|
359
|
-
} else {
|
|
360
|
-
console.error(`[agentdesk] hard isolation unavailable (${probe.reason}) — scoped-env only`);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const sessionMemoryPath = join(cwd, ".agentdesk", "session-memory.md");
|
|
364
|
-
const reviewVerdictPath = join(cwd, ".agentdesk", "review-verdict.md");
|
|
365
|
-
const MAX_REVIEW_RETRIES = 1; // One execution redo after a failed review, then force SUMMARY.
|
|
366
|
-
let totalInputTokens = 0, totalOutputTokens = 0, totalSteps = 0;
|
|
367
|
-
let handoff = false;
|
|
368
|
-
let reviewRetries = 0;
|
|
369
|
-
|
|
370
|
-
// Ordered queue of phases; REVIEW may re-enqueue EXECUTION before SUMMARY.
|
|
371
|
-
const queue = ["INTAKE", "PLAN", "EXECUTION", "REVIEW", "SUMMARY"];
|
|
372
|
-
|
|
373
|
-
while (queue.length > 0) {
|
|
374
|
-
const phase = queue.shift();
|
|
375
|
-
|
|
376
|
-
let sessionMemory = "";
|
|
377
|
-
try {
|
|
378
|
-
if (existsSync(sessionMemoryPath)) sessionMemory = readFileSync(sessionMemoryPath, "utf-8").trim();
|
|
379
|
-
} catch {}
|
|
380
|
-
|
|
381
|
-
// Clear any stale verdict before entering REVIEW so we don't read a previous run's result.
|
|
382
|
-
if (phase === "REVIEW") {
|
|
383
|
-
try { if (existsSync(reviewVerdictPath)) unlinkSync(reviewVerdictPath); } catch {}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
emit({ type: "phase:change", phase, model: modelLabelForPhase(phase, config?.phaseModels) });
|
|
387
|
-
|
|
388
|
-
const prompt = buildPhasedPrompt({
|
|
389
|
-
phase, taskId, taskLink, description,
|
|
390
|
-
createTask: phase === "INTAKE" ? createTask : false,
|
|
391
|
-
tracker, config, project, teamSections, sessionUrl, cwd, sessionMemory,
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const modelArgs = modelArgsForPhase(phase, config?.phaseModels);
|
|
395
|
-
console.error(`[agentdesk] phase ${phase} model: ${modelArgs.length ? modelArgs[1] : "default (no --model flag)"}`);
|
|
396
|
-
const result = await runSinglePhase({ prompt, cwd, env, teamNames, emit, modelArgs, scratchHome: sandbox.home, sessionId, tracker, config });
|
|
397
|
-
|
|
398
|
-
if (onChild) onChild(result.child);
|
|
399
|
-
|
|
400
|
-
totalInputTokens += result.inputTokens;
|
|
401
|
-
totalOutputTokens += result.outputTokens;
|
|
402
|
-
totalSteps += result.steps;
|
|
403
|
-
|
|
404
|
-
const hasMemory = existsSync(sessionMemoryPath);
|
|
405
|
-
|
|
406
|
-
if (result.exitCode !== 0 && !hasMemory) {
|
|
407
|
-
handoff = true;
|
|
408
|
-
|
|
409
|
-
let branch = "", diffStat = "";
|
|
410
|
-
try { branch = execSync("git branch --show-current", { cwd, encoding: "utf-8" }).trim(); } catch {}
|
|
411
|
-
try { diffStat = execSync("git diff --stat HEAD", { cwd, encoding: "utf-8" }).trim(); } catch {}
|
|
412
|
-
|
|
413
|
-
const resumePath = join(cwd, ".agentdesk-resume.md");
|
|
414
|
-
try {
|
|
415
|
-
writeFileSync(resumePath, [
|
|
416
|
-
`# AgentDesk Resume — ${taskId}`,
|
|
417
|
-
``, `Session: ${sessionUrl}`, `Phase: ${phase}`,
|
|
418
|
-
`Date: ${new Date().toISOString()}`,
|
|
419
|
-
``, `## Branch`, branch || "(none)",
|
|
420
|
-
``, `## Uncommitted changes`, diffStat || "(none)",
|
|
421
|
-
``, `## Notes`, `Phased session interrupted during ${phase}.`,
|
|
422
|
-
].join("\n"));
|
|
423
|
-
} catch {}
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// After REVIEW, check the verdict file. NEEDS_MORE_WORK -> loop back to EXECUTION (up to MAX_REVIEW_RETRIES).
|
|
428
|
-
if (phase === "REVIEW") {
|
|
429
|
-
let verdict = "";
|
|
430
|
-
try {
|
|
431
|
-
if (existsSync(reviewVerdictPath)) verdict = readFileSync(reviewVerdictPath, "utf-8").trim();
|
|
432
|
-
} catch {}
|
|
433
|
-
|
|
434
|
-
const needsMoreWork = /^NEEDS_MORE_WORK\b/i.test(verdict);
|
|
435
|
-
if (needsMoreWork && reviewRetries < MAX_REVIEW_RETRIES) {
|
|
436
|
-
reviewRetries++;
|
|
437
|
-
emit({ type: "agent:message", agent: "Jane", tag: "SAY",
|
|
438
|
-
message: `Review flagged gaps — returning to EXECUTION (retry ${reviewRetries}/${MAX_REVIEW_RETRIES}).` });
|
|
439
|
-
queue.unshift("EXECUTION", "REVIEW"); // redo execution, then review again
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Clean up internal marker files regardless of outcome
|
|
445
|
-
try { if (existsSync(reviewVerdictPath)) unlinkSync(reviewVerdictPath); } catch {}
|
|
446
|
-
|
|
447
|
-
const duration = `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
|
|
448
|
-
|
|
449
|
-
if (handoff) {
|
|
450
|
-
emit({ type: "session:end", duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, status: "handoff" });
|
|
451
|
-
} else {
|
|
452
|
-
// Clean exit — remove stale resume file
|
|
453
|
-
const resumePath = join(cwd, ".agentdesk-resume.md");
|
|
454
|
-
try { if (existsSync(resumePath)) unlinkSync(resumePath); } catch {}
|
|
455
|
-
emit({ type: "session:end", duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens });
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
sandbox.cleanup();
|
|
459
|
-
|
|
460
|
-
return { duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, handoff };
|
|
461
|
-
}
|