@cat-factory/executor-harness 1.94.0 → 1.98.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 (47) hide show
  1. package/README.md +16 -12
  2. package/dist/agent-capabilities.d.ts +61 -0
  3. package/dist/agent-capabilities.js +113 -0
  4. package/dist/agent-runner.d.ts +30 -2
  5. package/dist/agent-runner.js +146 -47
  6. package/dist/bootstrap-mode.js +1 -0
  7. package/dist/coding-agent.d.ts +2 -1
  8. package/dist/embed.d.ts +2 -1
  9. package/dist/embed.js +2 -1
  10. package/dist/failure.d.ts +19 -1
  11. package/dist/failure.js +40 -0
  12. package/dist/git.d.ts +6 -0
  13. package/dist/git.js +16 -9
  14. package/dist/inline.d.ts +6 -0
  15. package/dist/inline.js +6 -0
  16. package/dist/job.d.ts +2 -1
  17. package/dist/jsonl-stream.d.ts +70 -0
  18. package/dist/jsonl-stream.js +149 -0
  19. package/dist/pi-reduction.d.ts +136 -0
  20. package/dist/pi-reduction.js +303 -0
  21. package/dist/pi-workspace.d.ts +2 -1
  22. package/dist/pi-workspace.js +11 -1
  23. package/dist/pi.d.ts +8 -81
  24. package/dist/pi.js +124 -310
  25. package/dist/runner.d.ts +53 -0
  26. package/dist/runner.js +53 -3
  27. package/dist/structured-output.js +2 -1
  28. package/dist/tool-silence.d.ts +74 -0
  29. package/dist/tool-silence.js +99 -0
  30. package/package.json +4 -4
  31. package/src/agent-capabilities.ts +163 -0
  32. package/src/agent-runner.ts +185 -47
  33. package/src/agent.ts +1 -1
  34. package/src/bootstrap-mode.ts +2 -1
  35. package/src/coding-agent.ts +2 -1
  36. package/src/embed.ts +8 -5
  37. package/src/failure.ts +36 -9
  38. package/src/git.ts +17 -9
  39. package/src/inline.ts +6 -0
  40. package/src/job.ts +2 -1
  41. package/src/jsonl-stream.ts +149 -0
  42. package/src/pi-reduction.ts +359 -0
  43. package/src/pi-workspace.ts +12 -3
  44. package/src/pi.ts +144 -349
  45. package/src/runner.ts +116 -4
  46. package/src/structured-output.ts +2 -1
  47. package/src/tool-silence.ts +125 -0
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @cat-factory/executor-harness
2
2
 
3
3
  The payload that runs **inside** a per-run Cloudflare Container (or a
4
- [self-hosted runner](../../docs/runner-pool-integration.md)) to perform real
4
+ [self-hosted runner](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/runner-pool-integration.md)) to perform real
5
5
  repo work with the [Pi coding agent](https://github.com/earendil-works/pi).
6
6
 
7
7
  It is a thin TypeScript wrapper (a `node:http` server on `:8080`) that the
@@ -33,7 +33,7 @@ replayed `POST` **re-attaches** to the running job rather than starting a
33
33
  duplicate (the durable driver's retries/replays are safe). Pi's todo-tool counts
34
34
  are surfaced as `progress` while a job runs. The exact request/response shapes
35
35
  cat-factory sends are documented in
36
- [`docs/runner-pool-integration.md`](../../docs/runner-pool-integration.md).
36
+ [`docs/runner-pool-integration.md`](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/runner-pool-integration.md).
37
37
 
38
38
  `GET /jobs/{id}` is also the harness's observability channel: `spans`, `followUps`
39
39
  and `callMetrics` are **drain-on-read**; each poll returns what accumulated since
@@ -62,7 +62,7 @@ The implementation job (`POST /run`) is the canonical sequence:
62
62
  body's `proxyPhasePath` says the backend serves it, which is how a repair round's model spend
63
63
  stays distinguishable from the first pass's in telemetry; without that flag the plain path is
64
64
  used and the calls are recorded as unattributed
65
- (see [token-burn instrumentation](../../../docs/initiatives/token-burn-instrumentation.md)),
65
+ (see [token-burn instrumentation](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/token-burn-instrumentation.md)),
66
66
  3. **prepopulate dependencies**, when the job body carries `dependencyInstall`: the
67
67
  service's install command is run with `sh -c` in the checkout BEFORE the agent starts, so
68
68
  it reads real installed packages instead of inferring a library's capabilities from a
@@ -71,7 +71,7 @@ The implementation job (`POST /run`) is the canonical sequence:
71
71
  steps 6 and 7, which start a fresh agent) and the run continues either way. Whatever the
72
72
  install materialises is excluded from git first, so no later `git add -A` can sweep a
73
73
  dependency tree into the pull request (see
74
- [dependency prepopulation](../../../docs/initiatives/agent-dependency-prepopulation.md)),
74
+ [dependency prepopulation](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/agent-dependency-prepopulation.md)),
75
75
  4. **resolve the repo's pull-request template**, when this dispatch opens a PR (`src/pr-template.ts`):
