@cat-factory/executor-harness 1.62.0 → 1.64.2

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.
@@ -0,0 +1,211 @@
1
+ import { SUBAGENT_TOOL_NAMES } from './claude-stream.js';
2
+ // The harness's no-progress guard: the live anti-rabbithole bound every agent run is held to,
3
+ // plus the tool-name vocabulary it classifies calls with and the limits it reads from the
4
+ // environment. Extracted from `pi.ts` when the guard stopped being Pi's: it now also drives the
5
+ // claude-code subscription runner (`agent-runner.ts` feeds it via `observeSignal`), so the two
6
+ // harnesses share ONE definition of "this run has stopped making progress" — and the tool-name
7
+ // sets below deliberately cover both CLIs' vocabularies.
8
+ /**
9
+ * Tool-call signal read off a streamed Pi event, or undefined if not a tool call. Exported for
10
+ * `runPi`'s span emitter, which reads the same event for its per-tool trace spans.
11
+ */
12
+ export function toolCallSignal(event) {
13
+ // `tool_execution_end` is the canonical per-call stream event (statsFromEvents
14
+ // counts the same one), so the guard reads it and nothing else — no double count.
15
+ if (event.type !== 'tool_execution_end')
16
+ return undefined;
17
+ const name = typeof event.toolName === 'string' ? event.toolName : '';
18
+ return { name, isError: event.isError === true };
19
+ }
20
+ // `satisfies` (not a type annotation) so each property keeps its concrete `number`
21
+ // type — `maxConsecutiveWebCalls` is optional on the interface (callers may omit it),
22
+ // but the defaults always define it, so consumers reading it off here get a `number`.
23
+ export const DEFAULT_PROGRESS_GUARD_LIMITS = {
24
+ // Counts only non-exploration, non-planning calls (see EXPLORATION_TOOLS), so the
25
+ // ceiling can be generous without risking a false kill on a read-heavy large task.
26
+ maxToolCallsWithoutEdit: 40,
27
+ maxConsecutiveErrors: 12,
28
+ // A genuine research burst is a handful of searches; an uninterrupted run of this
29
+ // many web calls (with no read/edit/bash between) is a search loop, not progress.
30
+ maxConsecutiveWebCalls: 25,
31
+ };
32
+ // Tool names that mutate files, so a call to one clears the no-edit suspicion. Kept
33
+ // broad on purpose: different models/extensions name the same capability differently
34
+ // (`edit`/`write`, but also `apply_patch`/`patch`/`str_replace`/`multiedit`/`create`),
35
+ // and a false "no edits" reading would kill a run that IS making changes. Matched
36
+ // case-insensitively. NOTE: a file written purely via `bash` (e.g. a heredoc) is not
37
+ // recognised here — broaden or move to a working-tree signal if that becomes common.
38
+ const FILE_EDIT_TOOLS = new Set([
39
+ 'edit',
40
+ 'write',
41
+ 'apply_patch',
42
+ 'patch',
43
+ 'str_replace',
44
+ 'multiedit',
45
+ 'create',
46
+ // Claude Code tool names (the guard now runs on the claude-code stream too): Edit/Write/
47
+ // MultiEdit already match above; NotebookEdit is its own tool.
48
+ 'notebookedit',
49
+ ]);
50
+ // Planning/bookkeeping tools that are neither file edits nor the environment-probing
51
+ // the no-edit bound targets — the todo list the agent maintains as it works. These do
52
+ // NOT count toward `maxToolCallsWithoutEdit`: a run that diligently updates a long
53
+ // todo list before its first edit (common on a large task) would otherwise be killed
54
+ // for "no edits" purely from planning calls. They still reset the consecutive-error
55
+ // streak (a successful call means the agent isn't wedged). Matched case-insensitively.
56
+ // `todo` is Pi's tool; `TodoWrite` and the incremental `TaskCreate`/`TaskUpdate` pair are
57
+ // Claude Code's plan vocabularies — all pure bookkeeping, exempt from the no-edit bound.
58
+ const PLANNING_TOOLS = new Set(['todo', 'todowrite', 'taskcreate', 'taskupdate']);
59
+ // A subagent dispatch (Claude Code's `Agent`/`Task`) is exempt from the no-edit bound because
60
+ // the parent stream CANNOT see the edits it makes: only the dispatch and its terminal
61
+ // tool_result appear there, while every Edit/Write the subagent performs happens on a transcript
62
+ // the guard never reads (`subagents.ts` watches those separately, for usage/progress only). So a
63
+ // coder that fans its implementation out across subagents looks, to this guard, like a run making
64
+ // dozens of action calls and zero edits — and would be killed for making excellent progress.
65
+ // Counting them as edits instead would be worse (a read-only research subagent would then clear
66
+ // the suspicion the bound exists to hold), so they are neutral: they neither count toward the
67
+ // bound nor satisfy it. Sourced from the same set the slice tracker matches on, lower-cased for
68
+ // this module's case-insensitive comparison.
69
+ const SUBAGENT_DISPATCH_TOOLS = new Set([...SUBAGENT_TOOL_NAMES].map((name) => name.toLowerCase()));
70
+ // Read-only exploration tools: reading/searching the repo is legitimate work-up to an
71
+ // edit, NOT the environment-probing the no-edit bound targets, so they don't count
72
+ // toward `maxToolCallsWithoutEdit` (a large task may read/search dozens of files
73
+ // before its first edit). The bound thus counts only "action" calls — chiefly `bash`
74
+ // (the credential rabbit-hole's vector) — that have yet to produce an edit. Kept broad
75
+ // since models/extensions name the same capability differently. Matched case-insensitively.
76
+ const EXPLORATION_TOOLS = new Set([
77
+ 'read',
78
+ 'grep',
79
+ 'search',
80
+ 'glob',
81
+ 'ls',
82
+ 'list',
83
+ 'find',
84
+ 'tree',
85
+ 'cat',
86
+ 'view',
87
+ 'head',
88
+ 'tail',
89
+ 'stat',
90
+ // rpiv-web-tools (Pi) + Claude Code's WebSearch/WebFetch: querying/reading the web is
91
+ // read-only research up to an edit, not the environment-probing the no-edit bound targets.
92
+ 'web_search',
93
+ 'web_fetch',
94
+ 'websearch',
95
+ 'webfetch',
96
+ ]);
97
+ // The web-tool calls, tracked separately so an unbounded run of them (with no other tool
98
+ // call between) can be caught as a search loop — see `maxConsecutiveWebCalls`. Covers both
99
+ // Pi's `web_search`/`web_fetch` and Claude Code's `WebSearch`/`WebFetch`.
100
+ const WEB_TOOLS = new Set(['web_search', 'web_fetch', 'websearch', 'webfetch']);
101
+ /** Read {@link ProgressGuardLimits} from the environment, falling back to the defaults. */
102
+ export function progressGuardLimitsFromEnv(env = process.env) {
103
+ const num = (raw, fallback) => {
104
+ const n = Number(raw);
105
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
106
+ };
107
+ return {
108
+ maxToolCallsWithoutEdit: num(env.JOB_MAX_TOOLCALLS_WITHOUT_EDIT, DEFAULT_PROGRESS_GUARD_LIMITS.maxToolCallsWithoutEdit),
109
+ maxConsecutiveErrors: num(env.JOB_MAX_CONSECUTIVE_TOOL_ERRORS, DEFAULT_PROGRESS_GUARD_LIMITS.maxConsecutiveErrors),
110
+ maxConsecutiveWebCalls: num(env.JOB_MAX_CONSECUTIVE_WEB_CALLS, DEFAULT_PROGRESS_GUARD_LIMITS.maxConsecutiveWebCalls),
111
+ };
112
+ }
113
+ /**
114
+ * Apply per-knob overrides onto a base set of guard limits, ENFORCING loosen-only: an
115
+ * override can only RAISE a knob (more headroom), never lower it below the base. A
116
+ * larger value is more lenient for every knob (more no-edit tool calls / errors / web
117
+ * calls tolerated), so each result is `max(base, override)`. This is a hard guarantee,
118
+ * not a convention — a tuning entry (built-in or a custom kind's, which reaches this via
119
+ * an untrusted job body) that supplies a value TIGHTER than the base is clamped back up
120
+ * to the base rather than aborting a legitimately-progressing run. An absent/undefined
121
+ * knob keeps the base value untouched.
122
+ */
123
+ export function mergeGuardLimits(base, overrides) {
124
+ if (!overrides)
125
+ return base;
126
+ const loosen = (b, o) => typeof o === 'number' ? Math.max(b, o) : b;
127
+ return {
128
+ maxToolCallsWithoutEdit: loosen(base.maxToolCallsWithoutEdit, overrides.maxToolCallsWithoutEdit),
129
+ maxConsecutiveErrors: loosen(base.maxConsecutiveErrors, overrides.maxConsecutiveErrors),
130
+ // `maxConsecutiveWebCalls` is optional on the interface (callers may omit it), so
131
+ // fall back to the default before loosening — keeps `loosen`'s base a concrete number.
132
+ maxConsecutiveWebCalls: loosen(base.maxConsecutiveWebCalls ?? DEFAULT_PROGRESS_GUARD_LIMITS.maxConsecutiveWebCalls, overrides.maxConsecutiveWebCalls),
133
+ };
134
+ }
135
+ /**
136
+ * Live anti-rabbithole guard: fed each streamed Pi event, it returns a diagnostic
137
+ * reason the moment a run has plainly stopped making progress, so the harness can
138
+ * kill Pi early instead of letting it burn the whole budget (and then surface a
139
+ * useful failure instead of a generic "no file changes"). Pure and incremental so
140
+ * it can be unit-tested over a fixed event sequence.
141
+ */
142
+ export class ProgressGuard {
143
+ limits;
144
+ expectsEdits;
145
+ toolCalls = 0;
146
+ edits = 0;
147
+ consecutiveErrors = 0;
148
+ consecutiveWebCalls = 0;
149
+ constructor(limits,
150
+ /** When false (assess-only runs like the merger), the no-edit bound is skipped. */
151
+ expectsEdits = true) {
152
+ this.limits = limits;
153
+ this.expectsEdits = expectsEdits;
154
+ }
155
+ /** Feed one parsed Pi event; returns a diagnostic reason when the run should abort, else null. */
156
+ observe(event) {
157
+ const tool = toolCallSignal(event);
158
+ if (!tool)
159
+ return null;
160
+ return this.observeSignal(tool);
161
+ }
162
+ /**
163
+ * Feed one already-parsed tool-call signal (name + error flag), returning a diagnostic reason
164
+ * when the run should abort, else null. Split out of {@link observe} so a caller whose stream
165
+ * is NOT Pi's `tool_execution_end` envelope — the claude-code runner, which correlates a
166
+ * `tool_use` block's name with its `tool_result`'s `is_error` — can drive the SAME guard logic
167
+ * without synthesising a fake Pi event.
168
+ */
169
+ observeSignal(tool) {
170
+ const name = tool.name.toLowerCase();
171
+ // The error streak tracks ANY tool call (a planning call still proves the agent
172
+ // isn't wedged in a failing-op loop), so it's updated before the planning skip.
173
+ this.consecutiveErrors = tool.isError ? this.consecutiveErrors + 1 : 0;
174
+ if (this.consecutiveErrors >= this.limits.maxConsecutiveErrors) {
175
+ return (`no progress: ${this.consecutiveErrors} consecutive failing tool calls — the agent is stuck ` +
176
+ `retrying a failing operation rather than making progress. Aborting.`);
177
+ }
178
+ // Web search/fetch loop: web tools are read-only (they don't count toward the
179
+ // no-edit bound), so guard them separately — an uninterrupted streak of them is a
180
+ // research rabbit-hole. Any non-web tool call resets the streak.
181
+ if (WEB_TOOLS.has(name)) {
182
+ this.consecutiveWebCalls++;
183
+ const webCap = this.limits.maxConsecutiveWebCalls ?? DEFAULT_PROGRESS_GUARD_LIMITS.maxConsecutiveWebCalls;
184
+ if (this.consecutiveWebCalls >= webCap) {
185
+ return (`no progress: ${this.consecutiveWebCalls} consecutive web search/fetch calls without ` +
186
+ `any other action — the agent is stuck researching instead of doing the work. Aborting.`);
187
+ }
188
+ }
189
+ else {
190
+ this.consecutiveWebCalls = 0;
191
+ }
192
+ // Planning, read-only exploration and subagent-dispatch calls don't count toward the
193
+ // no-edit bound (see PLANNING_TOOLS / EXPLORATION_TOOLS / SUBAGENT_DISPATCH_TOOLS) —
194
+ // only "action" calls without an edit do.
195
+ if (PLANNING_TOOLS.has(name) ||
196
+ EXPLORATION_TOOLS.has(name) ||
197
+ SUBAGENT_DISPATCH_TOOLS.has(name)) {
198
+ return null;
199
+ }
200
+ this.toolCalls++;
201
+ if (FILE_EDIT_TOOLS.has(name))
202
+ this.edits++;
203
+ if (this.expectsEdits &&
204
+ this.edits === 0 &&
205
+ this.toolCalls >= this.limits.maxToolCallsWithoutEdit) {
206
+ return (`no progress: ${this.toolCalls} tool calls and not one file edit — the agent is exploring or ` +
207
+ `probing the environment without implementing anything. Aborting before it burns the whole run.`);
208
+ }
209
+ return null;
210
+ }
211
+ }
package/dist/progress.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { isObject } from './claude-stream.js';
2
- // The parent agent's own PLAN, as progress counts. This is one of the two redundant views a
3
- // pr-reviewer run produces (the other is the parallel-subagent dispatch view in
4
- // `subagents.ts`); {@link pickProgress} reconciles them.
2
+ // The parent agent's own PLAN, as progress counts. This is one of the two views a pr-reviewer
3
+ // run produces of the same slicing (the other is the parallel-subagent dispatch view in
4
+ // `subagents.ts`). The plan is the INVENTORY, the dispatches are the live STATUS, and
5
+ // {@link mergeProgress} folds them into the one list the board renders.
5
6
  //
