@cat-factory/executor-harness 1.50.14 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/executor-harness",
3
- "version": "1.50.14",
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.141.3",
30
- "@cat-factory/spend": "0.12.73"
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",
@@ -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
- // Anthropic itself authenticates with the subscription OAuth token; a
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
- // The parent's cumulative-usage fallback applies to the PARENT calls only (before the
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
@@ -31,6 +31,7 @@ import {
31
31
  unmergedPaths,
32
32
  } from './git.js'
33
33
  import type { PiRunStats, RunDiagnostics } from './pi.js'
34
+ import type { EffortReport } from './effort.js'
34
35
  import {
35
36
  makeDirClaimer,
36
37
  noChangesReason,
@@ -312,6 +313,15 @@ async function cloneServiceCheckout(
312
313
  return deriveWorkDir(dir, job.repo.serviceDirectory)
313
314
  }
314
315
 
316
+ /**
317
+ * Fold an agent's effort self-assessment (lifted from its sentinel file by `runAgentInWorkspace`)
318
+ * onto its final result. Every container mode routes its result through this so the report reaches
319
+ * the backend uniformly. A run that wrote no report passes through unchanged.
320
+ */
321
+ function mergeEffort(result: AgentResult, effortReport: EffortReport | undefined): AgentResult {
322
+ return effortReport ? { ...result, effortReport } : result
323
+ }
324
+
315
325
  /** Run one generic agent job end to end, dispatching on `mode`. */
316
326
  export async function handleAgent(job: AgentJob, opts: RunOptions = {}): Promise<AgentResult> {
317
327
  // Private-registry auth first, before any mode runs: every mode with a checkout may
@@ -558,6 +568,7 @@ async function runExploreMode(job: AgentJob, opts: RunOptions): Promise<AgentRes
558
568
  usage,
559
569
  callMetrics,
560
570
  diagnostics: runDiag,
571
+ effortReport,
561
572
  } = await runAgentInWorkspace(
562
573
  {
563
574
  dir: workDir,
@@ -582,10 +593,13 @@ async function runExploreMode(job: AgentJob, opts: RunOptions): Promise<AgentRes
582
593
  opts,
583
594
  )
584
595
 
585
- return await finalizeExploreResult(
586
- job,
587
- { summary, stats, stderrTail, usage, callMetrics, runDiag },
588
- { infra, infraSetupFields, logger, signal: opts.signal },
596
+ return mergeEffort(
597
+ await finalizeExploreResult(
598
+ job,
599
+ { summary, stats, stderrTail, usage, callMetrics, runDiag },
600
+ { infra, infraSetupFields, logger, signal: opts.signal },
601
+ ),
602
+ effortReport,
589
603
  )
590
604
  } finally {
591
605
  restoreSecrets()
@@ -808,17 +822,20 @@ async function runMultiRepoExplore(job: AgentJob, opts: RunOptions): Promise<Age
808
822
  },
809
823
  opts,
810
824
  )
811
- return finalizeExploreResult(
812
- job,
813
- {
814
- summary: run.summary,
815
- stats: run.stats,
816
- stderrTail: run.stderrTail,
817
- usage: run.usage,
818
- callMetrics: run.callMetrics,
819
- runDiag: run.diagnostics,
820
- },
821
- { infraSetupFields: {}, logger, signal: opts.signal },
825
+ return mergeEffort(
826
+ await finalizeExploreResult(
827
+ job,
828
+ {
829
+ summary: run.summary,
830
+ stats: run.stats,
831
+ stderrTail: run.stderrTail,
832
+ usage: run.usage,
833
+ callMetrics: run.callMetrics,
834
+ runDiag: run.diagnostics,
835
+ },
836
+ { infraSetupFields: {}, logger, signal: opts.signal },
837
+ ),
838
+ run.effortReport,
822
839
  )
823
840
  })
824
841
  }
@@ -886,6 +903,55 @@ async function runCodingMode(job: AgentJob, opts: RunOptions): Promise<AgentResu
886
903
  return result
887
904
  }
888
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
+
889
955
  /**
890
956
  * The ordinary single-repo coding flow: clone `branch` (or resume `newBranch`), run the agent,
891
957
  * commit + push to `pushBranch`, and open `pr` when one is set and the run produced changes. A
@@ -894,51 +960,13 @@ async function runCodingMode(job: AgentJob, opts: RunOptions): Promise<AgentResu
894
960
  */