76
76
  `.github/PULL_REQUEST_TEMPLATE.md` and its root/`docs/`/multi-template-directory variants, or
77
77
  GitLab's `.gitlab/merge_request_templates/`, read straight off the checkout (a symlinked template
@@ -85,11 +85,11 @@ The implementation job (`POST /run`) is the canonical sequence:
85
85
  6. **validate** the checkout, when the job body carries `validationChecks`: the service's
86
86
  configured check commands (install/lint/test/build) run with `sh -c` in the checkout, and
87
87
  while they fail and the attempt budget remains the agent is re-run with the captured output
88
- as its instruction (see [pre-PR validation](../../../docs/initiatives/pre-pr-validation.md)),
88
+ as its instruction (see [pre-PR validation](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/pre-pr-validation.md)),
89
89
  7. **prove the reproduction**, when the job body carries `reproduction`: the declared check is
90
90
  run against the pre-fix tree and the tree the PR will open from, in two freshly-created
91
91
  symmetric `git worktree` checkouts, and only red-then-green is reported as proof (see
92
- [bugfix reproduction proof](../../docs/adr/0033-bugfix-reproduction-proof.md)). Unlike
92
+ [bugfix reproduction proof](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/adr/0033-bugfix-reproduction-proof.md)). Unlike
93
93
  step 6 this NEVER gates the PR: a failed verification is fed back to the agent while budget
94
94
  remains, then recorded as `inconclusive`. It runs BEFORE step 6 so validation stays the last
95
95
  thing to touch the tree,
@@ -120,7 +120,7 @@ PR. Blueprint **commits onto a branch** (no history reset) and returns the tree.
120
120
  A job body may carry `skills[]` (procedural playbooks) and `mcpServers[]` (MCP tool servers): the
121
121
  harness MATERIALISES both and decides nothing about them; the backend has already resolved which
122
122
  apply and dropped what this harness cannot serve (see
123
- [`backend/docs/adr/0029-agent-kind-capabilities.md`](../../docs/adr/0029-agent-kind-capabilities.md)).
123
+ [`backend/docs/adr/0029-agent-kind-capabilities.md`](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/adr/0029-agent-kind-capabilities.md)).
124
124
 
125
125
  - **Skills** install natively under `CLAUDE_CONFIG_DIR/skills/<name>/` for a leased-credential
126
126
  claude-code run (the CLI discovers and invokes them), and under
@@ -210,9 +210,12 @@ Kimi / DeepSeek) and meters spend. The provider key never enters the container.
210
210
  | File | Responsibility |
211
211
  | ------------------ | ------------------------------------------------------------------------------------------------------- |
212
212
  | `src/server.ts` | HTTP entry point; routes `/health`, `/run`, `/bootstrap`, `/blueprint`, `/jobs/{id}`. |