6
7
  // The Claude Code CLI exposes the plan through TWO different tool vocabularies, and which one
7
8
  // a run uses depends on the CLI build, not on anything the harness controls:
@@ -190,17 +191,14 @@ export function createTaskPlanTracker() {
190
191
  };
191
192
  }
192
193
  /**
193
- * Reconcile the redundant views of the same work into the one to surface (ADR 0027 Defect B).
194
- * A pr-reviewer run has BOTH a parent plan (`TodoWrite` or `TaskCreate`/`TaskUpdate`) and the
195
- * `SliceTracker`'s subagent-dispatch view. The sequential shape advances the plan; the parallel
196
- * shape advances ONLY the slice tracker (the reviewer writes its plan once and the parallel
197
- * subagents report in-flight/complete). Neither alone covers both shapes, and gating the slice
198
- * tracker off whenever a plan exists (the original behaviour) pinned parallel runs at 0%.
199
- *
200
- * So prefer whichever view is further along: more `completed`, then more `inProgress` (an
201
- * all-pending plan must not beat live in-flight slices), then more `total` (the richer view — a
202
- * plan can carry an extra "aggregate" entry), else the plan. Pure + total; returns whichever
194
+ * Reconcile the parent's TWO plan vocabularies (`TodoWrite` snapshots vs the incremental
195
+ * `TaskCreate`/`TaskUpdate` pair) into one plan. A run uses one or the other, so this is a
196
+ * genuine either/or: prefer whichever is further along more `completed`, then more
197
+ * `inProgress`, then more `total` else the `TodoWrite` view. Pure + total; returns whichever
203
198
  * single input is present when only one is.
199
+ *
200
+ * This is NOT how the plan reconciles with the parallel-subagent view — those describe the same
201
+ * slices from two angles and are MERGED, see {@link mergeProgress}.
204
202
  */
