@cat-factory/executor-harness 1.78.0 → 1.82.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.
Files changed (55) hide show
  1. package/README.md +1 -0
  2. package/dist/agent-capabilities.d.ts +130 -0
  3. package/dist/agent-runner.d.ts +114 -0
  4. package/dist/agent-runner.js +15 -1
  5. package/dist/agent-shared.d.ts +18 -0
  6. package/dist/agent.d.ts +66 -0
  7. package/dist/bootstrap-mode.d.ts +20 -0
  8. package/dist/captured-command.d.ts +58 -0
  9. package/dist/claude-call-aggregator.d.ts +164 -0
  10. package/dist/claude-call-aggregator.js +123 -17
  11. package/dist/claude-stream.d.ts +56 -0
  12. package/dist/claude-stream.js +23 -0
  13. package/dist/coding-agent.d.ts +263 -0
  14. package/dist/dependency-install.d.ts +111 -0
  15. package/dist/effort.d.ts +19 -0
  16. package/dist/embed.d.ts +4 -0
  17. package/dist/failure.d.ts +42 -0
  18. package/dist/follow-ups.d.ts +28 -0
  19. package/dist/frontend-infra.d.ts +25 -0
  20. package/dist/fs-utils.d.ts +2 -0
  21. package/dist/git.d.ts +394 -0
  22. package/dist/host-markdown.d.ts +28 -0
  23. package/dist/inline.d.ts +10 -0
  24. package/dist/job.d.ts +666 -0
  25. package/dist/logger.d.ts +16 -0
  26. package/dist/onboarding-preseed.d.ts +24 -0
  27. package/dist/package-registries.d.ts +32 -0
  28. package/dist/pi-workspace.d.ts +194 -0
  29. package/dist/pi-workspace.js +4 -0
  30. package/dist/pi.d.ts +475 -0
  31. package/dist/pr-description.d.ts +85 -0
  32. package/dist/pr-template.d.ts +101 -0
  33. package/dist/process-exit.d.ts +7 -0
  34. package/dist/process.d.ts +19 -0
  35. package/dist/progress-guard.d.ts +88 -0
  36. package/dist/progress.d.ts +87 -0
  37. package/dist/redact.d.ts +31 -0
  38. package/dist/reproduction-proof.d.ts +224 -0
  39. package/dist/runner.d.ts +282 -0
  40. package/dist/runner.js +3 -0
  41. package/dist/server.d.ts +3 -0
  42. package/dist/structured-output.d.ts +75 -0
  43. package/dist/subagents.d.ts +88 -0
  44. package/dist/subagents.js +74 -4
  45. package/dist/transcript-retention.d.ts +21 -0
  46. package/dist/validation-checks.d.ts +159 -0
  47. package/dist/vcs-api.d.ts +73 -0
  48. package/dist/version.d.ts +2 -0
  49. package/package.json +9 -5
  50. package/src/agent-runner.ts +21 -2
  51. package/src/claude-call-aggregator.ts +181 -32
  52. package/src/claude-stream.ts +21 -0
  53. package/src/pi-workspace.ts +4 -0
  54. package/src/runner.ts +24 -0
  55. package/src/subagents.ts +57 -3
package/src/runner.ts CHANGED
@@ -2,6 +2,7 @@ import { redactSecrets } from './redact.js'
2
2
  import type { FollowUpLine } from './follow-ups.js'
3
3
  import type { ValidationReport } from './validation-checks.js'
4
4
  import type { ReproductionReport } from './reproduction-proof.js'
5
+ import type { SliceReview } from './subagents.js'
5
6
  import type { HarnessCallMetric, TodoProgress, ToolSpan } from './pi.js'
6
7
  import { log, type Logger } from './logger.js'
