@mjasnikovs/pi-task 0.37.7 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/shared/child-output.d.ts +19 -3
  2. package/dist/shared/child-output.js +21 -5
  3. package/dist/shared/git-runner.d.ts +39 -0
  4. package/dist/shared/git-runner.js +38 -0
  5. package/dist/task/accept-debt.d.ts +37 -61
  6. package/dist/task/accept-debt.js +67 -130
  7. package/dist/task/auto-orchestrator.d.ts +7 -57
  8. package/dist/task/auto-orchestrator.js +25 -499
  9. package/dist/task/child-runner.d.ts +2 -0
  10. package/dist/task/child-runner.js +74 -70
  11. package/dist/task/enforce-guidelines.d.ts +1 -1
  12. package/dist/task/enforce-guidelines.js +2 -2
  13. package/dist/task/external-context.d.ts +85 -7
  14. package/dist/task/external-context.js +100 -63
  15. package/dist/task/file-inventory.js +22 -41
  16. package/dist/task/final-gate.d.ts +80 -0
  17. package/dist/task/final-gate.js +102 -49
  18. package/dist/task/gate-deps.js +6 -23
  19. package/dist/task/git-state-guard.d.ts +1 -1
  20. package/dist/task/git-state-guard.js +1 -7
  21. package/dist/task/phases.js +40 -83
  22. package/dist/task/run-final-gate.d.ts +127 -0
  23. package/dist/task/run-final-gate.js +492 -0
  24. package/dist/task/task-gates.d.ts +20 -57
  25. package/dist/task/task-gates.js +11 -11
  26. package/dist/task/verify-work.d.ts +40 -32
  27. package/dist/task/verify-work.js +301 -241
  28. package/dist/workers/docs-core.d.ts +14 -0
  29. package/dist/workers/docs-core.js +28 -16
  30. package/dist/workers/fetch-core.d.ts +6 -1
  31. package/dist/workers/fetch-core.js +26 -33
  32. package/dist/workers/focused-extractor.d.ts +73 -0
  33. package/dist/workers/focused-extractor.js +72 -0
  34. package/dist/workers/pi-worker-docs.d.ts +1 -1
  35. package/dist/workers/pi-worker-docs.js +48 -42
  36. package/dist/workers/pi-worker-fetch.js +6 -8
  37. package/dist/workers/typeonly-log.d.ts +13 -0
  38. package/package.json +1 -1
@@ -32,22 +32,18 @@ import { startAutoLoader } from './widget.js';
32
32
  import { getParentContextWindow, resolveContextUsage } from './context-usage.js';
33
33
  import { buildGateDeps, collectTreeChanges } from './gate-deps.js';
34
34
  import { runGatesForTask } from './task-gates.js';
35
+ import { runFinalGateStage } from './run-final-gate.js';
35
36
  import { gitUnmergedPaths, gitStashRef } from './auto-commit.js';
36
37
  import { runFinalIntegrationGate, deriveOpenDebts } from './final-gate.js';
37
- import { describeDebt, recordFinalGateUnobservedDebt } from './accept-debt.js';
38
- import { ignoredWriteTrailLine, ignoredWriteDebtReason } from './write-guard.js';
39
- import { applyDemotions, isNonProgress, normalizeFailureDetail, rankedFirstFailure, unobservedDebtReason } from './final-gate-progress.js';
40
- 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';
41
38
  import { getConfig } from '../config/config.js';
42
39
  import { debugLogLevel, shouldLogDebug } from './debug-log.js';
43
- import { isYoloMode, yoloPickAnswer, yoloFinalGateChoice, YOLO_STAMP } from './yolo.js';
40
+ import { isYoloMode, yoloPickAnswer, YOLO_STAMP } from './yolo.js';
44
41
  import { configureResearchRun, resumeResearchRun } from '../workers/research-cache.js';
45
42
  import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, appendContracts } from './contracts.js';
46
43
  import { reconcileTitleSources } from './decompose-fidelity.js';
47
44
  import { granularityFloor, granularitySplitHint, isPlanShapeQuestion, isTooCoarse, planShapeIsHostsToAnswer, PLAN_SHAPE_ANSWER } from './decompose-granularity.js';
48
45
  import { mandatesTestsInSameChange, rewriteBatchTestPlan } from './batch-test-task.js';
49
- import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, writeOwnedRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, isCrossCuttingRequirement, appendCarriedRequirements, buildRequirementsLedger, readOwnedRequirements } from './requirements.js';
50
- import { unclaimedPendingRequirements } from './owned-freeze-reassign.js';
46
+ import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, writeOwnedRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, isCrossCuttingRequirement, appendCarriedRequirements, buildRequirementsLedger } from './requirements.js';
51
47
  import { decideAdoption, groundedCoverage } from './coverage-loop.js';
