@cat-factory/executor-harness 1.50.16 → 1.50.18
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/dist/agent-runner.js +55 -47
- package/dist/agent.js +16 -8
- package/dist/job.js +42 -24
- package/package.json +3 -3
- package/src/agent-runner.ts +67 -48
- package/src/agent.ts +50 -41
- package/src/job.ts +43 -23
package/dist/agent-runner.js
CHANGED
|
@@ -319,21 +319,7 @@ export async function runClaudeCode(opts) {
|
|
|
319
319
|
: join(homedir(), '.claude', 'skills');
|
|
320
320
|
await writeNativeSkill(skillsRoot, opts.skill).catch(() => { });
|
|
321
321
|
}
|
|
322
|
-
|
|
323
|
-
// non-Anthropic Claude-Code vendor (GLM via Z.ai, Kimi via Moonshot, DeepSeek)
|
|
324
|
-
// points Claude Code at its Anthropic-compatible endpoint with an auth-token key.
|
|
325
|
-
// Ambient mode injects neither — the CLI uses the developer's logged-in `~/.claude`.
|
|
326
|
-
const env = opts.ambientAuth
|
|
327
|
-
? {}
|
|
328
|
-
: {
|
|
329
|
-
CLAUDE_CONFIG_DIR: configHome,
|
|
330
|
-
...(opts.subscriptionBaseUrl
|
|
331
|
-
? {
|
|
332
|
-
ANTHROPIC_BASE_URL: opts.subscriptionBaseUrl,
|
|
333
|
-
ANTHROPIC_AUTH_TOKEN: opts.subscriptionToken,
|
|
334
|
-
}
|
|
335
|
-
: { CLAUDE_CODE_OAUTH_TOKEN: opts.subscriptionToken }),
|
|
336
|
-
};
|
|
322
|
+
const env = buildClaudeEnv(opts, configHome);
|
|
337
323
|
// ADR 0026 D3 (path corrected by ADR 0027 Defect A): while the run is live, tail the CLI's
|
|
338
324
|
// subagent `*.jsonl` transcripts so a parallel-subagent review keeps the inactivity
|
|
339
325
|
// heartbeat alive (any new bytes ⇒ `onActivity`) and its otherwise-invisible token spend is
|
|
@@ -369,38 +355,7 @@ export async function runClaudeCode(opts) {
|
|
|
369
355
|
...appendArgs,
|
|
370
356
|
],
|
|
371
357
|
}, prompt, opts, env, opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [], onEvent);
|
|
372
|
-
|
|
373
|
-
// subagent calls, which carry their own per-turn tokens, are concatenated).
|
|
374
|
-
attributeCumulativeUsage(calls, usage);
|
|
375
|
-
// Final drain of any subagent transcript writes that landed after the last poll, then
|
|
376
|
-
// fold the subagents' usage + per-call telemetry into the run's outcome — their tokens
|
|
377
|
-
// never appear on the parent stream, so this is the only place they are accounted.
|
|
378
|
-
await subagents?.stop();
|
|
379
|
-
const subUsage = subagents?.usage() ?? { inputTokens: 0, outputTokens: 0 };
|
|
380
|
-
const subCalls = subagents?.calls() ?? [];
|
|
381
|
-
const mergedCalls = [...calls, ...subCalls];
|
|
382
|
-
// INVARIANT (do not "fix" this into a double count): the run total is the parent usage
|
|
383
|
-
// PLUS the subagent usage because the two are disjoint sources. The parent `usage` here
|
|
384
|
-
// is the terminal `result` event's cumulative, which covers ONLY the parent loop — the
|
|
385
|
-
// ADR 0026 incident is itself the proof: a heavily subagent-parallelised review reported
|
|
386
|
-
// ~0 tokens, i.e. the parent stream (and its `result` total) never included the subagent
|
|
387
|
-
// spend. The subagent tokens live exclusively in the per-session `subagents/*.jsonl`
|
|
388
|
-
// transcripts, which the watcher reads and nothing else does; it deliberately EXCLUDES the
|
|
389
|
-
// sibling parent session transcript (whose usage `result` already totals), so neither
|
|
390
|
-
// `calls` nor `usage` can already contain the subagent spend.
|
|
391
|
-
const mergedUsage = usage || subUsage.inputTokens || subUsage.outputTokens
|
|
392
|
-
? {
|
|
393
|
-
inputTokens: (usage?.inputTokens ?? 0) + subUsage.inputTokens,
|
|
394
|
-
outputTokens: (usage?.outputTokens ?? 0) + subUsage.outputTokens,
|
|
395
|
-
}
|
|
396
|
-
: undefined;
|
|
397
|
-
return {
|
|
398
|
-
summary,
|
|
399
|
-
stats,
|
|
400
|
-
stderrTail,
|
|
401
|
-
...(mergedUsage ? { usage: mergedUsage } : {}),
|
|
402
|
-
...(mergedCalls.length ? { callMetrics: mergedCalls } : {}),
|
|
403
|
-
};
|
|
358
|
+
return await assembleClaudeOutcome({ summary, stats, stderrTail, calls, usage, subagents });
|
|
404
359
|
}
|
|
405
360
|
finally {
|
|
406
361
|
await subagents?.stop();
|
|
@@ -417,6 +372,59 @@ export async function runClaudeCode(opts) {
|
|
|
417
372
|
}
|
|
418
373
|
}
|
|
419
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Build the child-process env for the `claude` CLI: an isolated config home plus subscription
|
|
377
|
+
* auth (Anthropic OAuth token, or an Anthropic-compatible base URL + auth token for a
|
|
378
|
+
* non-Anthropic Claude-Code vendor like GLM/Kimi/DeepSeek), or an empty env in ambient mode
|
|
379
|
+
* (the developer's own logged-in `~/.claude` is used). Extracted from {@link runClaudeCode} to
|
|
380
|
+
* keep its cyclomatic complexity down; behaviour is a straight move of the original expression.
|
|
381
|
+
*/
|
|
382
|
+
function buildClaudeEnv(opts, configHome) {
|
|
383
|
+
if (opts.ambientAuth)
|
|
384
|
+
return {};
|
|
385
|
+
return {
|
|
386
|
+
CLAUDE_CONFIG_DIR: configHome,
|
|
387
|
+
...(opts.subscriptionBaseUrl
|
|
388
|
+
? {
|
|
389
|
+
ANTHROPIC_BASE_URL: opts.subscriptionBaseUrl,
|
|
390
|
+
ANTHROPIC_AUTH_TOKEN: opts.subscriptionToken,
|
|
391
|
+
}
|
|
392
|
+
: { CLAUDE_CODE_OAUTH_TOKEN: opts.subscriptionToken }),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Merge the parent-loop telemetry with the subagents' out-of-band usage + per-call metrics into
|
|
397
|
+
* the run outcome. INVARIANT (do not "fix" this into a double count): the run total is the parent
|
|
398
|
+
* usage PLUS the subagent usage because the two are disjoint sources — the parent `usage` (the
|
|
399
|
+
* terminal `result` event's cumulative) covers ONLY the parent loop, and the subagent tokens live
|
|
400
|
+
* exclusively in the per-session `subagents/*.jsonl` transcripts the watcher reads. Extracted from
|
|
401
|
+
* {@link runClaudeCode} verbatim to keep its cyclomatic complexity down.
|
|
402
|
+
*/
|
|
403
|
+
async function assembleClaudeOutcome(args) {
|
|
404
|
+
const { summary, stats, stderrTail, calls, usage, subagents } = args;
|
|
405
|
+
// The parent's cumulative-usage fallback applies to the PARENT calls only (before the
|
|
406
|
+
// subagent calls, which carry their own per-turn tokens, are concatenated).
|
|
407
|
+
attributeCumulativeUsage(calls, usage);
|
|
408
|
+
// Final drain of any subagent transcript writes that landed after the last poll, then
|
|
409
|
+
// fold the subagents' usage + per-call telemetry into the run's outcome.
|
|
410
|
+
await subagents?.stop();
|
|
411
|
+
const subUsage = subagents?.usage() ?? { inputTokens: 0, outputTokens: 0 };
|
|
412
|
+
const subCalls = subagents?.calls() ?? [];
|
|
413
|
+
const mergedCalls = [...calls, ...subCalls];
|
|
414
|
+
const mergedUsage = usage || subUsage.inputTokens || subUsage.outputTokens
|
|
415
|
+
? {
|
|
416
|
+
inputTokens: (usage?.inputTokens ?? 0) + subUsage.inputTokens,
|
|
417
|
+
outputTokens: (usage?.outputTokens ?? 0) + subUsage.outputTokens,
|
|
418
|
+
}
|
|
419
|
+
: undefined;
|
|
420
|
+
return {
|
|
421
|
+
summary,
|
|
422
|
+
stats,
|
|
423
|
+
stderrTail,
|
|
424
|
+
...(mergedUsage ? { usage: mergedUsage } : {}),
|
|
425
|
+
...(mergedCalls.length ? { callMetrics: mergedCalls } : {}),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
420
428
|
/** Map Claude Code's `TodoWrite` todos array onto subtask counts. */
|
|
421
429
|
function todosToProgress(todos) {
|
|
422
430
|
if (!Array.isArray(todos))
|
package/dist/agent.js
CHANGED
|
@@ -745,14 +745,12 @@ async function runCodingMode(job, opts) {
|
|
|
745
745
|
return result;
|
|
746
746
|
}
|
|
747
747
|
/**
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
* the in-place fixers (and for a seed-only kind like `repro-test`).
|
|
748
|
+
* Assemble the {@link runCodingAgent} spec for the ordinary single-repo coding flow. Extracted
|
|
749
|
+
* from {@link runSingleRepoCoding} so the many optional-field spreads don't inflate that
|
|
750
|
+
* function's cyclomatic complexity; the mapping is a straight field copy off `job`.
|
|
752
751
|
*/
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
const { summary, stats, stderrTail, pushed, usage, callMetrics, validation, effortReport } = await runCodingAgent({
|
|
752
|
+
function buildSingleRepoCodingSpec(job, pushBranch) {
|
|
753
|
+
return {
|
|
756
754
|
kind: 'agent',
|
|
757
755
|
jobId: job.jobId,
|
|
758
756
|
repo: job.repo,
|
|
@@ -789,7 +787,17 @@ async function runSingleRepoCoding(job, opts) {
|
|
|
789
787
|
},
|
|
790
788
|
}
|
|
791
789
|
: {}),
|
|
792
|
-
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* The ordinary single-repo coding flow: clone `branch` (or resume `newBranch`), run the agent,
|
|
794
|
+
* commit + push to `pushBranch`, and open `pr` when one is set and the run produced changes. A
|
|
795
|
+
* no-op is a failure for the implementer (`noChangesIsError` default) and a non-fatal no-op for
|
|
796
|
+
* the in-place fixers (and for a seed-only kind like `repro-test`).
|
|
797
|
+
*/
|
|
798
|
+
async function runSingleRepoCoding(job, opts) {
|
|
799
|
+
const pushBranch = job.pushBranch ?? job.newBranch ?? job.branch;
|
|
800
|
+
const { summary, stats, stderrTail, pushed, usage, callMetrics, validation, effortReport } = await runCodingAgent(buildSingleRepoCodingSpec(job, pushBranch), opts);
|
|
793
801
|
// Ralph loop: the harness-computed validation verdict, forwarded onto the coding result as
|
|
794
802
|
// `ralphVerdict` so the backend's `toRunResult` lifts it onto `AgentRunResult.ralphVerdict`.
|
|
795
803
|
const ralphVerdict = validation ? { ralphVerdict: validation } : {};
|
package/dist/job.js
CHANGED
|
@@ -555,6 +555,24 @@ function isReservedEnvName(key) {
|
|
|
555
555
|
const lower = key.toLowerCase();
|
|
556
556
|
return RESERVED_ENV_PREFIXES.some((p) => lower.startsWith(p));
|
|
557
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Collect only string→string entries from a raw `env` bag. A non-string value is dropped so a
|
|
560
|
+
* malformed binding can't inject `[object Object]` (or undefined) as an upstream URL. Reserved
|
|
561
|
+
* names that would break the toolchain or enable injection (PATH, NODE_OPTIONS, LD_PRELOAD, …) are
|
|
562
|
+
* dropped too: they are spread over `process.env` at build time, so a binding named `PATH` would
|
|
563
|
+
* replace it with a URL and the build would no longer find its tools. Extracted from the infra
|
|
564
|
+
* parsers to keep their cyclomatic complexity down.
|
|
565
|
+
*/
|
|
566
|
+
function parseInfraEnv(raw) {
|
|
567
|
+
const env = {};
|
|
568
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
569
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
570
|
+
if (key && !isReservedEnvName(key) && typeof val === 'string')
|
|
571
|
+
env[key] = val;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return env;
|
|
575
|
+
}
|
|
558
576
|
/** Parse the frontend UI-test infra spec (`kind: 'frontend'`), tolerating missing knobs. */
|
|
559
577
|
function parseFrontendInfraSpec(o) {
|
|
560
578
|
const packageManager = o.packageManager === 'pnpm' || o.packageManager === 'npm' || o.packageManager === 'yarn'
|
|
@@ -562,18 +580,7 @@ function parseFrontendInfraSpec(o) {
|
|
|
562
580
|
: undefined;
|
|
563
581
|
const serveMode = o.serveMode === 'static' || o.serveMode === 'command' ? o.serveMode : undefined;
|
|
564
582
|
const envInjection = o.envInjection === 'build' || o.envInjection === 'runtime' ? o.envInjection : undefined;
|
|
565
|
-
|
|
566
|
-
// binding can't inject `[object Object]` (or undefined) as an upstream URL. Reserved names
|
|
567
|
-
// that would break the toolchain or enable injection (PATH, NODE_OPTIONS, LD_PRELOAD, …) are
|
|
568
|
-
// dropped too: they are spread over `process.env` at build time, so a binding named `PATH`
|
|
569
|
-
// would replace it with a URL and the build would no longer find its tools.
|
|
570
|
-
const env = {};
|
|
571
|
-
if (typeof o.env === 'object' && o.env !== null) {
|
|
572
|
-
for (const [key, val] of Object.entries(o.env)) {
|
|
573
|
-
if (key && !isReservedEnvName(key) && typeof val === 'string')
|
|
574
|
-
env[key] = val;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
583
|
+
const env = parseInfraEnv(o.env);
|
|
577
584
|
const servePort = port(o.servePort);
|
|
578
585
|
const wiremockPort = port(o.wiremockPort);
|
|
579
586
|
// The app's monorepo subdirectory becomes the install/build/serve cwd, so it goes through the
|
|
@@ -732,11 +739,7 @@ function assembleAgentJob(o, mode, agentField, parts) {
|
|
|
732
739
|
ghToken: str(o.ghToken, 'ghToken'),
|
|
733
740
|
repo: parseRepoSpec(repo),
|
|
734
741
|
branch: str(o.branch, 'branch'),
|
|
735
|
-
...(
|
|
736
|
-
...(typeof o.webToolsGuidance === 'string' ? { webToolsGuidance: o.webToolsGuidance } : {}),
|
|
737
|
-
...(o.webSearch === true ? { webSearch: true } : {}),
|
|
738
|
-
...(o.full === true ? { full: true } : {}),
|
|
739
|
-
...(typeof o.mergeBase === 'string' && o.mergeBase ? { mergeBase: o.mergeBase } : {}),
|
|
742
|
+
...collectOptionalRequestFields(o),
|
|
740
743
|
...(bootstrap ? { bootstrap } : {}),
|
|
741
744
|
...(output ? { output } : {}),
|
|
742
745
|
...(contextFiles.length ? { contextFiles } : {}),
|
|
@@ -744,20 +747,35 @@ function assembleAgentJob(o, mode, agentField, parts) {
|
|
|
744
747
|
...(skill ? { skill } : {}),
|
|
745
748
|
...(testSecrets.length ? { testSecrets } : {}),
|
|
746
749
|
...(infra ? { infra } : {}),
|
|
747
|
-
...(typeof o.newBranch === 'string' && o.newBranch ? { newBranch: o.newBranch } : {}),
|
|
748
|
-
...(typeof o.pushBranch === 'string' && o.pushBranch ? { pushBranch: o.pushBranch } : {}),
|
|
749
|
-
...(typeof o.commitMessage === 'string' && o.commitMessage
|
|
750
|
-
? { commitMessage: o.commitMessage }
|
|
751
|
-
: {}),
|
|
752
750
|
...(pr ? { pr } : {}),
|
|
753
751
|
...(peerRepos.length ? { peerRepos } : {}),
|
|
754
752
|
...(referenceRepos.length ? { referenceRepos } : {}),
|
|
755
753
|
...(referenceBranches.length ? { referenceBranches } : {}),
|
|
756
754
|
...(reviewPrNumber !== undefined ? { reviewPrNumber } : {}),
|
|
755
|
+
...(guardLimits ? { guardLimits } : {}),
|
|
756
|
+
...(validation ? { validation } : {}),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* The optional {@link AgentJob} fields read directly off the request `o` (booleans + trimmed
|
|
761
|
+
* strings). Extracted from {@link assembleAgentJob} to keep its cyclomatic complexity down; every
|
|
762
|
+
* key is unique so grouping the conditional spreads is behaviour-neutral (spread order is
|
|
763
|
+
* irrelevant with no colliding keys).
|
|
764
|
+
*/
|
|
765
|
+
function collectOptionalRequestFields(o) {
|
|
766
|
+
return {
|
|
767
|
+
...(typeof o.githubApiBase === 'string' ? { githubApiBase: o.githubApiBase } : {}),
|
|
768
|
+
...(typeof o.webToolsGuidance === 'string' ? { webToolsGuidance: o.webToolsGuidance } : {}),
|
|
769
|
+
...(o.webSearch === true ? { webSearch: true } : {}),
|
|
770
|
+
...(o.full === true ? { full: true } : {}),
|
|
771
|
+
...(typeof o.mergeBase === 'string' && o.mergeBase ? { mergeBase: o.mergeBase } : {}),
|
|
772
|
+
...(typeof o.newBranch === 'string' && o.newBranch ? { newBranch: o.newBranch } : {}),
|
|
773
|
+
...(typeof o.pushBranch === 'string' && o.pushBranch ? { pushBranch: o.pushBranch } : {}),
|
|
774
|
+
...(typeof o.commitMessage === 'string' && o.commitMessage
|
|
775
|
+
? { commitMessage: o.commitMessage }
|
|
776
|
+
: {}),
|
|
757
777
|
...(o.noChangesIsError === false ? { noChangesIsError: false } : {}),
|
|
758
778
|
...(o.persistentCheckout === true ? { persistentCheckout: true } : {}),
|
|
759
779
|
...(o.streamFollowUps === true ? { streamFollowUps: true } : {}),
|
|
760
|
-
...(guardLimits ? { guardLimits } : {}),
|
|
761
|
-
...(validation ? { validation } : {}),
|
|
762
780
|
};
|
|
763
781
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/executor-harness",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.18",
|
|
4
4
|
"description": "Container payload: a thin TypeScript wrapper that runs the Pi coding agent against a cloned repo and opens a PR. Runs in the Cloudflare Container (and, in local native mode, as a host process); carries no secrets.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"hono": "^4.12.30",
|
|
27
27
|
"typescript": "7.0.2",
|
|
28
28
|
"vitest": "^4.1.10",
|
|
29
|
-
"@cat-factory/server": "0.
|
|
30
|
-
"@cat-factory/spend": "0.12.
|
|
29
|
+
"@cat-factory/server": "0.143.1",
|
|
30
|
+
"@cat-factory/spend": "0.12.75"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc -p tsconfig.json",
|
package/src/agent-runner.ts
CHANGED
|
@@ -424,21 +424,7 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
424
424
|
await writeNativeSkill(skillsRoot, opts.skill).catch(() => {})
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
|
|
428
|
-
// non-Anthropic Claude-Code vendor (GLM via Z.ai, Kimi via Moonshot, DeepSeek)
|
|
429
|
-
// points Claude Code at its Anthropic-compatible endpoint with an auth-token key.
|
|
430
|
-
// Ambient mode injects neither — the CLI uses the developer's logged-in `~/.claude`.
|
|
431
|
-
const env: Record<string, string> = opts.ambientAuth
|
|
432
|
-
? {}
|
|
433
|
-
: {
|
|
434
|
-
CLAUDE_CONFIG_DIR: configHome!,
|
|
435
|
-
...(opts.subscriptionBaseUrl
|
|
436
|
-
? {
|
|
437
|
-
ANTHROPIC_BASE_URL: opts.subscriptionBaseUrl,
|
|
438
|
-
ANTHROPIC_AUTH_TOKEN: opts.subscriptionToken!,
|
|
439
|
-
}
|
|
440
|
-
: { CLAUDE_CODE_OAUTH_TOKEN: opts.subscriptionToken! }),
|
|
441
|
-
}
|
|
427
|
+
const env = buildClaudeEnv(opts, configHome)
|
|
442
428
|
|
|
443
429
|
// ADR 0026 D3 (path corrected by ADR 0027 Defect A): while the run is live, tail the CLI's
|
|
444
430
|
// subagent `*.jsonl` transcripts so a parallel-subagent review keeps the inactivity
|
|
@@ -484,39 +470,7 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
484
470
|
onEvent,
|
|
485
471
|
)
|
|
486
472
|
|
|
487
|
-
|
|
488
|
-
// subagent calls, which carry their own per-turn tokens, are concatenated).
|
|
489
|
-
attributeCumulativeUsage(calls, usage)
|
|
490
|
-
// Final drain of any subagent transcript writes that landed after the last poll, then
|
|
491
|
-
// fold the subagents' usage + per-call telemetry into the run's outcome — their tokens
|
|
492
|
-
// never appear on the parent stream, so this is the only place they are accounted.
|
|
493
|
-
await subagents?.stop()
|
|
494
|
-
const subUsage = subagents?.usage() ?? { inputTokens: 0, outputTokens: 0 }
|
|
495
|
-
const subCalls = subagents?.calls() ?? []
|
|
496
|
-
const mergedCalls = [...calls, ...subCalls]
|
|
497
|
-
// INVARIANT (do not "fix" this into a double count): the run total is the parent usage
|
|
498
|
-
// PLUS the subagent usage because the two are disjoint sources. The parent `usage` here
|
|
499
|
-
// is the terminal `result` event's cumulative, which covers ONLY the parent loop — the
|
|
500
|
-
// ADR 0026 incident is itself the proof: a heavily subagent-parallelised review reported
|
|
501
|
-
// ~0 tokens, i.e. the parent stream (and its `result` total) never included the subagent
|
|
502
|
-
// spend. The subagent tokens live exclusively in the per-session `subagents/*.jsonl`
|
|
503
|
-
// transcripts, which the watcher reads and nothing else does; it deliberately EXCLUDES the
|
|
504
|
-
// sibling parent session transcript (whose usage `result` already totals), so neither
|
|
505
|
-
// `calls` nor `usage` can already contain the subagent spend.
|
|
506
|
-
const mergedUsage =
|
|
507
|
-
usage || subUsage.inputTokens || subUsage.outputTokens
|
|
508
|
-
? {
|
|
509
|
-
inputTokens: (usage?.inputTokens ?? 0) + subUsage.inputTokens,
|
|
510
|
-
outputTokens: (usage?.outputTokens ?? 0) + subUsage.outputTokens,
|
|
511
|
-
}
|
|
512
|
-
: undefined
|
|
513
|
-
return {
|
|
514
|
-
summary,
|
|
515
|
-
stats,
|
|
516
|
-
stderrTail,
|
|
517
|
-
...(mergedUsage ? { usage: mergedUsage } : {}),
|
|
518
|
-
...(mergedCalls.length ? { callMetrics: mergedCalls } : {}),
|
|
519
|
-
}
|
|
473
|
+
return await assembleClaudeOutcome({ summary, stats, stderrTail, calls, usage, subagents })
|
|
520
474
|
} finally {
|
|
521
475
|
await subagents?.stop()
|
|
522
476
|
if (configHome) {
|
|
@@ -533,6 +487,71 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
533
487
|
}
|
|
534
488
|
}
|
|
535
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Build the child-process env for the `claude` CLI: an isolated config home plus subscription
|
|
492
|
+
* auth (Anthropic OAuth token, or an Anthropic-compatible base URL + auth token for a
|
|
493
|
+
* non-Anthropic Claude-Code vendor like GLM/Kimi/DeepSeek), or an empty env in ambient mode
|
|
494
|
+
* (the developer's own logged-in `~/.claude` is used). Extracted from {@link runClaudeCode} to
|
|
495
|
+
* keep its cyclomatic complexity down; behaviour is a straight move of the original expression.
|
|
496
|
+
*/
|
|
497
|
+
function buildClaudeEnv(
|
|
498
|
+
opts: SubscriptionRunOptions,
|
|
499
|
+
configHome: string | undefined,
|
|
500
|
+
): Record<string, string> {
|
|
501
|
+
if (opts.ambientAuth) return {}
|
|
502
|
+
return {
|
|
503
|
+
CLAUDE_CONFIG_DIR: configHome!,
|
|
504
|
+
...(opts.subscriptionBaseUrl
|
|
505
|
+
? {
|
|
506
|
+
ANTHROPIC_BASE_URL: opts.subscriptionBaseUrl,
|
|
507
|
+
ANTHROPIC_AUTH_TOKEN: opts.subscriptionToken!,
|
|
508
|
+
}
|
|
509
|
+
: { CLAUDE_CODE_OAUTH_TOKEN: opts.subscriptionToken! }),
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Merge the parent-loop telemetry with the subagents' out-of-band usage + per-call metrics into
|
|
515
|
+
* the run outcome. INVARIANT (do not "fix" this into a double count): the run total is the parent
|
|
516
|
+
* usage PLUS the subagent usage because the two are disjoint sources — the parent `usage` (the
|
|
517
|
+
* terminal `result` event's cumulative) covers ONLY the parent loop, and the subagent tokens live
|
|
518
|
+
* exclusively in the per-session `subagents/*.jsonl` transcripts the watcher reads. Extracted from
|
|
519
|
+
* {@link runClaudeCode} verbatim to keep its cyclomatic complexity down.
|
|
520
|
+
*/
|
|
521
|
+
async function assembleClaudeOutcome(args: {
|
|
522
|
+
summary: string
|
|
523
|
+
stats: PiRunStats
|
|
524
|
+
stderrTail: string
|
|
525
|
+
calls: HarnessCallMetric[]
|
|
526
|
+
usage: { inputTokens: number; outputTokens: number } | undefined
|
|
527
|
+
subagents: ReturnType<typeof startSubagentWatcher> | undefined
|
|
528
|
+
}): Promise<PiRunOutcome> {
|
|
529
|
+
const { summary, stats, stderrTail, calls, usage, subagents } = args
|
|
530
|
+
// The parent's cumulative-usage fallback applies to the PARENT calls only (before the
|
|
531
|
+
// subagent calls, which carry their own per-turn tokens, are concatenated).
|
|
532
|
+
attributeCumulativeUsage(calls, usage)
|
|
533
|
+
// Final drain of any subagent transcript writes that landed after the last poll, then
|
|
534
|
+
// fold the subagents' usage + per-call telemetry into the run's outcome.
|
|
535
|
+
await subagents?.stop()
|
|
536
|
+
const subUsage = subagents?.usage() ?? { inputTokens: 0, outputTokens: 0 }
|
|
537
|
+
const subCalls = subagents?.calls() ?? []
|
|
538
|
+
const mergedCalls = [...calls, ...subCalls]
|
|
539
|
+
const mergedUsage =
|
|
540
|
+
usage || subUsage.inputTokens || subUsage.outputTokens
|
|
541
|
+
? {
|
|
542
|
+
inputTokens: (usage?.inputTokens ?? 0) + subUsage.inputTokens,
|
|
543
|
+
outputTokens: (usage?.outputTokens ?? 0) + subUsage.outputTokens,
|
|
544
|
+
}
|
|
545
|
+
: undefined
|
|
546
|
+
return {
|
|
547
|
+
summary,
|
|
548
|
+
stats,
|
|
549
|
+
stderrTail,
|
|
550
|
+
...(mergedUsage ? { usage: mergedUsage } : {}),
|
|
551
|
+
...(mergedCalls.length ? { callMetrics: mergedCalls } : {}),
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
536
555
|
/** Map Claude Code's `TodoWrite` todos array onto subtask counts. */
|
|
537
556
|
function todosToProgress(todos: unknown): TodoProgress | undefined {
|
|
538
557
|
if (!Array.isArray(todos)) return undefined
|
package/src/agent.ts
CHANGED
|
@@ -903,6 +903,55 @@ async function runCodingMode(job: AgentJob, opts: RunOptions): Promise<AgentResu
|
|
|
903
903
|
return result
|
|
904
904
|
}
|
|
905
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Assemble the {@link runCodingAgent} spec for the ordinary single-repo coding flow. Extracted
|
|
908
|
+
* from {@link runSingleRepoCoding} so the many optional-field spreads don't inflate that
|
|
909
|
+
* function's cyclomatic complexity; the mapping is a straight field copy off `job`.
|
|
910
|
+
*/
|
|
911
|
+
function buildSingleRepoCodingSpec(
|
|
912
|
+
job: AgentJob,
|
|
913
|
+
pushBranch: string,
|
|
914
|
+
): Parameters<typeof runCodingAgent>[0] {
|
|
915
|
+
return {
|
|
916
|
+
kind: 'agent',
|
|
917
|
+
jobId: job.jobId,
|
|
918
|
+
repo: job.repo,
|
|
919
|
+
cloneBranch: job.branch,
|
|
920
|
+
...(job.newBranch ? { newBranch: job.newBranch } : {}),
|
|
921
|
+
pushBranch,
|
|
922
|
+
ghToken: job.ghToken,
|
|
923
|
+
systemPrompt: job.systemPrompt,
|
|
924
|
+
userPrompt: job.userPrompt,
|
|
925
|
+
model: job.model,
|
|
926
|
+
harness: job.harness,
|
|
927
|
+
subscriptionToken: job.subscriptionToken,
|
|
928
|
+
subscriptionBaseUrl: job.subscriptionBaseUrl,
|
|
929
|
+
ambientAuth: job.ambientAuth,
|
|
930
|
+
proxyBaseUrl: job.proxyBaseUrl,
|
|
931
|
+
sessionToken: job.sessionToken,
|
|
932
|
+
commitMessage: job.commitMessage ?? job.pr?.title ?? 'Agent changes',
|
|
933
|
+
webToolsGuidance: job.webToolsGuidance,
|
|
934
|
+
webSearchProxy: job.webSearch,
|
|
935
|
+
guardLimits: job.guardLimits,
|
|
936
|
+
...(job.persistentCheckout ? { persistentCheckout: true } : {}),
|
|
937
|
+
...(job.streamFollowUps ? { streamFollowUps: true } : {}),
|
|
938
|
+
...(job.referenceBranches?.length ? { referenceBranches: job.referenceBranches } : {}),
|
|
939
|
+
// Repo-sourced skill (slice 2): installed harness-aware by runAgentInWorkspace.
|
|
940
|
+
...(job.skill ? { skill: job.skill } : {}),
|
|
941
|
+
// Ralph loop: run the completion command after the agent commits and report its verdict.
|
|
942
|
+
...(job.validation
|
|
943
|
+
? {
|
|
944
|
+
validation: {
|
|
945
|
+
command: job.validation.command,
|
|
946
|
+
...(job.validation.iteration !== undefined
|
|
947
|
+
? { iteration: job.validation.iteration }
|
|
948
|
+
: {}),
|
|
949
|
+
},
|
|
950
|
+
}
|
|
951
|
+
: {}),
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
906
955
|
/**
|
|
907
956
|
* The ordinary single-repo coding flow: clone `branch` (or resume `newBranch`), run the agent,
|
|
908
957
|
* commit + push to `pushBranch`, and open `pr` when one is set and the run produced changes. A
|
|
@@ -912,47 +961,7 @@ async function runCodingMode(job: AgentJob, opts: RunOptions): Promise<AgentResu
|
|
|
912
961
|
async function runSingleRepoCoding(job: AgentJob, opts: RunOptions): Promise<AgentResult> {
|
|
913
962
|
const pushBranch = job.pushBranch ?? job.newBranch ?? job.branch
|
|
914
963
|
const { summary, stats, stderrTail, pushed, usage, callMetrics, validation, effortReport } =
|
|
915
|
-
await runCodingAgent(
|
|
916
|
-
{
|
|
917
|
-
kind: 'agent',
|
|
918
|
-
jobId: job.jobId,
|
|
919
|
-
repo: job.repo,
|
|
920
|
-
cloneBranch: job.branch,
|
|
921
|
-
...(job.newBranch ? { newBranch: job.newBranch } : {}),
|
|
922
|
-
pushBranch,
|
|
923
|
-
ghToken: job.ghToken,
|
|
924
|
-
systemPrompt: job.systemPrompt,
|
|
925
|
-
userPrompt: job.userPrompt,
|
|
926
|
-
model: job.model,
|
|
927
|
-
harness: job.harness,
|
|
928
|
-
subscriptionToken: job.subscriptionToken,
|
|
929
|
-
subscriptionBaseUrl: job.subscriptionBaseUrl,
|
|
930
|
-
ambientAuth: job.ambientAuth,
|
|
931
|
-
proxyBaseUrl: job.proxyBaseUrl,
|
|
932
|
-
sessionToken: job.sessionToken,
|
|
933
|
-
commitMessage: job.commitMessage ?? job.pr?.title ?? 'Agent changes',
|
|
934
|
-
webToolsGuidance: job.webToolsGuidance,
|
|
935
|
-
webSearchProxy: job.webSearch,
|
|
936
|
-
guardLimits: job.guardLimits,
|
|
937
|
-
...(job.persistentCheckout ? { persistentCheckout: true } : {}),
|
|
938
|
-
...(job.streamFollowUps ? { streamFollowUps: true } : {}),
|
|
939
|
-
...(job.referenceBranches?.length ? { referenceBranches: job.referenceBranches } : {}),
|
|
940
|
-
// Repo-sourced skill (slice 2): installed harness-aware by runAgentInWorkspace.
|
|
941
|
-
...(job.skill ? { skill: job.skill } : {}),
|
|
942
|
-
// Ralph loop: run the completion command after the agent commits and report its verdict.
|
|
943
|
-
...(job.validation
|
|
944
|
-
? {
|
|
945
|
-
validation: {
|
|
946
|
-
command: job.validation.command,
|
|
947
|
-
...(job.validation.iteration !== undefined
|
|
948
|
-
? { iteration: job.validation.iteration }
|
|
949
|
-
: {}),
|
|
950
|
-
},
|
|
951
|
-
}
|
|
952
|
-
: {}),
|
|
953
|
-
},
|
|
954
|
-
opts,
|
|
955
|
-
)
|
|
964
|
+
await runCodingAgent(buildSingleRepoCodingSpec(job, pushBranch), opts)
|
|
956
965
|
// Ralph loop: the harness-computed validation verdict, forwarded onto the coding result as
|
|
957
966
|
// `ralphVerdict` so the backend's `toRunResult` lifts it onto `AgentRunResult.ralphVerdict`.
|
|
958
967
|
const ralphVerdict = validation ? { ralphVerdict: validation } : {}
|
package/src/job.ts
CHANGED
|
@@ -1124,6 +1124,24 @@ function isReservedEnvName(key: string): boolean {
|
|
|
1124
1124
|
return RESERVED_ENV_PREFIXES.some((p) => lower.startsWith(p))
|
|
1125
1125
|
}
|
|
1126
1126
|
|
|
1127
|
+
/**
|
|
1128
|
+
* Collect only string→string entries from a raw `env` bag. A non-string value is dropped so a
|
|
1129
|
+
* malformed binding can't inject `[object Object]` (or undefined) as an upstream URL. Reserved
|
|
1130
|
+
* names that would break the toolchain or enable injection (PATH, NODE_OPTIONS, LD_PRELOAD, …) are
|
|
1131
|
+
* dropped too: they are spread over `process.env` at build time, so a binding named `PATH` would
|
|
1132
|
+
* replace it with a URL and the build would no longer find its tools. Extracted from the infra
|
|
1133
|
+
* parsers to keep their cyclomatic complexity down.
|
|
1134
|
+
*/
|
|
1135
|
+
function parseInfraEnv(raw: unknown): Record<string, string> {
|
|
1136
|
+
const env: Record<string, string> = {}
|
|
1137
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
1138
|
+
for (const [key, val] of Object.entries(raw as Record<string, unknown>)) {
|
|
1139
|
+
if (key && !isReservedEnvName(key) && typeof val === 'string') env[key] = val
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return env
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1127
1145
|
/** Parse the frontend UI-test infra spec (`kind: 'frontend'`), tolerating missing knobs. */
|
|
1128
1146
|
function parseFrontendInfraSpec(o: Record<string, unknown>): FrontendInfraSpec {
|
|
1129
1147
|
const packageManager =
|
|
@@ -1133,17 +1151,7 @@ function parseFrontendInfraSpec(o: Record<string, unknown>): FrontendInfraSpec {
|
|
|
1133
1151
|
const serveMode = o.serveMode === 'static' || o.serveMode === 'command' ? o.serveMode : undefined
|
|
1134
1152
|
const envInjection =
|
|
1135
1153
|
o.envInjection === 'build' || o.envInjection === 'runtime' ? o.envInjection : undefined
|
|
1136
|
-
|
|
1137
|
-
// binding can't inject `[object Object]` (or undefined) as an upstream URL. Reserved names
|
|
1138
|
-
// that would break the toolchain or enable injection (PATH, NODE_OPTIONS, LD_PRELOAD, …) are
|
|
1139
|
-
// dropped too: they are spread over `process.env` at build time, so a binding named `PATH`
|
|
1140
|
-
// would replace it with a URL and the build would no longer find its tools.
|
|
1141
|
-
const env: Record<string, string> = {}
|
|
1142
|
-
if (typeof o.env === 'object' && o.env !== null) {
|
|
1143
|
-
for (const [key, val] of Object.entries(o.env as Record<string, unknown>)) {
|
|
1144
|
-
if (key && !isReservedEnvName(key) && typeof val === 'string') env[key] = val
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1154
|
+
const env = parseInfraEnv(o.env)
|
|
1147
1155
|
const servePort = port(o.servePort)
|
|
1148
1156
|
const wiremockPort = port(o.wiremockPort)
|
|
1149
1157
|
// The app's monorepo subdirectory becomes the install/build/serve cwd, so it goes through the
|
|
@@ -1376,11 +1384,7 @@ function assembleAgentJob(
|
|
|
1376
1384
|
ghToken: str(o.ghToken, 'ghToken'),
|
|
1377
1385
|
repo: parseRepoSpec(repo),
|
|
1378
1386
|
branch: str(o.branch, 'branch'),
|
|
1379
|
-
...(
|
|
1380
|
-
...(typeof o.webToolsGuidance === 'string' ? { webToolsGuidance: o.webToolsGuidance } : {}),
|
|
1381
|
-
...(o.webSearch === true ? { webSearch: true } : {}),
|
|
1382
|
-
...(o.full === true ? { full: true } : {}),
|
|
1383
|
-
...(typeof o.mergeBase === 'string' && o.mergeBase ? { mergeBase: o.mergeBase } : {}),
|
|
1387
|
+
...collectOptionalRequestFields(o),
|
|
1384
1388
|
...(bootstrap ? { bootstrap } : {}),
|
|
1385
1389
|
...(output ? { output } : {}),
|
|
1386
1390
|
...(contextFiles.length ? { contextFiles } : {}),
|
|
@@ -1388,20 +1392,36 @@ function assembleAgentJob(
|
|
|
1388
1392
|
...(skill ? { skill } : {}),
|
|
1389
1393
|
...(testSecrets.length ? { testSecrets } : {}),
|
|
1390
1394
|
...(infra ? { infra } : {}),
|
|
1391
|
-
...(typeof o.newBranch === 'string' && o.newBranch ? { newBranch: o.newBranch } : {}),
|
|
1392
|
-
...(typeof o.pushBranch === 'string' && o.pushBranch ? { pushBranch: o.pushBranch } : {}),
|
|
1393
|
-
...(typeof o.commitMessage === 'string' && o.commitMessage
|
|
1394
|
-
? { commitMessage: o.commitMessage }
|
|
1395
|
-
: {}),
|
|
1396
1395
|
...(pr ? { pr } : {}),
|
|
1397
1396
|
...(peerRepos.length ? { peerRepos } : {}),
|
|
1398
1397
|
...(referenceRepos.length ? { referenceRepos } : {}),
|
|
1399
1398
|
...(referenceBranches.length ? { referenceBranches } : {}),
|
|
1400
1399
|
...(reviewPrNumber !== undefined ? { reviewPrNumber } : {}),
|
|
1400
|
+
...(guardLimits ? { guardLimits } : {}),
|
|
1401
|
+
...(validation ? { validation } : {}),
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* The optional {@link AgentJob} fields read directly off the request `o` (booleans + trimmed
|
|
1407
|
+
* strings). Extracted from {@link assembleAgentJob} to keep its cyclomatic complexity down; every
|
|
1408
|
+
* key is unique so grouping the conditional spreads is behaviour-neutral (spread order is
|
|
1409
|
+
* irrelevant with no colliding keys).
|
|
1410
|
+
*/
|
|
1411
|
+
function collectOptionalRequestFields(o: Record<string, unknown>): Partial<AgentJob> {
|
|
1412
|
+
return {
|
|
1413
|
+
...(typeof o.githubApiBase === 'string' ? { githubApiBase: o.githubApiBase } : {}),
|
|
1414
|
+
...(typeof o.webToolsGuidance === 'string' ? { webToolsGuidance: o.webToolsGuidance } : {}),
|
|
1415
|
+
...(o.webSearch === true ? { webSearch: true } : {}),
|
|
1416
|
+
...(o.full === true ? { full: true } : {}),
|
|
1417
|
+
...(typeof o.mergeBase === 'string' && o.mergeBase ? { mergeBase: o.mergeBase } : {}),
|
|
1418
|
+
...(typeof o.newBranch === 'string' && o.newBranch ? { newBranch: o.newBranch } : {}),
|
|
1419
|
+
...(typeof o.pushBranch === 'string' && o.pushBranch ? { pushBranch: o.pushBranch } : {}),
|
|
1420
|
+
...(typeof o.commitMessage === 'string' && o.commitMessage
|
|
1421
|
+
? { commitMessage: o.commitMessage }
|
|
1422
|
+
: {}),
|
|
1401
1423
|
...(o.noChangesIsError === false ? { noChangesIsError: false } : {}),
|
|
1402
1424
|
...(o.persistentCheckout === true ? { persistentCheckout: true } : {}),
|
|
1403
1425
|
...(o.streamFollowUps === true ? { streamFollowUps: true } : {}),
|
|
1404
|
-
...(guardLimits ? { guardLimits } : {}),
|
|
1405
|
-
...(validation ? { validation } : {}),
|
|
1406
1426
|
}
|
|
1407
1427
|
}
|