@mjasnikovs/pi-task 0.18.38 → 0.18.40
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/task/accept-debt.d.ts +18 -1
- package/dist/task/accept-debt.js +20 -1
- package/dist/task/auto-io.d.ts +16 -0
- package/dist/task/auto-io.js +54 -0
- package/dist/task/auto-orchestrator.js +76 -8
- package/dist/task/gate-deps.js +36 -1
- package/dist/task/root-cause-repair.d.ts +122 -0
- package/dist/task/root-cause-repair.js +378 -0
- package/dist/task/task-gates.d.ts +31 -0
- package/dist/task/task-gates.js +58 -1
- package/dist/workers/pi-worker-docs.d.ts +7 -0
- package/dist/workers/pi-worker-docs.js +16 -0
- package/dist/workers/research-cache.d.ts +25 -17
- package/dist/workers/research-cache.js +122 -58
- package/dist/workers/shared.d.ts +8 -0
- package/dist/workers/shared.js +0 -0
- package/package.json +1 -1
|
@@ -29,8 +29,16 @@
|
|
|
29
29
|
* `lsof` and the sandbox had neither) and no further attempt can move it. The
|
|
30
30
|
* run is allowed to converge on the REMAINING checks, carrying this one here
|
|
31
31
|
* so the next run's gate re-checks and re-surfaces it rather than losing it.
|
|
32
|
+
* - 'root-cause' — the task's verify FAILed because of a PRE-EXISTING defect in a
|
|
33
|
+
* file a DIFFERENT task created, which this task's own work never touched (mx5
|
|
34
|
+
* run 14: TASK_0007's `test/teardown.ts` TRUNCATE bug FAILed TASK_0013 and
|
|
35
|
+
* TASK_0019). The current task is not at fault, so its work — and, at the
|
|
36
|
+
* enforce site, the enforce pass's edits — are KEPT rather than reverted; the
|
|
37
|
+
* defect is recorded here and a scoped repair task is queued into the plan
|
|
38
|
+
* (root-cause-repair.ts). Before this class existed the ledger recorded the same
|
|
39
|
+
* root cause twice and nothing ever scheduled a fix, so it survived ~24h.
|
|
32
40
|
*/
|
|
33
|
-
export type DebtOrigin = 'accepted' | 'enforce-revert' | 'frozen-blocked' | 'cross-task-deletion' | 'yolo-accepted' | 'final-gate';
|
|
41
|
+
export type DebtOrigin = 'accepted' | 'enforce-revert' | 'frozen-blocked' | 'cross-task-deletion' | 'yolo-accepted' | 'final-gate' | 'root-cause';
|
|
34
42
|
/** One recorded defect: the task, why its VERIFY failed, and how it was recorded. */
|
|
35
43
|
export interface AcceptDebt {
|
|
36
44
|
taskId: string;
|
|
@@ -105,6 +113,15 @@ export declare function recordYoloAcceptDebt(cwd: string, taskId: string, reason
|
|
|
105
113
|
* The taskId is the run's parent id — the demotion is a run-level decision.
|
|
106
114
|
*/
|
|
107
115
|
export declare function recordFinalGateUnobservedDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Record a ROOT-CAUSE debt (mx5 run 14 / PROMPT item 5): this task's verify FAILed
|
|
118
|
+
* on a pre-existing defect in a file ANOTHER task created and this task never
|
|
119
|
+
* touched. The current task is not at fault — its work (and, at the enforce site,
|
|
120
|
+
* the enforce pass's edits) is KEPT — but the defect is real and still in the tree,
|
|
121
|
+
* so it is recorded here and a scoped repair task is queued (root-cause-repair.ts).
|
|
122
|
+
* Behavioral/model-judged, so the final gate surfaces it rather than auto-closing it.
|
|
123
|
+
*/
|
|
124
|
+
export declare function recordRootCauseDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
108
125
|
/**
|
|
109
126
|
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
110
127
|
* recordCrossTaskDeletionDebt writes). Null on any other reason text — an
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -78,7 +78,8 @@ export function parseAcceptDebts(raw) {
|
|
|
78
78
|
|| origin === 'frozen-blocked'
|
|
79
79
|
|| origin === 'cross-task-deletion'
|
|
80
80
|
|| origin === 'yolo-accepted'
|
|
81
|
-
|| origin === 'final-gate'
|
|
81
|
+
|| origin === 'final-gate'
|
|
82
|
+
|| origin === 'root-cause') ?
|
|
82
83
|
{ origin: origin }
|
|
83
84
|
: {})
|
|
84
85
|
});
|
|
@@ -203,6 +204,21 @@ export async function recordFinalGateUnobservedDebt(cwd, taskId, reason) {
|
|
|
203
204
|
origin: 'final-gate'
|
|
204
205
|
});
|
|
205
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Record a ROOT-CAUSE debt (mx5 run 14 / PROMPT item 5): this task's verify FAILed
|
|
209
|
+
* on a pre-existing defect in a file ANOTHER task created and this task never
|
|
210
|
+
* touched. The current task is not at fault — its work (and, at the enforce site,
|
|
211
|
+
* the enforce pass's edits) is KEPT — but the defect is real and still in the tree,
|
|
212
|
+
* so it is recorded here and a scoped repair task is queued (root-cause-repair.ts).
|
|
213
|
+
* Behavioral/model-judged, so the final gate surfaces it rather than auto-closing it.
|
|
214
|
+
*/
|
|
215
|
+
export async function recordRootCauseDebt(cwd, taskId, reason) {
|
|
216
|
+
await appendDebt(cwd, {
|
|
217
|
+
taskId: taskId.trim(),
|
|
218
|
+
reason: normaliseReason(reason),
|
|
219
|
+
origin: 'root-cause'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
206
222
|
/**
|
|
207
223
|
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
208
224
|
* recordCrossTaskDeletionDebt writes). Null on any other reason text — an
|
|
@@ -357,6 +373,9 @@ export function describeDebt(d) {
|
|
|
357
373
|
if (d.origin === 'yolo-accepted') {
|
|
358
374
|
return 'auto-ACCEPTED by YOLO mode despite verify-FAIL (unattended — no human weighed this)';
|
|
359
375
|
}
|
|
376
|
+
if (d.origin === 'root-cause') {
|
|
377
|
+
return "verify FAILed on a PRE-EXISTING defect in another task's file that this task never touched (this task's work was kept; a scoped repair task was queued for the root cause)";
|
|
378
|
+
}
|
|
360
379
|
if (d.origin === 'final-gate') {
|
|
361
380
|
return 'final-gate check DEMOTED to UNOBSERVED (identical failure across two tree-changing fix attempts — unfalsifiable in that environment, never proven passing)';
|
|
362
381
|
}
|
package/dist/task/auto-io.d.ts
CHANGED
|
@@ -42,5 +42,21 @@ export declare function checkOffTask(cwd: string, id: string, index: number, pro
|
|
|
42
42
|
* of starting a brand-new task — matching how /task-resume behaves.
|
|
43
43
|
*/
|
|
44
44
|
export declare function stampTaskInProgress(cwd: string, id: string, index: number, producedId: string, title: string): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Insert a NEW unchecked entry directly after the `afterIndex`th checkbox — the
|
|
47
|
+
* mid-run plan mutation the root-cause repair channel needs (mx5 run 14 item 5:
|
|
48
|
+
* a repair task must land BEFORE the next dependent task, not at the end of the
|
|
49
|
+
* plan, or the defect keeps failing everything in between).
|
|
50
|
+
*
|
|
51
|
+
* MONOTONIC by construction (the run-12 replacement lesson): this only ever
|
|
52
|
+
* SPLICES a line in. No existing entry is rewritten, reordered, or dropped, and
|
|
53
|
+
* an already-present title is a no-op — so a plan can grow mid-run but never
|
|
54
|
+
* shrink, and a retried insert cannot duplicate. Returns whether a line was added.
|
|
55
|
+
*
|
|
56
|
+
* Later entries shift down by one, which is safe because the /task-auto loop
|
|
57
|
+
* re-reads and re-parses the plan at the top of every iteration and locates its
|
|
58
|
+
* next step by "first unchecked" rather than by a cached index.
|
|
59
|
+
*/
|
|
60
|
+
export declare function insertTaskAfter(cwd: string, id: string, afterIndex: number, title: string): Promise<boolean>;
|
|
45
61
|
/** Find the most-recently-updated resumable TASK_AUTO_* file, or null. */
|
|
46
62
|
export declare function findResumableAuto(cwd: string): Promise<string | null>;
|
package/dist/task/auto-io.js
CHANGED
|
@@ -140,6 +140,60 @@ export async function checkOffTask(cwd, id, index, producedId, title) {
|
|
|
140
140
|
export async function stampTaskInProgress(cwd, id, index, producedId, title) {
|
|
141
141
|
await rewriteTaskLine(cwd, id, index, () => `- [ ] ${producedId} ${title}`, 'stampTaskInProgress');
|
|
142
142
|
}
|
|
143
|
+
/** The bare title of a checkbox line (id stamp stripped), or null if not one. */
|
|
144
|
+
function entryTitle(line) {
|
|
145
|
+
const m = CHECKBOX_RE.exec(line.trim());
|
|
146
|
+
if (!m)
|
|
147
|
+
return null;
|
|
148
|
+
const rest = m[2].trim();
|
|
149
|
+
const idm = PRODUCED_ID_RE.exec(rest);
|
|
150
|
+
return idm ? idm[2].trim() : rest;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Insert a NEW unchecked entry directly after the `afterIndex`th checkbox — the
|
|
154
|
+
* mid-run plan mutation the root-cause repair channel needs (mx5 run 14 item 5:
|
|
155
|
+
* a repair task must land BEFORE the next dependent task, not at the end of the
|
|
156
|
+
* plan, or the defect keeps failing everything in between).
|
|
157
|
+
*
|
|
158
|
+
* MONOTONIC by construction (the run-12 replacement lesson): this only ever
|
|
159
|
+
* SPLICES a line in. No existing entry is rewritten, reordered, or dropped, and
|
|
160
|
+
* an already-present title is a no-op — so a plan can grow mid-run but never
|
|
161
|
+
* shrink, and a retried insert cannot duplicate. Returns whether a line was added.
|
|
162
|
+
*
|
|
163
|
+
* Later entries shift down by one, which is safe because the /task-auto loop
|
|
164
|
+
* re-reads and re-parses the plan at the top of every iteration and locates its
|
|
165
|
+
* next step by "first unchecked" rather than by a cached index.
|
|
166
|
+
*/
|
|
167
|
+
export async function insertTaskAfter(cwd, id, afterIndex, title) {
|
|
168
|
+
const clean = title.trim();
|
|
169
|
+
if (clean.length === 0)
|
|
170
|
+
return false;
|
|
171
|
+
const { body } = await readTaskFile(cwd, id);
|
|
172
|
+
const section = extractSection(body, 'tasks') ?? '';
|
|
173
|
+
const lines = section.split('\n');
|
|
174
|
+
// Duplicate check scans the WHOLE list first: an existing entry with this exact
|
|
175
|
+
// title (checked or not) means the plan already carries this step, wherever it
|
|
176
|
+
// sits relative to afterIndex — never add a second one.
|
|
177
|
+
if (lines.some(l => entryTitle(l) === clean))
|
|
178
|
+
return false;
|
|
179
|
+
let seen = -1;
|
|
180
|
+
let insertAt = -1;
|
|
181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
182
|
+
if (!CHECKBOX_RE.test(lines[i].trim()))
|
|
183
|
+
continue;
|
|
184
|
+
seen++;
|
|
185
|
+
insertAt = i + 1;
|
|
186
|
+
if (seen === afterIndex)
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
// An out-of-range index appends after the LAST checkbox rather than throwing:
|
|
190
|
+
// a plan that grew underneath the caller must still receive the entry.
|
|
191
|
+
if (insertAt === -1)
|
|
192
|
+
return false;
|
|
193
|
+
lines.splice(insertAt, 0, `- [ ] ${clean}`);
|
|
194
|
+
await setTaskSection(cwd, id, 'tasks', lines.join('\n'));
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
143
197
|
/** Find the most-recently-updated resumable TASK_AUTO_* file, or null. */
|
|
144
198
|
export async function findResumableAuto(cwd) {
|
|
145
199
|
await ensureTasksDir(cwd);
|
|
@@ -14,7 +14,8 @@ import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js'
|
|
|
14
14
|
import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT, DECOMPOSE_COVERAGE_PROMPT } from './auto-prompts.js';
|
|
15
15
|
import { GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT } from './prompts.js';
|
|
16
16
|
import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './question-dedup.js';
|
|
17
|
-
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
|
|
17
|
+
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, parseTaskList, checkOffTask, stampTaskInProgress, insertTaskAfter, findResumableAuto } from './auto-io.js';
|
|
18
|
+
import { drainRepairQueue, mergeRepairCandidates, planHasRepairFor, parseRepairTitleFile, buildRepairTitle, buildRepairScopeFence, extractFailingCommand } from './root-cause-repair.js';
|
|
18
19
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
|
|
19
20
|
import { readTextFile } from '../shared/fs-text.js';
|
|
20
21
|
import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
@@ -320,6 +321,63 @@ export function buildScopeFence(titles, currentIndex) {
|
|
|
320
321
|
+ `components, or flows owned by a later step.\n\n`
|
|
321
322
|
+ `The full plan (these run separately — do NOT implement them here):\n${listing}`);
|
|
322
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* The scope fence for step `currentIndex`, plus the REPAIR fence when that step is
|
|
326
|
+
* a queued root-cause repair. A repair title ("repair test/teardown.ts: …") reads
|
|
327
|
+
* to refine like any other feature step, and refine's job is to expand a title into
|
|
328
|
+
* a full spec — which is exactly how "repair the teardown" becomes "overhaul the
|
|
329
|
+
* test infrastructure" (the /task-auto drift lesson). The extra fence pins the one
|
|
330
|
+
* editable file and pins VERIFY to the command the defect was failing.
|
|
331
|
+
*/
|
|
332
|
+
function buildStepFence(titles, currentIndex) {
|
|
333
|
+
const base = buildScopeFence(titles, currentIndex);
|
|
334
|
+
const repairFile = parseRepairTitleFile(titles[currentIndex] ?? '');
|
|
335
|
+
if (!repairFile)
|
|
336
|
+
return base;
|
|
337
|
+
return `${base}\n\n${buildRepairScopeFence(repairFile, extractFailingCommand(titles[currentIndex] ?? ''))}`;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Drain the gate's root-cause repair queue into the running plan: one scoped
|
|
341
|
+
* repair step per accused FILE, spliced in directly after the step that just
|
|
342
|
+
* finished.
|
|
343
|
+
*
|
|
344
|
+
* Three bounds, all mandatory (mx5 run 14 item 5 gray areas):
|
|
345
|
+
* - DEDUP by file — run 14's two `test/teardown.ts` debts must yield ONE repair
|
|
346
|
+
* step, not two. mergeRepairCandidates collapses the drained queue, and
|
|
347
|
+
* planHasRepairFor rejects a file the plan already carries a repair for.
|
|
348
|
+
* - CAP 1 per file per RUN — planHasRepairFor counts CHECKED-OFF entries too, so
|
|
349
|
+
* a repair step that itself failed is never re-spawned; it lands in the
|
|
350
|
+
* accept-debt ledger like any other task. That is what stops a repair loop.
|
|
351
|
+
* - MONOTONIC — insertTaskAfter only splices; no existing entry is rewritten,
|
|
352
|
+
* reordered or dropped (the run-12 replacement lesson).
|
|
353
|
+
*
|
|
354
|
+
* Best-effort throughout: a fault here must never fail the run that produced the
|
|
355
|
+
* finding — the debt is already durably recorded either way.
|
|
356
|
+
*/
|
|
357
|
+
async function schedulePendingRepairs(cwd, id, afterIndex, ctx, deps) {
|
|
358
|
+
try {
|
|
359
|
+
const pending = await drainRepairQueue(cwd);
|
|
360
|
+
if (pending.length === 0)
|
|
361
|
+
return;
|
|
362
|
+
const { body } = await readTaskFile(cwd, id);
|
|
363
|
+
const titles = parseTaskList(body).map(e => e.title);
|
|
364
|
+
let at = afterIndex;
|
|
365
|
+
for (const repair of mergeRepairCandidates(pending)) {
|
|
366
|
+
if (planHasRepairFor(titles, repair.file))
|
|
367
|
+
continue;
|
|
368
|
+
const title = buildRepairTitle(repair);
|
|
369
|
+
if (!(await insertTaskAfter(cwd, id, at, title)))
|
|
370
|
+
continue;
|
|
371
|
+
titles.splice(at + 1, 0, title);
|
|
372
|
+
at += 1;
|
|
373
|
+
await deps.record?.(cwd, id, `plan: inserted scoped repair step after step ${afterIndex + 1} — ${title}`);
|
|
374
|
+
ctx.ui.notify(`${id}: queued a scoped repair for ${repair.file} (${repair.owner}'s file — root cause of ${repair.blamed.join(', ')}).`, 'warning');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// the plan is best-effort here; the underlying debt is already recorded
|
|
379
|
+
}
|
|
380
|
+
}
|
|
323
381
|
/** Plan phase: clarify → decompose → write AUTO file. Returns the new id, or null. */
|
|
324
382
|
export async function planAuto(ctx, cwd, feature, deps) {
|
|
325
383
|
// clarify — sequential & adaptive: ask one question at a time, feeding every
|
|
@@ -1341,7 +1399,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1341
1399
|
// matters when refine runs fresh (a resumed task past refine ignores
|
|
1342
1400
|
// it), but always supplied so a resume that restarts at refine is
|
|
1343
1401
|
// fenced too.
|
|
1344
|
-
planContext:
|
|
1402
|
+
planContext: buildStepFence(entries.map(e => e.title), next.index),
|
|
1345
1403
|
onStart: resumeId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
|
|
1346
1404
|
});
|
|
1347
1405
|
active = res.ctx ?? active;
|
|
@@ -1396,7 +1454,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1396
1454
|
title: next.title,
|
|
1397
1455
|
tag: id,
|
|
1398
1456
|
// Fence an AUTOFIX re-run against re-expanding the whole spec.
|
|
1399
|
-
planContext:
|
|
1457
|
+
planContext: buildStepFence(entries.map(e => e.title), next.index),
|
|
1400
1458
|
// res.ok === true means runner.run() completed, so res.taskId is the
|
|
1401
1459
|
// allocated TASK_NNNN id (never empty here). The parent task-list
|
|
1402
1460
|
// check-off runs after verify passes/accepts and before the commit,
|
|
@@ -1426,6 +1484,14 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1426
1484
|
announceDone(active, `${id} stopped at "${next.title}"${why} — fix and run /task-auto-resume.`, 'error');
|
|
1427
1485
|
return;
|
|
1428
1486
|
}
|
|
1487
|
+
// ROOT-CAUSE REPAIR (mx5 run 14 item 5): the gate may have attributed a
|
|
1488
|
+
// FAIL to a pre-existing defect in a file some OTHER task created. It can
|
|
1489
|
+
// only QUEUE that finding — mutating the plan is this loop's job. Drain
|
|
1490
|
+
// the queue and splice a scoped repair step in right after the step that
|
|
1491
|
+
// just finished, so the defect is fixed BEFORE the next dependent task
|
|
1492
|
+
// trips over it too (run 14 recorded the same `test/teardown.ts` cause
|
|
1493
|
+
// twice, scheduled nothing, and the bug outlived ~24h of the run).
|
|
1494
|
+
await schedulePendingRepairs(cwd, id, next.index, active, deps);
|
|
1429
1495
|
// gate.kind === 'done' → fall through to the next task, after checking
|
|
1430
1496
|
// no landmine stash was left behind by anything that ran in between.
|
|
1431
1497
|
if (deps.stashRef && stashBefore !== undefined) {
|
|
@@ -1518,13 +1584,15 @@ async function handleTaskAutoResume(_args, ctx) {
|
|
|
1518
1584
|
autoRunning = true;
|
|
1519
1585
|
armTerminalCancel(ctx);
|
|
1520
1586
|
try {
|
|
1521
|
-
// Reuse the interrupted run's research-cache id
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1587
|
+
// Reuse the interrupted run's research-cache id, dropping only the entries whose
|
|
1588
|
+
// own package moved version (F10). mx5 run 13 resumed three times and each
|
|
1589
|
+
// resume's fresh id discarded a working 201-entry cache; run 14 then showed a
|
|
1590
|
+
// whole-file freshness gate can never hold on a greenfield run that installs
|
|
1591
|
+
// packages as it goes, so invalidation is per entry. See resumeResearchRun.
|
|
1525
1592
|
const research = await resumeResearchRun(cwd, getConfig().researchCache);
|
|
1526
1593
|
if (research.reused) {
|
|
1527
|
-
logPlanDebug(cwd, `research cache: resume reused ${research.entries} entr(ies)`
|
|
1594
|
+
logPlanDebug(cwd, `research cache: resume reused ${research.entries} entr(ies), `
|
|
1595
|
+
+ `dropped ${research.dropped} stale`);
|
|
1528
1596
|
}
|
|
1529
1597
|
const abort = new AbortController();
|
|
1530
1598
|
// Resume only runs the loop (runTask); no planning children, so the loader
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -22,7 +22,8 @@ import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-
|
|
|
22
22
|
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
23
23
|
import { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
24
24
|
import { readContracts } from './contracts.js';
|
|
25
|
-
import { recordAcceptDebt, recordEnforceRevertDebt, recordFrozenBlockedDebt, recordCrossTaskDeletionDebt, recordYoloAcceptDebt } from './accept-debt.js';
|
|
25
|
+
import { recordAcceptDebt, recordEnforceRevertDebt, recordFrozenBlockedDebt, recordCrossTaskDeletionDebt, recordYoloAcceptDebt, recordRootCauseDebt } from './accept-debt.js';
|
|
26
|
+
import { recordRepairCandidate } from './root-cause-repair.js';
|
|
26
27
|
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
27
28
|
import { runFinalIntegrationGate, discoverGateCommandLabels } from './final-gate.js';
|
|
28
29
|
import { runFinalGateAutofix } from './final-gate-fix.js';
|
|
@@ -471,6 +472,40 @@ export function buildGateDeps(params) {
|
|
|
471
472
|
// deliverable this task's diff deletes, ACCEPTed into a commit anyway —
|
|
472
473
|
// the final gate re-checks it (resolved iff the file is back in the tree).
|
|
473
474
|
recordCrossTaskDeletionDebt: (cwd2, taskId, deletion) => recordCrossTaskDeletionDebt(cwd2, taskId, deletion),
|
|
475
|
+
// ROOT-CAUSE channel (mx5 run 14 item 5): a FAIL another task's untouched
|
|
476
|
+
// file caused is recorded as its own debt class and queued as a scoped
|
|
477
|
+
// repair task, instead of being blamed on — and reverted out of — the task
|
|
478
|
+
// that merely tripped over it.
|
|
479
|
+
recordRootCauseDebt: (cwd2, taskId, reason) => recordRootCauseDebt(cwd2, taskId, reason),
|
|
480
|
+
recordRepairCandidate: (cwd2, candidate) => recordRepairCandidate(cwd2, candidate),
|
|
481
|
+
// file → introducing task, the provenance half of the discriminator.
|
|
482
|
+
introducedBy: (cwd2, rel) => Promise.resolve(taskThatIntroduced(cwd2, rel)),
|
|
483
|
+
// The authorship half: which files THIS task's work touched. `worktree` is
|
|
484
|
+
// the pre-commit verify site (uncommitted changes); `committed` is the
|
|
485
|
+
// post-commit enforce site, where the task snapshot and the ENFORCE commit
|
|
486
|
+
// are the last two commits. Any git fault returns null, which stands the
|
|
487
|
+
// channel down entirely rather than guessing.
|
|
488
|
+
touchedFiles: async (cwd2, scope) => {
|
|
489
|
+
try {
|
|
490
|
+
if (scope === 'worktree') {
|
|
491
|
+
const r = await git(cwd2, ['status', '--porcelain'], signal);
|
|
492
|
+
if (r.exitCode !== 0)
|
|
493
|
+
return null;
|
|
494
|
+
const c = parseTreeChanges(r.stdout);
|
|
495
|
+
return [...c.modified, ...c.added, ...c.deleted];
|
|
496
|
+
}
|
|
497
|
+
const r = await git(cwd2, ['log', '-n', '2', '--name-only', '--format=', 'HEAD'], signal);
|
|
498
|
+
if (r.exitCode !== 0)
|
|
499
|
+
return null;
|
|
500
|
+
return r.stdout
|
|
501
|
+
.split('\n')
|
|
502
|
+
.map(l => l.trim())
|
|
503
|
+
.filter(l => l.length > 0);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
},
|
|
474
509
|
// Frozen-path write-deny (see frozen-path-guard.ts): the concrete paths this
|
|
475
510
|
// task's spec forbids modifying, so the gate sequence can UNDO any edit the
|
|
476
511
|
// enforce EDIT pass makes to them before those edits are committed. Reads the
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** One file accused of causing another task's verify FAIL. */
|
|
2
|
+
export interface RepairCandidate {
|
|
3
|
+
/** Repo-relative path of the accused file. */
|
|
4
|
+
file: string;
|
|
5
|
+
/** The task whose commit introduced `file` (provenance). */
|
|
6
|
+
owner: string;
|
|
7
|
+
/** One-line summary of the defect, lifted from the FAIL text. */
|
|
8
|
+
defect: string;
|
|
9
|
+
/** The task whose verify FAILed because of it. */
|
|
10
|
+
blamedTask: string;
|
|
11
|
+
/** The failing command from the debt — becomes the repair task's VERIFY. */
|
|
12
|
+
verifyCommand?: string;
|
|
13
|
+
}
|
|
14
|
+
/** True when the FAIL text blames the ENVIRONMENT rather than a file. */
|
|
15
|
+
export declare function isEnvironmentAttributed(text: string): boolean;
|
|
16
|
+
interface Accusation {
|
|
17
|
+
file: string;
|
|
18
|
+
/** Character distance between the blame cue and the path token. */
|
|
19
|
+
distance: number;
|
|
20
|
+
/** The clause the accusation was made in — the defect summary source. */
|
|
21
|
+
clause: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The single file a FAIL text accuses, or null. Scans every blame cue, pairs it
|
|
25
|
+
* with the NEAREST path token within {@link BLAME_WINDOW} characters (a cue and its
|
|
26
|
+
* subject sit adjacent in practice: "pre-existing teardown bug in
|
|
27
|
+
* `test/teardown.ts`"), and keeps the closest pair overall. One FAIL has one root
|
|
28
|
+
* cause, so this deliberately returns at most one accusation rather than every
|
|
29
|
+
* path the reason happens to name.
|
|
30
|
+
*/
|
|
31
|
+
export declare function findAccusedFile(text: string): Accusation | null;
|
|
32
|
+
/**
|
|
33
|
+
* Collapse whitespace and clamp — a stored defect summary is one short line that
|
|
34
|
+
* has to read well inside a plan title. The gate's own verdict boilerplate ("work
|
|
35
|
+
* did not verify: ") and a leading repeat of the accused file are stripped: the
|
|
36
|
+
* title already names both the step kind and the file, so repeating them there
|
|
37
|
+
* spends the clamp budget on nothing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function summariseDefect(clause: string, file?: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* A runnable command quoted in the FAIL text — the repair task's VERIFY, per the
|
|
42
|
+
* requirement that it re-run the exact command the debt failed on. Only the first
|
|
43
|
+
* backticked token that STARTS like a shell command (optionally env-prefixed)
|
|
44
|
+
* qualifies, so prose in backticks is never mistaken for a command.
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractFailingCommand(text: string): string | undefined;
|
|
47
|
+
export interface RootCauseInput {
|
|
48
|
+
/** The verify gate's FAIL reason. */
|
|
49
|
+
failReason: string;
|
|
50
|
+
/** The resolution research's rationale, when one ran ('' otherwise). */
|
|
51
|
+
rationale?: string;
|
|
52
|
+
/** The task whose verify FAILed. */
|
|
53
|
+
currentTaskId: string;
|
|
54
|
+
/**
|
|
55
|
+
* Paths the CURRENT task's own work touches. `null` means unknown (git
|
|
56
|
+
* unavailable) — the channel stands down rather than guessing, so an
|
|
57
|
+
* unreadable tree can only cost a repair task, never spawn a wrong one.
|
|
58
|
+
*/
|
|
59
|
+
touched: string[] | null;
|
|
60
|
+
/** file → introducing task (task-provenance.ts). May throw; a throw reads
|
|
61
|
+
* as unknown provenance. */
|
|
62
|
+
introducedBy: (rel: string) => string | null | Promise<string | null>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The repair candidate a verify FAIL justifies, or null. All three conditions from
|
|
66
|
+
* the module header must hold; anything unknown or environment-shaped returns null.
|
|
67
|
+
*/
|
|
68
|
+
export declare function findRepairCandidate(input: RootCauseInput): Promise<RepairCandidate | null>;
|
|
69
|
+
export declare function repairQueueFile(cwd: string): string;
|
|
70
|
+
/** Parse the stored queue. Malformed lines are skipped, never thrown on. */
|
|
71
|
+
export declare function parseRepairQueue(raw: string): RepairCandidate[];
|
|
72
|
+
/** Append one candidate. Best-effort — the queue never blocks a gate. */
|
|
73
|
+
export declare function recordRepairCandidate(cwd: string, c: RepairCandidate): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Read the queue and CLEAR it. Draining is what makes the "cap 1 repair task per
|
|
76
|
+
* file per run" bound hold without a second ledger: whatever is drained either
|
|
77
|
+
* becomes a plan entry (which is then itself the dedup key — see
|
|
78
|
+
* {@link planHasRepairFor}) or was already covered by one.
|
|
79
|
+
*/
|
|
80
|
+
export declare function drainRepairQueue(cwd: string): Promise<RepairCandidate[]>;
|
|
81
|
+
/** One merged repair per file, carrying every task the defect FAILed. */
|
|
82
|
+
export interface MergedRepair {
|
|
83
|
+
file: string;
|
|
84
|
+
owner: string;
|
|
85
|
+
defect: string;
|
|
86
|
+
blamed: string[];
|
|
87
|
+
verifyCommand?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Collapse candidates by file — MANDATORY dedup: run 14's two teardown.ts debts
|
|
91
|
+
* (TASK_0013, TASK_0019) must yield exactly ONE repair task naming both. First
|
|
92
|
+
* record wins for defect/command (they describe the same fault); blamed tasks
|
|
93
|
+
* accumulate in first-seen order.
|
|
94
|
+
*/
|
|
95
|
+
export declare function mergeRepairCandidates(candidates: RepairCandidate[]): MergedRepair[];
|
|
96
|
+
/** Machine-recognisable prefix, so a repair entry can be found in a plan again. */
|
|
97
|
+
export declare const REPAIR_TITLE_PREFIX = "repair ";
|
|
98
|
+
/**
|
|
99
|
+
* The plan title for a repair task, in the fixed shape
|
|
100
|
+
* `repair <file>: <defect> (root cause of TASK_A, TASK_B debts)`. The file sits
|
|
101
|
+
* immediately after the prefix so {@link parseRepairTitleFile} can recover it —
|
|
102
|
+
* that recovery is both the dedup key and how the loop knows to attach the
|
|
103
|
+
* repair scope fence.
|
|
104
|
+
*/
|
|
105
|
+
export declare function buildRepairTitle(r: MergedRepair): string;
|
|
106
|
+
/** The file a repair title names, or null when the title is not a repair entry. */
|
|
107
|
+
export declare function parseRepairTitleFile(title: string): string | null;
|
|
108
|
+
/**
|
|
109
|
+
* Is a repair for `file` ALREADY in the plan? This is the cap-1-per-file-per-run
|
|
110
|
+
* bound: it counts checked-off entries too, so a repair task that ran and FAILed
|
|
111
|
+
* is never re-spawned — it lands in the accept-debt ledger like any other task.
|
|
112
|
+
*/
|
|
113
|
+
export declare function planHasRepairFor(titles: string[], file: string): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* The extra scope fence a repair entry carries into refine. Without it, refine
|
|
116
|
+
* re-expands "repair test/teardown.ts: parameterized table names in TRUNCATE"
|
|
117
|
+
* into "overhaul the test infrastructure" — the /task-auto drift lesson. The
|
|
118
|
+
* fence pins the single editable file and pins the VERIFY to the exact command
|
|
119
|
+
* the debt failed on.
|
|
120
|
+
*/
|
|
121
|
+
export declare function buildRepairScopeFence(file: string, verifyCommand?: string): string;
|
|
122
|
+
export {};
|