205
203
  export function pickProgress(todo, slice) {
206
204
  if (!todo)
@@ -215,3 +213,114 @@ export function pickProgress(todo, slice) {
215
213
  return slice.total > todo.total ? slice : todo;
216
214
  return todo;
217
215
  }
216
+ /** Status ordering, so a merge can only ever ADVANCE an entry, never walk it back. */
217
+ const STATUS_RANK = {
218
+ pending: 0,
219
+ in_progress: 1,
220
+ completed: 2,
221
+ };
222
+ /**
223
+ * Words that carry no identity in a slice label, so `Review identity/auth slice` (the subagent
224
+ * description) and `identity/auth` (the plan entry's subject) compare equal.
225
+ */
226
+ const LABEL_FILLER = new Set([
227
+ 'a',
228
+ 'agent',
229
+ 'an',
230
+ 'and',
231
+ 'chunk',
232
+ 'chunks',
233
+ 'for',
234
+ 'of',
235
+ 'pass',
236
+ 'review',
237
+ 'reviewing',
238
+ 'slice',
239
+ 'slices',
240
+ 'subagent',
241
+ 'the',
242
+ ]);
243
+ /**
244
+ * A slice label reduced to its identifying words, for pairing a plan entry with the subagent
245
+ * dispatched to review it. Case, punctuation and the boilerplate around the slice name all
246
+ * differ between the two vocabularies; the slice NAME does not.
247
+ */
248
+ export function sliceLabelKey(label) {
249
+ return label
250
+ .toLowerCase()
251
+ .replace(/[^a-z0-9]+/g, ' ')
252
+ .split(' ')
253
+ .filter((w) => w.length > 0 && !LABEL_FILLER.has(w))
254
+ .join(' ');
255
+ }
256
+ /** Advance an entry to the stronger of its current status and the dispatch's. */
257
+ function advance(entry, status) {
258
+ if (STATUS_RANK[status] > STATUS_RANK[entry.status])
259
+ entry.status = status;
260
+ entry.paired = true;
261
+ }
262
+ /**
263
+ * MERGE the parent's plan with the `SliceTracker`'s subagent-dispatch view into the single list
264
+ * the board renders (ADR 0027 Defect B, corrected).
265
+ *
266
+ * The two are not competing answers, they are two halves of one: the plan is the INVENTORY (it
267
+ * names every slice, including the ones not dispatched yet, which is the only place a `pending`
268
+ * slice exists at all), and the dispatch view is the live STATUS (the plan advances only when
269
+ * the agent remembers to update it, which it does unreliably). Picking whichever looked "further
270
+ * along" — the previous behaviour — made the rendered list SHRINK the moment the first subagent
271
+ * returned: the dispatch view won on `completed`, and it only knows the slices dispatched so far,
272
+ * so every queued slice vanished from the window and reappeared one at a time as it was dispatched.
273
+ *
274
+ * Pairing is by normalised label ({@link sliceLabelKey}) — exact first, then containment — and
275
+ * finally positionally into the leftover pending entries, in dispatch order (the agent dispatches
276
+ * in plan order). A dispatch that pairs with nothing is APPENDED rather than dropped, so the list
277
+ * is at worst a union and can never lose a slice. Statuses only ever advance, so a plan entry the
278
+ * agent already marked done is not walked back by a re-dispatch.
279
+ *
280
+ * Pure + total. Falls back to {@link pickProgress} when either side carries counts but no items
281
+ * (nothing to merge onto).
282
+ */
283
+ export function mergeProgress(plan, slice) {
284
+ if (!plan)
285
+ return slice;
286
+ if (!slice)
287
+ return plan;
288
+ const planItems = plan.items ?? [];
289
+ const sliceItems = slice.items ?? [];
290
+ if (planItems.length === 0 || sliceItems.length === 0)
291
+ return pickProgress(plan, slice);
292
+ const entries = planItems.map((i) => ({
293
+ label: i.label,
294
+ status: normalizeStatus(i.status),
295
+ key: sliceLabelKey(i.label),
296
+ paired: false,
297
+ }));
298
+ const take = (match) => entries.find((e) => !e.paired && match(e));
299
+ // Pass 1 — the same slice named the same way.
300
+ // Pass 2 — one label contains the other (a dispatch description often expands the plan's short
301
+ // name). Length-guarded so a one-word residue can't match everything.
302
+ // Pass 3 — no words in common at all (renamed between planning and dispatch): absorb into the
303
+ // still-untouched pending entries in dispatch order.
304
+ // Anything still unpaired is a slice the plan never mentioned, so it JOINS the list.
305
+ const matchers = [
306
+ (key) => (e) => key.length > 0 && e.key === key,
307
+ (key) => (e) => key.length >= 3 && e.key.length >= 3 && (e.key.includes(key) || key.includes(e.key)),
308
+ () => (e) => e.status === 'pending',
309
+ ];
310
+ let unpaired = sliceItems;
311
+ for (const matcher of matchers) {
312
+ const rest = [];
313
+ for (const item of unpaired) {
314
+ const hit = take(matcher(sliceLabelKey(item.label)));
315
+ if (hit)
316
+ advance(hit, normalizeStatus(item.status));
317
+ else
318
+ rest.push(item);
319
+ }
320
+ unpaired = rest;
321
+ }
322
+ return toProgress([
323
+ ...entries.map((e) => ({ label: e.label, status: e.status })),
324
+ ...unpaired.map((i) => ({ label: i.label, status: normalizeStatus(i.status) })),
325
+ ]);
326
+ }
package/dist/subagents.js CHANGED
@@ -1,57 +1,8 @@
1
1
  import { readdir, stat } from 'node:fs/promises';
2
2
  import { createReadStream } from 'node:fs';
3
3
  import { basename, join } from 'node:path';
4
- import { claudeAssistantContent, claudeCallUsage, isObject, redactBody } from './claude-stream.js';
4
+ import { claudeAssistantContent, claudeCallUsage, isObject, redactBody, SUBAGENT_TOOL_NAMES, } from './claude-stream.js';
5
5
  import { publishCallMetric } from './pi.js';
6
- // ADR 0026 D2.1 + D3, corrected by ADR 0027. When the Claude Code CLI reviews a large PR
7
- // it fans the work out across parallel `Task` subagents. Two things then go dark to the
8
- // harness, which only reads the PARENT process's stream-json stdout:
9
- //
10
- // - the parent stream falls quiet for the whole (potentially 15+ minute) parallel
11
- // review, so the inactivity heartbeat freezes and a healthy run looks wedged (P3);
12
- // - every subagent's token spend is written to a SEPARATE `subagents/*.jsonl`
13
- // transcript under the CLI's config home and never reaches the parent stream, so
14
- // the run's telemetry reports ~0 tokens while hundreds of thousands are spent (P3).
15
- //
16
- // This module closes both without disabling the (context-bounding, ADR-0023-wanted)
17
- // subagent parallelism:
18
- //
19
- // - {@link createSliceTracker} derives the slice plan + per-slice progress from the
20
- // PARENT stream alone — the subagent-dispatch tool_use and its terminal tool_result
21
- // DO appear there (only the subagent's intermediate turns don't), so slices/progress
22
- // need no file watching (D2.1). `pickProgress` (./progress.ts) reconciles it with the
23
- // parent's own plan (ADR 0027 Defect B);
24
- // - {@link startSubagentWatcher} tails the `subagents/*.jsonl` transcripts for the
25
- // heartbeat (any new bytes ⇒ `onActivity`) and sums each subagent turn's usage into
26
- // the run's telemetry (D3).
27
- //
28
- // The CLI does NOT write those transcripts to `<configHome>/subagents` (the location ADR
29
- // 0026 assumed, which never exists — ADR 0027 Defect A). It writes them PER SESSION under
30
- // `<configHome>/projects/<encoded-cwd>/<session-uuid>/subagents/agent-*.jsonl`, and the
31
- // session-uuid dir isn't known before the CLI mints it — so the watcher is pointed at the
32
- // `projects` root and DISCOVERS the `subagents/` dir by walking (see
33
- // {@link findSubagentTranscripts}).
34
- //
35
- // Both degrade gracefully: the CLI's subagent transcript layout is not a stable contract,
36
- // so a missing directory, an unreadable file, or an unparseable line is swallowed and the
37
- // harness falls back to today's parent-stream-only behaviour.
38
- // ---------------------------------------------------------------------------
39
- // Slice / progress tracking off the PARENT stream (D2.1)
40
- // ---------------------------------------------------------------------------
41
- /**
42
- * The tool names the Claude Code CLI dispatches a parallel subagent under. `Agent` is what the
43
- * shipped schema declares (`AgentInput` in `sdk-tools.d.ts`, carrying `description` / `prompt` /
44
- * `subagent_type`); `Task` is the older name for the same dispatch. Both are matched because the
45
- * harness runs against whatever CLI the image happens to bundle, and matching only the old name
46
- * is what left a CLI 2.1.x pr-review reporting no slices at all.
47
- *
48
- * Note the asymmetry: keeping the legacy `Task` here is the one place a CLI rename could produce a
49
- * FALSE signal rather than merely no signal — if a future build were to name a plain task-list
50
- * tool `Task`, its writes would be counted as in-flight slices. We accept that because no shipped
51
- * build does (the incremental plan tool is `TaskCreate`/`TaskUpdate`, tracked separately in
52
- * `progress.ts`), and dropping legacy coverage is the more likely regression.
53
- */
54
- const SUBAGENT_TOOL_NAMES = new Set(['Agent', 'Task']);
55
6
  export function createSliceTracker() {
56
7
  // Insertion-ordered so the progress `items` render in dispatch order.
57
8
  const slices = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/executor-harness",
3
- "version": "1.62.0",
3
+ "version": "1.64.2",
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,9 +26,9 @@
26
26
  "hono": "^4.12.32",
27
27
  "typescript": "7.0.2",
28
28
  "vitest": "^4.1.10",
29
- "@cat-factory/kernel": "0.168.0",
30
- "@cat-factory/spend": "0.12.97",
31
- "@cat-factory/server": "0.158.0"
29
+ "@cat-factory/kernel": "0.171.0",
30
+ "@cat-factory/server": "0.162.0",
31
+ "@cat-factory/spend": "0.12.100"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsc -p tsconfig.json",