@cleocode/core 2026.9.4 → 2026.9.5

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.
@@ -66,6 +66,17 @@ export interface LimitedCommand {
66
66
  readonly confined: boolean;
67
67
  /** Ceiling applied, in MiB; `null` when unconfined. */
68
68
  readonly memoryMaxMb: number | null;
69
+ /**
70
+ * The explicit transient-unit name this command runs under, or `null` when
71
+ * unconfined.
72
+ *
73
+ * Reported so a caller can stop or enumerate the scope. `suite-reaper.ts`
74
+ * already reaps via `systemctl --user stop <unitName>` and could never
75
+ * target a heavy-tool scope while the name was systemd's auto-generated one.
76
+ *
77
+ * @task T12221 (gh#1396)
78
+ */
79
+ readonly unitName: string | null;
69
80
  }
70
81
  /**
71
82
  * Memory ceiling for one heavy-tool invocation, in MiB.
@@ -98,6 +109,50 @@ export declare function resolveMemoryMaxMb(totalRamGib?: number, env?: NodeJS.Pr
98
109
  export declare function cgroupConfinementAvailable(env?: NodeJS.ProcessEnv): boolean;
99
110
  /** Reset the cached probe. Test-only. @internal */
100
111
  export declare function _resetConfinementCache(): void;
112
+ /**
113
+ * Build the transient-unit name a confined heavy tool runs under.
114
+ *
115
+ * `cleo-tool-<canonical>-<rootHash8>-<rand8>.scope`, e.g.
116
+ * `cleo-tool-test-3f9a21c4-b7e01d55.scope`. Each component earns its place:
117
+ *
118
+ * | Component | Defends against |
119
+ * |---------------|---------------------------------------------------|
120
+ * | `cleo-tool-` | *(legibility)* `systemctl --user list-units 'cleo-tool-*'` enumerates exactly CLEO's heavy-tool scopes, distinct from `cleo-agent-*`. |
121
+ * | `<canonical>` | *(legibility)* an operator sees WHICH tool is stuck without opening anything. |
122
+ * | `<rootHash8>` | two worktrees of one repo, and two projects on one host — both have different execution roots, so each gets its own namespace and either can be reaped without touching the other. |
123
+ * | `<rand8>` | two concurrent verifies of the same tool in the same tree, which differ in nothing else; and PID reuse, by not using the PID. |
124
+ *
125
+ * **Deliberately not the PID.** The PID is the component that makes systemd's
126
+ * auto-generated `run-p<pid>-i<id>` collide, and it defends nothing randomness
127
+ * does not. Including it out of familiarity would import the failure mode this
128
+ * name exists to remove.
129
+ *
130
+ * `rootHash8` is taken off the execution root — the same identity the cache key
131
+ * already uses (T12112 / gh#1220) — so the scope and the cache agree on what
132
+ * "the tree under test" means rather than each deciding separately.
133
+ *
134
+ * At ~38 characters of `[a-z0-9-]` this is far inside systemd's 256-byte unit
135
+ * name limit and uses only characters valid in a unit name.
136
+ *
137
+ * @param canonical - the heavy tool being run.
138
+ * @param executionRoot - absolute path of the tree the tool runs in.
139
+ * @returns a unit name ending in `.scope`.
140
+ *
141
+ * @task T12221 (gh#1396)
142
+ */
143
+ export declare function buildToolScopeUnitName(canonical: CanonicalTool, executionRoot: string): string;
144
+ /**
145
+ * Is `cmd` a `systemd-run` invocation?
146
+ *
147
+ * Compares the basename so an absolute path (`/usr/bin/systemd-run`) is
148
+ * recognised as readily as the bare name a `testing.command` usually carries.
149
+ *
150
+ * @param cmd - the executable a resolved tool command starts with.
151
+ * @returns `true` when the command is itself systemd-run.
152
+ *
153
+ * @task T12221 (gh#1396)
154
+ */
155
+ export declare function isSystemdRunCommand(cmd: string): boolean;
101
156
  /**
102
157
  * Wrap a heavy tool command so the kernel bounds its whole process tree.
103
158
  *
@@ -130,6 +185,12 @@ export declare function withMemoryLimit(canonical: CanonicalTool, cmd: string, a
130
185
  totalRamGib?: number;
131
186
  env?: NodeJS.ProcessEnv;
132
187
  available?: boolean;
188
+ /**
189
+ * Tree the tool runs in, used for the unit name's `rootHash8`. Defaults to
190
+ * `process.cwd()` so existing callers keep working; the tool-cache passes
191
+ * the execution root it already resolved.
192
+ */
193
+ executionRoot?: string;
133
194
  }): LimitedCommand;