7
8
  import {
@@ -47,6 +48,14 @@ export interface RunOptions {
47
48
  * attempt is final, and the loop republishes a whole new one — with a fresh `at` — per round.
48
49
  */
49
50
  onReproductionProof?: (report: ReproductionReport) => void
51
+ /**
52
+ * Receives the full set of per-slice reviews a parallel review has captured, republished each
53
+ * time a slice's subagent returns. Latest-wins (NOT a drain buffer), for the same reason as
54
+ * {@link onValidationReport} but with more at stake: these carry the slices' actual review work,
55
+ * and a review whose aggregation never finishes is recoverable ONLY from what the backend
56
+ * already persisted. Absent for a job that dispatched no subagents.
57
+ */
58
+ onSliceReviews?: (reviews: SliceReview[]) => void
50
59
  /**
51
60
  * Receives each per-call telemetry row the moment the agent's CLI stream yields it, so a
52
61
  * run's model calls reach `llm_call_metrics` WHILE it runs rather than only in its terminal
@@ -203,6 +212,18 @@ export interface JobView<TResult extends JobResultBase = JobResultBase> {
203
212
  * that carried no reproduction declaration.
204
213
  */
205
214
  reproductionReport?: ReproductionReport
215
+ /**
216
+ * The per-slice reviews captured so far on a parallel (subagent-fanned) review — each slice's
217
+ * label, whether its subagent returned, and its verbatim report. A whole-value latest publish
218
+ * like {@link validationReport}, not drain-on-read.
219
+ *
220
+ * This is the durable half of a PR review. The reviewer returns `slices`/`findings` only in its
221
+ * TERMINAL structured output, so before this existed a review killed mid-run (or one whose
222
+ * aggregation pass wedged) lost every finished slice and could only be re-run from zero. The
223
+ * backend persists these onto the step as they arrive, which is what a manual resume re-aggregates
224
+ * from. Absent for a job that dispatched no subagents.
225
+ */
226
+ sliceReviews?: SliceReview[]
206
227
  }
207
228
 
208
229
  interface JobEntry<TResult extends JobResultBase> extends JobView<TResult> {
@@ -473,6 +494,9 @@ export class JobRegistry<TJob = unknown, TResult extends JobResultBase = JobResu
473
494
  onValidationReport: (report) => {
474
495
  entry.validationReport = report
475
496
  },
497
+ onSliceReviews: (reviews) => {
498
+ entry.sliceReviews = reviews
499
+ },
476
500
  onReproductionProof: (report) => {
477
501
  entry.reproductionReport = report
478
502
  },
package/src/subagents.ts CHANGED
@@ -4,6 +4,7 @@ import { basename, join } from 'node:path'
4
4
  import {
5
5
  claudeAssistantContent,
6
6
  claudeCallUsage,
7
+ claudeToolResultText,
7
8
  isObject,
8
9
  redactBody,
9
10
  SUBAGENT_TOOL_NAMES,
@@ -54,20 +55,52 @@ import { publishCallMetric, type HarnessCallMetric, type TodoProgress } from './
54
55
  // Slice / progress tracking off the PARENT stream (D2.1)
55
56
  // ---------------------------------------------------------------------------
56
57
 
58
+ /**
59
+ * How much of one slice's terminal report is kept. A slice review is prose (findings for a handful
60
+ * of files), not a transcript, so this is far above a real report while still bounding what a
61
+ * runaway subagent can push onto the step — the reports ride the job view on every poll and are
62
+ * persisted on the run.
63
+ */
64
+ export const SLICE_REPORT_MAX_CHARS = 24_000
65
+
57
66
  interface TrackedSlice {
58
67
  /** The dispatch's tool_use id, used to pair the terminal tool_result. */
59
68
  toolUseId: string
60
69
  /** The subagent's description (`Review <slice> slice`), rendered as the progress label. */
61
70
  description: string
62
71
  done: boolean
72
+ /**
73
+ * The subagent's verbatim terminal report, captured from the paired `tool_result`; undefined
74
+ * until it lands. This is the slice's actual review work — the reason a stuck aggregation no
75
+ * longer costs the whole run (see `prReviewSliceReviewSchema`). Truncated to
76
+ * {@link SLICE_REPORT_MAX_CHARS} and scrubbed of leased credentials before it leaves here.
77
+ */
78
+ report?: string
79
+ }
80
+
81
+ /** One slice's live review, as published on the job view. Mirrors `prReviewSliceReviewSchema`. */
82
+ export interface SliceReview {
83
+ label: string
84
+ status: 'in_progress' | 'completed'
85
+ report?: string | null
63
86
  }
64
87
 
65
88
  /** Tracks parallel subagents seen on the parent stream to derive slice progress. */
66
89
  export interface SliceTracker {
67
90
  /** Feed an `assistant` message's content blocks: registers any subagent dispatches. */
68
91
  onAssistant(content: unknown[]): void
69
- /** Feed a `user` message's content blocks: marks the paired subagent(s) complete. */
92
+ /**
93
+ * Feed a `user` message's content blocks: marks the paired subagent(s) complete AND captures
94
+ * each one's terminal report (see {@link SliceTracker.sliceReviews}).
95
+ */
70
96
  onUser(content: unknown[]): void
97
+ /**
98
+ * Every dispatched slice with its status and captured report, in dispatch order — the durable
99
+ * half of this tracker. Published as a whole value on each poll (NOT drain-on-read): the set
100
+ * only grows, and a dropped poll response must never permanently lose a finished slice's
101
+ * review, which is the entire point of capturing it. Empty when nothing was dispatched.
102
+ */
103
+ sliceReviews(): SliceReview[]
71
104
  /** Whether any `Task` subagent has been dispatched (⇒ this run parallelised). */
72
105
  hasSlices(): boolean
73
106
  /**
@@ -80,7 +113,12 @@ export interface SliceTracker {
80
113
  progress(): TodoProgress | undefined
81
114
  }
82
115
 
83
- export function createSliceTracker(): SliceTracker {
116
+ /**
117
+ * @param secrets Leased-credential strings scrubbed from every captured report. A subagent can
118
+ * echo a token it saw in the checkout, and these reports are persisted on the run, so they are
119
+ * redacted on the way in rather than trusting each consumer to do it.
120
+ */
121
+ export function createSliceTracker(secrets: string[] = []): SliceTracker {
84
122
  // Insertion-ordered so the progress `items` render in dispatch order.
85
123
  const slices = new Map<string, TrackedSlice>()
86
124
 
@@ -106,9 +144,25 @@ export function createSliceTracker(): SliceTracker {
106
144
  if (!isObject(block) || block.type !== 'tool_result') continue
107
145
  const id = typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined
108
146
  const slice = id ? slices.get(id) : undefined
109
- if (slice) slice.done = true
147
+ if (!slice) continue
148
+ slice.done = true
149
+ // The report is captured here or nowhere: this `tool_result` is the only place the
150
+ // subagent's findings appear on the parent stream, and the next poll may be the last one
151
+ // this job ever answers.
152
+ const report = redactBody(claudeToolResultText(block), secrets).trim()
153
+ if (report) slice.report = report.slice(0, SLICE_REPORT_MAX_CHARS)
110
154
  }
111
155
  },
156
+ sliceReviews() {
157
+ return [...slices.values()].map((s) => ({
158
+ label: s.description,
159
+ status: (s.done ? 'completed' : 'in_progress') as 'completed' | 'in_progress',
160
+ // A slice that finished but whose result carried no readable text is reported as
161
+ // completed with a null report rather than being dropped: a resume must still know it
162
+ // does not need re-reviewing, and silently omitting it would send it round again.
163
+ report: s.report ?? null,
164
+ }))
165
+ },
112
166
  hasSlices() {
113
167
  return slices.size > 0
114
168
  },