52
48
  import { findSpecDanglingArtifacts, titlesCoverArtifact, danglingMissingText, danglingCarryText } from './artifact-closure.js';
53
49
  import { LAUNCH_EXTRACT_PROMPT, enumerateScriptCandidates, parseScriptLines, keepGroundedScripts, appendDeclaredScripts } from './launch-contract.js';
@@ -1151,502 +1147,32 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1151
1147
  if (!next) {
1152
1148
  // FINAL INTEGRATION GATE: every task passed its own per-slice gates,
1153
1149
  // but per-slice green has shipped a dead app twice (mx5 runs 3 & 5:
1154
- // statics clean, every protected route 500ing). Run the project's
1155
- // OWN whole-repo commands once, unaided, before declaring the run
1156
- // complete. On a FAIL the user decides: accept (complete anyway) or
1157
- // leave the run failed a resume re-enters this branch and re-runs
1158
- // the gate, so fixing and resuming converges.
1159
- // SAFE CHECKPOINT (pre-final-gate): every task is checked off and
1160
- // committed and the whole-repo gate has not started. A resume
1161
- // re-enters this same branch and runs the gate then, so the run is
1162
- // left exactly where it was — not silently declared complete.
1163
- if (cancelCheckpoint('pre-final-gate')) {
1164
- announceDone(active, `${id} cancelled before the final integration gate resume with /task-auto-resume.`, 'warning');
1150
+ // statics clean, every protected route 500ing). The run-level stage
1151
+ // runs the project's OWN whole-repo commands once, unaided, before the
1152
+ // run is declared complete, and resolves a FAIL with the user. It
1153
+ // touches none of this loop's per-task state see run-final-gate.ts —
1154
+ // and every outcome it returns is terminal: a resume re-enters this
1155
+ // same branch and re-runs the gate, so fixing and resuming converges.
1156
+ const stage = await runFinalGateStage(active, deps, {
1157
+ cwd,
1158
+ runId: id,
1159
+ // The parent plan (the task list) is what lets the gate tell a
1160
+ // served app from a CLI the boot check requires a listener only
1161
+ // for the former (mx5 run 10: a CSS watcher satisfied "still alive").
1162
+ planText: body,
1163
+ taskCount: entries.length
1164
+ });
1165
+ if (stage.kind === 'cancelled') {
1166
+ announceDone(active, stage.message, 'warning');
1165
1167
  return;
1166
1168
  }
1167
- // Set when the gate finished having observed NOTHING dynamic. Declared
1168
- // out here so the run-completion announcement below can say so: a run
1169
- // that completes on statics alone must not be announced the same way as
1170
- // one whose product was actually exercised.
1171
- let unobservedNote = null;
1172
- if (deps.finalGate) {
1173
- active.ui.notify(`${id}: running final integration gate…`, 'info');
1174
- // Run-level gate trail on the parent task file — same durable
1175
- // auditability contract as the per-task `## gates` records.
1176
- const recGate = async (line) => {
1177
- try {
1178
- await deps.record?.(cwd, id, line);
1179
- }
1180
- catch {
1181
- // recording must never break the gate
1182
- }
1183
- };
1184
- // Hand the parent plan (the task list) to the gate so it can tell a
1185
- // served app from a CLI: the boot check requires a listener only for
1186
- // the former (mx5 run 10 — a CSS watcher satisfied "still alive").
1187
- // Trail EVERY aggregated failure entry (mx5 run 13): the gate now
1188
- // runs all sections and ranks the list; a single sliced reason
1189
- // line would re-hide everything past the first entry.
1190
- const trailGateFail = async (f) => {
1191
- const list = f.failures ?? [f.reason];
1192
- if (list.length <= 1) {
1193
- await recGate(`final-gate: FAIL — ${f.reason.slice(0, 300)}`);
1194
- return;
1195
- }
1196
- await recGate(`final-gate: FAIL — ${list.length} failures (ranked, most load-bearing first)`);
1197
- for (const [i, entry] of list.entries()) {
1198
- await recGate(`final-gate FAIL ${i + 1}/${list.length}: ${entry.slice(0, 300)}`);
1199
- }
1200
- };
1201
- let fin = await deps.finalGate(cwd, body);
1202
- // Record the outcome symmetrically (mx5 run 10 item 7): only FAIL was
1203
- // ever trailed, so a PASSing gate was indistinguishable from a gate
1204
- // that never ran. The PASS reason names the commands that were run.
1205
- // THREE verdicts, not two (final-gate.ts unobservedVerdict): a gate
1206
- // that observed nothing dynamic is UNOBSERVED, never PASS — IAR1
1207
- // shipped `PASS — no integration command found` twice while carrying
1208
- // open verify-FAIL debt. It does not block (justified there), but it
1209
- // is labelled here, warned about, and recorded as durable debt so the
1210
- // next run's gate re-surfaces it.
1211
- if (fin.ok && fin.unobserved) {
1212
- unobservedNote = fin.unobserved;
1213
- await recGate(`final-gate: UNOBSERVED — ${fin.reason.slice(0, 300)}`);
1214
- await recordFinalGateUnobservedDebt(cwd, id, fin.unobserved);
1215
- active.ui.notify(`${id}: the final integration gate observed NOTHING dynamic — `
1216
- + 'the run completed on static checks alone. Nothing verified '
1217
- + 'that the assembled product builds, boots or works.', 'warning');
1218
- }
1219
- else if (fin.ok) {
1220
- await recGate(`final-gate: PASS — ${fin.reason.slice(0, 300)}`);
1221
- }
1222
- else {
1223
- await trailGateFail(fin);
1224
- }
1225
- // ACCEPT-debt re-check surfacing (mx5 run 4 B3 / run 8 TASK_0012):
1226
- // tasks the user accepted despite a verify-FAIL that the gate could
1227
- // not prove resolved against the current tree. Surface them at the
1228
- // gate moment — on PASS or FAIL — so a run never completes silently
1229
- // carrying an accepted defect. Informational: the per-task ACCEPT was
1230
- // already a human decision, so this reports, it does not re-fail.
1231
- const debtKey = (d) => `${d.taskId}\t${d.reason}`;
1232
- const surfaceOpenDebts = async (debts) => {
1233
- if (debts.length === 0)
1234
- return;
1235
- for (const d of debts) {
1236
- await recGate(`defect STILL OPEN — ${d.taskId || '(unknown task)'}: ${describeDebt(d)}: ${d.reason.slice(0, 240)}${d.conflict ? ` [CONFLICTING CLAIM — ${d.conflict}]` : ''}`);
1237
- }
1238
- active.ui.notify(`${id}: ${debts.length} recorded verify-FAIL defect(s) are STILL unresolved at run end — see the gate trail.`, 'warning');
1239
- };
1240
- // What was REPORTED, so a post-autofix re-derivation can be
1241
- // compared against it rather than blindly re-printed.
1242
- let reportedDebts = fin.openDebts ?? [];
1243
- await surfaceOpenDebts(reportedDebts);
1244
- // An owned obligation a task DETACHED (its own spec froze the
1245
- // only file that could satisfy it, nexttask 2) and no later
1246
- // task claimed. Detach never deletes the quote, so the run
1247
- // ends holding it — say so, or the resolution would be a
1248
- // quieter version of the deletion it exists to prevent.
1249
- const unclaimed = unclaimedPendingRequirements(await readOwnedRequirements(cwd).catch(() => []));
1250
- for (const o of unclaimed) {
1251
- await recGate(`owned requirement UNCLAIMED — "${o.quote.slice(0, 200)}"`
1252
- + ` [frozen in "${o.title.slice(0, 60)}"; no task claimed`
1253
- + ` ${(o.pending ?? []).join(', ')}]`);
1254
- }
1255
- if (unclaimed.length > 0) {
1256
- active.ui.notify(`${id}: ${unclaimed.length} authoritative design requirement(s) ended the run`
1257
- + ' owned by NO task — the task they were mapped to could not touch the'
1258
- + ' file, and nothing else claimed it. See the gate trail.', 'warning');
1259
- }
1260
- /**
1261
- * nexttask 6 (mx5 run 18). The lines above are emitted from the
1262
- * FIRST gate result; the converged-autofix paths below used to
1263
- * rebuild `fin` as `{ok, reason}`, so `openDebts` did not survive
1264
- * the fix pass — the run's last word on its own defects was a
1265
- * snapshot of a tree that no longer existed, and no code path
1266
- * could clear, re-check or act on it. Re-derive here, against the
1267
- * tree the run actually ends with, and correct the record.
1268
- *
1269
- * FP-safe by inheritance: `deriveOpenDebts` auto-closes only what
1270
- * a deterministic check can stand behind (a static-class debt when
1271
- * the statics provably pass, a cross-task-deletion whose file is
1272
- * back). Anything model-judged or behavioral STAYS OPEN —
1273
- * `inv-no-false-clear`. `staticOk` is therefore only ever passed
1274
- * true where the gate itself just passed the statics.
1275
- */
1276
- const reconcileDebts = async (staticOk) => {
1277
- if (!deps.recheckOpenDebts)
1278
- return;
1279
- let fresh;
1280
- try {
1281
- fresh = await deps.recheckOpenDebts(cwd, staticOk);
1282
- }
1283
- catch {
1284
- // A ledger read fault is inconclusive: say nothing rather
1285
- // than imply the defects cleared.
1286
- return;
1287
- }
1288
- fin = {
1289
- ...fin,
1290
- openDebts: fresh.openDebts,
1291
- ...(fresh.debtNote ? { debtNote: fresh.debtNote } : {})
1292
- };
1293
- // Per-debt evidence from the VERIFY-COMMAND re-check
1294
- // (nexttask 5): which command was re-run and what it did. A
1295
- // close that cannot be read back from the trail is a close
1296
- // nobody can audit, and an INCONCLUSIVE re-run is worth
1297
- // saying out loud — it is the difference between "still
1298
- // broken" and "nothing could observe it".
1299
- for (const line of fresh.trail ?? []) {
1300
- await recGate(`defect re-check: ${line}`);
1301
- }
1302
- // Identity is (task, origin, reason), but a RESOLUTION claim
1303
- // needs more than a key miss: a ledger entry whose TEXT changed
1304
- // is the same defect re-recorded, never a fix. So a debt counts
1305
- // as closed only when nothing for that (task, origin) survives.
1306
- const slot = (d) => `${d.taskId}\t${d.origin ?? ''}`;
1307
- const before = new Set(reportedDebts.map(debtKey));
1308
- const after = new Set(fresh.openDebts.map(debtKey));
1309
- const beforeSlots = new Set(reportedDebts.map(slot));
1310
- const afterSlots = new Set(fresh.openDebts.map(slot));
1311
- const closed = reportedDebts.filter(d => !after.has(debtKey(d)) && !afterSlots.has(slot(d)));
1312
- const added = fresh.openDebts.filter(d => !before.has(debtKey(d)) && !beforeSlots.has(slot(d)));
1313
- if (closed.length === 0 && added.length === 0) {
1314
- if (reportedDebts.length > 0) {
1315
- await recGate(`defect re-check after autofix: all ${reportedDebts.length} defect(s) `
1316
- + 'above re-derived against the FINAL tree and still open');
1317
- }
1318
- return;
1319
- }
1320
- for (const d of closed) {
1321
- await recGate(`defect RESOLVED — ${d.taskId || '(unknown task)'}: `
1322
- + `${d.reason.slice(0, 240)}`);
1323
- }
1324
- await surfaceOpenDebts(added);
1325
- reportedDebts = fresh.openDebts;
1326
- await recGate(`defect re-check after autofix: ${closed.length} resolved, `
1327
- + `${fresh.openDebts.length} still open (re-derived against the FINAL tree)`);
1328
- };
1329
- // Resolution loop: Leave-failed (recommended) / Autofix (bounded,
1330
- // model-driven fix pass + gate re-run — run 7's gap: the picker
1331
- // had NO automated fix path) / Accept. The user always decides;
1332
- // after MAX_FINAL_GATE_AUTOFIX attempts that still FAIL the
1333
- // autofix card is withdrawn so the loop cannot run unbounded.
1334
- let fixAttempts = 0;
1335
- // Gitignored paths the fix passes have written so far in this
1336
- // resolution loop (mx5 run 19). Accumulated across attempts: a
1337
- // `.env` written by a failed attempt is still on disk for the next
1338
- // one, and that attempt's own before/after diff cannot see it.
1339
- let ignoredWritten = [];
1340
- // Sub-fixes a non-converging autofix attempt left uncommitted.
1341
- // Refreshed after every attempt; drives the picker note and the
1342
- // terminal commit (mx5 run 13 PROMPT 4 item 3, run 14 item 2b).
1343
- let stranded = [];
1344
- const refreshStranded = async () => {
1345
- if (!deps.pendingChanges)
1346
- return;
1347
- try {
1348
- stranded = await deps.pendingChanges(cwd);
1349
- }
1350
- catch {
1351
- // Inconclusive: say nothing rather than claim a clean tree.
1352
- stranded = [];
1353
- }
1354
- };
1355
- // NON-PROGRESS / UNFALSIFIABLE-CHECK state (mx5 run 14 item 2a).
1356
- // `prevFailSig` is the previous attempt's normalized ranked-first
1357
- // failure; `demoted` holds the signatures already carried as debt,
1358
- // so a re-run that still reports them does not re-fail the gate.
1359
- let prevFailSig = null;
1360
- const demoted = new Set();
1361
- // Set when a write-guard rejected an attempt whose edits could NOT
1362
- // be discarded: REJECTED edits are then sitting in the tree and
1363
- // must never be committed by the terminal paths below.
1364
- let rejectedEditsInTree = false;
1365
- // Commit whatever guard-clean repairs the fix passes left, on ANY
1366
- // terminal non-converged outcome. Run 14 ended on LEAVE with 13
1367
- // real repairs dirty in the tree after an unattended run — the
1368
- // next checkout would have destroyed them silently.
1369
- const commitStranded = async (outcome) => {
1370
- if (stranded.length === 0)
1371
- return;
1372
- if (rejectedEditsInTree) {
1373
- await recGate(`final-gate: NOT committing ${stranded.length} working-tree change(s) — a `
1374
- + `write-guard rejected an attempt and its edits could not be discarded, `
1375
- + `so the tree holds REJECTED edits: ${stranded.slice(0, 8).join(', ')}`);
1376
- return;
1377
- }
1378
- // REPORT WHAT ACTUALLY HAPPENED (mx5 run 20). This bound the
1379
- // CommitResult to `sha` and interpolated it, so the trail
1380
- // read "committed 5 stranded fix-pass change(s) as
1381
- // [object Object]". Worse than cosmetic: `commit` returns
1382
- // {committed, reason?, note?} and NEVER a sha, the
1383
- // `committed` field was never read, and gitCommitAll
1384
- // returns {committed:false} WITHOUT throwing on an unmerged
1385
- // index — so on that path the catch below never fires and
1386
- // the trail claimed a commit over changes that were still
1387
- // sitting in the working tree.
1388
- const notCommitted = async (why) => {
1389
- await recGate(`final-gate: could NOT commit ${stranded.length} stranded fix-pass `
1390
- + `change(s) (${why}) — they remain UNCOMMITTED in the working `
1391
- + `tree: ${stranded.slice(0, 8).join(', ')}`);
1392
- };
1393
- try {
1394
- const res = await deps.commit(cwd, STRANDED_FIX_COMMIT(id, outcome));
1395
- if (res.committed) {
1396
- await recGate(`final-gate: committed ${stranded.length} stranded fix-pass change(s)`
1397
- + `${res.note ? ` (${res.note})` : ''} — ${stranded.slice(0, 8).join(', ')}`);
1398
- }
1399
- else {
1400
- await notCommitted(res.reason ?? 'unknown');
1401
- }
1402
- }
1403
- catch (err) {
1404
- // Never break the terminal path over this — but say so, so
1405
- // the changes are not silently lost.
1406
- await notCommitted(err instanceof Error ? err.message : String(err));
1407
- }
1408
- };
1409
- while (!fin.ok) {
1410
- const canAutofix = deps.finalGateFix !== undefined && fixAttempts < MAX_FINAL_GATE_AUTOFIX;
1411
- // The picker question shows the debts (the HUMAN weighs them);
1412
- // the autofix seed below deliberately does not — mx5 run 11's
1413
- // fix child executed a debt claim as an `rm` instruction.
1414
- const question = `Final integration gate FAILED for ${id}.\n\n${fin.reason}${fin.debtNote ?? ''}\n\n`
1415
- + 'All tasks are checked off — this is the whole-repo check '
1416
- + '(the project’s own test/build/static commands, run unaided).'
1417
- + (fixAttempts > 0 ?
1418
- `\n\nAutofix attempts so far: ${fixAttempts}/${MAX_FINAL_GATE_AUTOFIX}.`
1419
- : '')
1420
- // Never let a partial repair be invisible at the moment
1421
- // the human decides (run 13: a bunfig fix that made
1422
- // `bun run test` pass 116/116 was stranded by an ACCEPT).
1423
- + strandedFixNote(stranded);
1424
- // YOLO: keep autofixing WHILE the card is still offered — the
1425
- // loop withdraws it after MAX_FINAL_GATE_AUTOFIX, so the cap
1426
- // that bounds a non-converging fix pass still bounds this —
1427
- // then LEAVE the run failed. Never 'accept': an unattended run
1428
- // that could not green the whole-repo gate has not produced a
1429
- // working project, and mx5 run 13 shows what an accepted FAIL
1430
- // looks like afterwards (a shipped app that 404s at `/`).
1431
- const yoloFinal = yoloFinalGateChoice(isYoloMode(), canAutofix);
1432
- if (yoloFinal !== null) {
1433
- await recGate(`final-gate: auto-chose ${yoloFinal.action.toUpperCase()} ${YOLO_STAMP}`);
1434
- }
1435
- const answer = yoloFinal !== null ?
1436
- yoloFinal.action === 'autofix' ?
1437
- FINAL_AUTOFIX_VALUE
1438
- : FINAL_LEAVE_VALUE
1439
- : await new SessionUI(active).ask({
1440
- localTitle: 'Final integration gate failed — how should pi proceed?',
1441
- displayQuestion: question,
1442
- question,
1443
- recommended: FINAL_LEAVE_LABEL,
1444
- recommended2: canAutofix ? FINAL_AUTOFIX_LABEL : FINAL_ACCEPT_LABEL,
1445
- allowSkip: false,
1446
- options: [
1447
- { label: FINAL_LEAVE_LABEL, value: FINAL_LEAVE_VALUE },
1448
- ...(canAutofix ?
1449
- [
1450
- {
1451
- label: FINAL_AUTOFIX_LABEL,
1452
- value: FINAL_AUTOFIX_VALUE
1453
- }
1454
- ]
1455
- : []),
1456
- { label: FINAL_ACCEPT_LABEL, value: FINAL_ACCEPT_VALUE }
1457
- ]
1458
- });
1459
- const choice = classifyFinalGateAnswer(answer);
1460
- if (choice.action === 'accept') {
1461
- await recGate('final-gate: FAIL accepted by user');
1462
- // STRANDED SUB-FIXES: the run completes here, so anything
1463
- // the fix pass repaired but never committed would be lost
1464
- // to the next `git checkout` while HEAD keeps the defect
1465
- // it fixed. Commit it as its own, named commit — the
1466
- // ACCEPT is a decision about the FAILING gate, never an
1467
- // instruction to throw away work (mx5 run 13 item 3).
1468
- await commitStranded('accepted');
1469
- active.ui.notify(`${id}: final integration gate FAIL accepted by user — completing.`
1470
- + (stranded.length > 0 ?
1471
- ` ${stranded.length} uncommitted fix-pass change(s) committed separately.`
1472
- : ''), 'warning');
1473
- break;
1474
- }
1475
- if (choice.action === 'autofix' && canAutofix) {
1476
- fixAttempts += 1;
1477
- await recGate(`final-gate: user chose AUTOFIX (attempt ${fixAttempts}/${MAX_FINAL_GATE_AUTOFIX})`);
1478
- active.ui.notify(`${id}: final-gate autofix (${fixAttempts}/${MAX_FINAL_GATE_AUTOFIX}) — bounded fix pass, then the gate re-runs…`, 'info');
1479
- const seed = choice.guidance ?
1480
- `${fin.reason}\n\nUser guidance: ${choice.guidance}`
1481
- : fin.reason;
1482
- const fix = await deps.finalGateFix(active, cwd, seed, ignoredWritten);
1483
- // IGNORED-PATH WRITES (mx5 run 19). The pass wrote file(s)
1484
- // git ignores, so they are not in the commit and a fresh
1485
- // clone does not have them. Trailed on EVERY outcome — a
1486
- // rejected attempt's tracked edits are discarded while its
1487
- // ignored writes survive on disk — and carried forward, so a
1488
- // later attempt's PASS is judged against everything this loop
1489
- // wrote, not just its own attempt. PATH NAMES ONLY: an ignored
1490
- // file's contents (`.env` is the canonical case) never enter a
1491
- // log, a debt or a child prompt.
1492
- if (fix.ignoredWrites && fix.ignoredWrites.length > 0) {
1493
- ignoredWritten = [
1494
- ...new Set([...ignoredWritten, ...fix.ignoredWrites])
1495
- ].sort();
1496
- await recGate(ignoredWriteTrailLine(fix.ignoredWrites));
1497
- // Debt only where a verdict can rest on the file: the
1498
- // probe proved the gate needs it, or the question stayed
1499
- // open. A write the gate demonstrably does NOT need is
1500
- // trailed and nothing more — a ledger full of scratch
1501
- // files is a ledger nobody reads.
1502
- if (fix.ignoredDependent !== false) {
1503
- await recordFinalGateUnobservedDebt(cwd, id, ignoredWriteDebtReason(fix.ignoredWrites, fix.ignoredDependent));
1504
- }
1505
- }
1506
- if (fix.ok) {
1507
- await deps.commit(cwd, `FINAL GATE AUTOFIX (${id})`);
1508
- // A converged re-run that observed nothing dynamic is
1509
- // UNOBSERVED on this door too — never announce it as a
1510
- // PASS just because it arrived via autofix.
1511
- if (fix.unobserved) {
1512
- unobservedNote = fix.unobserved;
1513
- await recordFinalGateUnobservedDebt(cwd, id, fix.unobserved);
1514
- }
1515
- await recGate(`final-gate: autofix ${fix.unobserved ? 'ended UNOBSERVED' : 'converged'} — ${fix.reason.slice(0, 200)}`);
1516
- active.ui.notify(`${id}: final integration gate ${fix.unobserved ? 'is UNOBSERVED' : 'PASSES'} after autofix — ${fix.reason.slice(0, 140)}`, fix.unobserved ? 'warning' : 'info');
1517
- fin = { ok: true, reason: fix.reason };
1518
- // The gate itself just passed, statics included, so
1519
- // `staticOk` here is proof rather than assumption.
1520
- await reconcileDebts(true);
1521
- break;
1522
- }
1523
- await recGate(`final-gate: autofix attempt ${fixAttempts} failed — ${fix.reason.slice(0, 200)}`);
1524
- // A guard that rejected an attempt WITHOUT discarding leaves
1525
- // rejected edits behind: the terminal paths must not commit
1526
- // the tree after that (the cheat guard stays intact).
1527
- if (fix.guardTripped === true && fix.editsDiscarded !== true) {
1528
- rejectedEditsInTree = true;
1529
- }
1530
- // The attempt's edits survive a non-convergence (only a
1531
- // guard trip discards). Find out what they are NOW, so
1532
- // the next picker shows them and a terminal outcome commits them.
1533
- await refreshStranded();
1534
- if (stranded.length > 0) {
1535
- await recGate(`final-gate: autofix attempt ${fixAttempts} left ${stranded.length} `
1536
- + `uncommitted change(s) — ${stranded.slice(0, 8).join(', ')}`);
1537
- }
1538
- active.ui.notify(`${id}: final-gate autofix did not converge — ${fix.reason.slice(0, 140)}`, 'warning');
1539
- // NON-PROGRESS CLASSIFIER (mx5 run 14 item 2a). An attempt
1540
- // that changed the tree, re-ran the gate, and got back the
1541
- // SAME ranked-first failure as the previous such attempt is
1542
- // evidence about the CHECK, not the fix: run 14 burned all
1543
- // three attempts on a boot probe that could not observe a
1544
- // listener in that sandbox at all. Demote that one check to
1545
- // UNOBSERVED-with-debt and let the REMAINING checks decide.
1546
- const detail = rankedFirstFailure({
1547
- reason: fix.gateReason,
1548
- failures: fix.gateFailures
1549
- });
1550
- const edited = fix.gateReason !== undefined && stranded.length > 0;
1551
- if (detail !== null
1552
- && isNonProgress({
1553
- previousSignature: prevFailSig,
1554
- currentDetail: detail,
1555
- edited
1556
- })) {
1557
- demoted.add(normalizeFailureDetail(detail));
1558
- prevFailSig = null;
1559
- const debtReason = unobservedDebtReason(detail);
1560
- await recordFinalGateUnobservedDebt(cwd, id, debtReason);
1561
- await recGate(`final-gate: check DEMOTED to UNOBSERVED after ${fixAttempts} tree-changing `
1562
- + `attempts returned an identical failure — carried as debt (origin final-gate) `
1563
- + `and re-checked by the next run's gate: ${detail.slice(0, 240)}`);
1564
- active.ui.notify(`${id}: final-gate check is unfalsifiable in this environment — carried as debt; `
1565
- + 'the remaining checks decide convergence.', 'warning');
1566
- }
1567
- else {
1568
- prevFailSig =
1569
- detail !== null ? normalizeFailureDetail(detail) : null;
1570
- }
1571
- // Convergence on the REMAINING checks: a demoted signature no
1572
- // longer counts against the gate. Nothing left ⇒ the run
1573
- // converges carrying the demotion as debt, and the fix passes'
1574
- // repairs are committed rather than stranded.
1575
- if (demoted.size > 0 && fix.gateReason !== undefined) {
1576
- const remaining = applyDemotions(fix.gateFailures ?? [fix.gateReason], demoted);
1577
- if (remaining.length === 0) {
1578
- await deps.commit(cwd, `FINAL GATE AUTOFIX (${id})`);
1579
- const converged = `converged on all remaining checks; ${demoted.size} check(s) `
1580
- + 'carried as UNOBSERVED debt (unfalsifiable in this environment)';
1581
- await recGate(`final-gate: ${converged}`);
1582
- active.ui.notify(`${id}: final integration gate converged — ${converged}.`, 'warning');
1583
- fin = { ok: true, reason: converged };
1584
- // Converged on the REMAINING checks only: one or
1585
- // more were DEMOTED as unfalsifiable here, and the
1586
- // statics may be among them. No proof ⇒ pass false,
1587
- // so nothing static-class can auto-close on this
1588
- // door (inv-no-false-clear).
1589
- await reconcileDebts(false);
1590
- break;
1591
- }
1592
- }
1593
- // Work from the FRESH gate failure when the fix pass got
1594
- // as far as re-running the gate; otherwise keep the last.
1595
- // The full ranked list rides along (and is re-trailed
1596
- // when fresh) so the next picker and the next fix seed
1597
- // still carry every entry, not just the first. The debt
1598
- // note is carried so the next picker still shows the
1599
- // open claims (the seed never includes it).
1600
- // Demoted checks are stripped from what rides forward, so the
1601
- // next picker and the next fix seed target only what is still
1602
- // falsifiable — never re-aiming the child at the check the
1603
- // classifier just proved it cannot move.
1604
- const freshFailures = fix.gateReason !== undefined ? fix.gateFailures : fin.failures;
1605
- const carried = freshFailures !== undefined ?
1606
- applyDemotions(freshFailures, demoted)
1607
- : undefined;
1608
- fin = {
1609
- ok: false,
1610
- reason: (demoted.size > 0
1611
- && carried !== undefined
1612
- && carried.length > 0) ?
1613
- carried[0]
1614
- : (fix.gateReason ?? fin.reason),
1615
- failures: carried,
1616
- debtNote: fin.debtNote
1617
- };
1618
- if (fix.gateReason !== undefined
1619
- && (fix.gateFailures?.length ?? 0) > 1) {
1620
- await trailGateFail(fin);
1621
- }
1622
- continue;
1623
- }
1624
- // Leave failed — the dismissal default, unchanged from the
1625
- // two-option picker (an unavailable autofix demotes here too).
1626
- await recGate(yoloFinal !== null ?
1627
- `final-gate: left failed — autofix budget spent, nobody to ask ${YOLO_STAMP}`
1628
- : 'final-gate: left failed (user)');
1629
- // Leaving the run failed is TERMINAL for an unattended run, so
1630
- // the fix passes' guard-clean repairs are committed here too —
1631
- // run 14 left 13 of them dirty for a `git checkout` to destroy
1632
- // (mx5 run 13 item 3, run 14 item 2b). The user still owns the
1633
- // outcome; they own it with the work in HEAD, named in the trail.
1634
- await commitStranded('left-failed');
1635
- await updateTaskFrontMatter(cwd, id, { state: 'failed' });
1636
- announceDone(active, `${id} finished all tasks but FAILED the final integration gate — ${fin.reason.slice(0, 200)} — fix and /task-auto-resume (the gate re-runs).`
1637
- + (stranded.length > 0 ?
1638
- ` NOTE: ${stranded.length} fix-pass change(s) were committed separately (${stranded.slice(0, 4).join(', ')}).`
1639
- : ''), 'error');
1640
- return;
1641
- }
1169
+ if (stage.kind === 'failed') {
1170
+ await updateTaskFrontMatter(cwd, id, { state: 'failed' });
1171
+ announceDone(active, stage.message, 'error');
1172
+ return;
1642
1173
  }
1643
1174
  await updateTaskFrontMatter(cwd, id, { state: 'completed' });
1644
- announceDone(active, `${id} complete — all ${entries.length} tasks done.`
1645
- + (unobservedNote ?
1646
- ' WARNING: the final integration gate was UNOBSERVED — it ran no '
1647
- + 'dynamic check at all, so "complete" here means the statics '
1648
- + 'passed and nothing more. Carried as debt for the next run.'
1649
- : ''), unobservedNote ? 'warning' : 'info');
1175
+ announceDone(active, stage.message, stage.level);
1650
1176
  return;
1651
1177
  }
1652
1178
  // REFUSE to start on a conflicted tree: an unmerged index dooms every
@@ -66,6 +66,8 @@ export type { PhaseDeps };
66
66
  * If the child leaks a tool call as plain text (wrong dialect — never executed),
67
67
  * re-prompt with a correction hint up to MAX_LEAK_RETRIES times; if it keeps
68
68
  * leaking, throw LeakedToolCallError rather than returning the unexecuted call.
69
+ * Empty completions and connection-class model errors share that same budget —
70
+ * see triageChildResult, which decides every one of those cases.
69
71
  */
70
72
  export declare function runPhaseChild(deps: PhaseDeps, name: string, tools: string, prompt: string): Promise<string>;
71
73
  export declare function formatLoopHint(hit: LoopHit): string;