895
961
  async function runSingleRepoCoding(job: AgentJob, opts: RunOptions): Promise<AgentResult> {
896
962
  const pushBranch = job.pushBranch ?? job.newBranch ?? job.branch
897
- const { summary, stats, stderrTail, pushed, usage, callMetrics, validation } =
898
- await runCodingAgent(
899
- {
900
- kind: 'agent',
901
- jobId: job.jobId,
902
- repo: job.repo,
903
- cloneBranch: job.branch,
904
- ...(job.newBranch ? { newBranch: job.newBranch } : {}),
905
- pushBranch,
906
- ghToken: job.ghToken,
907
- systemPrompt: job.systemPrompt,
908
- userPrompt: job.userPrompt,
909
- model: job.model,
910
- harness: job.harness,
911
- subscriptionToken: job.subscriptionToken,
912
- subscriptionBaseUrl: job.subscriptionBaseUrl,
913
- ambientAuth: job.ambientAuth,
914
- proxyBaseUrl: job.proxyBaseUrl,
915
- sessionToken: job.sessionToken,
916
- commitMessage: job.commitMessage ?? job.pr?.title ?? 'Agent changes',
917
- webToolsGuidance: job.webToolsGuidance,
918
- webSearchProxy: job.webSearch,
919
- guardLimits: job.guardLimits,
920
- ...(job.persistentCheckout ? { persistentCheckout: true } : {}),
921
- ...(job.streamFollowUps ? { streamFollowUps: true } : {}),
922
- ...(job.referenceBranches?.length ? { referenceBranches: job.referenceBranches } : {}),
923
- // Repo-sourced skill (slice 2): installed harness-aware by runAgentInWorkspace.
924
- ...(job.skill ? { skill: job.skill } : {}),
925
- // Ralph loop: run the completion command after the agent commits and report its verdict.
926
- ...(job.validation
927
- ? {
928
- validation: {
929
- command: job.validation.command,
930
- ...(job.validation.iteration !== undefined
931
- ? { iteration: job.validation.iteration }
932
- : {}),
933
- },
934
- }
935
- : {}),
936
- },
937
- opts,
938
- )
963
+ const { summary, stats, stderrTail, pushed, usage, callMetrics, validation, effortReport } =
964
+ await runCodingAgent(buildSingleRepoCodingSpec(job, pushBranch), opts)
939
965
  // Ralph loop: the harness-computed validation verdict, forwarded onto the coding result as
940
966
  // `ralphVerdict` so the backend's `toRunResult` lifts it onto `AgentRunResult.ralphVerdict`.
941
967
  const ralphVerdict = validation ? { ralphVerdict: validation } : {}
968
+ // The agent's effort self-assessment, spread onto every result path below (mirrors ralphVerdict).
969
+ const effort = effortReport ? { effortReport } : {}
942
970
 
943
971
  if (!pushed) {
944
972
  // A no-op: a failure for the implementer, a clean non-event for the fixers.
@@ -951,6 +979,7 @@ async function runSingleRepoCoding(job: AgentJob, opts: RunOptions): Promise<Age
951
979
  ...(usage ? { usage } : {}),
952
980
  ...(callMetrics ? { callMetrics } : {}),
953
981
  ...ralphVerdict,
982
+ ...effort,
954
983
  }
955
984
  }
956
985
  return {
@@ -962,6 +991,7 @@ async function runSingleRepoCoding(job: AgentJob, opts: RunOptions): Promise<Age
962
991
  failureCause: 'no-changes',
963
992
  ...(usage ? { usage } : {}),
964
993
  ...(callMetrics ? { callMetrics } : {}),
994
+ ...effort,
965
995
  }
966
996
  }
967
997
 
@@ -995,6 +1025,7 @@ async function runSingleRepoCoding(job: AgentJob, opts: RunOptions): Promise<Age
995
1025
  stats,
996
1026
  ...(usage ? { usage } : {}),
997
1027
  ...(callMetrics ? { callMetrics } : {}),
1028
+ ...effort,
998
1029
  }
999
1030
  }
1000
1031
  return {
@@ -1010,6 +1041,7 @@ async function runSingleRepoCoding(job: AgentJob, opts: RunOptions): Promise<Age
1010
1041
  failureCause: 'no-changes',
1011
1042
  ...(usage ? { usage } : {}),
1012
1043
  ...(callMetrics ? { callMetrics } : {}),
1044
+ ...effort,
1013
1045
  }
1014
1046
  }