213
- | `src/runner.ts` | `JobRegistry`: async job lifecycle, idempotent on `jobId`, progress tracking. |
213
+ | `src/runner.ts` | `JobRegistry`: async job lifecycle, idempotent on `jobId`, progress tracking, and the three per-job watchdogs (max-duration, inactivity, tool-silence). |
214
+ | `src/jsonl-stream.ts` | The BOUNDS on a child CLI's streams, shared by both runners: `JsonlLineReader` frames its JSONL stdout while refusing to buffer a runaway record, `BoundedTail` keeps a capped tail of raw output for failure quoting. Both watchdog timers and the poll endpoints share one event loop with this parsing, so an unbounded buffer here is how a container stops answering polls with no watchdog having fired. |
214
215
  | `src/job.ts` | Request types + validators for the job specs. |
215
216
  | `src/pi.ts` | Pi provider config, non-interactive run, JSON-line event + todo-progress parsing, global `AGENTS.md` guidance. |
217
+ | `src/pi-reduction.ts` | Reducing a Pi event stream to what the run PRODUCED (summary, stats, diagnostics, terminal failure), FOLDED as records stream rather than over a retained array — memory is O(largest record), not O(records). The array-taking entry points offline tooling uses are defined in terms of the same reducer. |
218
+ | `src/tool-silence.ts` | The tool-silence watchdog (F13) and the `ToolProgressWindow` an agent stream opens, beats and closes. Separate from the phase marker on purpose: a window is only meaningful while something able to reset it is running. |
216
219
  | `src/git.ts` | clone / branch / commit / push + GitHub PR creation; bootstrap history reset + force-push. |
217
220
  | `src/bootstrap.ts` | The `/bootstrap` handler (clone-or-empty → adapt → reinit + force-push). |
218
221
  | `src/blueprint.ts` | The `/blueprint` handler (decompose → render `blueprints/` → commit on branch). |
@@ -240,6 +243,7 @@ runner):
240
243
  | `PORT` | `8080` | HTTP port the harness listens on. |
241
244
  | `JOB_MAX_DURATION_MS` | `3600000` (60m) | Hard ceiling on a job's wall-clock time; force-fails after. |
242
245
  | `JOB_INACTIVITY_MS` | `600000` (10m) | Kills a hung agent that produces no output for this long. |
246
+ | `JOB_TOOL_SILENCE_MS` | half `JOB_MAX_DURATION_MS` (30m at its default) | Kills an agent that keeps producing output but completes no tool call for this long: the "chatty hang" neither watchdog above can see, since streamed output resets the inactivity timer on every chunk while nothing gets done (stuck-run audit F13). Armed ONLY while an agent CLI that reports completed tool calls is running (each runner opens its own window and closes it on exit), so clone / dependency install / push / a validation loop's check commands sit outside it — they are activity-silent by nature and bounded by their own per-command timeouts — and each repair pass opens a fresh window. Derived from the job ceiling rather than fixed. It fires only when output arrived during the window that elapsed, which is what leaves a genuinely quiet run to `JOB_INACTIVITY_MS` and its clearer diagnostic. `0` disables it. |
243
247
  | `JOB_MAX_CONSECUTIVE_MCP_CALLS` | `40` | Consecutive tool-server (`mcp__*`) calls with no other tool call between before the run counts as a lookup loop. The counter-bound the no-edit exemption above owes; a per-kind `tuning.guardLimits` entry can only RAISE it. |
244
248
  | `JOB_MAX_CONSECUTIVE_NON_ACTION_CALLS` | `200` | Consecutive calls of ANY no-edit-exempt family (reads, searches, web, tool servers, subagent dispatches) with no action call between them. The backstop above the per-family caps, since each of those resets on a call outside its own family; sized as a backstop rather than a research judgement, and reset by any `bash`/edit. |
245
249
  | `JOB_COLD_START_MS` | `120000` (2m) | First-output window (ADR 0026 D4). A job that has produced nothing this long records a cold-start diagnostic (a likely onboarding/auth wedge) WITHOUT being killed: logged, exposed on `GET /jobs/{id}`, and folded into the failure `detail` if the job goes on to fail. `0` disables it. |