134
195
  /**
135
196
  * One sentence describing the memory bound a given tool runs under, for use in
@@ -157,4 +218,62 @@ export declare function withMemoryLimit(canonical: CanonicalTool, cmd: string, a
157
218
  * @task T12116
158
219
  */
159
220
  export declare function describeMemoryLimit(canonical: CanonicalTool, env?: NodeJS.ProcessEnv): string;
221
+ /**
222
+ * Decide whether a non-zero exit came from CLEO's own confinement wrapper
223
+ * failing to start, rather than from the wrapped tool running and failing.
224
+ *
225
+ * ## Why this cannot be done with the exit code
226
+ *
227
+ * `systemd-run --scope` is transparent on success: it exits with the WRAPPED
228
+ * command's status. Measured on systemd 259 — a scope that starts and runs
229
+ * `sh -c 'exit 7'` makes `systemd-run` exit `7`. When it cannot create the
230
+ * unit it exits `1` and the wrapped command never runs.
231
+ *
232
+ * `1` is also the exit code of a test suite with a failing test. So the two
233
+ * outcomes CLEO most needs to tell apart — "the harness never started" and
234
+ * "the suite ran and was red" — are identical in the exit code, and differ
235
+ * only in stderr. That is the whole of gh#1397: not a missing error code
236
+ * ({@link validateTool} has had `E_EVIDENCE_TOOL_UNAVAILABLE` all along) but a
237
+ * missing route into it.
238
+ *
239
+ * ## Scope of the claim
240
+ *
241
+ * Returns the matched diagnostic line so the caller can quote the real reason
242
+ * rather than paraphrase it.
243
+ *
244
+ * Gated on whether the spawned command was a `systemd-run` invocation AT ALL —
245
+ * NOT on whether CLEO added it. Those two were the same question until
246
+ * double-wrap detection landed; now a project that pins its own
247
+ * `systemd-run` test command is spawned unconfined by CLEO, and a failure of
248
+ * ITS wrapper still means the suite never ran. Keying on
249
+ * {@link LimitedCommand.confined} alone would have reported exactly the
250
+ * gh#1396 project's harness failures as red suites — the defect this function
251
+ * exists to remove, reintroduced by the fix for its sibling.
252
+ *
253
+ * This is a stderr match, so it is not airtight: a project whose own test
254
+ * output contained one of these strings verbatim AND exited non-zero would be
255
+ * misreported. That is judged acceptable against the alternative, which is the
256
+ * status quo of reporting every harness failure as a red suite. The markers
257
+ * are `systemd-run`'s, the check is gated on CLEO having wrapped the call, and
258
+ * the returned message is quoted rather than asserted.
259
+ *
260
+ * @param stderr - captured stderr from the spawn.
261
+ * @param viaSystemdRun - whether the spawned command was a systemd-run invocation,
262
+ * whether CLEO added it or the project pinned it.
263
+ * @returns the matched diagnostic line, or `null` when the wrapper started fine.
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * confinementStartupFailure(
268
+ * 'Failed to start transient scope unit: Unit run-p1-i2.scope was already loaded.',
269
+ * true,
270
+ * );
271
+ * // => 'Failed to start transient scope unit: Unit run-p1-i2.scope was already loaded.'
272
+ * confinementStartupFailure('2 tests failed', true); // => null
273
+ * confinementStartupFailure('Failed to connect to bus', false); // => null (not systemd-run)
274
+ * ```
275
+ *
276
+ * @task T12116 (gh#1397)
277
+ */
278
+ export declare function confinementStartupFailure(stderr: string, viaSystemdRun: boolean): string | null;
160
279
  //# sourceMappingURL=heavy-tool-limit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"heavy-tool-limit.d.ts","sourceRoot":"","sources":["../../src/tasks/heavy-tool-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,iEAAiE;AACjE,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,8EAA8E;AAC9E,eAAO,MAAM,4BAA4B,OAAQ,CAAC;AAElD,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAEhD,mEAAmE;AACnE,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD,sDAAsD;AACtD,eAAO,MAAM,WAAW,wBAAwB,CAAC;AAEjD,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,GAAE,MAA+B,EAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAMR;AAKD;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAgCxF;AAED,mDAAmD;AACnD,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,aAAa,EACxB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,IAAI,GAAE;IACJ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;CAChB,GACL,cAAc,CAsChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,aAAa,EACxB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAUR"}
