@mjasnikovs/pi-task 0.38.15 → 0.38.17
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/config/config.d.ts +26 -0
- package/dist/config/config.js +68 -17
- package/dist/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +18 -14
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +211 -218
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +109 -1
- package/dist/task/boot-probe.js +139 -23
- 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/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- 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 +16 -31
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +55 -89
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +75 -46
- package/dist/task/phases.d.ts +120 -34
- package/dist/task/phases.js +221 -134
- package/dist/task/plan-orchestrator.js +2 -2
- package/dist/task/plan-rounds.d.ts +86 -0
- package/dist/task/plan-rounds.js +105 -0
- package/dist/task/plan-session.d.ts +31 -21
- package/dist/task/plan-session.js +97 -120
- package/dist/task/qa-transcript.d.ts +100 -0
- package/dist/task/qa-transcript.js +99 -0
- package/dist/task/question-source.d.ts +117 -0
- package/dist/task/question-source.js +174 -0
- 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/serve-entry.js +6 -57
- package/dist/task/shipped-source.d.ts +67 -0
- package/dist/task/shipped-source.js +144 -0
- package/dist/task/task-gates.d.ts +9 -1
- package/dist/task/task-gates.js +27 -6
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- 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/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- 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 +35 -25
- 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
|
@@ -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
|
}
|
package/dist/task/serve-entry.js
CHANGED
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
*
|
|
39
39
|
* Ground truth is the file tree only. No model, no network.
|
|
40
40
|
*/
|
|
41
|
-
import {
|
|
41
|
+
import { readFileSync } from 'node:fs';
|
|
42
42
|
import * as path from 'node:path';
|
|
43
|
+
import { shippedSources, stripCommentLines, SOURCE_JS_RE } from './shipped-source.js';
|
|
43
44
|
/** A server-app construction: framework, and the regex that recognises it. */
|
|
44
45
|
const CONSTRUCT_PATTERNS = [
|
|
45
46
|
{ re: /\bnew\s+Hono\s*[<(]/, construct: 'new Hono()' },
|
|
@@ -180,62 +181,10 @@ export function opaqueLauncher(cwd) {
|
|
|
180
181
|
}
|
|
181
182
|
return null;
|
|
182
183
|
}
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
const
|
|
187
|
-
const SKIP_FILE_RE = /\.(?:test|spec|stories|bench)\.[a-z]+$|\.d\.[mc]?ts$/i;
|
|
188
|
-
const SCAN_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/i;
|
|
189
|
-
const MAX_SCAN_FILES = 3000;
|
|
190
|
-
const MAX_FILE_BYTES = 400_000;
|
|
191
|
-
/** Authored sources, bounded and in deterministic order. */
|
|
192
|
-
function scanCandidates(cwd) {
|
|
193
|
-
const out = [];
|
|
194
|
-
const walk = (rel) => {
|
|
195
|
-
if (out.length >= MAX_SCAN_FILES)
|
|
196
|
-
return;
|
|
197
|
-
let entries;
|
|
198
|
-
try {
|
|
199
|
-
entries = readdirSync(path.join(cwd, rel)).sort();
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
for (const name of entries) {
|
|
205
|
-
if (out.length >= MAX_SCAN_FILES)
|
|
206
|
-
return;
|
|
207
|
-
const relPath = rel === '' ? name : `${rel}/${name}`;
|
|
208
|
-
let st;
|
|
209
|
-
try {
|
|
210
|
-
st = statSync(path.join(cwd, relPath));
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
if (st.isDirectory()) {
|
|
216
|
-
if (name.startsWith('.') || SKIP_DIR_RE.test(name))
|
|
217
|
-
continue;
|
|
218
|
-
walk(relPath);
|
|
219
|
-
}
|
|
220
|
-
else if (st.isFile() && st.size <= MAX_FILE_BYTES) {
|
|
221
|
-
if (SKIP_FILE_RE.test(name))
|
|
222
|
-
continue;
|
|
223
|
-
if (SCAN_RE.test(name))
|
|
224
|
-
out.push(relPath);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
walk('');
|
|
229
|
-
return out;
|
|
230
|
-
}
|
|
231
|
-
/** Strip comment-only lines — a `Bun.serve` quoted in a comment is not a bind, and
|
|
232
|
-
* a commented-out catch-all is not a route. Inline comments are left alone. */
|
|
233
|
-
function stripCommentLines(src) {
|
|
234
|
-
return src
|
|
235
|
-
.split('\n')
|
|
236
|
-
.filter(l => !/^\s*(?:\/\/|\*|\/\*)/.test(l))
|
|
237
|
-
.join('\n');
|
|
238
|
-
}
|
|
184
|
+
// The tree walk, the caps, the skip sets and the comment strip live in
|
|
185
|
+
// task/shipped-source.ts — this scan and artifact-closure's were the same walker
|
|
186
|
+
// written twice, and their skip sets had drifted apart on `bench`/`benchmarks`.
|
|
187
|
+
const scanCandidates = (cwd) => shippedSources(cwd, { ext: SOURCE_JS_RE });
|
|
239
188
|
/** A `/…/flags` literal whose body carries a regex METACHARACTER — `[`, `\`, `+`,
|
|
240
189
|
* `?`, `|`, a group. A path string like `'/api/admin'` has none, so it survives. */
|
|
241
190
|
const REGEX_LITERAL_RE = /\/(?![*/])((?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\\n])+)\/[gimsuyd]*/g;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What counts as SHIPPED SOURCE — the input every run-level closure scan reads.
|
|
3
|
+
*
|
|
4
|
+
* `CLOSURE_SCANS` (final-gate.ts) deepened the DRIVER of these scans: the fault
|
|
5
|
+
* isolation, the rank, the stage. It did not unify their INPUT, and the copies
|
|
6
|
+
* had drifted.
|
|
7
|
+
*
|
|
8
|
+
* - `scanCandidates` existed twice, near-byte-identical — `serve-entry.ts` and
|
|
9
|
+
* `artifact-closure.ts` — same `readdirSync().sort()` → `statSync` → recurse,
|
|
10
|
+
* same 3000-file cap, same 400 KB per-file cap, same dot-dir rule.
|
|
11
|
+
* - The skip sets had diverged: serve-entry carried `bench|benchmarks` and
|
|
12
|
+
* `*.bench.*`; artifact-closure did not, so a dangling artifact reference in a
|
|
13
|
+
* benchmark file was a run-level finding while the same file was invisible to
|
|
14
|
+
* the sibling scan. Nothing in either file acknowledged the other.
|
|
15
|
+
* - The same extension regex was declared twice under two names (`SCAN_RE`,
|
|
16
|
+
* `SCAN_JS_RE`), and `stripCommentLines` was byte-identical in both.
|
|
17
|
+
* - `.pi-tasks` was hardcoded into both skip sets rather than derived from
|
|
18
|
+
* `TASKS_DIR_NAME`.
|
|
19
|
+
*
|
|
20
|
+
* The locality proof is in the suites: `artifact-closure.test.ts` has 28
|
|
21
|
+
* references to the pure extractors and 5 calls to the driver, and NO test in the
|
|
22
|
+
* cluster asserted a skip set at all. That is the shape recorded under
|
|
23
|
+
* `resolveTypeSource` — pure functions extracted for testability while the real
|
|
24
|
+
* logic stayed in how they are CALLED.
|
|
25
|
+
*
|
|
26
|
+
* NOT unified here: `env-template-closure.ts`. It asks a different question —
|
|
27
|
+
* which TRACKED files could read an env var, including `.py`/`.go`, including
|
|
28
|
+
* tests — and answering it over this walk would silently change which env
|
|
29
|
+
* findings a run produces. Its comment rule is not this one either: it is a
|
|
30
|
+
* per-line predicate that also treats `#` as a comment opener, which it must,
|
|
31
|
+
* and which would be wrong for JS/TS. Recorded as a real divergence rather than
|
|
32
|
+
* harmonised; changing it is an env-policy change with its own A/B.
|
|
33
|
+
*/
|
|
34
|
+
/** Authored JS/TS. The one declaration — it was two constants under two names. */
|
|
35
|
+
export declare const SOURCE_JS_RE: RegExp;
|
|
36
|
+
/** Markup a produced artifact can be referenced from. */
|
|
37
|
+
export declare const SOURCE_HTML_RE: RegExp;
|
|
38
|
+
/** Bounds. A scan is a gate step, not a search: it must terminate on any tree. */
|
|
39
|
+
export declare const MAX_SCAN_FILES = 3000;
|
|
40
|
+
export declare const MAX_FILE_BYTES = 400000;
|
|
41
|
+
/** Is this directory NAME (not path) one no closure scan descends into? */
|
|
42
|
+
export declare function isSkippedDir(name: string): boolean;
|
|
43
|
+
/** Is this file NAME one no closure scan reads? */
|
|
44
|
+
export declare function isSkippedFile(name: string): boolean;
|
|
45
|
+
export interface ShippedSourceOptions {
|
|
46
|
+
/** Which extensions this scan reads. */
|
|
47
|
+
ext: RegExp;
|
|
48
|
+
/**
|
|
49
|
+
* Extra ROOT-LEVEL directory names to exclude. artifact-closure's own: a
|
|
50
|
+
* produced output tree re-referencing its own chunks is noise, and which dirs
|
|
51
|
+
* are produced is discovered per run, so it cannot live in the static set.
|
|
52
|
+
*/
|
|
53
|
+
excludeRoots?: ReadonlySet<string>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Walk `cwd` for shipped sources — bounded, deterministic order, never throws.
|
|
57
|
+
*
|
|
58
|
+
* Unreadable directories and unstattable entries are skipped rather than fatal:
|
|
59
|
+
* a closure scan runs against whatever tree the implementation left behind.
|
|
60
|
+
*/
|
|
61
|
+
export declare function shippedSources(cwd: string, opts: ShippedSourceOptions): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Strip comment-only lines. A `Bun.serve` quoted in a comment is not a bind, a
|
|
64
|
+
* commented-out catch-all is not a route, and a path in a comment is not a
|
|
65
|
+
* runtime read. Inline comments are left alone — strings may contain `//`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function stripCommentLines(src: string): string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What counts as SHIPPED SOURCE — the input every run-level closure scan reads.
|
|
3
|
+
*
|
|
4
|
+
* `CLOSURE_SCANS` (final-gate.ts) deepened the DRIVER of these scans: the fault
|
|
5
|
+
* isolation, the rank, the stage. It did not unify their INPUT, and the copies
|
|
6
|
+
* had drifted.
|
|
7
|
+
*
|
|
8
|
+
* - `scanCandidates` existed twice, near-byte-identical — `serve-entry.ts` and
|
|
9
|
+
* `artifact-closure.ts` — same `readdirSync().sort()` → `statSync` → recurse,
|
|
10
|
+
* same 3000-file cap, same 400 KB per-file cap, same dot-dir rule.
|
|
11
|
+
* - The skip sets had diverged: serve-entry carried `bench|benchmarks` and
|
|
12
|
+
* `*.bench.*`; artifact-closure did not, so a dangling artifact reference in a
|
|
13
|
+
* benchmark file was a run-level finding while the same file was invisible to
|
|
14
|
+
* the sibling scan. Nothing in either file acknowledged the other.
|
|
15
|
+
* - The same extension regex was declared twice under two names (`SCAN_RE`,
|
|
16
|
+
* `SCAN_JS_RE`), and `stripCommentLines` was byte-identical in both.
|
|
17
|
+
* - `.pi-tasks` was hardcoded into both skip sets rather than derived from
|
|
18
|
+
* `TASKS_DIR_NAME`.
|
|
19
|
+
*
|
|
20
|
+
* The locality proof is in the suites: `artifact-closure.test.ts` has 28
|
|
21
|
+
* references to the pure extractors and 5 calls to the driver, and NO test in the
|
|
22
|
+
* cluster asserted a skip set at all. That is the shape recorded under
|
|
23
|
+
* `resolveTypeSource` — pure functions extracted for testability while the real
|
|
24
|
+
* logic stayed in how they are CALLED.
|
|
25
|
+
*
|
|
26
|
+
* NOT unified here: `env-template-closure.ts`. It asks a different question —
|
|
27
|
+
* which TRACKED files could read an env var, including `.py`/`.go`, including
|
|
28
|
+
* tests — and answering it over this walk would silently change which env
|
|
29
|
+
* findings a run produces. Its comment rule is not this one either: it is a
|
|
30
|
+
* per-line predicate that also treats `#` as a comment opener, which it must,
|
|
31
|
+
* and which would be wrong for JS/TS. Recorded as a real divergence rather than
|
|
32
|
+
* harmonised; changing it is an env-policy change with its own A/B.
|
|
33
|
+
*/
|
|
34
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
import { TASKS_DIR_NAME } from './task-types.js';
|
|
37
|
+
/**
|
|
38
|
+
* Directories never scanned: VCS/dep trees, build output (bundled copies of the
|
|
39
|
+
* same sources), and test/fixture/example/doc/bench trees — a test that stands up
|
|
40
|
+
* a throwaway listener is not the app's launch, a doc snippet is not code, and a
|
|
41
|
+
* benchmark references fixture paths freely.
|
|
42
|
+
*/
|
|
43
|
+
const SKIP_DIRS = new Set([
|
|
44
|
+
'.git',
|
|
45
|
+
'node_modules',
|
|
46
|
+
TASKS_DIR_NAME,
|
|
47
|
+
'dist',
|
|
48
|
+
'build',
|
|
49
|
+
'out',
|
|
50
|
+
'coverage',
|
|
51
|
+
'target',
|
|
52
|
+
'vendor',
|
|
53
|
+
'__pycache__',
|
|
54
|
+
'.venv',
|
|
55
|
+
'venv',
|
|
56
|
+
'tmp',
|
|
57
|
+
'test',
|
|
58
|
+
'tests',
|
|
59
|
+
'__tests__',
|
|
60
|
+
'__mocks__',
|
|
61
|
+
'__fixtures__',
|
|
62
|
+
'fixtures',
|
|
63
|
+
'e2e',
|
|
64
|
+
'examples',
|
|
65
|
+
'example',
|
|
66
|
+
'docs',
|
|
67
|
+
'doc',
|
|
68
|
+
'bench',
|
|
69
|
+
'benchmarks'
|
|
70
|
+
]);
|
|
71
|
+
const SKIP_FILE_RE = /\.(?:test|spec|stories|bench)\.[a-z]+$|\.d\.[mc]?ts$/i;
|
|
72
|
+
/** Authored JS/TS. The one declaration — it was two constants under two names. */
|
|
73
|
+
export const SOURCE_JS_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/i;
|
|
74
|
+
/** Markup a produced artifact can be referenced from. */
|
|
75
|
+
export const SOURCE_HTML_RE = /\.html?$/i;
|
|
76
|
+
/** Bounds. A scan is a gate step, not a search: it must terminate on any tree. */
|
|
77
|
+
export const MAX_SCAN_FILES = 3000;
|
|
78
|
+
export const MAX_FILE_BYTES = 400_000;
|
|
79
|
+
/** Is this directory NAME (not path) one no closure scan descends into? */
|
|
80
|
+
export function isSkippedDir(name) {
|
|
81
|
+
return name.startsWith('.') || SKIP_DIRS.has(name);
|
|
82
|
+
}
|
|
83
|
+
/** Is this file NAME one no closure scan reads? */
|
|
84
|
+
export function isSkippedFile(name) {
|
|
85
|
+
return SKIP_FILE_RE.test(name);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Walk `cwd` for shipped sources — bounded, deterministic order, never throws.
|
|
89
|
+
*
|
|
90
|
+
* Unreadable directories and unstattable entries are skipped rather than fatal:
|
|
91
|
+
* a closure scan runs against whatever tree the implementation left behind.
|
|
92
|
+
*/
|
|
93
|
+
export function shippedSources(cwd, opts) {
|
|
94
|
+
const out = [];
|
|
95
|
+
const walk = (rel) => {
|
|
96
|
+
if (out.length >= MAX_SCAN_FILES)
|
|
97
|
+
return;
|
|
98
|
+
let entries;
|
|
99
|
+
try {
|
|
100
|
+
entries = readdirSync(path.join(cwd, rel)).sort();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const name of entries) {
|
|
106
|
+
if (out.length >= MAX_SCAN_FILES)
|
|
107
|
+
return;
|
|
108
|
+
const relPath = rel === '' ? name : `${rel}/${name}`;
|
|
109
|
+
let st;
|
|
110
|
+
try {
|
|
111
|
+
st = statSync(path.join(cwd, relPath));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (st.isDirectory()) {
|
|
117
|
+
if (isSkippedDir(name))
|
|
118
|
+
continue;
|
|
119
|
+
if (rel === '' && opts.excludeRoots?.has(name))
|
|
120
|
+
continue;
|
|
121
|
+
walk(relPath);
|
|
122
|
+
}
|
|
123
|
+
else if (st.isFile() && st.size <= MAX_FILE_BYTES) {
|
|
124
|
+
if (isSkippedFile(name))
|
|
125
|
+
continue;
|
|
126
|
+
if (opts.ext.test(name))
|
|
127
|
+
out.push(relPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
walk('');
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Strip comment-only lines. A `Bun.serve` quoted in a comment is not a bind, a
|
|
136
|
+
* commented-out catch-all is not a route, and a path in a comment is not a
|
|
137
|
+
* runtime read. Inline comments are left alone — strings may contain `//`.
|
|
138
|
+
*/
|
|
139
|
+
export function stripCommentLines(src) {
|
|
140
|
+
return src
|
|
141
|
+
.split('\n')
|
|
142
|
+
.filter(l => !/^\s*(?:\/\/|\*|\/\*)/.test(l))
|
|
143
|
+
.join('\n');
|
|
144
|
+
}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
32
32
|
import type { RunSingleTaskResult } from './orchestrator.js';
|
|
33
33
|
import type { CommitResult } from './auto-commit.js';
|
|
34
|
-
import type
|
|
34
|
+
import { type VerifyOutcome } from './verify-work.js';
|
|
35
35
|
import type { EnforceOutcome } from './enforce-guidelines.js';
|
|
36
36
|
import { type ResolutionOutcome, type ResolutionChoice } from './verify-resolution.js';
|
|
37
37
|
import { type RepairCandidate } from './root-cause-repair.js';
|
|
@@ -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';
|