@mjasnikovs/pi-task 0.38.14 → 0.38.16

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 (62) hide show
  1. package/dist/shared/child-process.js +9 -16
  2. package/dist/task/accept-debt.d.ts +7 -5
  3. package/dist/task/accept-debt.js +16 -13
  4. package/dist/task/auto-orchestrator.js +38 -36
  5. package/dist/task/autofix-ledger.d.ts +113 -0
  6. package/dist/task/autofix-ledger.js +152 -0
  7. package/dist/task/boot-probe.d.ts +63 -1
  8. package/dist/task/boot-probe.js +98 -2
  9. package/dist/task/child-runner.d.ts +50 -6
  10. package/dist/task/child-runner.js +48 -69
  11. package/dist/task/command-run.d.ts +49 -6
  12. package/dist/task/command-run.js +154 -18
  13. package/dist/task/external-context.d.ts +9 -12
  14. package/dist/task/external-context.js +5 -5
  15. package/dist/task/failure-classifier.d.ts +9 -1
  16. package/dist/task/failure-classifier.js +9 -0
  17. package/dist/task/final-gate-fix.d.ts +22 -26
  18. package/dist/task/final-gate-fix.js +2 -7
  19. package/dist/task/final-gate.d.ts +10 -2
  20. package/dist/task/final-gate.js +49 -88
  21. package/dist/task/gate-deps.js +20 -13
  22. package/dist/task/orchestrator.d.ts +33 -24
  23. package/dist/task/orchestrator.js +66 -44
  24. package/dist/task/phases.d.ts +58 -34
  25. package/dist/task/phases.js +140 -113
  26. package/dist/task/plan-orchestrator.js +2 -2
  27. package/dist/task/repo-health-check.d.ts +21 -21
  28. package/dist/task/repo-health-check.js +43 -112
  29. package/dist/task/run-end.d.ts +77 -0
  30. package/dist/task/run-end.js +37 -0
  31. package/dist/task/run-final-gate.js +71 -79
  32. package/dist/task/task-gates.d.ts +8 -0
  33. package/dist/task/task-gates.js +23 -4
  34. package/dist/task/terminal-outcome.d.ts +1 -1
  35. package/dist/task/terminal-outcome.js +12 -0
  36. package/dist/workers/brave-search.d.ts +7 -0
  37. package/dist/workers/brave-search.js +36 -55
  38. package/dist/workers/ddg-search.d.ts +1 -1
  39. package/dist/workers/ddg-search.js +27 -47
  40. package/dist/workers/exa-search.d.ts +2 -2
  41. package/dist/workers/exa-search.js +53 -68
  42. package/dist/workers/html-clean.js +67 -88
  43. package/dist/workers/http-request.d.ts +74 -0
  44. package/dist/workers/http-request.js +103 -0
  45. package/dist/workers/npm-version.js +37 -42
  46. package/dist/workers/pi-worker-core.d.ts +13 -2
  47. package/dist/workers/pi-worker-core.js +12 -17
  48. package/dist/workers/pi-worker-docs.d.ts +1 -1
  49. package/dist/workers/pi-worker-docs.js +49 -68
  50. package/dist/workers/pi-worker-fetch.d.ts +1 -1
  51. package/dist/workers/pi-worker-fetch.js +20 -21
  52. package/dist/workers/pi-worker-search.js +6 -4
  53. package/dist/workers/pi-worker.js +5 -4
  54. package/dist/workers/search-core.d.ts +1 -1
  55. package/dist/workers/search-core.js +36 -42
  56. package/dist/workers/search-types.d.ts +13 -0
  57. package/dist/workers/search-types.js +27 -0
  58. package/dist/workers/shared.d.ts +51 -11
  59. package/dist/workers/shared.js +0 -0
  60. package/dist/workers/worker-channels.d.ts +60 -0
  61. package/dist/workers/worker-channels.js +98 -0
  62. package/package.json +1 -1
@@ -27,24 +27,156 @@
27
27
  * carried nine injectable probes for the gate's boot half while its command half
28
28
  * had none.
29
29
  */
