@mycompbox/compbox-cli 1.18.0 → 1.19.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/.tsbuildinfo +1 -1
- package/commands/configure.d.ts.map +1 -1
- package/commands/configure.js +31 -11
- package/commands/configure.js.map +1 -1
- package/commands/doctor.d.ts.map +1 -1
- package/commands/doctor.js +1 -1
- package/commands/doctor.js.map +1 -1
- package/commands/login.d.ts +2 -0
- package/commands/login.d.ts.map +1 -1
- package/commands/login.js +28 -6
- package/commands/login.js.map +1 -1
- package/commands/run.d.ts.map +1 -1
- package/commands/run.js +37 -2
- package/commands/run.js.map +1 -1
- package/index.js +887 -184
- package/package.json +1 -1
- package/utils/agent-invoker.d.ts +31 -58
- package/utils/agent-invoker.d.ts.map +1 -1
- package/utils/agent-invoker.js +95 -186
- package/utils/agent-invoker.js.map +1 -1
- package/utils/autostart.d.ts +41 -0
- package/utils/autostart.d.ts.map +1 -0
- package/utils/autostart.js +122 -0
- package/utils/autostart.js.map +1 -0
- package/utils/key-run.d.ts +32 -0
- package/utils/key-run.d.ts.map +1 -0
- package/utils/key-run.js +77 -0
- package/utils/key-run.js.map +1 -0
- package/utils/loopback.d.ts +3 -3
- package/utils/loopback.d.ts.map +1 -1
- package/utils/loopback.js +3 -3
- package/utils/loopback.js.map +1 -1
- package/utils/mailbox-client.d.ts +21 -0
- package/utils/mailbox-client.d.ts.map +1 -1
- package/utils/mailbox-client.js +34 -0
- package/utils/mailbox-client.js.map +1 -1
- package/utils/mcp-config-writer.d.ts +2 -0
- package/utils/mcp-config-writer.d.ts.map +1 -1
- package/utils/mcp-config-writer.js +9 -1
- package/utils/mcp-config-writer.js.map +1 -1
- package/utils/repo-setup.d.ts +44 -0
- package/utils/repo-setup.d.ts.map +1 -0
- package/utils/repo-setup.js +178 -0
- package/utils/repo-setup.js.map +1 -0
- package/utils/run-workspace.d.ts +114 -0
- package/utils/run-workspace.d.ts.map +1 -0
- package/utils/run-workspace.js +264 -0
- package/utils/run-workspace.js.map +1 -0
package/package.json
CHANGED
package/utils/agent-invoker.d.ts
CHANGED
|
@@ -32,7 +32,11 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import Anthropic from '@anthropic-ai/sdk';
|
|
34
34
|
import { spawn } from 'node:child_process';
|
|
35
|
-
import { type PxpipeRouting, type InterviewSeedPayload } from '@mycompbox/shared';
|
|
35
|
+
import { type PxpipeRouting, type InterviewSeedPayload, extractCompletionJson, looksLikeCompletionAttempt, meetsComplianceTarget, buildAuditFeedback, COMPLETION_REPAIR_INSTRUCTION, type ComplianceSummary, type EpicRequestDraft } from '@mycompbox/shared';
|
|
36
|
+
export { extractCompletionJson, looksLikeCompletionAttempt, meetsComplianceTarget, buildAuditFeedback, COMPLETION_REPAIR_INSTRUCTION, };
|
|
37
|
+
export type { ComplianceSummary, EpicRequestDraft };
|
|
38
|
+
import { type KeyRun } from './key-run.js';
|
|
39
|
+
import type { FinishResult, RunWorkspaceManager } from './run-workspace.js';
|
|
36
40
|
import type { MailboxAttachment, MailboxClient, MailboxJob, MailboxJobTurn } from './mailbox-client.js';
|
|
37
41
|
/**
|
|
38
42
|
* pxpipe routing (ENG-136). Anthropic SDK options carrying the proxy `baseURL`
|
|
@@ -94,30 +98,6 @@ export interface InvokeResult {
|
|
|
94
98
|
status: 'done' | 'failed';
|
|
95
99
|
result: Record<string, unknown>;
|
|
96
100
|
}
|
|
97
|
-
export interface EpicRequestDraft {
|
|
98
|
-
title: string;
|
|
99
|
-
problemStatement: string;
|
|
100
|
-
proposedSolution: string;
|
|
101
|
-
impactAssessment: string;
|
|
102
|
-
/**
|
|
103
|
-
* Plain-text markdown description synthesized by the model from the whole
|
|
104
|
-
* conversation — sent as the Epic Request's top-level `description` field
|
|
105
|
-
* (distinct from structuredDesc). REQUIRED: compliance-validation's
|
|
106
|
-
* ER_NO_DESCRIPTION rule (see .github/skills/internal/compliance-validation)
|
|
107
|
-
* fails critical if this is empty, regardless of how good structuredDesc
|
|
108
|
-
* is — verified live: an epic request created with only the 3 structured
|
|
109
|
-
* fields populated (no description) scored 12/100, critical-fail, on
|
|
110
|
-
* ER_NO_DESCRIPTION, ER_NO_PROBLEM_STATEMENT, and ER_NO_IMPACT_ASSESSMENT
|
|
111
|
-
* (the latter two because a single terse summary sentence fell just under
|
|
112
|
-
* the rubric's 50-word/30-word minimums — 47 and 25 words respectively).
|
|
113
|
-
*/
|
|
114
|
-
description: string;
|
|
115
|
-
successMetrics?: string;
|
|
116
|
-
targetAudience?: string;
|
|
117
|
-
alternatives?: string;
|
|
118
|
-
dependencies?: string;
|
|
119
|
-
estimatedEffort?: string;
|
|
120
|
-
}
|
|
121
101
|
/**
|
|
122
102
|
* ENG-149 — parse a claimed job's `payload` as an interview-CONTINUATION seed
|
|
123
103
|
* (the ENG-148 dispatch shape), or return null for a normal fresh-start job
|
|
@@ -132,15 +112,6 @@ export declare function parseSeedPayload(payload: unknown): InterviewSeedPayload
|
|
|
132
112
|
* prompt for a continuation, otherwise the module-level fresh-start prompt.
|
|
133
113
|
*/
|
|
134
114
|
export declare function resolveSystemPrompt(seed: InterviewSeedPayload | null): string;
|
|
135
|
-
/** The server-computed compliance summary the daemon gates the interview on. */
|
|
136
|
-
export interface ComplianceSummary {
|
|
137
|
-
overall: number;
|
|
138
|
-
tier: 'pass' | 'advisory' | 'critical-fail';
|
|
139
|
-
issues: {
|
|
140
|
-
message: string;
|
|
141
|
-
tier: 'critical' | 'advisory';
|
|
142
|
-
}[];
|
|
143
|
-
}
|
|
144
115
|
/**
|
|
145
116
|
* Submit the current draft and read back the server-computed compliance score
|
|
146
117
|
* (bug #2 — audit-aware finishing). Fresh interview: the FIRST submit
|
|
@@ -156,30 +127,6 @@ export declare function submitAndScore(ctx: {
|
|
|
156
127
|
erId: string;
|
|
157
128
|
compliance: ComplianceSummary | null;
|
|
158
129
|
}>;
|
|
159
|
-
/**
|
|
160
|
-
* Whether the request is good enough to finish. Reaching the target tier
|
|
161
|
-
* (`pass` = >=90 with no criticals) finishes early. A null score is fail-open:
|
|
162
|
-
* the gate can only act on a score the server actually returned.
|
|
163
|
-
*/
|
|
164
|
-
export declare function meetsComplianceTarget(compliance: ComplianceSummary | null): boolean;
|
|
165
|
-
/**
|
|
166
|
-
* Turn a failing compliance score into a corrective instruction: fix by
|
|
167
|
-
* EXPANDING what's already known where possible, otherwise ask ONE question.
|
|
168
|
-
*/
|
|
169
|
-
export declare function buildAuditFeedback(compliance: ComplianceSummary): string;
|
|
170
|
-
/**
|
|
171
|
-
* Extracts a completion draft from a model reply, tolerant of the common LLM
|
|
172
|
-
* JSON defects (fenced/plain/bare, trailing commas, and — the big one — literal
|
|
173
|
-
* newlines inside string values). Returns null only when no repair strategy
|
|
174
|
-
* yields a VALID {@link isEpicRequestDraft} object.
|
|
175
|
-
*/
|
|
176
|
-
export declare function extractCompletionJson(text: string): EpicRequestDraft | null;
|
|
177
|
-
/**
|
|
178
|
-
* True when a reply is clearly an ATTEMPT to finish (a JSON completion) even if
|
|
179
|
-
* it failed to parse/validate — so the daemon can correct the model instead of
|
|
180
|
-
* relaying raw JSON to the employee as if it were a question (which loops).
|
|
181
|
-
*/
|
|
182
|
-
export declare function looksLikeCompletionAttempt(text: string): boolean;
|
|
183
130
|
/**
|
|
184
131
|
* Runs the epic_request interview end-to-end: converses with the user via
|
|
185
132
|
* post-turn/next-job until it has a title + the 3 required structured-desc
|
|
@@ -226,6 +173,19 @@ export declare const PLANNER_RUN_TIMEOUT_MS: number;
|
|
|
226
173
|
* connected" exactly while an agent was running.
|
|
227
174
|
*/
|
|
228
175
|
export declare const PLANNER_HEARTBEAT_INTERVAL_MS: number;
|
|
176
|
+
/**
|
|
177
|
+
* Finishes a run made on the person's API key: reads Claude Code's JSON
|
|
178
|
+
* result, reports the run's usage to CompBox (best-effort), and returns the
|
|
179
|
+
* plain text the run would otherwise have printed.
|
|
180
|
+
*/
|
|
181
|
+
export declare function takeKeyRunResult(ctx: {
|
|
182
|
+
mailboxClient: MailboxClient;
|
|
183
|
+
machineToken: string;
|
|
184
|
+
jobId: string;
|
|
185
|
+
}, keyRun: KeyRun, stdout: string): {
|
|
186
|
+
text: string;
|
|
187
|
+
reported: Promise<void>;
|
|
188
|
+
};
|
|
229
189
|
/**
|
|
230
190
|
* Build the `claude` argv for a headless planner run (H1). Pure + exported so a
|
|
231
191
|
* unit test can assert the exact command shape: the `@planner` routing prompt,
|
|
@@ -280,6 +240,8 @@ export interface StartExecutionPlanCtx {
|
|
|
280
240
|
epicRequestId: string;
|
|
281
241
|
/** Resolved project repo dir (may be undefined/invalid — validated here). */
|
|
282
242
|
projectDir: string | undefined;
|
|
243
|
+
/** Run on the person's saved API key instead of this computer's Claude sign-in. */
|
|
244
|
+
keyRun?: KeyRun;
|
|
283
245
|
}
|
|
284
246
|
export interface StartExecutionPlanDeps {
|
|
285
247
|
/** Injectable child spawner (defaults to node's `spawn`) — tests never spawn a real `claude`. */
|
|
@@ -361,6 +323,8 @@ export interface StartExecutionOrchestrateCtx {
|
|
|
361
323
|
epicId: string;
|
|
362
324
|
/** Resolved project repo dir (may be undefined/invalid — validated here). */
|
|
363
325
|
projectDir: string | undefined;
|
|
326
|
+
/** Run on the person's saved API key instead of this computer's Claude sign-in. */
|
|
327
|
+
keyRun?: KeyRun;
|
|
364
328
|
}
|
|
365
329
|
export interface StartExecutionOrchestrateDeps {
|
|
366
330
|
/** Injectable child spawner (defaults to node's `spawn`) — tests never spawn a real `claude`. */
|
|
@@ -377,7 +341,16 @@ export interface StartExecutionOrchestrateDeps {
|
|
|
377
341
|
heartbeat?: () => void | Promise<void>;
|
|
378
342
|
heartbeatIntervalMs?: number;
|
|
379
343
|
timeoutMs?: number;
|
|
344
|
+
/**
|
|
345
|
+
* Runs the orchestrator on its own new branch in a separate git worktree and
|
|
346
|
+
* finishes with a pushed branch + pull request (see run-workspace.ts). The
|
|
347
|
+
* daemon always passes one; when omitted the run happens directly in the
|
|
348
|
+
* project folder, as before.
|
|
349
|
+
*/
|
|
350
|
+
workspace?: Pick<RunWorkspaceManager, 'prepare' | 'finish'>;
|
|
380
351
|
}
|
|
352
|
+
/** Plain-language summary of where a run's code ended up. */
|
|
353
|
+
export declare function describeRunBranch(finish: FinishResult): string;
|
|
381
354
|
/**
|
|
382
355
|
* Turn a detected needs-review marker into the job result the web renders
|
|
383
356
|
* (ENG-165-3). Names the halting gate (and its reason/phase/feature when the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-invoker.d.ts","sourceRoot":"","sources":["../../src/utils/agent-invoker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAa,MAAM,oBAAoB,CAAC;AAItD,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-invoker.d.ts","sourceRoot":"","sources":["../../src/utils/agent-invoker.ts"],"names":[],"mappings":"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"}
|
package/utils/agent-invoker.js
CHANGED
|
@@ -35,9 +35,13 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
35
35
|
import { mkdirSync } from 'node:fs';
|
|
36
36
|
import { homedir } from 'node:os';
|
|
37
37
|
import { join } from 'node:path';
|
|
38
|
-
import { resolveRouting, buildInterviewSystemPrompt, INTERVIEW_DEFINITION, } from '@mycompbox/shared';
|
|
38
|
+
import { resolveRouting, buildInterviewSystemPrompt, INTERVIEW_DEFINITION, extractCompletionJson, looksLikeCompletionAttempt, meetsComplianceTarget, buildAuditFeedback, COMPLETION_REPAIR_INSTRUCTION, } from '@mycompbox/shared';
|
|
39
|
+
// Completion parsing lives in @mycompbox/shared so CompBox's server-side
|
|
40
|
+
// (API-key) interview finishes the same way; re-exported for existing callers.
|
|
41
|
+
export { extractCompletionJson, looksLikeCompletionAttempt, meetsComplianceTarget, buildAuditFeedback, COMPLETION_REPAIR_INSTRUCTION, };
|
|
39
42
|
import { claudeChildEnvPath, claudeNotFoundMessage, resolveClaudeBinary, } from './claude-runtime.js';
|
|
40
43
|
import { isGitRepoDir } from './config-store.js';
|
|
44
|
+
import { keyRunArgs, keyRunEnv, parseClaudeJsonResult } from './key-run.js';
|
|
41
45
|
// Fable is the model CompBox agents run on (ENG-E26): it is pxpipe's 100/100
|
|
42
46
|
// image reader, so routing it through the proxy cuts input tokens without
|
|
43
47
|
// quality loss. Opus/Sonnet are deliberately NOT used here — pxpipe passes
|
|
@@ -55,7 +59,6 @@ const MAX_INTERVIEW_TURNS = 18;
|
|
|
55
59
|
// many re-submit/re-audit rounds we allow before finalizing with the best draft
|
|
56
60
|
// (so we never loop forever, and never strand the request at a failing score).
|
|
57
61
|
const MAX_QUALITY_ROUNDS = 4;
|
|
58
|
-
const TARGET_COMPLIANCE_TIER = 'pass';
|
|
59
62
|
const REPLY_POLL_INTERVAL_MS = 3_000;
|
|
60
63
|
const REPLY_TIMEOUT_MS = 10 * 60 * 1_000; // 10 minutes
|
|
61
64
|
/** Per-turn wall-clock budget for a single `claude -p` child process. */
|
|
@@ -287,182 +290,6 @@ export async function submitAndScore(ctx, currentErId, draft) {
|
|
|
287
290
|
const created = await createPersonalEpicRequest(ctx.apiBaseUrl, ctx.userApiToken, draft);
|
|
288
291
|
return { erId: created.id, compliance: created.compliance };
|
|
289
292
|
}
|
|
290
|
-
/**
|
|
291
|
-
* Whether the request is good enough to finish. Reaching the target tier
|
|
292
|
-
* (`pass` = >=90 with no criticals) finishes early. A null score is fail-open:
|
|
293
|
-
* the gate can only act on a score the server actually returned.
|
|
294
|
-
*/
|
|
295
|
-
export function meetsComplianceTarget(compliance) {
|
|
296
|
-
return !compliance || compliance.tier === TARGET_COMPLIANCE_TIER;
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Turn a failing compliance score into a corrective instruction: fix by
|
|
300
|
-
* EXPANDING what's already known where possible, otherwise ask ONE question.
|
|
301
|
-
*/
|
|
302
|
-
export function buildAuditFeedback(compliance) {
|
|
303
|
-
const ordered = [
|
|
304
|
-
...compliance.issues.filter((i) => i.tier === 'critical'),
|
|
305
|
-
...compliance.issues.filter((i) => i.tier === 'advisory'),
|
|
306
|
-
].slice(0, 8);
|
|
307
|
-
const list = ordered.length > 0
|
|
308
|
-
? ordered.map((i) => `- ${i.message}`).join('\n')
|
|
309
|
-
: '- (no specific issues returned; expand the required fields with more concrete detail)';
|
|
310
|
-
return `The epic request you just produced scored ${String(compliance.overall)}/100 (tier: ${compliance.tier}). It must reach 90/100 with ZERO critical issues before the interview can finish. Fix these:
|
|
311
|
-
${list}
|
|
312
|
-
|
|
313
|
-
If you can resolve these by EXPANDING what the employee already told you — a longer, more complete and concrete problemStatement / proposedSolution / impactAssessment / description that comfortably clear the length minimums — re-emit the completion JSON now with richer fields. Do NOT re-ask something you already have an answer for. Only if you genuinely need NEW information from the employee, ask ONE focused question to get it.`;
|
|
314
|
-
}
|
|
315
|
-
/** The optional draft keys, kept in one place so the guard and parser agree. */
|
|
316
|
-
const OPTIONAL_DRAFT_KEYS = [
|
|
317
|
-
'successMetrics',
|
|
318
|
-
'targetAudience',
|
|
319
|
-
'alternatives',
|
|
320
|
-
'dependencies',
|
|
321
|
-
'estimatedEffort',
|
|
322
|
-
];
|
|
323
|
-
/** Type guard validating the model's completion JSON has the required shape. */
|
|
324
|
-
function isEpicRequestDraft(value) {
|
|
325
|
-
if (typeof value !== 'object' || value === null)
|
|
326
|
-
return false;
|
|
327
|
-
const v = value;
|
|
328
|
-
// 🔴 We deliberately do NOT require a `done: true` marker. Fable frequently
|
|
329
|
-
// emits a complete, valid completion object WITHOUT `done` (verified against a
|
|
330
|
-
// real stuck interview), which caused the daemon to reject a finished request
|
|
331
|
-
// and relay the raw JSON back to the user forever. The completion signal is the
|
|
332
|
-
// PRESENCE of all required fields as non-empty strings — a plain-text question
|
|
333
|
-
// never carries these, so this is unambiguous.
|
|
334
|
-
const nonEmpty = (x) => typeof x === 'string' && x.trim().length > 0;
|
|
335
|
-
const requiredOk = nonEmpty(v.title) &&
|
|
336
|
-
nonEmpty(v.problemStatement) &&
|
|
337
|
-
nonEmpty(v.proposedSolution) &&
|
|
338
|
-
nonEmpty(v.impactAssessment) &&
|
|
339
|
-
nonEmpty(v.description);
|
|
340
|
-
if (!requiredOk)
|
|
341
|
-
return false;
|
|
342
|
-
// Optional fields, when present, must be strings (a wrong-typed optional is a
|
|
343
|
-
// malformed completion, not a silently-ignored one).
|
|
344
|
-
for (const key of OPTIONAL_DRAFT_KEYS) {
|
|
345
|
-
if (key in v && v[key] !== undefined && typeof v[key] !== 'string')
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
348
|
-
return true;
|
|
349
|
-
}
|
|
350
|
-
/** Removes trailing commas before } or ] — a common LLM JSON defect. */
|
|
351
|
-
function stripTrailingCommas(json) {
|
|
352
|
-
return json.replace(/,(\s*[}\]])/g, '$1');
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Escapes raw control chars (newline/carriage-return/tab) that appear INSIDE a
|
|
356
|
-
* JSON string literal — the single most common reason a model's completion JSON
|
|
357
|
-
* fails `JSON.parse` (long multi-line description/problem fields written with
|
|
358
|
-
* literal line breaks). Walks the text tracking string state so control chars
|
|
359
|
-
* OUTSIDE strings (formatting whitespace) are left untouched.
|
|
360
|
-
*/
|
|
361
|
-
function escapeRawControlCharsInStrings(json) {
|
|
362
|
-
let out = '';
|
|
363
|
-
let inString = false;
|
|
364
|
-
let escaped = false;
|
|
365
|
-
for (const ch of json) {
|
|
366
|
-
if (escaped) {
|
|
367
|
-
out += ch;
|
|
368
|
-
escaped = false;
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
if (ch === '\\') {
|
|
372
|
-
out += ch;
|
|
373
|
-
escaped = true;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (ch === '"') {
|
|
377
|
-
inString = !inString;
|
|
378
|
-
out += ch;
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
if (inString && (ch === '\n' || ch === '\r' || ch === '\t')) {
|
|
382
|
-
out += ch === '\n' ? '\\n' : ch === '\r' ? '\\r' : '\\t';
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
out += ch;
|
|
386
|
-
}
|
|
387
|
-
return out;
|
|
388
|
-
}
|
|
389
|
-
/** Candidate JSON substrings to try, in priority order, from a model reply. */
|
|
390
|
-
function completionJsonCandidates(text) {
|
|
391
|
-
const candidates = [];
|
|
392
|
-
// A fenced block (```json … ``` or a plain ``` … ``` fence), case-insensitive.
|
|
393
|
-
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
|
|
394
|
-
if (fenced?.[1])
|
|
395
|
-
candidates.push(fenced[1]);
|
|
396
|
-
const trimmed = text.trim();
|
|
397
|
-
if (trimmed.startsWith('{'))
|
|
398
|
-
candidates.push(trimmed);
|
|
399
|
-
// Last resort: the widest {...} span (handles prose wrapped around the JSON).
|
|
400
|
-
const first = text.indexOf('{');
|
|
401
|
-
const last = text.lastIndexOf('}');
|
|
402
|
-
if (first !== -1 && last > first)
|
|
403
|
-
candidates.push(text.slice(first, last + 1));
|
|
404
|
-
return candidates;
|
|
405
|
-
}
|
|
406
|
-
/** Builds the draft from a validated parse (required fields + non-empty optionals). */
|
|
407
|
-
function toDraft(parsed) {
|
|
408
|
-
const draft = {
|
|
409
|
-
title: parsed.title,
|
|
410
|
-
problemStatement: parsed.problemStatement,
|
|
411
|
-
proposedSolution: parsed.proposedSolution,
|
|
412
|
-
impactAssessment: parsed.impactAssessment,
|
|
413
|
-
description: parsed.description,
|
|
414
|
-
};
|
|
415
|
-
for (const key of OPTIONAL_DRAFT_KEYS) {
|
|
416
|
-
const raw = parsed[key];
|
|
417
|
-
if (typeof raw === 'string' && raw.trim().length > 0) {
|
|
418
|
-
draft[key] = raw;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return draft;
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Extracts a completion draft from a model reply, tolerant of the common LLM
|
|
425
|
-
* JSON defects (fenced/plain/bare, trailing commas, and — the big one — literal
|
|
426
|
-
* newlines inside string values). Returns null only when no repair strategy
|
|
427
|
-
* yields a VALID {@link isEpicRequestDraft} object.
|
|
428
|
-
*/
|
|
429
|
-
export function extractCompletionJson(text) {
|
|
430
|
-
for (const candidate of completionJsonCandidates(text)) {
|
|
431
|
-
for (const variant of [
|
|
432
|
-
candidate,
|
|
433
|
-
stripTrailingCommas(candidate),
|
|
434
|
-
escapeRawControlCharsInStrings(candidate),
|
|
435
|
-
escapeRawControlCharsInStrings(stripTrailingCommas(candidate)),
|
|
436
|
-
]) {
|
|
437
|
-
try {
|
|
438
|
-
const parsed = JSON.parse(variant);
|
|
439
|
-
if (isEpicRequestDraft(parsed))
|
|
440
|
-
return toDraft(parsed);
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
// Try the next repair variant / candidate.
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* True when a reply is clearly an ATTEMPT to finish (a JSON completion) even if
|
|
451
|
-
* it failed to parse/validate — so the daemon can correct the model instead of
|
|
452
|
-
* relaying raw JSON to the employee as if it were a question (which loops).
|
|
453
|
-
*/
|
|
454
|
-
export function looksLikeCompletionAttempt(text) {
|
|
455
|
-
// Must open a JSON object (fenced or bare) — note we do NOT require a closing
|
|
456
|
-
// brace, so a truncated/malformed completion still counts.
|
|
457
|
-
const hasObject = /```(?:json)?\s*\{/i.test(text) || text.trim().startsWith('{');
|
|
458
|
-
if (!hasObject)
|
|
459
|
-
return false;
|
|
460
|
-
// ...carrying completion field names (or a `done` marker) — even if malformed
|
|
461
|
-
// or missing `done`. A plain-text question never matches this.
|
|
462
|
-
return /"(?:done|title|problemStatement|proposedSolution|impactAssessment|description)"\s*:/i.test(text);
|
|
463
|
-
}
|
|
464
|
-
/** Correction sent to the model when its completion JSON couldn't be used. */
|
|
465
|
-
const COMPLETION_REPAIR_INSTRUCTION = 'Your last message looked like a completion but I could NOT parse it as a valid JSON object with all required fields. Re-emit ONLY a single valid JSON object inside a ```json code fence — no other text — with every string value properly escaped (escape any internal double-quotes as \\" and any line breaks as \\n), and make sure title, problemStatement, proposedSolution, impactAssessment, and description are all present and non-empty:\n```json\n{"done": true, "title": "...", "problemStatement": "...", "proposedSolution": "...", "impactAssessment": "...", "description": "..."}\n```';
|
|
466
293
|
/** How many times to ask the model to fix a malformed completion before failing. */
|
|
467
294
|
const MAX_JSON_REPAIRS = 3;
|
|
468
295
|
function sleep(ms) {
|
|
@@ -922,6 +749,25 @@ export const PLANNER_HEARTBEAT_INTERVAL_MS = 20 * 1_000;
|
|
|
922
749
|
const PLANNER_FINAL_TURN_MAX_CHARS = 4_000;
|
|
923
750
|
/** The literal marker token the orchestrator emits (ENG-162); kept in sync for compatibility. */
|
|
924
751
|
const ORCHESTRATOR_REVIEW_MARKER = 'COMPBOX_NEEDS_HUMAN_REVIEW';
|
|
752
|
+
/**
|
|
753
|
+
* Finishes a run made on the person's API key: reads Claude Code's JSON
|
|
754
|
+
* result, reports the run's usage to CompBox (best-effort), and returns the
|
|
755
|
+
* plain text the run would otherwise have printed.
|
|
756
|
+
*/
|
|
757
|
+
export function takeKeyRunResult(ctx, keyRun, stdout) {
|
|
758
|
+
const parsed = parseClaudeJsonResult(stdout, keyRun.model);
|
|
759
|
+
if (!parsed)
|
|
760
|
+
return { text: stdout, reported: Promise.resolve() };
|
|
761
|
+
const reported = ctx.mailboxClient
|
|
762
|
+
.reportAiUsage(ctx.machineToken, ctx.jobId, parsed.usage)
|
|
763
|
+
.catch(() => undefined);
|
|
764
|
+
return { text: parsed.text, reported };
|
|
765
|
+
}
|
|
766
|
+
async function settleKeyRun(ctx, keyRun, stdout) {
|
|
767
|
+
const { text, reported } = takeKeyRunResult(ctx, keyRun, stdout);
|
|
768
|
+
await reported;
|
|
769
|
+
return text;
|
|
770
|
+
}
|
|
925
771
|
/**
|
|
926
772
|
* Build the `claude` argv for a headless planner run (H1). Pure + exported so a
|
|
927
773
|
* unit test can assert the exact command shape: the `@planner` routing prompt,
|
|
@@ -1099,16 +945,17 @@ export async function invokeStartExecutionPlan(ctx, deps = {}) {
|
|
|
1099
945
|
}
|
|
1100
946
|
const routing = await resolveRoutingImpl();
|
|
1101
947
|
const command = resolveClaudeBinary() ?? 'claude';
|
|
1102
|
-
const args = buildPlannerArgs(ctx.epicRequestId);
|
|
948
|
+
const args = ctx.keyRun ? keyRunArgs(buildPlannerArgs(ctx.epicRequestId)) : buildPlannerArgs(ctx.epicRequestId);
|
|
1103
949
|
// pxpipe (ENG-136): inject ANTHROPIC_BASE_URL when routed. COMPBOX_NONINTERACTIVE
|
|
1104
950
|
// forces the planner's headless path even if the prompt flag were dropped. PATH
|
|
1105
951
|
// is augmented (ENG-135) so `claude` and any helpers it spawns resolve.
|
|
1106
952
|
const routedEnv = claudeSpawnEnv(process.env, routing);
|
|
1107
|
-
const
|
|
953
|
+
const baseEnv = {
|
|
1108
954
|
...routedEnv,
|
|
1109
955
|
COMPBOX_NONINTERACTIVE: '1',
|
|
1110
956
|
PATH: claudeChildEnvPath(routedEnv.PATH),
|
|
1111
957
|
};
|
|
958
|
+
const childEnv = ctx.keyRun ? keyRunEnv(baseEnv, ctx.keyRun) : baseEnv;
|
|
1112
959
|
// H4: relay a START turn immediately so the web shows the run began even if the
|
|
1113
960
|
// planner produces no stdout for minutes. postTurn failures are swallowed —
|
|
1114
961
|
// streaming is best-effort and must never fail the job.
|
|
@@ -1171,6 +1018,8 @@ export async function invokeStartExecutionPlan(ctx, deps = {}) {
|
|
|
1171
1018
|
});
|
|
1172
1019
|
child.on('close', (code) => {
|
|
1173
1020
|
void (async () => {
|
|
1021
|
+
if (ctx.keyRun)
|
|
1022
|
+
stdout = await settleKeyRun(ctx, ctx.keyRun, stdout);
|
|
1174
1023
|
const combined = `${stdout}\n${stderr}`;
|
|
1175
1024
|
// A needs-review outcome wins regardless of exit code: the planner may
|
|
1176
1025
|
// exit 0 after emitting the marker. Surface it to the web (not silent).
|
|
@@ -1356,6 +1205,18 @@ export function defaultToolchainPresent(_dir) {
|
|
|
1356
1205
|
}
|
|
1357
1206
|
return false;
|
|
1358
1207
|
}
|
|
1208
|
+
/** Plain-language summary of where a run's code ended up. */
|
|
1209
|
+
export function describeRunBranch(finish) {
|
|
1210
|
+
if (finish.prUrl)
|
|
1211
|
+
return `Pull request: ${finish.prUrl}`;
|
|
1212
|
+
if (finish.compareUrl)
|
|
1213
|
+
return `Branch ${finish.branch} was pushed. Open the pull request here: ${finish.compareUrl}`;
|
|
1214
|
+
if (finish.commits === 0)
|
|
1215
|
+
return 'No code changes were made.';
|
|
1216
|
+
if (finish.pushed)
|
|
1217
|
+
return `Branch ${finish.branch} was pushed.`;
|
|
1218
|
+
return `The work is on branch ${finish.branch}${finish.keptWorktree ? ` (in ${finish.keptWorktree})` : ''}.`;
|
|
1219
|
+
}
|
|
1359
1220
|
/** Read a string field off a parsed marker object, or undefined. */
|
|
1360
1221
|
function markerString(marker, key) {
|
|
1361
1222
|
const v = marker?.[key];
|
|
@@ -1427,23 +1288,39 @@ export async function invokeStartExecutionOrchestrate(ctx, deps = {}) {
|
|
|
1427
1288
|
if (!toolchainPresent(projectDir)) {
|
|
1428
1289
|
return { status: 'failed', result: { error: toolchainMissingMessage(projectDir) } };
|
|
1429
1290
|
}
|
|
1291
|
+
// Each run gets its own branch + worktree, so it never touches the person's
|
|
1292
|
+
// checkout and always ends as a reviewable pull request.
|
|
1293
|
+
let workspace = null;
|
|
1294
|
+
let workspaceNotes = [];
|
|
1295
|
+
if (deps.workspace) {
|
|
1296
|
+
const prepared = await deps.workspace.prepare(projectDir, ctx.epicId);
|
|
1297
|
+
if (!prepared.ok) {
|
|
1298
|
+
return { status: 'failed', result: { error: prepared.error } };
|
|
1299
|
+
}
|
|
1300
|
+
workspace = prepared.workspace;
|
|
1301
|
+
workspaceNotes = prepared.notes;
|
|
1302
|
+
}
|
|
1303
|
+
const runDir = workspace?.dir ?? projectDir;
|
|
1430
1304
|
const routing = await resolveRoutingImpl();
|
|
1431
1305
|
const command = resolveClaudeBinary() ?? 'claude';
|
|
1432
|
-
const args = buildOrchestratorArgs(ctx.epicId);
|
|
1306
|
+
const args = ctx.keyRun ? keyRunArgs(buildOrchestratorArgs(ctx.epicId)) : buildOrchestratorArgs(ctx.epicId);
|
|
1433
1307
|
// pxpipe (ENG-136): inject ANTHROPIC_BASE_URL when routed. COMPBOX_NONINTERACTIVE
|
|
1434
1308
|
// forces the orchestrator's headless serial-in-process path (H3) even if the
|
|
1435
1309
|
// prompt flag were dropped. PATH is augmented (ENG-135) so `claude` and any
|
|
1436
1310
|
// helpers it spawns resolve on a launchd/GUI-spawned daemon.
|
|
1437
1311
|
const routedEnv = claudeSpawnEnv(process.env, routing);
|
|
1438
|
-
const
|
|
1312
|
+
const baseEnv = {
|
|
1439
1313
|
...routedEnv,
|
|
1440
1314
|
COMPBOX_NONINTERACTIVE: '1',
|
|
1441
1315
|
PATH: claudeChildEnvPath(routedEnv.PATH),
|
|
1442
1316
|
};
|
|
1317
|
+
const childEnv = ctx.keyRun ? keyRunEnv(baseEnv, ctx.keyRun) : baseEnv;
|
|
1443
1318
|
// H4: relay a START turn immediately so the web shows the run began even if the
|
|
1444
1319
|
// orchestrator produces no stdout for minutes. postTurn failures are swallowed.
|
|
1445
1320
|
try {
|
|
1446
|
-
await ctx.mailboxClient.postTurn(ctx.machineToken, ctx.jobId, `Started the orchestrator for epic ${ctx.epicId} (headless, non-interactive). This executes the full epic — every phase and feature — and may take a long time.`
|
|
1321
|
+
await ctx.mailboxClient.postTurn(ctx.machineToken, ctx.jobId, `Started the orchestrator for epic ${ctx.epicId} (headless, non-interactive). This executes the full epic — every phase and feature — and may take a long time.` +
|
|
1322
|
+
(workspace ? ` Working on a new branch, ${workspace.branch}, which will end as a pull request into ${workspace.baseBranch}.` : '') +
|
|
1323
|
+
(workspaceNotes.length > 0 ? ` Note: ${workspaceNotes.join(' ')}` : ''));
|
|
1447
1324
|
}
|
|
1448
1325
|
catch {
|
|
1449
1326
|
// Best-effort start turn.
|
|
@@ -1453,7 +1330,7 @@ export async function invokeStartExecutionOrchestrate(ctx, deps = {}) {
|
|
|
1453
1330
|
let stderr = '';
|
|
1454
1331
|
let settled = false;
|
|
1455
1332
|
const child = spawnImpl(command, args, {
|
|
1456
|
-
cwd:
|
|
1333
|
+
cwd: runDir,
|
|
1457
1334
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1458
1335
|
env: childEnv,
|
|
1459
1336
|
});
|
|
@@ -1502,6 +1379,9 @@ export async function invokeStartExecutionOrchestrate(ctx, deps = {}) {
|
|
|
1502
1379
|
});
|
|
1503
1380
|
});
|
|
1504
1381
|
child.on('close', (code) => {
|
|
1382
|
+
// Usage is reported in the background; the run's outcome doesn't wait on it.
|
|
1383
|
+
if (ctx.keyRun)
|
|
1384
|
+
stdout = takeKeyRunResult(ctx, ctx.keyRun, stdout).text;
|
|
1505
1385
|
const combined = `${stdout}\n${stderr}`;
|
|
1506
1386
|
// ENG-165-3: a HARD-gate needs-review outcome wins regardless of exit code
|
|
1507
1387
|
// — the orchestrator emits the marker and STOPs, and may exit 0. Surface
|
|
@@ -1529,11 +1409,40 @@ export async function invokeStartExecutionOrchestrate(ctx, deps = {}) {
|
|
|
1529
1409
|
finish({ status: 'done', result: { epicId: ctx.epicId } });
|
|
1530
1410
|
});
|
|
1531
1411
|
});
|
|
1412
|
+
// Commit what's left, push the branch and open the pull request (draft when
|
|
1413
|
+
// the run stopped early), then attach where the code is to the job result.
|
|
1414
|
+
let branchLine = '';
|
|
1415
|
+
if (deps.workspace && workspace) {
|
|
1416
|
+
const resultError = outcome.result;
|
|
1417
|
+
const stopReason = typeof resultError.message === 'string'
|
|
1418
|
+
? resultError.message
|
|
1419
|
+
: typeof resultError.error === 'string'
|
|
1420
|
+
? resultError.error.split('\n')[0]
|
|
1421
|
+
: undefined;
|
|
1422
|
+
const finished = await deps.workspace.finish({
|
|
1423
|
+
workspace,
|
|
1424
|
+
epicId: ctx.epicId,
|
|
1425
|
+
jobId: ctx.jobId,
|
|
1426
|
+
completed: outcome.status === 'done',
|
|
1427
|
+
...(stopReason ? { stopReason } : {}),
|
|
1428
|
+
});
|
|
1429
|
+
branchLine = describeRunBranch(finished);
|
|
1430
|
+
outcome.result = {
|
|
1431
|
+
...outcome.result,
|
|
1432
|
+
branch: finished.branch,
|
|
1433
|
+
commits: finished.commits,
|
|
1434
|
+
pushed: finished.pushed,
|
|
1435
|
+
...(finished.prUrl ? { prUrl: finished.prUrl } : {}),
|
|
1436
|
+
...(finished.compareUrl ? { compareUrl: finished.compareUrl } : {}),
|
|
1437
|
+
...(finished.notes.length > 0 ? { branchNotes: finished.notes } : {}),
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1532
1440
|
// H4: relay a FINAL turn summarizing the outcome (best-effort).
|
|
1533
1441
|
try {
|
|
1534
|
-
const summary = outcome.status === 'done'
|
|
1442
|
+
const summary = (outcome.status === 'done'
|
|
1535
1443
|
? `Execution complete — epic ${ctx.epicId} finished all phases.`
|
|
1536
|
-
: `Orchestrator run ended: ${JSON.stringify(outcome.result).slice(0, ORCHESTRATOR_FINAL_TURN_MAX_CHARS)}
|
|
1444
|
+
: `Orchestrator run ended: ${JSON.stringify(outcome.result).slice(0, ORCHESTRATOR_FINAL_TURN_MAX_CHARS)}`) +
|
|
1445
|
+
(branchLine ? `\n${branchLine}` : '');
|
|
1537
1446
|
await ctx.mailboxClient.postTurn(ctx.machineToken, ctx.jobId, summary);
|
|
1538
1447
|
}
|
|
1539
1448
|
catch {
|