@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.
- package/dist/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +16 -13
- package/dist/task/auto-orchestrator.js +38 -36
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +63 -1
- package/dist/task/boot-probe.js +98 -2
- package/dist/task/child-runner.d.ts +50 -6
- package/dist/task/child-runner.js +48 -69
- package/dist/task/command-run.d.ts +49 -6
- package/dist/task/command-run.js +154 -18
- package/dist/task/external-context.d.ts +9 -12
- package/dist/task/external-context.js +5 -5
- package/dist/task/failure-classifier.d.ts +9 -1
- package/dist/task/failure-classifier.js +9 -0
- package/dist/task/final-gate-fix.d.ts +22 -26
- package/dist/task/final-gate-fix.js +2 -7
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +49 -88
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +66 -44
- package/dist/task/phases.d.ts +58 -34
- package/dist/task/phases.js +140 -113
- package/dist/task/plan-orchestrator.js +2 -2
- package/dist/task/repo-health-check.d.ts +21 -21
- package/dist/task/repo-health-check.js +43 -112
- package/dist/task/run-end.d.ts +77 -0
- package/dist/task/run-end.js +37 -0
- package/dist/task/run-final-gate.js +71 -79
- package/dist/task/task-gates.d.ts +8 -0
- package/dist/task/task-gates.js +23 -4
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/workers/brave-search.d.ts +7 -0
- package/dist/workers/brave-search.js +36 -55
- package/dist/workers/ddg-search.d.ts +1 -1
- package/dist/workers/ddg-search.js +27 -47
- package/dist/workers/exa-search.d.ts +2 -2
- package/dist/workers/exa-search.js +53 -68
- package/dist/workers/html-clean.js +67 -88
- package/dist/workers/http-request.d.ts +74 -0
- package/dist/workers/http-request.js +103 -0
- package/dist/workers/npm-version.js +37 -42
- package/dist/workers/pi-worker-core.d.ts +13 -2
- package/dist/workers/pi-worker-core.js +12 -17
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +49 -68
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +20 -21
- package/dist/workers/pi-worker-search.js +6 -4
- package/dist/workers/pi-worker.js +5 -4
- package/dist/workers/search-core.d.ts +1 -1
- package/dist/workers/search-core.js +36 -42
- package/dist/workers/search-types.d.ts +13 -0
- package/dist/workers/search-types.js +27 -0
- package/dist/workers/shared.d.ts +51 -11
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-channels.d.ts +60 -0
- package/dist/workers/worker-channels.js +98 -0
- package/package.json +1 -1
package/dist/task/command-run.js
CHANGED
|
@@ -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 {
|
|
30
|
+
import { spawn } from 'node:child_process';
|
|
31
31
|
import { isCommandNotFound, resolveRunner, runnerEnv } from './runner-resolve.js';
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
112
|
-
const docsRawFn =
|
|
113
|
-
const fetchRawFn =
|
|
114
|
-
const npmVersionFn =
|
|
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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
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>;
|