@mjasnikovs/pi-task 0.38.15 → 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
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
* environment gap, not a code fault, so that command is SKIPPED — only a command that
|
|
30
30
|
* actually ran and returned non-zero fails the check.
|
|
31
31
|
*/
|
|
32
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
33
32
|
import { existsSync, readFileSync } from 'node:fs';
|
|
34
33
|
import * as path from 'node:path';
|
|
35
|
-
import { resolveRunner, runnerEnv
|
|
34
|
+
import { resolveRunner, runnerEnv } from './runner-resolve.js';
|
|
35
|
+
import { classifyCommandRun, spawnCommand } from './command-run.js';
|
|
36
36
|
/** How much of a failing command's output to keep — bounded so a wedged tool that
|
|
37
37
|
* spews megabytes cannot bloat the trail. stderr leads (a crash trace lives there). */
|
|
38
38
|
const HEALTH_OUTPUT_MAX_LINES = 40;
|
|
@@ -106,141 +106,72 @@ export function discoverHealthCommands(cwd) {
|
|
|
106
106
|
}
|
|
107
107
|
return { ecosystem: null, cmds: [] };
|
|
108
108
|
}
|
|
109
|
-
/**
|
|
110
|
-
* Verdict for ONE finished command: 'skip' (environment gap — cannot conclude),
|
|
111
|
-
* 'pass', or the FAIL outcome. Shared by the sync and async runners so their
|
|
112
|
-
* semantics cannot drift apart — the async runner exists only to stop blocking the
|
|
113
|
-
* event loop, and a behaviour difference between the two would be a silent gate
|
|
114
|
-
* change rather than a UI fix.
|
|
115
|
-
*/
|
|
116
|
-
function classifyHealthRun(bin, args, ecosystem, r) {
|
|
117
|
-
// Tool missing (ENOENT) or killed by timeout → cannot conclude; skip it.
|
|
118
|
-
if (r.failedToStart || r.status === null)
|
|
119
|
-
return 'skip';
|
|
120
|
-
// "Command not found" INSIDE the script chain (e.g. `bun run lint` before
|
|
121
|
-
// node_modules exists — seen live failing TASK_0001's first verify). Same
|
|
122
|
-
// environment gap as ENOENT, just surfaced through the runner's shell —
|
|
123
|
-
// as exit 127 where a posix shell ran it, else by the runner's own wording
|
|
124
|
-
// (Windows bun reports the miss itself and exits 1).
|
|
125
|
-
if (isCommandNotFound(r.status, `${r.stdout ?? ''}\n${r.stderr ?? ''}`))
|
|
126
|
-
return 'skip';
|
|
127
|
-
if (r.status !== 0) {
|
|
128
|
-
return {
|
|
129
|
-
ok: false,
|
|
130
|
-
reason: `\`${bin} ${args.join(' ')}\` exited ${r.status}`,
|
|
131
|
-
ecosystem,
|
|
132
|
-
output: captureHealthOutput(r.stdout, r.stderr)
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
return 'pass';
|
|
136
|
-
}
|
|
137
109
|
/** The nothing-to-run outcome, shared by both runners. */
|
|
138
110
|
function noCommandOutcome(ecosystem) {
|
|
139
111
|
return { ok: true, reason: 'no repo-wide static-analysis command found', ecosystem, output: '' };
|
|
140
112
|
}
|
|
141
113
|
/**
|
|
142
114
|
* Run the discovered static checks whole-repo and let the real exit codes decide.
|
|
143
|
-
* Deterministic and synchronous under the hood (a wrapper keeps the caller async).
|
|
144
115
|
*
|
|
145
116
|
* - No manifest / no static command → ok (nothing can regress).
|
|
146
|
-
* - A command that CANNOT run (ENOENT / null exit
|
|
117
|
+
* - A command that CANNOT run (ENOENT / null exit / 127 inside the chain) → skipped,
|
|
147
118
|
* treated as an environment gap, not a fault.
|
|
148
119
|
* - A command that ran and exited non-zero → the first such failure is returned.
|
|
149
120
|
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
121
|
+
* This module owns DISCOVERY and its own output policy; running a command and
|
|
122
|
+
* deciding what its ending MEANS is `command-run.ts`'s. It used to own those too —
|
|
123
|
+
* `HealthRun`, `classifyHealthRun` and `spawnHealthCommand` were a second statement
|
|
124
|
+
* of the gap ladder, with no injectable runner, so every classification case in the
|
|
125
|
+
* suite spawned a real shell. `command-run.ts`'s own header notes that this module
|
|
126
|
+
* "had solved exactly this shape years earlier" and the gate never adopted it; this
|
|
127
|
+
* is the adoption, in the other direction.
|
|
152
128
|
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
129
|
+
* `captureHealthOutput` stays this module's own: 40 lines of a linter's report is a
|
|
130
|
+
* real difference from `outputTail`'s 400 characters, and that is a parameter, not a
|
|
131
|
+
* thing to unify.
|
|
132
|
+
*
|
|
133
|
+
* `onCommand` lets the caller name the running command in a live status line — the
|
|
134
|
+
* gate runs this immediately after the implementation turn ends, when the impl
|
|
135
|
+
* widget has just been cleared.
|
|
157
136
|
*/
|
|
158
|
-
export function runRepoHealthCheck(cwd,
|
|
137
|
+
export async function runRepoHealthCheck(cwd, opts = {}) {
|
|
159
138
|
const { ecosystem, cmds } = discoverHealthCommands(cwd);
|
|
160
139
|
if (!ecosystem || cmds.length === 0)
|
|
161
140
|
return noCommandOutcome(ecosystem);
|
|
141
|
+
const run = opts.run ?? spawnCommand;
|
|
162
142
|
for (const [bin, args] of cmds) {
|
|
143
|
+
opts.onCommand?.(`${bin} ${args.join(' ')}`);
|
|
163
144
|
// Runner resolution (mx5 run 16): a PATH-stripped environment must not
|
|
164
145
|
// silently skip the statics when the runner sits at a known install
|
|
165
146
|
// location; the resolved dir also rides on PATH for the script chain.
|
|
166
147
|
const runner = resolveRunner(bin);
|
|
167
|
-
const r =
|
|
148
|
+
const r = await run({
|
|
168
149
|
cwd,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
150
|
+
bin: runner.bin,
|
|
151
|
+
args,
|
|
152
|
+
timeoutMs: opts.timeoutMs ?? 600_000,
|
|
153
|
+
env: runnerEnv(runner),
|
|
154
|
+
...(opts.signal === undefined ? {} : { signal: opts.signal })
|
|
172
155
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
156
|
+
// The DECISION comes from the shared ladder; the OUTPUT is this module's own
|
|
157
|
+
// policy. `captureHealthOutput` keeps 40 lines of a linter's report where the
|
|
158
|
+
// ladder's `tail` keeps 400 characters, and that difference is real — a
|
|
159
|
+
// truncated lint report is unactionable. So the run is classified, not
|
|
160
|
+
// consumed: the verdict decides, the raw streams are what we show.
|
|
161
|
+
// `runtimeGap: false` — this ladder is NARROWER than the gate's. The
|
|
162
|
+
// browser/runtime row was written for the gate's TEST commands; here the
|
|
163
|
+
// commands are lint and typecheck, and its pattern matches ordinary
|
|
164
|
+
// English, so a genuine report quoting "browsers are not installed" would
|
|
165
|
+
// skip the static check and certify the repo healthy.
|
|
166
|
+
const verdict = classifyCommandRun(r, [], { runtimeGap: false });
|
|
167
|
+
if (verdict.outcome !== 'fail')
|
|
180
168
|
continue;
|
|
181
|
-
return
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
* result shape (status null when killed, failedToStart on ENOENT). */
|
|
187
|
-
function spawnHealthCommand(bin, args, cwd, timeoutMs, signal) {
|
|
188
|
-
return new Promise(resolve => {
|
|
189
|
-
const runner = resolveRunner(bin);
|
|
190
|
-
let stdout = '';
|
|
191
|
-
let stderr = '';
|
|
192
|
-
let settled = false;
|
|
193
|
-
const child = spawn(runner.bin, args, { cwd, env: runnerEnv(runner) });
|
|
194
|
-
const done = (r) => {
|
|
195
|
-
if (settled)
|
|
196
|
-
return;
|
|
197
|
-
settled = true;
|
|
198
|
-
clearTimeout(timer);
|
|
199
|
-
signal?.removeEventListener('abort', onAbort);
|
|
200
|
-
resolve(r);
|
|
201
|
-
};
|
|
202
|
-
const kill = () => {
|
|
203
|
-
try {
|
|
204
|
-
child.kill('SIGKILL');
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
/* already gone */
|
|
208
|
-
}
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
reason: `\`${bin} ${args.join(' ')}\` exited ${verdict.status}`,
|
|
172
|
+
ecosystem,
|
|
173
|
+
output: captureHealthOutput(r.stdout, r.stderr)
|
|
209
174
|
};
|
|
210
|
-
const timer = setTimeout(kill, timeoutMs);
|
|
211
|
-
timer.unref?.();
|
|
212
|
-
const onAbort = () => kill();
|
|
213
|
-
signal?.addEventListener('abort', onAbort, { once: true });
|
|
214
|
-
child.stdout?.on('data', (d) => {
|
|
215
|
-
stdout += d.toString();
|
|
216
|
-
});
|
|
217
|
-
child.stderr?.on('data', (d) => {
|
|
218
|
-
stderr += d.toString();
|
|
219
|
-
});
|
|
220
|
-
child.on('error', () => done({ failedToStart: true, status: null, stdout, stderr }));
|
|
221
|
-
child.on('close', (code) => done({ failedToStart: false, status: code, stdout, stderr }));
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Same check, same verdicts, without blocking the event loop.
|
|
226
|
-
*
|
|
227
|
-
* The gate runs this immediately after the implementation turn ends, when the impl
|
|
228
|
-
* widget has just been cleared — the sync version froze the whole TUI there for the
|
|
229
|
-
* duration of the project's lint (MEASURED: 0 of 686 expected 100ms timer ticks
|
|
230
|
-
* fired during a 69s aiz-client run), so no spinner, clock or queued notify could
|
|
231
|
-
* paint. `onCommand` lets the caller name the running command in a live status line.
|
|
232
|
-
*/
|
|
233
|
-
export async function runRepoHealthCheckAsync(cwd, opts = {}) {
|
|
234
|
-
const { ecosystem, cmds } = discoverHealthCommands(cwd);
|
|
235
|
-
if (!ecosystem || cmds.length === 0)
|
|
236
|
-
return noCommandOutcome(ecosystem);
|
|
237
|
-
for (const [bin, args] of cmds) {
|
|
238
|
-
opts.onCommand?.(`${bin} ${args.join(' ')}`);
|
|
239
|
-
const r = await spawnHealthCommand(bin, args, cwd, opts.timeoutMs ?? 600_000, opts.signal);
|
|
240
|
-
const verdict = classifyHealthRun(bin, args, ecosystem, r);
|
|
241
|
-
if (verdict === 'skip' || verdict === 'pass')
|
|
242
|
-
continue;
|
|
243
|
-
return verdict;
|
|
244
175
|
}
|
|
245
176
|
return { ok: true, reason: `${ecosystem}: static checks passed`, ecosystem, output: '' };
|
|
246
177
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-end — how a single /task run ENDED, named once.
|
|
3
|
+
*
|
|
4
|
+
* `TaskRunner.run` returned `void` and never threw, so `runSingleTask` learned
|
|
5
|
+
* what it had just done by RE-READING the task file's front matter, narrowing
|
|
6
|
+
* that to `ok: boolean`, and smuggling the rest out of the `withSession` closure
|
|
7
|
+
* through three mutable captures. Both commands then re-derived a cause the
|
|
8
|
+
* runner already had: `classifyFailure` names the ending exactly, and
|
|
9
|
+
* `handleFailure` threw the name away.
|
|
10
|
+
*
|
|
11
|
+
* The live consequence was a wrong report. `/task-cancel` during a gated run
|
|
12
|
+
* writes `cancelled` to the file; `ok` is `state === 'completed'`, so it is
|
|
13
|
+
* false; `!res.ok` calls `markResumable`, which overwrites `cancelled` with
|
|
14
|
+
* `failed`, and announces a red *"stopped — fix and run /task-resume"*.
|
|
15
|
+
* `/task-auto` hits the same arm for the same input, because its cancel branch
|
|
16
|
+
* only consults a module global that `/task-cancel` never sets.
|
|
17
|
+
*
|
|
18
|
+
* The file is still written — it is what a RESUME reads. What changed is that it
|
|
19
|
+
* is no longer the channel this process uses to talk to itself.
|
|
20
|
+
*/
|
|
21
|
+
/** Why a run stopped. Exactly one of these is true of any finished run. */
|
|
22
|
+
export type RunEnd =
|
|
23
|
+
/** Every phase ran and the spec was delivered. */
|
|
24
|
+
{
|
|
25
|
+
kind: 'completed';
|
|
26
|
+
}
|
|
27
|
+
/** The user cancelled — via /task-cancel, ESC, or an aborted signal. */
|
|
28
|
+
| {
|
|
29
|
+
kind: 'cancelled';
|
|
30
|
+
}
|
|
31
|
+
/** A phase threw. `reason` is `FailureClass.reason`, already trimmed. */
|
|
32
|
+
| {
|
|
33
|
+
kind: 'failed';
|
|
34
|
+
reason?: string;
|
|
35
|
+
}
|
|
36
|
+
/** The implementation turn was interrupted and left resumable. */
|
|
37
|
+
| {
|
|
38
|
+
kind: 'interrupted';
|
|
39
|
+
}
|
|
40
|
+
/** No fresh session could be started, so nothing ran at all. */
|
|
41
|
+
| {
|
|
42
|
+
kind: 'no-session';
|
|
43
|
+
};
|
|
44
|
+
export type RunEndKind = RunEnd['kind'];
|
|
45
|
+
/**
|
|
46
|
+
* What a command does about each ending.
|
|
47
|
+
*
|
|
48
|
+
* `resumable` and `announce` are the two facts the two hand-written ladders
|
|
49
|
+
* disagreed about, and the disagreement is where the cancel bug lived: a
|
|
50
|
+
* `cancelled` run was falling into the `failed` arm and being marked resumable.
|
|
51
|
+
* The WORDING stays per-command — `/task` says "resume with /task-resume" where
|
|
52
|
+
* `/task-auto` says "/task-auto-resume" — so only the policy is shared.
|
|
53
|
+
*/
|
|
54
|
+
export interface RunEndPolicy {
|
|
55
|
+
/** Mark the task resumable (overwrites its state with `failed`). */
|
|
56
|
+
resumable: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Does this ending FAIL the plan that contains the task?
|
|
59
|
+
*
|
|
60
|
+
* Strictly narrower than `resumable`, and the distinction is load-bearing: a
|
|
61
|
+
* declined-steer interrupt leaves the inner task resumable but the PLAN in
|
|
62
|
+
* progress, so `/task-auto-resume` re-delivers that task's spec. A fault fails
|
|
63
|
+
* the plan too. Only `/task-auto` reads this — a bare `/task` has no plan.
|
|
64
|
+
*/
|
|
65
|
+
failsRun: boolean;
|
|
66
|
+
/** The notify level for this ending. */
|
|
67
|
+
level: 'info' | 'warning' | 'error';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The one table. A run that the USER stopped is not resumable-as-failed: its
|
|
71
|
+
* file already says `cancelled`, and rewriting that to `failed` both lies in the
|
|
72
|
+
* ledger and turns a deliberate stop into a red error the user has to read as a
|
|
73
|
+
* fault.
|
|
74
|
+
*/
|
|
75
|
+
export declare const RUN_END_POLICY: Record<RunEndKind, RunEndPolicy>;
|
|
76
|
+
/** Did the run deliver a spec? The single question the old `ok` boolean answered. */
|
|
77
|
+
export declare function runSucceeded(end: RunEnd): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-end — how a single /task run ENDED, named once.
|
|
3
|
+
*
|
|
4
|
+
* `TaskRunner.run` returned `void` and never threw, so `runSingleTask` learned
|
|
5
|
+
* what it had just done by RE-READING the task file's front matter, narrowing
|
|
6
|
+
* that to `ok: boolean`, and smuggling the rest out of the `withSession` closure
|
|
7
|
+
* through three mutable captures. Both commands then re-derived a cause the
|
|
8
|
+
* runner already had: `classifyFailure` names the ending exactly, and
|
|
9
|
+
* `handleFailure` threw the name away.
|
|
10
|
+
*
|
|
11
|
+
* The live consequence was a wrong report. `/task-cancel` during a gated run
|
|
12
|
+
* writes `cancelled` to the file; `ok` is `state === 'completed'`, so it is
|
|
13
|
+
* false; `!res.ok` calls `markResumable`, which overwrites `cancelled` with
|
|
14
|
+
* `failed`, and announces a red *"stopped — fix and run /task-resume"*.
|
|
15
|
+
* `/task-auto` hits the same arm for the same input, because its cancel branch
|
|
16
|
+
* only consults a module global that `/task-cancel` never sets.
|
|
17
|
+
*
|
|
18
|
+
* The file is still written — it is what a RESUME reads. What changed is that it
|
|
19
|
+
* is no longer the channel this process uses to talk to itself.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The one table. A run that the USER stopped is not resumable-as-failed: its
|
|
23
|
+
* file already says `cancelled`, and rewriting that to `failed` both lies in the
|
|
24
|
+
* ledger and turns a deliberate stop into a red error the user has to read as a
|
|
25
|
+
* fault.
|
|
26
|
+
*/
|
|
27
|
+
export const RUN_END_POLICY = {
|
|
28
|
+
completed: { resumable: false, failsRun: false, level: 'info' },
|
|
29
|
+
cancelled: { resumable: false, failsRun: false, level: 'warning' },
|
|
30
|
+
interrupted: { resumable: true, failsRun: false, level: 'warning' },
|
|
31
|
+
failed: { resumable: true, failsRun: true, level: 'error' },
|
|
32
|
+
'no-session': { resumable: false, failsRun: false, level: 'warning' }
|
|
33
|
+
};
|
|
34
|
+
/** Did the run deliver a spec? The single question the old `ok` boolean answered. */
|
|
35
|
+
export function runSucceeded(end) {
|
|
36
|
+
return end.kind === 'completed';
|
|
37
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AutofixLedger } from './autofix-ledger.js';
|
|
1
2
|
import { describeDebt, recordDebt } from './accept-debt.js';
|
|
2
3
|
import { cancelCheckpoint } from './cancel-points.js';
|
|
3
4
|
import { SessionUI } from '../remote/bridge.js';
|
|
@@ -5,7 +6,9 @@ import { isYoloMode, yoloFinalGateChoice, YOLO_STAMP } from './yolo.js';
|
|
|
5
6
|
import { ignoredWriteTrailLine, ignoredWriteDebtReason } from './write-guard.js';
|
|
6
7
|
import { readOwnedRequirements } from './requirements.js';
|
|
7
8
|
import { unclaimedPendingRequirements } from './owned-freeze-reassign.js';
|
|
8
|
-
|
|
9
|
+
// `final-gate-progress.ts`'s five pure functions are `AutofixLedger`'s internals
|
|
10
|
+
// now — each was called from exactly ONE site inside this loop, extracted for
|
|
11
|
+
// testability while the ordering and carry-forward decisions stayed out here.
|
|
9
12
|
import { classifyFinalGateAnswer, MAX_FINAL_GATE_AUTOFIX, FINAL_LEAVE_LABEL, FINAL_LEAVE_VALUE, FINAL_ACCEPT_LABEL, FINAL_ACCEPT_VALUE, FINAL_AUTOFIX_LABEL, FINAL_AUTOFIX_VALUE, STRANDED_FIX_COMMIT, strandedFixNote } from './final-gate-fix.js';
|
|
10
13
|
/**
|
|
11
14
|
* Show the run-end picker and return the raw answer. Card ORDER is fixed —
|
|
@@ -227,45 +230,34 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
227
230
|
// pass + gate re-run — run 7's gap: the picker had NO automated fix path) /
|
|
228
231
|
// Accept. The user always decides; after MAX_FINAL_GATE_AUTOFIX attempts that
|
|
229
232
|
// still FAIL the autofix card is withdrawn so the loop cannot run unbounded.
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
// 4 item 3, run 14 item 2b).
|
|
239
|
-
let stranded = [];
|
|
233
|
+
// What this loop RECORDS, and the decisions that record makes: the attempt
|
|
234
|
+
// count and its bound, the accumulated gitignored writes, the stranded
|
|
235
|
+
// sub-fixes, the previous failure signature, the demoted set and the
|
|
236
|
+
// rejected-edits flag. Six closure-threaded locals before, and the
|
|
237
|
+
// non-progress rule applied downstream from the evidence it judges — the shape
|
|
238
|
+
// `final-gate-progress.ts`'s own comment names as the run-21 defect.
|
|
239
|
+
// `GateTally`'s twin, one altitude up (autofix-ledger.ts).
|
|
240
|
+
const ledger = new AutofixLedger(MAX_FINAL_GATE_AUTOFIX);
|
|
240
241
|
const refreshStranded = async () => {
|
|
241
242
|
if (!deps.pendingChanges)
|
|
242
243
|
return;
|
|
243
244
|
try {
|
|
244
|
-
|
|
245
|
+
ledger.setStranded(await deps.pendingChanges(cwd));
|
|
245
246
|
}
|
|
246
247
|
catch {
|
|
247
248
|
// Inconclusive: say nothing rather than claim a clean tree.
|
|
248
|
-
|
|
249
|
+
ledger.setStranded([]);
|
|
249
250
|
}
|
|
250
251
|
};
|
|
251
|
-
// NON-PROGRESS / UNFALSIFIABLE-CHECK state (mx5 run 14 item 2a). `prevFailSig` is
|
|
252
|
-
// the previous attempt's normalized ranked-first failure; `demoted` holds the
|
|
253
|
-
// signatures already carried as debt, so a re-run that still reports them does not
|
|
254
|
-
// re-fail the gate.
|
|
255
|
-
let prevFailSig = null;
|
|
256
|
-
const demoted = new Set();
|
|
257
|
-
// Set when a write-guard rejected an attempt whose edits could NOT be discarded:
|
|
258
|
-
// REJECTED edits are then sitting in the tree and must never be committed by the
|
|
259
|
-
// terminal paths below.
|
|
260
|
-
let rejectedEditsInTree = false;
|
|
261
252
|
// Commit whatever guard-clean repairs the fix passes left, on ANY terminal
|
|
262
253
|
// non-converged outcome. Run 14 ended on LEAVE with 13 real repairs dirty in the
|
|
263
254
|
// tree after an unattended run — the next checkout would have destroyed them
|
|
264
255
|
// silently.
|
|
265
256
|
const commitStranded = async (outcome) => {
|
|
257
|
+
const stranded = ledger.stranded();
|
|
266
258
|
if (stranded.length === 0)
|
|
267
259
|
return;
|
|
268
|
-
if (
|
|
260
|
+
if (!ledger.mayCommitTree()) {
|
|
269
261
|
await recGate(`final-gate: NOT committing ${stranded.length} working-tree change(s) — a `
|
|
270
262
|
+ `write-guard rejected an attempt and its edits could not be discarded, `
|
|
271
263
|
+ `so the tree holds REJECTED edits: ${stranded.slice(0, 8).join(', ')}`);
|
|
@@ -300,20 +292,20 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
300
292
|
}
|
|
301
293
|
};
|
|
302
294
|
while (!fin.ok) {
|
|
303
|
-
const canAutofix = deps.finalGateFix !== undefined &&
|
|
295
|
+
const canAutofix = deps.finalGateFix !== undefined && ledger.canAutofix();
|
|
304
296
|
// The picker question shows the debts (the HUMAN weighs them); the autofix seed
|
|
305
297
|
// below deliberately does not — mx5 run 11's fix child executed a debt claim as
|
|
306
298
|
// an `rm` instruction.
|
|
307
299
|
const question = `Final integration gate FAILED for ${id}.\n\n${fin.reason}${fin.debtNote ?? ''}\n\n`
|
|
308
300
|
+ 'All tasks are checked off — this is the whole-repo check '
|
|
309
301
|
+ '(the project’s own test/build/static commands, run unaided).'
|
|
310
|
-
+ (
|
|
311
|
-
`\n\nAutofix attempts so far: ${
|
|
302
|
+
+ (ledger.attempts() > 0 ?
|
|
303
|
+
`\n\nAutofix attempts so far: ${ledger.attempts()}/${MAX_FINAL_GATE_AUTOFIX}.`
|
|
312
304
|
: '')
|
|
313
305
|
// Never let a partial repair be invisible at the moment the human decides
|
|
314
306
|
// (run 13: a bunfig fix that made `bun run test` pass 116/116 was stranded
|
|
315
307
|
// by an ACCEPT).
|
|
316
|
-
+ strandedFixNote(stranded);
|
|
308
|
+
+ strandedFixNote([...ledger.stranded()]);
|
|
317
309
|
// YOLO: keep autofixing WHILE the card is still offered — the loop withdraws it
|
|
318
310
|
// after MAX_FINAL_GATE_AUTOFIX, so the cap that bounds a non-converging fix pass
|
|
319
311
|
// still bounds this — then LEAVE the run failed. Never 'accept': an unattended
|
|
@@ -339,17 +331,17 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
339
331
|
// instruction to throw away work (mx5 run 13 item 3).
|
|
340
332
|
await commitStranded('accepted');
|
|
341
333
|
active.ui.notify(`${id}: final integration gate FAIL accepted by user — completing.`
|
|
342
|
-
+ (stranded.length > 0 ?
|
|
343
|
-
` ${stranded.length} uncommitted fix-pass change(s) committed separately.`
|
|
334
|
+
+ (ledger.stranded().length > 0 ?
|
|
335
|
+
` ${ledger.stranded().length} uncommitted fix-pass change(s) committed separately.`
|
|
344
336
|
: ''), 'warning');
|
|
345
337
|
break;
|
|
346
338
|
}
|
|
347
339
|
if (choice.action === 'autofix' && canAutofix) {
|
|
348
|
-
|
|
349
|
-
await recGate(`final-gate: user chose AUTOFIX (attempt ${
|
|
350
|
-
active.ui.notify(`${id}: final-gate autofix (${
|
|
340
|
+
const attempt = ledger.attempt();
|
|
341
|
+
await recGate(`final-gate: user chose AUTOFIX (attempt ${attempt}/${MAX_FINAL_GATE_AUTOFIX})`);
|
|
342
|
+
active.ui.notify(`${id}: final-gate autofix (${attempt}/${MAX_FINAL_GATE_AUTOFIX}) — bounded fix pass, then the gate re-runs…`, 'info');
|
|
351
343
|
const seed = choice.guidance ? `${fin.reason}\n\nUser guidance: ${choice.guidance}` : fin.reason;
|
|
352
|
-
const fix = await deps.finalGateFix(active, cwd, seed,
|
|
344
|
+
const fix = await deps.finalGateFix(active, cwd, seed, [...ledger.ignoredWrites()]);
|
|
353
345
|
// IGNORED-PATH WRITES (mx5 run 19). The pass wrote file(s) git ignores, so
|
|
354
346
|
// they are not in the commit and a fresh clone does not have them. Trailed
|
|
355
347
|
// on EVERY outcome — a rejected attempt's tracked edits are discarded while
|
|
@@ -358,7 +350,7 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
358
350
|
// own attempt. PATH NAMES ONLY: an ignored file's contents (`.env` is the
|
|
359
351
|
// canonical case) never enter a log, a debt or a child prompt.
|
|
360
352
|
if (fix.ignoredWrites && fix.ignoredWrites.length > 0) {
|
|
361
|
-
|
|
353
|
+
ledger.wroteIgnored(fix.ignoredWrites);
|
|
362
354
|
await recGate(ignoredWriteTrailLine(fix.ignoredWrites));
|
|
363
355
|
// Debt only where a verdict can rest on the file: the probe proved the
|
|
364
356
|
// gate needs it, or the question stayed open. A write the gate
|
|
@@ -379,26 +371,30 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
379
371
|
}
|
|
380
372
|
await recGate(`final-gate: autofix ${fix.unobserved ? 'ended UNOBSERVED' : 'converged'} — ${fix.reason.slice(0, 200)}`);
|
|
381
373
|
active.ui.notify(`${id}: final integration gate ${fix.unobserved ? 'is UNOBSERVED' : 'PASSES'} after autofix — ${fix.reason.slice(0, 140)}`, fix.unobserved ? 'warning' : 'info');
|
|
382
|
-
|
|
374
|
+
// The gate's own outcome, whole, with this door's reason on it. It
|
|
375
|
+
// used to be a two-key literal, so `openDebts` and `observedFailures`
|
|
376
|
+
// were dropped and `reconcileDebts` was the only thing putting one of
|
|
377
|
+
// them back — the recorded mx5 run-18 defect.
|
|
378
|
+
fin = { ...(fix.gate ?? fin), ok: true, reason: fix.reason };
|
|
383
379
|
// The gate itself just passed, statics included, so `staticOk` here is
|
|
384
380
|
// proof rather than assumption.
|
|
385
381
|
await reconcileDebts(true);
|
|
386
382
|
break;
|
|
387
383
|
}
|
|
388
|
-
await recGate(`final-gate: autofix attempt ${
|
|
384
|
+
await recGate(`final-gate: autofix attempt ${attempt} failed — ${fix.reason.slice(0, 200)}`);
|
|
389
385
|
// A guard that rejected an attempt WITHOUT discarding leaves rejected edits
|
|
390
386
|
// behind: the terminal paths must not commit the tree after that (the cheat
|
|
391
387
|
// guard stays intact).
|
|
392
388
|
if (fix.guardTripped === true && fix.editsDiscarded !== true) {
|
|
393
|
-
|
|
389
|
+
ledger.rejectedEditsRemain();
|
|
394
390
|
}
|
|
395
391
|
// The attempt's edits survive a non-convergence (only a guard trip
|
|
396
392
|
// discards). Find out what they are NOW, so the next picker shows them and
|
|
397
393
|
// a terminal outcome commits them.
|
|
398
394
|
await refreshStranded();
|
|
399
|
-
if (stranded.length > 0) {
|
|
400
|
-
await recGate(`final-gate: autofix attempt ${
|
|
401
|
-
+ `uncommitted change(s) — ${stranded.slice(0, 8).join(', ')}`);
|
|
395
|
+
if (ledger.stranded().length > 0) {
|
|
396
|
+
await recGate(`final-gate: autofix attempt ${attempt} left ${ledger.stranded().length} `
|
|
397
|
+
+ `uncommitted change(s) — ${ledger.stranded().slice(0, 8).join(', ')}`);
|
|
402
398
|
}
|
|
403
399
|
active.ui.notify(`${id}: final-gate autofix did not converge — ${fix.reason.slice(0, 140)}`, 'warning');
|
|
404
400
|
// NON-PROGRESS CLASSIFIER (mx5 run 14 item 2a). An attempt that changed the
|
|
@@ -407,46 +403,32 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
407
403
|
// burned all three attempts on a boot probe that could not observe a
|
|
408
404
|
// listener in that sandbox at all. Demote that one check to
|
|
409
405
|
// UNOBSERVED-with-debt and let the REMAINING checks decide.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
// pattern, which is the mistake `isNonProgress` already made once.
|
|
418
|
-
const observed = fix.gateObservedFailures?.includes(detail ?? '') === true;
|
|
419
|
-
if (detail !== null
|
|
420
|
-
&& isNonProgress({
|
|
421
|
-
previousSignature: prevFailSig,
|
|
422
|
-
currentDetail: detail,
|
|
423
|
-
edited,
|
|
424
|
-
observed
|
|
425
|
-
})) {
|
|
426
|
-
demoted.add(normalizeFailureDetail(detail));
|
|
427
|
-
prevFailSig = null;
|
|
428
|
-
await carryDebt(unobservedDebtReason(detail));
|
|
429
|
-
await recGate(`final-gate: check DEMOTED to UNOBSERVED after ${fixAttempts} tree-changing `
|
|
406
|
+
//
|
|
407
|
+
// The judgement, the observed check and the signature carry-forward are
|
|
408
|
+
// the ledger's — made where the evidence is, not downstream from it.
|
|
409
|
+
const verdict = ledger.judge(fix.gate, fix.gate !== undefined && ledger.stranded().length > 0);
|
|
410
|
+
if (verdict.demoted) {
|
|
411
|
+
await carryDebt(verdict.debtReason);
|
|
412
|
+
await recGate(`final-gate: check DEMOTED to UNOBSERVED after ${attempt} tree-changing `
|
|
430
413
|
+ `attempts returned an identical failure — carried as debt (origin final-gate) `
|
|
431
|
-
+ `and re-checked by the next run's gate: ${detail.slice(0, 240)}`);
|
|
414
|
+
+ `and re-checked by the next run's gate: ${verdict.detail.slice(0, 240)}`);
|
|
432
415
|
active.ui.notify(`${id}: final-gate check is unfalsifiable in this environment — carried as debt; `
|
|
433
416
|
+ 'the remaining checks decide convergence.', 'warning');
|
|
434
417
|
}
|
|
435
|
-
else {
|
|
436
|
-
prevFailSig = detail !== null ? normalizeFailureDetail(detail) : null;
|
|
437
|
-
}
|
|
438
418
|
// Convergence on the REMAINING checks: a demoted signature no longer counts
|
|
439
419
|
// against the gate. Nothing left ⇒ the run converges carrying the demotion
|
|
440
420
|
// as debt, and the fix passes' repairs are committed rather than stranded.
|
|
441
|
-
if (
|
|
442
|
-
const remaining =
|
|
443
|
-
if (remaining.length === 0) {
|
|
421
|
+
if (ledger.hasDemotions() && fix.gate !== undefined) {
|
|
422
|
+
const remaining = ledger.remaining(fix.gate);
|
|
423
|
+
if (remaining !== undefined && remaining.length === 0) {
|
|
444
424
|
await deps.commit(cwd, `FINAL GATE AUTOFIX (${id})`);
|
|
445
|
-
const converged = `converged on all remaining checks; ${
|
|
425
|
+
const converged = `converged on all remaining checks; ${ledger.demotedCount()} check(s) `
|
|
446
426
|
+ 'carried as UNOBSERVED debt (unfalsifiable in this environment)';
|
|
447
427
|
await recGate(`final-gate: ${converged}`);
|
|
448
428
|
active.ui.notify(`${id}: final integration gate converged — ${converged}.`, 'warning');
|
|
449
|
-
|
|
429
|
+
// The gate's own outcome, with this door's reason on it — the
|
|
430
|
+
// whole value, so `openDebts` survives the assignment.
|
|
431
|
+
fin = { ...fix.gate, ok: true, reason: converged };
|
|
450
432
|
// Converged on the REMAINING checks only: one or more were DEMOTED
|
|
451
433
|
// as unfalsifiable here, and the statics may be among them. No
|
|
452
434
|
// proof ⇒ pass false, so nothing static-class can auto-close on
|
|
@@ -464,17 +446,27 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
464
446
|
// next picker and the next fix seed target only what is still falsifiable —
|
|
465
447
|
// never re-aiming the child at the check the classifier just proved it
|
|
466
448
|
// cannot move.
|
|
467
|
-
|
|
468
|
-
|
|
449
|
+
// Outcome to outcome. The base is the FRESH gate outcome when the fix
|
|
450
|
+
// pass got as far as re-running it, otherwise the one we already hold —
|
|
451
|
+
// and either way it arrives whole, so nothing (`openDebts`,
|
|
452
|
+
// `observedFailures`) is dropped by the assignment. This used to be a
|
|
453
|
+
// literal with four keys, and the field it omitted is the recorded mx5
|
|
454
|
+
// run-18 defect.
|
|
455
|
+
const base = fix.gate ?? fin;
|
|
456
|
+
const carried = base.failures === undefined ? undefined : ledger.remaining(base);
|
|
469
457
|
fin = {
|
|
458
|
+
...base,
|
|
470
459
|
ok: false,
|
|
471
|
-
reason:
|
|
460
|
+
reason: ledger.hasDemotions() && carried !== undefined && carried.length > 0 ?
|
|
472
461
|
carried[0]
|
|
473
|
-
:
|
|
474
|
-
failures: carried,
|
|
475
|
-
|
|
462
|
+
: base.reason,
|
|
463
|
+
...(carried === undefined ? {} : { failures: carried }),
|
|
464
|
+
// The debt NOTE is the caller's running one: the next picker must
|
|
465
|
+
// still show the open claims, and the fresh gate's own note is
|
|
466
|
+
// reconciled separately against the final tree.
|
|
467
|
+
...(fin.debtNote === undefined ? {} : { debtNote: fin.debtNote })
|
|
476
468
|
};
|
|
477
|
-
if (fix.
|
|
469
|
+
if (fix.gate !== undefined && (fix.gate.failures?.length ?? 0) > 1) {
|
|
478
470
|
await trailGateFail(fin);
|
|
479
471
|
}
|
|
480
472
|
continue;
|
|
@@ -492,8 +484,8 @@ export async function runFinalGateStage(active, deps, p) {
|
|
|
492
484
|
return {
|
|
493
485
|
kind: 'failed',
|
|
494
486
|
message: `${id} finished all tasks but FAILED the final integration gate — ${fin.reason.slice(0, 200)} — fix and /task-auto-resume (the gate re-runs).`
|
|
495
|
-
+ (stranded.length > 0 ?
|
|
496
|
-
` NOTE: ${stranded.length} fix-pass change(s) were committed separately (${stranded.slice(0, 4).join(', ')}).`
|
|
487
|
+
+ (ledger.stranded().length > 0 ?
|
|
488
|
+
` NOTE: ${ledger.stranded().length} fix-pass change(s) were committed separately (${ledger.stranded().slice(0, 4).join(', ')}).`
|
|
497
489
|
: '')
|
|
498
490
|
};
|
|
499
491
|
}
|
|
@@ -248,6 +248,14 @@ export type GateResult = {
|
|
|
248
248
|
kind: 'interrupted';
|
|
249
249
|
ctx: ExtensionCommandContext;
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The USER cancelled an AUTOFIX re-run. Distinct from `interrupted`: the task
|
|
253
|
+
* file already says `cancelled` and must not be demoted to `failed` over it.
|
|
254
|
+
*/
|
|
255
|
+
| {
|
|
256
|
+
kind: 'cancelled';
|
|
257
|
+
ctx: ExtensionCommandContext;
|
|
258
|
+
}
|
|
251
259
|
/** An AUTOFIX re-run's implementation itself failed. */
|
|
252
260
|
| {
|
|
253
261
|
kind: 'failed';
|