1015
1047
  return {
@@ -1021,6 +1053,7 @@ async function runSingleRepoCoding(job: AgentJob, opts: RunOptions): Promise<Age
1021
1053
  ...(usage ? { usage } : {}),
1022
1054
  ...(callMetrics ? { callMetrics } : {}),
1023
1055
  ...ralphVerdict,
1056
+ ...effort,
1024
1057
  }
1025
1058
  }
1026
1059
  return {
@@ -1031,6 +1064,7 @@ async function runSingleRepoCoding(job: AgentJob, opts: RunOptions): Promise<Age
1031
1064
  ...(usage ? { usage } : {}),
1032
1065
  ...(callMetrics ? { callMetrics } : {}),
1033
1066
  ...ralphVerdict,
1067
+ ...effort,
1034
1068
  }
1035
1069
  }
1036
1070
 
@@ -1100,23 +1134,24 @@ async function runConflictResolution(job: AgentJob, opts: RunOptions): Promise<A
1100
1134
  const diff = await conflictDiff(dir, conflicted, signal)
1101
1135
  const userPrompt = buildConflictPrompt(mergeBase, job.branch, conflicted, diff, job.userPrompt)
1102
1136
 
1103
- const { summary, stats, stderrTail, usage, callMetrics } = await runAgentInWorkspace(
1104
- {
1105
- dir,
1106
- systemPrompt: job.systemPrompt,
1107
- userPrompt,
1108
- model: job.model,
1109
- harness: job.harness,
1110
- subscriptionToken: job.subscriptionToken,
1111
- subscriptionBaseUrl: job.subscriptionBaseUrl,
1112
- ambientAuth: job.ambientAuth,
1113
- proxyBaseUrl: job.proxyBaseUrl,
1114
- sessionToken: job.sessionToken,
1115
- contextFiles: job.contextFiles,
1116
- guardLimits: job.guardLimits,
1117
- },
1118
- opts,
1119
- )
1137
+ const { summary, stats, stderrTail, usage, callMetrics, effortReport } =
1138
+ await runAgentInWorkspace(
1139
+ {
1140
+ dir,
1141
+ systemPrompt: job.systemPrompt,
1142
+ userPrompt,
1143
+ model: job.model,
1144
+ harness: job.harness,
1145
+ subscriptionToken: job.subscriptionToken,
1146
+ subscriptionBaseUrl: job.subscriptionBaseUrl,
1147
+ ambientAuth: job.ambientAuth,
1148
+ proxyBaseUrl: job.proxyBaseUrl,
1149
+ sessionToken: job.sessionToken,
1150
+ contextFiles: job.contextFiles,
1151
+ guardLimits: job.guardLimits,
1152
+ },
1153
+ opts,
1154
+ )
1120
1155
 
1121
1156
  // Never push a half-resolved tree: if any conflict markers / unmerged paths remain,
1122
1157
  // the PR would still be broken. Fail so the engine can retry / notify.
@@ -1125,30 +1160,36 @@ async function runConflictResolution(job: AgentJob, opts: RunOptions): Promise<A
1125
1160
  logger.error('agent(conflict): unresolved conflicts remain, refusing to push', {
1126
1161
  unresolved: unresolved.length,
1127
1162
  })
1128
- return {
1129
- pushed: false,
1130
- branch: job.branch,
1131
- summary,
1132
- stats,
1133
- error: unresolvedReason(unresolved, stats, stderrTail),
1134
- failureCause: 'agent',
1135
- ...(usage ? { usage } : {}),
1136
- ...(callMetrics ? { callMetrics } : {}),
1137
- }
1163
+ return mergeEffort(
1164
+ {
1165
+ pushed: false,
1166
+ branch: job.branch,
1167
+ summary,
1168
+ stats,
1169
+ error: unresolvedReason(unresolved, stats, stderrTail),
1170
+ failureCause: 'agent',
1171
+ ...(usage ? { usage } : {}),
1172
+ ...(callMetrics ? { callMetrics } : {}),
1173
+ },
1174
+ effortReport,
1175
+ )
1138
1176
  }
1139
1177
  // Complete the merge commit with the agent's resolution staged, then push.
1140
1178
  await commitAll(dir, `Merge ${mergeBase} into ${job.branch}`, signal)
1141
1179
  opts.onPhase?.('push')
1142
1180
  logger.info('agent(conflict): pushing resolved branch', { ...stats })
1143
1181
  await pushBranch(dir, job.branch, job.ghToken, signal)