@@ -279,7 +283,7 @@ docker.io/<org>/cat-factory-executor:<version>
279
283
  Each is tagged with the package `version`, the commit `sha-…`, and `latest`.
280
284
 
281
285
  **CI** does this automatically:
282
- [`.github/workflows/docker-publish.yml`](../../../.github/workflows/docker-publish.yml)
286
+ [`.github/workflows/docker-publish.yml`](https://github.com/kibertoad/cat-factory/blob/main/.github/workflows/docker-publish.yml)
283
287
  republishes on every push to `main` that touches image content (`src/**`,
284
288
  `Dockerfile`, `tsconfig.json`, `package.json`). Docker Hub is gated on the
285
289
  `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` repo secrets; without them it publishes
@@ -303,9 +307,9 @@ via env vars (`REGISTRIES`, `GHCR_OWNER`, `DOCKERHUB_ORG`, `TAG`, `PUSH_LATEST`,
303
307
 
304
308
  A backend deployment references the image from `wrangler.toml`
305
309
  (`[[containers]] image = "ghcr.io/<owner>/cat-factory-executor:<version>"`: see
306
- [`deploy/backend`](../../../deploy/backend)); a self-hosted runner pool pulls the
307
- same image (see [`docs/runner-pool-integration.md`](../../docs/runner-pool-integration.md)).
310
+ [`deploy/backend`](https://github.com/kibertoad/cat-factory/tree/main/deploy/backend)); a self-hosted runner pool pulls the
311
+ same image (see [`docs/runner-pool-integration.md`](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/runner-pool-integration.md)).
308
312
  The worker library's own test/dev `wrangler.toml` still references this
309
313
  `Dockerfile` by local path so the acceptance suite can build it. Because the
310
314
  version is the image tag, **bump this package via a changeset whenever you change
311
- image content** (see [`CONTRIBUTING.md`](../../../CONTRIBUTING.md)).
315
+ image content** (see [`CONTRIBUTING.md`](https://github.com/kibertoad/cat-factory/blob/main/CONTRIBUTING.md)).
@@ -42,6 +42,67 @@ export interface McpServerSpec {
42
42
  */
43
43
  secretKeys?: string[];
44
44
  }
45
+ /**
46
+ * What the agent's CLI reported about ONE wired tool server when it started up.
47
+ *
48
+ * This is the OBSERVED half of the run's tool-server record, and it answers a question the
49
+ * backend's own half structurally cannot: the dispatch record says why the platform WITHHELD a
50
+ * tool, while this says a server the platform wired failed to start anyway. A vendor endpoint
51
+ * that 500s, an `npx` package that no longer resolves, a credential the vendor has since revoked
52
+ * — every one of those leaves the prompt promising a tool the agent then cannot call, and before
53
+ * this the only evidence was the agent saying so in prose, if it noticed at all.
54
+ *
55
+ * OBSERVED, never decided: nothing here changes what the run does. The harness reports what the
56
+ * CLI said and the backend records it beside what it decided; no code path branches on it.
57
+ */
58
+ export interface ObservedMcpServer {
59
+ /** The server id the CLI named — the same id the backend declared (`--strict-mcp-config`). */
60
+ id: string;
61
+ status: ObservedMcpStatus;
62
+ /**
63
+ * How many of the CLI's exposed tools belong to this server (`mcp__<id>__…`).
64
+ *
65
+ * ABSENT and `0` are different facts and both are worth having: absent means this image counted
66
+ * nothing for the server, while `0` means it counted and the server contributed none: a server
67
+ * that connected and exposes nothing, which reads to the agent exactly like a server that was
68
+ * never wired. Never defaulted to 0.
69
+ *
70
+ * Two things leave it absent, and neither is "the server has no tools": the CLI listed no tools
71
+ * at all, or the tool namespace cannot say which of two declared servers a name belongs to (see
72
+ * {@link tallyToolsByServer}).
73
+ */
74
+ toolCount?: number;
75
+ }
76
+ /**
77
+ * The status vocabulary of {@link ObservedMcpServer}, normalised from the CLI's own word.
78
+ *
79
+ * A CLOSED list mapped from an OPEN one, which is why `unknown` is a member rather than a reason
80
+ * to drop the row. The CLI's status strings are a third party's vocabulary and it may add to them;
81
+ * a server whose status this image cannot name is still a server the CLI knows about, and the
82
+ * honest report is "it was there, this image could not read its state" rather than silence
83
+ * (which reads as a server the CLI never mentioned) or a guess at `ready` (which would report a
84
+ * dead tool as a live one, the precise failure the whole unavailability vocabulary exists to
85
+ * prevent).
86
+ *
87
+ * `unknown` covers two causes that share one remedy, which is why they share one member: a word
88
+ * this image cannot map, and a word the CLI uses for a state that is not resolved YET. Neither
89
+ * says anything about the server, and the surface paints neither as a fault.
90
+ */
91
+ export type ObservedMcpStatus = 'ready' | 'failed' | 'needs_auth' | 'unknown';
92
+ /**
93
+ * Read the claude-code CLI's startup report (`{"type":"system","subtype":"init"}`) into one
94
+ * {@link ObservedMcpServer} per server the CLI knows about.
95
+ *
96
+ * The CLI announces its resolved session ONCE, before the first model call: which MCP servers it
97
+ * loaded and with what status, and the flat list of tool names it will expose. Both halves are
98
+ * read here because neither answers the question alone — a `ready` server exposing no tools is as
99
+ * useless to the agent as a failed one, and a tool count with no status cannot say why.
100
+ *
101
+ * Returns `undefined` when the event names no servers at all, which keeps "this run wired none"
102
+ * and "this image observed none" from collapsing into an empty list on the backend's record.
103
+ * Pure, so the parsing is testable without a CLI: {@link runClaudeCode} feeds it the raw event.
104
+ */
105
+ export declare function observeClaudeMcpInit(event: Record<string, unknown>): ObservedMcpServer[] | undefined;
45
106
  /**
46
107
  * The credential values carried by a run's tool servers, for {@link registerKnownSecrets}. An MCP
47
108
  * server that fails to start routinely echoes its own argv or request headers into stderr, and
@@ -1,5 +1,118 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
+ /**
4
+ * Map one status word from the CLI onto {@link ObservedMcpStatus}.
5
+ *
6
+ * The synonyms are grouped rather than listed one-to-one because the CLI has spelled the same
7
+ * two states more than one way across versions (`connected`/`ready`, `failed`/`error`), and an
8
+ * image that pinned the exact spelling would silently start reporting `unknown` for every server
9
+ * on a CLI upgrade — a regression that looks identical to a genuine outage.
10
+ */
11
+ function normalizeMcpStatus(value) {
12
+ if (typeof value !== 'string')
13
+ return 'unknown';
14
+ const status = value.trim().toLowerCase();
15
+ if (status === 'connected' || status === 'ready' || status === 'ok')
16
+ return 'ready';
17
+ if (status === 'failed' || status === 'error')
18
+ return 'failed';
19
+ // The vendor spells the OAuth-required state with a hyphen; the underscore form costs nothing
20
+ // to accept and is what a JSON-ish vocabulary tends to drift toward.
21
+ if (status === 'needs-auth' || status === 'needs_auth')
22
+ return 'needs_auth';
23
+ // Everything else, INCLUDING the CLI's `pending`. A server still handshaking when the session
24
+ // was announced has no resolved state, which is exactly what `unknown` says, and `needs_auth` is
25
+ // the tempting wrong guess for it: the surface paints that one amber as "waiting for you to
26
+ // authorize it", sending an operator to re-issue a working credential for a server that was
27
+ // merely slow and came up a second later.
28
+ return 'unknown';
29
+ }
30
+ /**
31
+ * Read the claude-code CLI's startup report (`{"type":"system","subtype":"init"}`) into one
32
+ * {@link ObservedMcpServer} per server the CLI knows about.
33
+ *
34
+ * The CLI announces its resolved session ONCE, before the first model call: which MCP servers it
35
+ * loaded and with what status, and the flat list of tool names it will expose. Both halves are
36
+ * read here because neither answers the question alone — a `ready` server exposing no tools is as
37
+ * useless to the agent as a failed one, and a tool count with no status cannot say why.
38
+ *
39
+ * Returns `undefined` when the event names no servers at all, which keeps "this run wired none"
40
+ * and "this image observed none" from collapsing into an empty list on the backend's record.
41
+ * Pure, so the parsing is testable without a CLI: {@link runClaudeCode} feeds it the raw event.
42
+ */
43
+ export function observeClaudeMcpInit(event) {
44
+ if (event.type !== 'system' || event.subtype !== 'init')
45
+ return undefined;
46
+ const reported = event.mcp_servers;
47
+ if (!Array.isArray(reported) || reported.length === 0)
48
+ return undefined;
49
+ const rows = [];
50
+ const declared = new Set();
51
+ for (const entry of reported) {
52
+ if (typeof entry !== 'object' || entry === null)
53
+ continue;
54
+ const record = entry;
55
+ const id = sanitizeServerId(record.name);
56
+ // An id this image cannot hold is dropped rather than reported under a mangled name: the
57
+ // whole row is only useful if it JOINS the backend's declaration, and `--strict-mcp-config`
58
+ // means every server the CLI loaded came from the config this harness wrote.
59
+ if (!id || declared.has(id))
60
+ continue;
61
+ declared.add(id);
62
+ rows.push({ id, status: normalizeMcpStatus(record.status) });
63
+ }
64
+ if (rows.length === 0)
65
+ return undefined;
66
+ // Counted from the CLI's own tool list rather than from a per-server field, because there is no
67
+ // per-server field: the CLI flattens every server's tools into one array namespaced by server
68
+ // id. A missing/non-array list leaves every count ABSENT rather than 0 (see `toolCount`).
69
+ const tally = tallyToolsByServer(event.tools, declared);
70
+ return rows.map((row) => ({
71
+ ...row,
72
+ ...(tally && !tally.ambiguous.has(row.id) ? { toolCount: tally.counts.get(row.id) ?? 0 } : {}),
73
+ }));
74
+ }
75
+ /**
76
+ * Tally the CLI's flat tool list (`mcp__<id>__<tool>`) against the servers the SAME event
77
+ * declared, or `undefined` when it carried no list, which is the distinction
78
+ * {@link ObservedMcpServer.toolCount} preserves.
79
+ *
80
+ * Matched against the declared ids rather than split on the first `__`, because the id vocabulary
81
+ * ({@link MCP_SERVER_ID_PATTERN}) permits an underscore: a server named `code__search` owns
82
+ * `mcp__code__search__query`, which a first-separator split files under a server called `code`,
83
+ * leaving the real one reporting `toolCount: 0`. That is the single most diagnostic value on the
84
+ * field, so the mis-split renders a fully healthy server as one that started and exposes nothing.
85
+ *
86
+ * The same underscore makes genuine ambiguity representable: with both `code` and `code__search`
87
+ * declared, `mcp__code__search__query` is a name either could own and nothing in the report says
88
+ * which. Neither server is counted then, and both are named `ambiguous` so their count stays
89
+ * absent. Guessing an owner would move a real tool onto the wrong server and take the other's
90
+ * count to a `0` that reads as a fault.
91
+ */
92
+ function tallyToolsByServer(tools, declared) {
93
+ if (!Array.isArray(tools))
94
+ return undefined;
95
+ const counts = new Map();
96
+ const ambiguous = new Set();
97
+ for (const tool of tools) {
98
+ if (typeof tool !== 'string' || !tool.startsWith('mcp__'))
99
+ continue;
100
+ const owners = [];
101
+ for (const id of declared) {
102
+ const prefix = `mcp__${id}__`;
103
+ // The tool name after the prefix must be non-empty: `mcp__slack__` names no tool.
104
+ if (tool.length > prefix.length && tool.startsWith(prefix))
105
+ owners.push(id);
106
+ }
107
+ const [owner] = owners;
108
+ if (owners.length === 1 && owner)
109
+ counts.set(owner, (counts.get(owner) ?? 0) + 1);
110
+ else
111
+ for (const id of owners)
112
+ ambiguous.add(id);
113
+ }
114
+ return { counts, ambiguous };
115
+ }
3
116
  /**
4
117
  * The credential values carried by a run's tool servers, for {@link registerKnownSecrets}. An MCP
5
118
  * server that fails to start routinely echoes its own argv or request headers into stderr, and
@@ -1,6 +1,7 @@
1
- import type { Logger } from './logger.js';
1
+ import { type Logger } from './logger.js';
2
+ import { type ToolProgressWindow } from './tool-silence.js';
2
3
  import { type HarnessCallMetric, type PiRunOutcome, type TodoProgress, type ToolSpan } from './pi.js';
3
- import { type McpServerSpec, type SkillSpec } from './agent-capabilities.js';
4
+ import { type McpServerSpec, type ObservedMcpServer, type SkillSpec } from './agent-capabilities.js';
4
5
  import { type ProgressGuardLimits } from './progress-guard.js';
5
6
  import { type SliceReview } from './subagents.js';
6
7
  /** Which subscription harness to run (the Pi harness uses `runPi` directly). */
@@ -79,6 +80,18 @@ export interface SubscriptionRunOptions {
79
80
  * DID dies with the container.
80
81
  */
81
82
  onSpan?: (span: ToolSpan) => void;
83
+ /**
84
+ * Opens this stream's tool-silence window (see `RunOptions.beginToolWindow`), closed when the
85
+ * CLI exits. Both subscription CLIs report tool activity — claude-code on the `tool_result`
86
+ * turn that answers each call, codex on its tool/command/exec events — so a window either
87
+ * opens is one the run can beat. It is deliberately NOT tied to {@link onSpan}: the trajectory
88
+ * is an observability opt-in, and the codex stream produces none at all while still doing tool
89
+ * work, which a span-keyed window would have read as a run making no progress.
90
+ *
91
+ * A caller with no tool loop (the inline one-shot completion) passes nothing; see the note at
92
+ * `handleInline`.
93
+ */
94
+ beginToolWindow?: () => ToolProgressWindow;
82
95
  /**
83
96
  * Called with the FULL set of per-slice reviews each time one lands, so the backend can persist
84
97
  * a parallel review's completed work as it happens instead of only from the terminal result.
@@ -92,6 +105,21 @@ export interface SubscriptionRunOptions {
92
105
  * same row still rides the result, so a lost poll response costs nothing.
93
106
  */
94
107
  onCallMetric?: (call: HarnessCallMetric) => void;
108
+ /**
109
+ * Called once with what the CLI reported about the tool servers it loaded, the moment it
110
+ * announces its resolved session (see {@link observeClaudeMcpInit}).
111
+ *
112
+ * The one thing the backend's own dispatch record cannot answer: it knows why it WITHHELD a
113
+ * tool, and this says a server it wired failed to start anyway. Reported even when every server
114
+ * came up, because "observed, all healthy" and "this image observed nothing" are different
115
+ * facts about a run and only the first one clears a wired server of suspicion.
116
+ *
117
+ * Whole-value latest-wins, not a delta — the CLI announces its session once, so a second call
118
+ * would only ever be a re-announcement of the same set. A harness whose CLI reports nothing
119
+ * (codex today) never calls this, which is what leaves the backend's record honestly empty
120
+ * rather than claiming every server failed.
121
+ */
122
+ onToolServers?: (observed: ObservedMcpServer[]) => void;
95
123
  /**
96
124
  * The per-job child logger (jobId/repo/branch correlation). Threaded so the retained
97
125
  * session-transcript path is logged for the run when the isolated config home is torn down.