@mjasnikovs/pi-task 0.37.7 → 0.38.0
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-output.d.ts +19 -3
- package/dist/shared/child-output.js +21 -5
- package/dist/shared/git-runner.d.ts +39 -0
- package/dist/shared/git-runner.js +38 -0
- package/dist/task/accept-debt.d.ts +27 -58
- package/dist/task/accept-debt.js +60 -130
- package/dist/task/auto-orchestrator.d.ts +7 -57
- package/dist/task/auto-orchestrator.js +25 -499
- package/dist/task/child-runner.d.ts +2 -0
- package/dist/task/child-runner.js +74 -70
- package/dist/task/enforce-guidelines.d.ts +1 -1
- package/dist/task/enforce-guidelines.js +2 -2
- package/dist/task/external-context.d.ts +85 -7
- package/dist/task/external-context.js +100 -63
- package/dist/task/file-inventory.js +22 -41
- package/dist/task/final-gate.d.ts +80 -0
- package/dist/task/final-gate.js +102 -49
- package/dist/task/gate-deps.js +6 -23
- package/dist/task/git-state-guard.d.ts +1 -1
- package/dist/task/git-state-guard.js +1 -7
- package/dist/task/phases.js +40 -83
- package/dist/task/run-final-gate.d.ts +127 -0
- package/dist/task/run-final-gate.js +492 -0
- package/dist/task/task-gates.d.ts +20 -57
- package/dist/task/task-gates.js +11 -11
- package/dist/task/verify-work.d.ts +40 -32
- package/dist/task/verify-work.js +301 -241
- package/dist/workers/docs-core.d.ts +14 -0
- package/dist/workers/docs-core.js +28 -16
- package/dist/workers/fetch-core.d.ts +6 -1
- package/dist/workers/fetch-core.js +26 -33
- package/dist/workers/focused-extractor.d.ts +73 -0
- package/dist/workers/focused-extractor.js +72 -0
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +48 -42
- package/dist/workers/pi-worker-fetch.js +6 -8
- package/dist/workers/typeonly-log.d.ts +13 -0
- 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,
|
|
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
|
|
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).
|
|
1155
|
-
// OWN whole-repo commands once, unaided, before
|
|
1156
|
-
// complete
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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,
|
|
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;
|