1144
- return {
1145
- pushed: true,
1146
- branch: job.branch,
1147
- summary,
1148
- stats,
1149
- ...(usage ? { usage } : {}),
1150
- ...(callMetrics ? { callMetrics } : {}),
1151
- }
1182
+ return mergeEffort(
1183
+ {
1184
+ pushed: true,
1185
+ branch: job.branch,
1186
+ summary,
1187
+ stats,
1188
+ ...(usage ? { usage } : {}),
1189
+ ...(callMetrics ? { callMetrics } : {}),
1190
+ },
1191
+ effortReport,
1192
+ )
1152
1193
  })
1153
1194
  }
1154
1195
 
@@ -1239,22 +1280,23 @@ async function runBootstrap(job: AgentJob, opts: RunOptions): Promise<AgentResul
1239
1280
 
1240
1281
  opts.onPhase?.('agent')
1241
1282
  logger.info('agent(bootstrap): running agent')
1242
- const { summary, stats, stderrTail, usage, callMetrics } = await runAgentInWorkspace(
1243
- {
1244
- dir,
1245
- systemPrompt: job.systemPrompt,
1246
- userPrompt: job.userPrompt,
1247
- model: job.model,
1248
- harness: job.harness,
1249
- subscriptionToken: job.subscriptionToken,
1250
- subscriptionBaseUrl: job.subscriptionBaseUrl,
1251
- ambientAuth: job.ambientAuth,
1252
- proxyBaseUrl: job.proxyBaseUrl,
1253
- sessionToken: job.sessionToken,
1254
- guardLimits: job.guardLimits,
1255
- },
1256
- opts,
1257
- )
1283
+ const { summary, stats, stderrTail, usage, callMetrics, effortReport } =
1284
+ await runAgentInWorkspace(
1285
+ {
1286
+ dir,
1287
+ systemPrompt: job.systemPrompt,
1288
+ userPrompt: job.userPrompt,
1289
+ model: job.model,
1290
+ harness: job.harness,
1291
+ subscriptionToken: job.subscriptionToken,
1292
+ subscriptionBaseUrl: job.subscriptionBaseUrl,
1293
+ ambientAuth: job.ambientAuth,
1294
+ proxyBaseUrl: job.proxyBaseUrl,
1295
+ sessionToken: job.sessionToken,
1296
+ guardLimits: job.guardLimits,
1297
+ },
1298
+ opts,
1299
+ )
1258
1300
 
1259
1301
  // Guard against a no-op run: Pi can exit cleanly having done nothing (e.g. it never
1260
1302
  // reached the model), and a force-push would then publish an empty tree — leaving the
@@ -1263,14 +1305,17 @@ async function runBootstrap(job: AgentJob, opts: RunOptions): Promise<AgentResul
1263
1305
  if (!(await producedRepoContent(dir, !fromScratch, signal))) {
1264
1306
  const error = bootstrapNoOpReason(!fromScratch, stats, summary, stderrTail)
1265
1307
  logger.error('agent(bootstrap): agent produced no content, refusing to push', { ...stats })
1266
- return {
1267
- summary,
1268
- stats,
1269
- error,
1270
- failureCause: 'agent',
1271
- ...(usage ? { usage } : {}),
1272
- ...(callMetrics ? { callMetrics } : {}),
1273
- }
1308
+ return mergeEffort(
1309
+ {
1310
+ summary,
1311
+ stats,
1312
+ error,
1313
+ failureCause: 'agent',
1314
+ ...(usage ? { usage } : {}),
1315
+ ...(callMetrics ? { callMetrics } : {}),
1316
+ },
1317
+ effortReport,
1318
+ )
1274
1319
  }
1275
1320
 
1276
1321
  opts.onPhase?.('push')
@@ -1286,13 +1331,16 @@ async function runBootstrap(job: AgentJob, opts: RunOptions): Promise<AgentResul
1286
1331
  : `Bootstrap from ${job.repo.owner}/${job.repo.name}`,
1287
1332
  })
1288
1333
  logger.info('agent(bootstrap): complete', { defaultBranch: boot.target.defaultBranch })
1289
- return {
1290
- defaultBranch: boot.target.defaultBranch,
1291
- summary,
1292
- stats,
1293
- ...(usage ? { usage } : {}),
1294
- ...(callMetrics ? { callMetrics } : {}),
1295
- }
1334
+ return mergeEffort(
1335
+ {
1336
+ defaultBranch: boot.target.defaultBranch,
1337
+ summary,
1338
+ stats,
1339
+ ...(usage ? { usage } : {}),
1340
+ ...(callMetrics ? { callMetrics } : {}),
1341
+ },
1342
+ effortReport,
1343
+ )
1296
1344
  })
1297
1345
  }
1298
1346