30
- import { spawnSync } from 'node:child_process';
30
+ import { spawn } from 'node:child_process';
31
31
  import { isCommandNotFound, resolveRunner, runnerEnv } from './runner-resolve.js';
32
- /** The real runner. */
33
- export const spawnCommand = spec => {
34
- const r = spawnSync(spec.bin, spec.args, {
32
+ /**
33
+ * How much of ONE stream may be held in the HOST process, and how it is split.
34
+ *
35
+ * `spawnSync` bounded this at its 1 MB default `maxBuffer`. The async runner had
36
+ * no bound at all: two strings grew in the TUI's own process for as long as a
37
+ * command under the 900s cap kept talking.
38
+ *
39
+ * BOTH ENDS are kept, because both are read. `isCommandNotFound` and the two gap
40
+ * regexes match wording a runner prints FIRST; `outputTail` and every failure
41
+ * reason take the LAST 400 characters. A single-ended cap loses one of them.
42
+ */
43
+ const OUTPUT_HEAD_CAP = 256 * 1024;
44
+ const OUTPUT_TAIL_CAP = 768 * 1024;
45
+ /** One stream, bounded, keeping its head and its tail with the middle elided. */
46
+ class BoundedOutput {
47
+ head = '';
48
+ tail = '';
49
+ total = 0;
50
+ push(chunk) {
51
+ this.total += chunk.length;
52
+ let rest = chunk;
53
+ if (this.head.length < OUTPUT_HEAD_CAP) {
54
+ const room = OUTPUT_HEAD_CAP - this.head.length;
55
+ this.head += rest.slice(0, room);
56
+ rest = rest.slice(room);
57
+ }
58
+ if (rest.length === 0)
59
+ return;
60
+ this.tail = (this.tail + rest).slice(-OUTPUT_TAIL_CAP);
61
+ }
62
+ toString() {
63
+ const elided = this.total - this.head.length - this.tail.length;
64
+ return elided > 0 ?
65
+ `${this.head}\n…[${elided} characters elided]…\n${this.tail}`
66
+ : this.head + this.tail;
67
+ }
68
+ }
69
+ /**
70
+ * After the child EXITS, how long its pipes may still deliver buffered data
71
+ * before the run is reported. Not a wait for the pipes to CLOSE — that is the
72
+ * bug below — just the turn or two the reader needs to hand over what it has.
73
+ */
74
+ const DRAIN_MS = 50;
75
+ /**
76
+ * The real runner: one bounded child, output collected, never rejects.
77
+ *
78
+ * A kill — by the wall clock or by the caller's cancel — reads as `status: null`,
79
+ * which the gap ladder already treats as "nothing was observed".
80
+ *
81
+ * THE RUN SETTLES ON THE CHILD, NOT ON THE PIPE. `close` fires only once every
82
+ * stdio pipe has reached EOF, and a backgrounded grandchild INHERITS stdout: a
83
+ * seed script that starts a daemon, a build that leaves a watcher, a launch
84
+ * script. Waiting for `close` there is waiting for the grandchild, which no
85
+ * timeout can reach — SIGKILL goes to the direct child and the inherited pipe
86
+ * survives it. So `exit` settles the run, and the deadline settles it itself.
87
+ */
88
+ export const spawnCommand = spec => new Promise(resolve => {
89
+ const out = new BoundedOutput();
90
+ const err = new BoundedOutput();
91
+ let settled = false;
92
+ let exitStatus = null;
93
+ let exited = false;
94
+ let endedStreams = 0;
95
+ let drain;
96
+ const child = spawn(spec.bin, spec.args, {
35
97
  cwd: spec.cwd,
36
- encoding: 'utf8',
37
- timeout: spec.timeoutMs,
98
+ // stdin CLOSED. `spawnSync` gave the child none; the default `spawn`
99
+ // stdio is a live pipe nobody ever ends, so a check that reads stdin —
100
+ // a `cat`-style pipeline, a tool that prompts, a pager — blocked until
101
+ // the kill timer: 600s for repo-health, 900s for a gate command.
102
+ stdio: ['ignore', 'pipe', 'pipe'],
38
103
  ...(spec.env ? { env: spec.env } : {})
39
104
  });
40
- return {
41
- failedToStart: r.error !== undefined && r.error !== null,
42
- ...(r.error ? { failureMessage: r.error.message } : {}),
43
- status: r.status,
44
- stdout: r.stdout ?? '',
45
- stderr: r.stderr ?? ''
105
+ const done = (status, failure) => {
106
+ if (settled)
107
+ return;
108
+ settled = true;
109
+ clearTimeout(timer);
110
+ clearTimeout(drain);
111
+ spec.signal?.removeEventListener('abort', killAndSettle);
112
+ resolve({
113
+ failedToStart: failure !== undefined,
114
+ ...(failure === undefined ? {} : { failureMessage: failure }),
115
+ status,
116
+ stdout: out.toString(),
117
+ stderr: err.toString()
118
+ });
119
+ };
120
+ const expectedStreams = (child.stdout ? 1 : 0) + (child.stderr ? 1 : 0);
121
+ const settleIfDrained = () => {
122
+ if (exited && endedStreams >= expectedStreams)
123
+ done(exitStatus);
124
+ };
125
+ const kill = () => {
126
+ try {
127
+ child.kill('SIGKILL');
128
+ }
129
+ catch {
130
+ /* already gone */
131
+ }
132
+ };
133
+ /**
134
+ * The deadline and the cancel both END the run. The kill only reaches the
135
+ * direct child, so this cannot wait to observe its effect — it kills, gives
136
+ * the pipes one drain, and reports `status: null` regardless.
137
+ */
138
+ const killAndSettle = () => {
139
+ kill();
140
+ clearTimeout(drain);
141
+ drain = setTimeout(() => done(null), DRAIN_MS);
46
142
  };
47
- };
143
+ // NOT unref'd. With `spawnSync`'s own `timeout` gone this timer is the only
144
+ // bound left on every gate command, repo-health command and ACCEPT-debt
145
+ // re-run — and an unref'd timer is MEASURED in this repo never to fire at
146
+ // all on Windows (0/20s), which would leave all of them unbounded. It is
147
+ // cleared the moment the run settles, so it holds the loop open only while
148
+ // a command the caller is awaiting anyway is still running.
149
+ const timer = setTimeout(killAndSettle, spec.timeoutMs);
150
+ if (spec.signal) {
151
+ if (spec.signal.aborted)
152
+ killAndSettle();
153
+ else
154
+ spec.signal.addEventListener('abort', killAndSettle, { once: true });
155
+ }
156
+ child.stdout?.on('data', (d) => out.push(d.toString()));
157
+ child.stderr?.on('data', (d) => err.push(d.toString()));
158
+ child.stdout?.on('end', () => {
159
+ endedStreams++;
160
+ settleIfDrained();
161
+ });
162
+ child.stderr?.on('end', () => {
163
+ endedStreams++;
164
+ settleIfDrained();
165
+ });
166
+ child.on('error', (e) => done(null, e.message));
167
+ child.on('exit', (code) => {
168
+ exited = true;
169
+ exitStatus = code;
170
+ // Both ends of the same question: settle now if the pipes are already
171
+ // at EOF, otherwise settle after one short drain rather than waiting on
172
+ // whoever else is holding them.
173
+ settleIfDrained();
174
+ if (!settled) {
175
+ clearTimeout(drain);
176
+ drain = setTimeout(() => done(exitStatus), DRAIN_MS);
177
+ }
178
+ });
179
+ });
48
180
  /**
49
181
  * A non-zero exit whose output shows an EXTERNAL runtime dependency is missing, not
50
182
  * a code fault: a browser suite (Playwright/Cypress) whose browser binaries or system
@@ -116,14 +248,17 @@ export function outputTail(stdout, stderr, limit = 400) {
116
248
  * `gapPatterns` are the EXTRA output shapes this particular command may treat as
117
249
  * an environment gap (see INFRA_GAP_OUTPUT_RE). Empty for an ordinary check.
118
250
  */
119
- export function classifyCommandRun(run, gapPatterns = []) {
251
+ export function classifyCommandRun(run, gapPatterns = [], opts = {}) {
120
252
  // A clean exit is a pass before any gap shape is consulted: gap patterns
121
253
  // describe output, and passing output can legitimately mention a database or
122
254
  // a browser.
123
255
  if (!run.failedToStart && run.status === 0)
124
256
  return { outcome: 'pass' };
125
257
  const output = `${run.stdout}\n${run.stderr}`;
258
+ const runtimeGap = opts.runtimeGap ?? true;
126
259
  for (const rule of GAP_RULES) {
260
+ if (rule.id === 'missing-runtime' && !runtimeGap)
261
+ continue;
127
262
  if (rule.applies(run, output, gapPatterns)) {
128
263
  return { outcome: 'gap', gap: rule.id, detail: rule.detail(run) };
129
264
  }
@@ -160,19 +295,20 @@ function leadingBin(line) {
160
295
  * failure, missing tool, unreachable database, timeout, no POSIX shell — leaves the
161
296
  * debt exactly as open as it was.
162
297
  */
163
- export function runVerifyCommandLine(cwd, line, timeoutMs, extraGapRe,
298
+ export async function runVerifyCommandLine(cwd, line, timeoutMs, extraGapRe,
164
299
  /** The spawner. Injected so a re-run's outcome can be tested without one. */
165
- run = spawnCommand) {
300
+ run = spawnCommand, signal) {
166
301
  const bin = leadingBin(line);
167
302
  const runner = bin === null ? null : resolveRunner(bin);
168
303
  // A VERIFY line is a SHELL line, not an argv — env prefixes, `&&` and
169
304
  // redirects are all ordinary there — so the runner spawns `sh -c`.
170
- const verdict = classifyCommandRun(run({
305
+ const verdict = classifyCommandRun(await run({
171
306
  cwd,
172
307
  bin: 'sh',
173
308
  args: ['-c', line],
174
309
  timeoutMs,
175
- env: runner ? runnerEnv(runner) : { ...process.env }
310
+ env: runner ? runnerEnv(runner) : { ...process.env },
311
+ ...(signal === undefined ? {} : { signal })
176
312
  }),
177
313
  // Infrastructure counts as a gap on EVERY debt re-run, not only on
178
314
  // request: an unreachable database cannot tell us whether the code is
@@ -19,12 +19,16 @@
19
19
  * {@link ExternalContextLookups} — an adapter, expressible only since the
20
20
  * focused-extractor seam landed.
21
21
  */
22
- import { docsRaw } from '../workers/docs-core.js';
23
- import { fetchRaw } from '../workers/fetch-core.js';
24
- import { npmVersionLookup, type NpmVersionInfo } from '../workers/npm-version.js';
22
+ import { type NpmVersionInfo } from '../workers/npm-version.js';
25
23
  import type { SearchCoreInput, SearchCoreResult } from '../workers/search-core.js';
26
24
  import type { PhaseDeps } from './child-runner.js';
27
- type GatherDeps = Pick<PhaseDeps, 'cwd' | 'signal' | 'recordSubStep'>;
25
+ /**
26
+ * What assembling the block needs off the phase's deps: where it runs, when to
27
+ * stop, where to trail a sub-step — and, for the research binding, the four
28
+ * lookup seams. They are fields on `PhaseDeps` (not a second bag) because the
29
+ * bag was a trailing parameter no production caller could reach.
30
+ */
31
+ type GatherDeps = Pick<PhaseDeps, 'cwd' | 'signal' | 'recordSubStep' | 'docsRaw' | 'fetchRaw' | 'npmVersionLookup' | 'searchFn'>;
28
32
  /** What a target lookup contributes to the block. */
29
33
  export interface ExternalTargetResult {
30
34
  /** Emitted as an `### npm:` block ahead of every body. Absent for url targets. */
@@ -89,13 +93,6 @@ export interface ExternalContextPolicy {
89
93
  * is nothing to enrich (no targets, or every lookup failed).
90
94
  */
91
95
  export declare function buildExternalContext(source: string, deps: GatherDeps, lookups: ExternalContextLookups, policy?: ExternalContextPolicy): Promise<string>;
92
- /** Injectable workers so enrichment is testable without spawning real lookups. */
93
- export interface ExternalContextDeps {
94
- docsRaw?: typeof docsRaw;
95
- fetchRaw?: typeof fetchRaw;
96
- searchFn?: (input: SearchCoreInput) => Promise<SearchCoreResult>;
97
- npmVersionLookup?: typeof npmVersionLookup;
98
- }
99
96
  /**
100
97
  * The RESEARCH-phase binding: raw workers, no caps, live versions for every
101
98
  * named dep, truncated bodies, timed, and short-circuited when there is nothing
@@ -103,5 +100,5 @@ export interface ExternalContextDeps {
103
100
  *
104
101
  * Returns the `EXTERNAL CONTEXT\n…\n\n` block for the refined spec, or `''`.
105
102
  */
106
- export declare function gatherExternalContext(refined: string, deps: GatherDeps, researchDeps?: ExternalContextDeps): Promise<string>;
103
+ export declare function gatherExternalContext(refined: string, deps: GatherDeps): Promise<string>;
107
104
  export {};
@@ -108,10 +108,10 @@ export async function buildExternalContext(source, deps, lookups, policy = {}) {
108
108
  *
109
109
  * Returns the `EXTERNAL CONTEXT\n…\n\n` block for the refined spec, or `''`.
110
110
  */
111
- export async function gatherExternalContext(refined, deps, researchDeps = {}) {
112
- const docsRawFn = researchDeps.docsRaw ?? docsRaw;
113
- const fetchRawFn = researchDeps.fetchRaw ?? fetchRaw;
114
- const npmVersionFn = researchDeps.npmVersionLookup ?? npmVersionLookup;
111
+ export async function gatherExternalContext(refined, deps) {
112
+ const docsRawFn = deps.docsRaw ?? docsRaw;
113
+ const fetchRawFn = deps.fetchRaw ?? fetchRaw;
114
+ const npmVersionFn = deps.npmVersionLookup ?? npmVersionLookup;
115
115
  const docsQuery = refined.split('\n').find(l => l.trim()) ?? refined;
116
116
  return buildExternalContext(refined, deps, {
117
117
  docs: async (pkg) => {
@@ -135,7 +135,7 @@ export async function gatherExternalContext(refined, deps, researchDeps = {}) {
135
135
  const r = await fetchRawFn({ url, signal: deps.signal });
136
136
  return { body: r.markdown.slice(0, RAW_BODY_LIMIT) };
137
137
  },
138
- search: researchDeps.searchFn
138
+ search: deps.searchFn
139
139
  }, {
140
140
  versionLookup: pkg => npmVersionFn(pkg, { signal: deps.signal }),
141
141
  subStepLabel: 'enrichment',
@@ -12,4 +12,12 @@ export interface FailureClass {
12
12
  level: NotifyLevel;
13
13
  }
14
14
  export declare function classifyFailure(err: unknown, aborted: boolean): FailureClass;
15
- export declare function handleFailure(err: unknown, ctx: ExtensionCommandContext, cwd: string, id: string, aborted: boolean): Promise<void>;
15
+ /**
16
+ * Persist, flash and announce a failure — and RETURN the classification.
17
+ *
18
+ * It used to return `void`, so the name it had just computed died here and the
19
+ * caller learned how the run ended by re-reading the task file's front matter and
20
+ * narrowing it to a boolean. Handing the value back is what lets `TaskRunner.run`
21
+ * say `RunEnd` instead.
22
+ */
23
+ export declare function handleFailure(err: unknown, ctx: ExtensionCommandContext, cwd: string, id: string, aborted: boolean): Promise<FailureClass>;
@@ -74,6 +74,14 @@ export function classifyFailure(err, aborted) {
74
74
  level: 'error'
75
75
  };
76
76
  }
77
+ /**
78
+ * Persist, flash and announce a failure — and RETURN the classification.
79
+ *
80
+ * It used to return `void`, so the name it had just computed died here and the
81
+ * caller learned how the run ended by re-reading the task file's front matter and
82
+ * narrowing it to a boolean. Handing the value back is what lets `TaskRunner.run`
83
+ * say `RunEnd` instead.
84
+ */
77
85
  export async function handleFailure(err, ctx, cwd, id, aborted) {
78
86
  const c = classifyFailure(err, aborted);
79
87
  await updateTaskFrontMatter(cwd, id, { state: c.state, reason: c.reason });
@@ -82,4 +90,5 @@ export async function handleFailure(err, ctx, cwd, id, aborted) {
82
90
  // Mirror to remote viewers — ctx.ui.notify is terminal-only, so without this
83
91
  // the remote view shows nothing when a task fails.
84
92
  publishLifecycleNotice(`${id} ${c.notify}`, c.level);
93
+ return c;
85
94
  }
@@ -1,3 +1,4 @@
1
+ import type { FinalGateOutcome } from './final-gate.js';
1
2
  import { type TreeChangeSummary, type IgnoredSnapshot } from './write-guard.js';
2
3
  /** Same bounded-fix contract as lint-fix: edit in place, bash exists to RUN the
3
4
  * failing command (and the project's own tooling), not to mutate git state. */
@@ -97,22 +98,24 @@ export interface FinalFixResult {
97
98
  ok: boolean;
98
99
  /** Human-readable outcome (converged gate reason, or why the attempt failed). */
99
100
  reason: string;
100
- /** On a did-not-converge outcome: the FRESH gate failure, so the caller's next
101
- * picker (and next fix attempt) works from the current state, not the stale one. */
102
- gateReason?: string;
103
- /** The fresh gate's individual ranked failures (see FinalGateOutcome.failures),
104
- * so the caller can trail each entry — never just the first. */
105
- gateFailures?: string[];
106
- /** …and which of them a PROBE returned after OBSERVING (see
107
- * FinalGateOutcome.observedFailures). Carried so the caller's non-progress
108
- * classifier can ask the probe's own verdict instead of guessing from the
109
- * failure text a check that was observed to FAIL is never demote-eligible
110
- * (nexttask 19A). */
111
- gateObservedFailures?: string[];
112
- /** On a converged outcome: the re-run gate's UNOBSERVED note, if it observed
113
- * nothing dynamic (see FinalGateOutcome.unobserved). Carried so the caller
114
- * labels a converge-on-statics-alone the same way it labels a first-pass one —
115
- * "converged" must never quietly mean "we stopped being able to check". */
101
+ /**
102
+ * The re-run gate's OUTCOME, whole present whenever the gate actually ran
103
+ * (absent only when the fix child self-declared blocked and the re-run was
104
+ * skipped).
105
+ *
106
+ * This used to be four flattened `gate*` mirrors, and the flattening lost
107
+ * things. `openDebts` never crossed at all, so `runFinalGateStage` rebuilt
108
+ * `fin` as a literal three times and each literal dropped it — the recorded
109
+ * mx5 run-18 defect, fixed by RE-DERIVING the field (`reconcileDebts`) rather
110
+ * than by keeping the value. Then 19A had to push `observedFailures` across the
111
+ * same wall as a third parallel field and re-pair it downstream by
112
+ * `gateObservedFailures?.includes(detail)` — a membership test that exists only
113
+ * because the pairing was broken in transit.
114
+ */
115
+ gate?: FinalGateOutcome;
116
+ /** On a converged outcome: the UNOBSERVED note the CALLER should show. It is
117
+ * the gate's own note plus any downgrade this fix pass added (see the
118
+ * ignored-dependency probe below), so it is not simply `gate.unobserved`. */
116
119
  unobserved?: string;
117
120
  /** Gitignored path(s) this fix pass wrote, exempt classes already removed (see
118
121
  * write-guard.ts). Present whether or not the gate converged — the caller
@@ -141,16 +144,9 @@ export interface FinalFixDeps {
141
144
  /** Re-run the final integration gate — the only arbiter of convergence.
142
145
  * Converges only when the gate's FULL aggregated failure list is empty
143
146
  * (ok=true); `failures` rides through so the caller sees every entry. */
144
- gate: (cwd: string) => Promise<{
145
- ok: boolean;
146
- reason: string;
147
- failures?: string[];
148
- /** Which of them a probe returned after OBSERVING (nexttask 19A) — carried
149
- * through so the caller's demote decision can ask the probe's own verdict
150
- * instead of re-deriving observability from the failure string. */
151
- observedFailures?: string[];
152
- unobserved?: string;
153
- }>;
147
+ /** Returns the gate's own outcome type, not a structural copy of five of its
148
+ * fields — a re-declaration is how `openDebts` came to be silently absent. */
149
+ gate: (cwd: string) => Promise<FinalGateOutcome>;
154
150
  /** Labels of every currently-discoverable gate command (static + integration),
155
151
  * for the shrink guard. Pure discovery — nothing is executed. */
156
152
  discoverLabels: (cwd: string) => string[];
@@ -334,13 +334,7 @@ export async function runFinalGateAutofix(deps) {
334
334
  }
335
335
  const fin = await deps.gate(deps.cwd);
336
336
  if (!fin.ok) {
337
- return withIgnored({
338
- ok: false,
339
- reason: `did not converge: ${fin.reason}`,
340
- gateReason: fin.reason,
341
- gateFailures: fin.failures,
342
- ...(fin.observedFailures ? { gateObservedFailures: fin.observedFailures } : {})
343
- });
337
+ return withIgnored({ ok: false, reason: `did not converge: ${fin.reason}`, gate: fin });
344
338
  }
345
339
  // IGNORED-DEPENDENCY DOWNGRADE (mx5 run 19). The gate says PASS; the question
346
340
  // this answers is whether that PASS belongs to the REPOSITORY or only to this
@@ -368,6 +362,7 @@ export async function runFinalGateAutofix(deps) {
368
362
  return withIgnored({
369
363
  ok: true,
370
364
  reason: fin.reason,
365
+ gate: fin,
371
366
  ...(notes.length > 0 ? { unobserved: notes.join(' ') } : {}),
372
367
  ...(ignoredDependent !== undefined ? { ignoredDependent } : {})
373
368
  });
@@ -1,6 +1,6 @@
1
1
  import { type HealthCommand } from './repo-health-check.js';
2
2
  import { deriveOpenDebts, rerunDebtVerifyCommand, type AcceptDebt } from './accept-debt.js';
3
- import { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners, type BootDeps } from './boot-probe.js';
3
+ import { discoverBootCommand, detectsServedApp, runBootCheck, runBootSection, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners, type BootDeps } from './boot-probe.js';
4
4
  import { type CommandRunner } from './command-run.js';
5
5
  import { taskThatIntroduced } from './task-provenance.js';
6
6
  import { type EnvClosure } from './env-template-closure.js';
@@ -133,8 +133,9 @@ export declare function discoverGateCommandBodies(cwd: string): Record<string, s
133
133
  export { runVerifyCommandLine, type VerifyRerunOutcome } from './command-run.js';
134
134
  export { observabilityGapFailure, unobservedVerdict };
135
135
  export { taskThatIntroduced };
136
- export { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
136
+ export { discoverBootCommand, detectsServedApp, runBootCheck, runBootSection, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
137
137
  export type { BootDeps };
138
+ export type { BootSectionVerdict } from './boot-probe.js';
138
139
  export { deriveOpenDebts, rerunDebtVerifyCommand };
139
140
  /**
140
141
  * Where in the gate a closure scan runs. The two stages are NOT interchangeable
@@ -259,5 +260,12 @@ export interface FinalGateOptions {
259
260
  envClosure?: (cwd: string) => EnvClosure;
260
261
  /** The repo's tracked file list, or null when it cannot be determined. */
261
262
  trackedFiles?: (cwd: string) => string[] | null;
263
+ /**
264
+ * The run's cancel. Reaches every command the gate spawns — repo-health, the
265
+ * lockfile/integration/launch sections and the ACCEPT-debt re-runs. Nothing
266
+ * could be cancelled while `CommandRunner` was synchronous: the event loop
267
+ * never got a turn in which to notice.
268
+ */
269
+ signal?: AbortSignal;
262
270
  }
263
271
  export declare function runFinalIntegrationGate(cwd: string, opts?: FinalGateOptions): Promise<FinalGateOutcome>;