1
+ {"version":3,"file":"heavy-tool-limit.d.ts","sourceRoot":"","sources":["../../src/tasks/heavy-tool-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,iEAAiE;AACjE,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,8EAA8E;AAC9E,eAAO,MAAM,4BAA4B,OAAQ,CAAC;AAElD,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAEhD,mEAAmE;AACnE,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD,sDAAsD;AACtD,eAAO,MAAM,WAAW,wBAAwB,CAAC;AAEjD,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC;;;;;;;;;OASG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,GAAE,MAA+B,EAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAMR;AAKD;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAgCxF;AAED,mDAAmD;AACnD,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAI9F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,aAAa,EACxB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,IAAI,GAAE;IACJ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACnB,GACL,cAAc,CAiGhB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,aAAa,EACxB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAaR;AAwBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAS/F"}
@@ -46,6 +46,7 @@
46
46
  * @task T12116
47
47
  */
48
48
  import { spawnSync } from 'node:child_process';
49
+ import { createHash, randomBytes } from 'node:crypto';
49
50
  import { totalmem } from 'node:os';
50
51
  import { isHeavyTool } from './heavy-tool-env.js';
51
52
  /** Fraction of host RAM one heavy tool invocation may occupy. */
@@ -130,6 +131,57 @@ export function cgroupConfinementAvailable(env = process.env) {
130
131
  export function _resetConfinementCache() {
131
132
  _available = null;
132
133
  }
134
+ /**
135
+ * Build the transient-unit name a confined heavy tool runs under.
136
+ *
137
+ * `cleo-tool-<canonical>-<rootHash8>-<rand8>.scope`, e.g.
138
+ * `cleo-tool-test-3f9a21c4-b7e01d55.scope`. Each component earns its place:
139
+ *
140
+ * | Component | Defends against |
141
+ * |---------------|---------------------------------------------------|
142
+ * | `cleo-tool-` | *(legibility)* `systemctl --user list-units 'cleo-tool-*'` enumerates exactly CLEO's heavy-tool scopes, distinct from `cleo-agent-*`. |
143
+ * | `<canonical>` | *(legibility)* an operator sees WHICH tool is stuck without opening anything. |
144
+ * | `<rootHash8>` | two worktrees of one repo, and two projects on one host — both have different execution roots, so each gets its own namespace and either can be reaped without touching the other. |
145
+ * | `<rand8>` | two concurrent verifies of the same tool in the same tree, which differ in nothing else; and PID reuse, by not using the PID. |
146
+ *
147
+ * **Deliberately not the PID.** The PID is the component that makes systemd's
148
+ * auto-generated `run-p<pid>-i<id>` collide, and it defends nothing randomness
149
+ * does not. Including it out of familiarity would import the failure mode this
150
+ * name exists to remove.
151
+ *
152
+ * `rootHash8` is taken off the execution root — the same identity the cache key
153
+ * already uses (T12112 / gh#1220) — so the scope and the cache agree on what
154
+ * "the tree under test" means rather than each deciding separately.
155
+ *
156
+ * At ~38 characters of `[a-z0-9-]` this is far inside systemd's 256-byte unit
157
+ * name limit and uses only characters valid in a unit name.
158
+ *
159
+ * @param canonical - the heavy tool being run.
160
+ * @param executionRoot - absolute path of the tree the tool runs in.
161
+ * @returns a unit name ending in `.scope`.
162
+ *
163
+ * @task T12221 (gh#1396)
164
+ */
165
+ export function buildToolScopeUnitName(canonical, executionRoot) {
166
+ const rootHash8 = createHash('sha256').update(executionRoot).digest('hex').slice(0, 8);
167
+ const rand8 = randomBytes(4).toString('hex');
168
+ return `cleo-tool-${canonical}-${rootHash8}-${rand8}.scope`;
169
+ }
170
+ /**
171
+ * Is `cmd` a `systemd-run` invocation?
172
+ *
173
+ * Compares the basename so an absolute path (`/usr/bin/systemd-run`) is
174
+ * recognised as readily as the bare name a `testing.command` usually carries.
175
+ *
176
+ * @param cmd - the executable a resolved tool command starts with.
177
+ * @returns `true` when the command is itself systemd-run.
178
+ *
179
+ * @task T12221 (gh#1396)
180
+ */
181
+ export function isSystemdRunCommand(cmd) {
182
+ const base = cmd.split('/').pop() ?? cmd;
183
+ return base === 'systemd-run';
184
+ }
133
185
  /**
134
186
  * Wrap a heavy tool command so the kernel bounds its whole process tree.
135
187
  *
@@ -164,18 +216,75 @@ export function withMemoryLimit(canonical, cmd, args, opts = {}) {
164
216
  // the spawn deadline. Four independent literals agreeing by coincidence is
165
217
  // how a fifth heavy tool gets a long deadline and no memory bound.
166
218
  if (!isHeavyTool(canonical)) {
167
- return { cmd, args, confined: false, memoryMaxMb: null };
219
+ return { cmd, args, confined: false, memoryMaxMb: null, unitName: null };
220
+ }
221
+ // gh#1396: do NOT wrap a command that already carries its own resource
222
+ // wrapper. `axiom-analytics` pins
223
+ //
224
+ // testing.command = "systemd-run --user --scope --quiet -p MemoryMax=8G \
225
+ // -p MemorySwapMax=0 -- env … pnpm exec vitest run"
226
+ //
227
+ // and `parseCommandString` splits on whitespace, so `cmd` is literally
228
+ // `systemd-run`. Prepending a second one nests two transient scopes, and
229
+ // `systemd-run --scope` execs its payload rather than forking — so the inner
230
+ // client regenerates a unit name the outer has already registered.
231
+ // Measured: nested 5/5 collide with
232
+ // "Unit run-p<pid>-i<id>.scope was already loaded or has a fragment file";
233
+ // a single invocation, 0/6.
234
+ //
235
+ // The collision is the symptom. The defect is the double wrap, and declining
236
+ // it is the rule this codebase has already adopted twice for exactly this
237
+ // shape: `heapCapApplied()` in `bin/cleo.js` does not re-exec when an
238
+ // operator's own `NODE_OPTIONS` already sets a heap cap, and
239
+ // `mergeNodeOptions()` lets an existing explicit value outrank our default.
240
+ // A project whose test command IS `systemd-run -p MemoryMax=8G` has made
241
+ // that same explicit choice, and a second cap it did not ask for is the
242
+ // thing to avoid, not merely the name clash.
243
+ //
244
+ // Deliberately systemd-run-SPECIFIC. `env`, `nice`, `taskset` and `timeout`
245
+ // are all plausible leading tokens too, but none of them imposes the cgroup
246
+ // memory bound this function exists to apply, so treating them as
247
+ // "already confined" would silently drop the ceiling. Detecting general
248
+ // intent would be guessing; this detects our own mechanism only.
249
+ if (isSystemdRunCommand(cmd)) {
250
+ return { cmd, args, confined: false, memoryMaxMb: null, unitName: null };
168
251
  }
169
252
  const available = opts.available ?? cgroupConfinementAvailable(env);
170
253
  if (!available) {
171
- return { cmd, args, confined: false, memoryMaxMb: null };
254
+ return { cmd, args, confined: false, memoryMaxMb: null, unitName: null };
172
255
  }
173
256
  const memoryMaxMb = resolveMemoryMaxMb(opts.totalRamGib ?? totalmem() / 1024 ** 3, env);
257
+ // CWD-OK: do NOT replace this with `resolveOrCwd(opts.executionRoot)`, which
258
+ // is what the project-root lint recommends. That helper falls back to
259
+ // `getProjectRoot()`, and `getProjectRoot()` DELIBERATELY collapses a git
260
+ // worktree to the main repo (paths.ts, step 2.5: "the canonical project root
261
+ // is the MAIN repo, not the worktree dir"). That is correct for locating
262
+ // state, and wrong here: every worktree of a repo would then hash to the same
263
+ // `rootHash8`, so two worktrees running `test` concurrently would build the
264
+ // same unit-name namespace — reintroducing a collision class inside the fix
265
+ // for a collision bug, and destroying the property this component exists to
266
+ // provide (see {@link buildToolScopeUnitName}).
267
+ //
268
+ // The production call site (`tool-cache.ts`) ALWAYS passes `executionRoot` —
269
+ // the same execution root the cache key uses — so this fallback is reached
270
+ // only by callers that do not identify a tree: tests, and any future
271
+ // in-process caller. For those, the process cwd is the best available answer
272
+ // and a wrong-but-unique namespace is harmless, whereas a collapsed one is
273
+ // not. Making `executionRoot` required would remove this line entirely and
274
+ // is the right follow-up.
275
+ const unitName = buildToolScopeUnitName(canonical, opts.executionRoot ?? process.cwd()); // CWD-OK: getProjectRoot() collapses worktrees to the main repo, which would give every worktree the same rootHash8 — see the note above
174
276
  return {
175
277
  cmd: 'systemd-run',
176
278
  args: [
177
279
  '--user',
178
280
  '--scope',
281
+ // gh#1396: an EXPLICIT name. Without `--unit=` systemd generates
282
+ // `run-p<pid>-i<dbus-id>`, which is unstoppable by name (so
283
+ // `suite-reaper.ts` cannot target it) and collides outright when two
284
+ // systemd-run clients share a pid. `--scope` picks the unit TYPE and
285
+ // `--unit=` supplies its NAME; they are independent and compose, which
286
+ // the TSDoc here asserted otherwise for the whole life of this defect.
287
+ `--unit=${unitName}`,
179
288
  '--quiet',
180
289
  '--collect',
181
290
  '-p',
@@ -191,6 +300,7 @@ export function withMemoryLimit(canonical, cmd, args, opts = {}) {
191
300
  ],
192
301
  confined: true,
193
302
  memoryMaxMb,
303
+ unitName,
194
304
  };
195
305
  }
196
306
  /**
@@ -219,7 +329,10 @@ export function withMemoryLimit(canonical, cmd, args, opts = {}) {
219
329
  * @task T12116
220
330
  */
221
331
  export function describeMemoryLimit(canonical, env = process.env) {
222
- const limited = withMemoryLimit(canonical, 'probe', [], { env });
332
+ // `executionRoot` is a stable sentinel, not a real path: this probe reads
333
+ // only `confined` and `memoryMaxMb` and discards the unit name, so making it
334
+ // read the process cwd would be a side effect with no consumer.
335
+ const limited = withMemoryLimit(canonical, 'probe', [], { env, executionRoot: 'probe' });
223
336
  if (!limited.confined || limited.memoryMaxMb === null)
224
337
  return '';
225
338
  return (` This tool runs inside a memory-bounded systemd scope ` +
@@ -228,4 +341,93 @@ export function describeMemoryLimit(canonical, env = process.env) {
228
341
  `though an external kill looks identical. Raise it with ${MEMORY_MAX_ENV}=<megabytes>, ` +
229
342
  `reduce the tool's own worker count, or set ${DISABLE_ENV}=1 to run unconfined.`);
230
343
  }
344
+ /**
345
+ * `systemd-run`'s own pre-exec failure diagnostics.
346
+ *
347
+ * Each of these is printed by `systemd-run` itself, BEFORE it execs the wrapped
348
+ * command, when it cannot create the transient unit. They are not produced by
349
+ * the tool being wrapped, because the tool never ran.
350
+ *
351
+ * Matched case-insensitively against the captured stderr. The list is
352
+ * deliberately narrow: every entry is a message `systemd-run` emits on the
353
+ * failure path, not a generic systemd string that could plausibly appear in a
354
+ * project's own test output.
355
+ *
356
+ * @task T12116 (gh#1397)
357
+ */
358
+ const CONFINEMENT_STARTUP_MARKERS = [
359
+ 'failed to start transient scope unit',
360
+ 'failed to start transient service unit',
361
+ 'failed to connect to bus',
362
+ 'failed to create bus connection',
363
+ 'interactive authentication required',
364
+ ];
365
+ /**
366
+ * Decide whether a non-zero exit came from CLEO's own confinement wrapper
367
+ * failing to start, rather than from the wrapped tool running and failing.
368
+ *
369
+ * ## Why this cannot be done with the exit code
370
+ *
371
+ * `systemd-run --scope` is transparent on success: it exits with the WRAPPED
372
+ * command's status. Measured on systemd 259 — a scope that starts and runs
373
+ * `sh -c 'exit 7'` makes `systemd-run` exit `7`. When it cannot create the
374
+ * unit it exits `1` and the wrapped command never runs.
375
+ *
376
+ * `1` is also the exit code of a test suite with a failing test. So the two
377
+ * outcomes CLEO most needs to tell apart — "the harness never started" and
378
+ * "the suite ran and was red" — are identical in the exit code, and differ
379
+ * only in stderr. That is the whole of gh#1397: not a missing error code
380
+ * ({@link validateTool} has had `E_EVIDENCE_TOOL_UNAVAILABLE` all along) but a
381
+ * missing route into it.
382
+ *
383
+ * ## Scope of the claim
384
+ *
385
+ * Returns the matched diagnostic line so the caller can quote the real reason
386
+ * rather than paraphrase it.
387
+ *
388
+ * Gated on whether the spawned command was a `systemd-run` invocation AT ALL —
389
+ * NOT on whether CLEO added it. Those two were the same question until
390
+ * double-wrap detection landed; now a project that pins its own
391
+ * `systemd-run` test command is spawned unconfined by CLEO, and a failure of
392
+ * ITS wrapper still means the suite never ran. Keying on
393
+ * {@link LimitedCommand.confined} alone would have reported exactly the
394
+ * gh#1396 project's harness failures as red suites — the defect this function
395
+ * exists to remove, reintroduced by the fix for its sibling.
396
+ *
397
+ * This is a stderr match, so it is not airtight: a project whose own test
398
+ * output contained one of these strings verbatim AND exited non-zero would be
399
+ * misreported. That is judged acceptable against the alternative, which is the
400
+ * status quo of reporting every harness failure as a red suite. The markers
401
+ * are `systemd-run`'s, the check is gated on CLEO having wrapped the call, and
402
+ * the returned message is quoted rather than asserted.
403
+ *
404
+ * @param stderr - captured stderr from the spawn.
405
+ * @param viaSystemdRun - whether the spawned command was a systemd-run invocation,
406
+ * whether CLEO added it or the project pinned it.
407
+ * @returns the matched diagnostic line, or `null` when the wrapper started fine.
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * confinementStartupFailure(
412
+ * 'Failed to start transient scope unit: Unit run-p1-i2.scope was already loaded.',
413
+ * true,
414
+ * );
415
+ * // => 'Failed to start transient scope unit: Unit run-p1-i2.scope was already loaded.'
416
+ * confinementStartupFailure('2 tests failed', true); // => null
417
+ * confinementStartupFailure('Failed to connect to bus', false); // => null (not systemd-run)
418
+ * ```
419
+ *
420
+ * @task T12116 (gh#1397)
421
+ */
422
+ export function confinementStartupFailure(stderr, viaSystemdRun) {
423
+ if (!viaSystemdRun || stderr === '')
424
+ return null;
425
+ for (const line of stderr.split('\n')) {
426
+ const haystack = line.toLowerCase();
427
+ if (CONFINEMENT_STARTUP_MARKERS.some((m) => haystack.includes(m))) {
428
+ return line.trim();
429
+ }
430
+ }
431
+ return null;
432
+ }
231
433
  //# sourceMappingURL=heavy-tool-limit.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"heavy-tool-limit.js","sourceRoot":"","sources":["../../src/tasks/heavy-tool-limit.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"heavy-tool-limit.js","sourceRoot":"","sources":["../../src/tasks/heavy-tool-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,iEAAiE;AACjE,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAE5C,8EAA8E;AAC9E,MAAM,CAAC,MAAM,4BAA4B,GAAG,KAAK,CAAC;AAElD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,CAAC;AAEhD,mEAAmE;AACnE,MAAM,CAAC,MAAM,cAAc,GAAG,yBAAyB,CAAC;AAExD,sDAAsD;AACtD,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAyBjD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,kBAAkB,CAChC,cAAsB,QAAQ,EAAE,GAAG,IAAI,IAAI,CAAC,EAC5C,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IAChE,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IAE/D,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,GAAG,uBAAuB,CAAC,CAAC;IACzE,OAAO,IAAI,CAAC,GAAG,CAAC,4BAA4B,EAAE,IAAI,CAAC,GAAG,CAAC,0BAA0B,EAAE,OAAO,CAAC,CAAC,CAAC;AAC/F,CAAC;AAED,2DAA2D;AAC3D,IAAI,UAAU,GAAmB,IAAI,CAAC;AAEtC;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7E,IAAI,GAAG,CAAC,WAAW,CAAC,KAAK,GAAG;QAAE,OAAO,KAAK,CAAC;IAC3C,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IAC/C,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IAE3C,4EAA4E;IAC5E,0EAA0E;IAC1E,wEAAwE;IACxE,6EAA6E;IAC7E,qEAAqE;IACrE,wEAAwE;IACxE,mDAAmD;IACnD,MAAM,KAAK,GAAG,SAAS,CAAC,aAAa,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE;QAC9D,KAAK,EAAE,QAAQ;QACf,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;IACH,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtC,UAAU,GAAG,KAAK,CAAC;QACnB,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE,mBAAmB,CAAC,EAAE;QACpE,QAAQ,EAAE,OAAO;QACjB,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAE7C,0EAA0E;IAC1E,iEAAiE;IACjE,UAAU,GAAG,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,UAAU,CAAC;IAE/D,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,sBAAsB;IACpC,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAwB,EAAE,aAAqB;IACpF,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvF,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC7C,OAAO,aAAa,SAAS,IAAI,SAAS,IAAI,KAAK,QAAQ,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,GAAG,CAAC;IACzC,OAAO,IAAI,KAAK,aAAa,CAAC;AAChC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,eAAe,CAC7B,SAAwB,EACxB,GAAW,EACX,IAAuB,EACvB,OAUI,EAAE;IAEN,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IAEpC,4EAA4E;IAC5E,2EAA2E;IAC3E,mEAAmE;IACnE,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3E,CAAC;IAED,uEAAuE;IACvE,kCAAkC;IAClC,EAAE;IACF,4EAA4E;IAC5E,yEAAyE;IACzE,EAAE;IACF,uEAAuE;IACvE,yEAAyE;IACzE,6EAA6E;IAC7E,mEAAmE;IACnE,oCAAoC;IACpC,2EAA2E;IAC3E,4BAA4B;IAC5B,EAAE;IACF,6EAA6E;IAC7E,0EAA0E;IAC1E,sEAAsE;IACtE,6DAA6D;IAC7D,4EAA4E;IAC5E,yEAAyE;IACzE,wEAAwE;IACxE,6CAA6C;IAC7C,EAAE;IACF,4EAA4E;IAC5E,4EAA4E;IAC5E,kEAAkE;IAClE,wEAAwE;IACxE,iEAAiE;IACjE,IAAI,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3E,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,0BAA0B,CAAC,GAAG,CAAC,CAAC;IACpE,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3E,CAAC;IAED,MAAM,WAAW,GAAG,kBAAkB,CAAC,IAAI,CAAC,WAAW,IAAI,QAAQ,EAAE,GAAG,IAAI,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;IACxF,6EAA6E;IAC7E,sEAAsE;IACtE,0EAA0E;IAC1E,6EAA6E;IAC7E,yEAAyE;IACzE,8EAA8E;IAC9E,4EAA4E;IAC5E,4EAA4E;IAC5E,4EAA4E;IAC5E,gDAAgD;IAChD,EAAE;IACF,6EAA6E;IAC7E,2EAA2E;IAC3E,qEAAqE;IACrE,6EAA6E;IAC7E,2EAA2E;IAC3E,2EAA2E;IAC3E,0BAA0B;IAC1B,MAAM,QAAQ,GAAG,sBAAsB,CAAC,SAAS,EAAE,IAAI,CAAC,aAAa,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,yIAAyI;IAElO,OAAO;QACL,GAAG,EAAE,aAAa;QAClB,IAAI,EAAE;YACJ,QAAQ;YACR,SAAS;YACT,iEAAiE;YACjE,4DAA4D;YAC5D,qEAAqE;YACrE,qEAAqE;YACrE,uEAAuE;YACvE,uEAAuE;YACvE,UAAU,QAAQ,EAAE;YACpB,SAAS;YACT,WAAW;YACX,IAAI;YACJ,aAAa,WAAW,GAAG;YAC3B,mEAAmE;YACnE,uEAAuE;YACvE,4DAA4D;YAC5D,IAAI;YACJ,iBAAiB;YACjB,IAAI;YACJ,GAAG;YACH,GAAG,IAAI;SACR;QACD,QAAQ,EAAE,IAAI;QACd,WAAW;QACX,QAAQ;KACT,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAAwB,EACxB,MAAyB,OAAO,CAAC,GAAG;IAEpC,0EAA0E;IAC1E,6EAA6E;IAC7E,gEAAgE;IAChE,MAAM,OAAO,GAAG,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;IACzF,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,WAAW,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACjE,OAAO,CACL,wDAAwD;QACxD,cAAc,OAAO,CAAC,WAAW,iDAAiD;QAClF,uFAAuF;QACvF,0DAA0D,cAAc,gBAAgB;QACxF,8CAA8C,WAAW,uBAAuB,CACjF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,2BAA2B,GAAsB;IACrD,sCAAsC;IACtC,wCAAwC;IACxC,0BAA0B;IAC1B,iCAAiC;IACjC,qCAAqC;CACtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAc,EAAE,aAAsB;IAC9E,IAAI,CAAC,aAAa,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QACpC,IAAI,2BAA2B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -69,7 +69,7 @@ export interface ToolCacheEntry {
69
69
  source: string;
70
70
  /** Git HEAD sha at execution time. */
71
71
  head: string | null;
72
- /** sha256 of `git status --porcelain` (uncommitted tree fingerprint). */
72
+ /** sha256 of `git diff HEAD` — CONTENT of the uncommitted tracked changes (gh#1452). */
73
73
  dirtyFingerprint: string | null;
74
74
  /**
75
75
  * Absolute, symlink-resolved path of the tree the tool actually ran in.
@@ -161,6 +161,24 @@ export interface ToolRunResult {
161
161
  * @task T12112 (gh#1220)
162
162
  */
163
163
  executionRoot: string;
164
+ /**
165
+ * The diagnostic line from CLEO's own confinement wrapper when it failed to
166
+ * START the tool, else `null`.
167
+ *
168
+ * gh#1397: `systemd-run --scope` is transparent on success — it exits with
169
+ * the wrapped command's status — but exits `1` when it cannot create the
170
+ * transient unit, and `1` is also what a suite with a failing test exits
171
+ * with. "The harness never started" and "the suite ran and was red" are
172
+ * therefore identical in `exitCode`, and CLEO reported both as
173
+ * `E_EVIDENCE_TOOL_FAILED`: a fast red suite. Five occurrences of gh#1396
174
+ * produced no diagnosis for exactly this reason.
175
+ *
176
+ * Non-null means the tool did NOT run, so nothing is cached and the caller
177
+ * must report an unavailable harness rather than a failing tool.
178
+ *
179
+ * @task T12116 (gh#1397)
180
+ */
181
+ harnessFailure: string | null;
164
182
  /** Full cache entry — useful for audit / debugging. */
165
183
  entry: ToolCacheEntry;
166
184
  }
@@ -437,44 +455,6 @@ export declare function isEntryUsable(entry: Partial<ToolCacheEntry>): boolean;
437
455
  * @task T1534
438
456
  */
439
457
  export declare function captureHead(projectRoot: string): Promise<string | null>;
440
- /**
441
- * Capture a fingerprint of the uncommitted TRACKED tree by sha256-hashing
442
- * `git status --porcelain=v1 --untracked-files=no`. Returns `null` for
443
- * non-git roots.
444
- *
445
- * Two repos with identical tracked content but different uncommitted edits
446
- * produce different fingerprints — so editing a tracked file before
447
- * re-verifying always invalidates the cache for tools sensitive to that file.
448
- *
449
- * ## Why untracked files are excluded (gh#1221)
450
- *
451
- * The fingerprint is captured BEFORE the tool spawns and is the key the
452
- * result is stored under. When untracked files were included, a tool that
453
- * emitted any untracked artifact changed the fingerprint that the NEXT call
454
- * computes — so the tool invalidated its own cache entry simply by running,
455
- * and the cache could never hit. This is not hypothetical or
456
- * multi-agent-specific: in this repo `coverage/`, `.vitest/` and `*.log` are
457
- * not gitignored, so a single suite run is enough. A one-line marker file is
458
- * enough.
459
- *
460
- * Excluding untracked files fixes that at the root, rather than maintaining a
461
- * per-project list of build-output paths that rots as tooling changes.
462
- *
463
- * TRADEOFF (deliberate): a brand-new UNTRACKED source or test file no longer
464
- * invalidates the cache either, so `cleo verify` can return a cached pass
465
- * that did not exercise it. Commit the file (HEAD moves, the key moves) or
466
- * force a fresh run with {@link RunToolOptions.bypassCache} — surfaced as
467
- * `CLEO_EVIDENCE_FRESH=1`. The alternative — a maintained exclude list — trades
468
- * a loud, documented staleness for a silent, per-project one.
469
- *
470
- * The cache directory itself (`.cleo/cache/`) and other CLEO-managed runtime
471
- * state (`.cleo/tasks.db`, `.cleo/brain.db`, journal/log files) remain
472
- * excluded via pathspec so a tracked-and-modified CLEO file cannot reintroduce
473
- * the same self-invalidation.
474
- *
475
- * @task T1534
476
- * @task T12112 (gh#1221)
477
- */
478
458
  export declare function captureDirtyFingerprint(projectRoot: string): Promise<string | null>;
479
459
  /**
480
460
  * Resolve the absolute path for a cache entry by key.
@@ -1 +1 @@
1
- {"version":3,"file":"tool-cache.d.ts","sourceRoot":"","sources":["../../src/tasks/tool-cache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-cache.d.ts","sourceRoot":"","sources":["../../src/tasks/tool-cache.ts"],"names":[],"mappings":"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"}