@mjasnikovs/pi-task 0.41.0 → 0.42.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.
- package/dist/task/accept-debt.d.ts +16 -0
- package/dist/task/accept-debt.js +45 -3
- package/dist/task/auto-io.d.ts +7 -0
- package/dist/task/auto-io.js +12 -0
- package/dist/task/auto-orchestrator.js +67 -9
- package/dist/task/gate-deps.js +2 -1
- package/dist/task/health-repair.d.ts +68 -0
- package/dist/task/health-repair.js +124 -0
- package/dist/task/implementation-guards.d.ts +4 -0
- package/dist/task/implementation-guards.js +16 -0
- package/dist/task/task-gates.d.ts +13 -1
- package/dist/task/task-gates.js +46 -4
- package/dist/task/verify-resolution.d.ts +4 -1
- package/dist/task/verify-resolution.js +8 -2
- package/dist/task/verify-work.d.ts +7 -0
- package/dist/task/verify-work.js +6 -1
- package/package.json +1 -1
|
@@ -89,6 +89,12 @@ export interface AcceptDebt {
|
|
|
89
89
|
* string is the VERIFY-block line itself (`inv-command-provenance`).
|
|
90
90
|
*/
|
|
91
91
|
verifyCommand?: string;
|
|
92
|
+
/**
|
|
93
|
+
* The task whose verified work CLOSED this debt (a health repair whose check
|
|
94
|
+
* went green — see closeHealthDebts). A closed debt stays in the ledger as the
|
|
95
|
+
* record of what fixed it, and no re-check reads it again.
|
|
96
|
+
*/
|
|
97
|
+
resolvedBy?: string;
|
|
92
98
|
}
|
|
93
99
|
export declare function acceptDebtFile(cwd: string): string;
|
|
94
100
|
/** The raw stored ledger ('' when none recorded yet). Parse with parseAcceptDebts. */
|
|
@@ -146,6 +152,16 @@ export declare function crossTaskDeletionReason(deletion: {
|
|
|
146
152
|
export declare function extractDeletedDebtPath(reason: string): string | null;
|
|
147
153
|
/** Overwrite the ledger with exactly these records (used to prune resolved debts). */
|
|
148
154
|
export declare function writeAcceptDebts(cwd: string, debts: AcceptDebt[]): Promise<void>;
|
|
155
|
+
/** The debts nothing has closed yet — the only ones a re-check may read. */
|
|
156
|
+
export declare function readOpenAcceptDebts(cwd: string): Promise<AcceptDebt[]>;
|
|
157
|
+
/**
|
|
158
|
+
* Close every open static-class debt that names `command`, stamping the task
|
|
159
|
+
* whose verified work made that check pass again. Returns the debts closed.
|
|
160
|
+
* A reason that quotes the command is the whole match: the health-check reason
|
|
161
|
+
* (`repo health: \`bun run lint\` exited 1`) and its inherited form both do,
|
|
162
|
+
* and nothing else in the ledger quotes a health command. Best-effort.
|
|
163
|
+
*/
|
|
164
|
+
export declare function closeHealthDebts(cwd: string, command: string, resolvedBy: string): Promise<AcceptDebt[]>;
|
|
149
165
|
/**
|
|
150
166
|
* STATIC-CLASS debt: one whose accepted FAIL was the deterministic whole-repo static
|
|
151
167
|
* health check (`repo health: …`, the prefix runWorkVerification's repoHealth branch
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -113,6 +113,7 @@ export function parseAcceptDebts(raw) {
|
|
|
113
113
|
// 4th field: the verbatim VERIFY command the reason names.
|
|
114
114
|
// Absent in every legacy record, and absent in most new ones.
|
|
115
115
|
const verifyCommand = parts[3]?.trim();
|
|
116
|
+
const resolvedBy = parts[4]?.trim();
|
|
116
117
|
out.push({
|
|
117
118
|
taskId: parts[0].trim(),
|
|
118
119
|
reason: parts[1].trim(),
|
|
@@ -120,7 +121,8 @@ export function parseAcceptDebts(raw) {
|
|
|
120
121
|
// implicit class, so an absent origin and a spelled-out 'accepted' must
|
|
121
122
|
// parse to the same record.
|
|
122
123
|
...(isKnownOrigin(origin) && origin !== 'accepted' ? { origin } : {}),
|
|
123
|
-
...(verifyCommand !== undefined && verifyCommand.length > 0 ? { verifyCommand } : {})
|
|
124
|
+
...(verifyCommand !== undefined && verifyCommand.length > 0 ? { verifyCommand } : {}),
|
|
125
|
+
...(resolvedBy !== undefined && resolvedBy.length > 0 ? { resolvedBy } : {})
|
|
124
126
|
});
|
|
125
127
|
}
|
|
126
128
|
return out;
|
|
@@ -141,6 +143,16 @@ function serialize(d) {
|
|
|
141
143
|
// only for the non-accepted classes, so old readers/files round-trip unchanged.
|
|
142
144
|
// The 4th verify-command field forces the origin field to be written (positional
|
|
143
145
|
// format) — 'accepted' spelled out there parses back to the same absent origin.
|
|
146
|
+
// The 5th field likewise forces an (empty) 4th.
|
|
147
|
+
if (d.resolvedBy !== undefined && d.resolvedBy.length > 0) {
|
|
148
|
+
return [
|
|
149
|
+
d.taskId,
|
|
150
|
+
d.reason,
|
|
151
|
+
d.origin ?? 'accepted',
|
|
152
|
+
d.verifyCommand ?? '',
|
|
153
|
+
d.resolvedBy
|
|
154
|
+
].join(FIELD_SEP);
|
|
155
|
+
}
|
|
144
156
|
if (d.verifyCommand !== undefined && d.verifyCommand.length > 0) {
|
|
145
157
|
return [d.taskId, d.reason, d.origin ?? 'accepted', d.verifyCommand].join(FIELD_SEP);
|
|
146
158
|
}
|
|
@@ -234,6 +246,33 @@ export function extractDeletedDebtPath(reason) {
|
|
|
234
246
|
export async function writeAcceptDebts(cwd, debts) {
|
|
235
247
|
await ledger.write(cwd, debts);
|
|
236
248
|
}
|
|
249
|
+
/** The debts nothing has closed yet — the only ones a re-check may read. */
|
|
250
|
+
export async function readOpenAcceptDebts(cwd) {
|
|
251
|
+
return (await readAcceptDebts(cwd)).filter(d => d.resolvedBy === undefined);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Close every open static-class debt that names `command`, stamping the task
|
|
255
|
+
* whose verified work made that check pass again. Returns the debts closed.
|
|
256
|
+
* A reason that quotes the command is the whole match: the health-check reason
|
|
257
|
+
* (`repo health: \`bun run lint\` exited 1`) and its inherited form both do,
|
|
258
|
+
* and nothing else in the ledger quotes a health command. Best-effort.
|
|
259
|
+
*/
|
|
260
|
+
export async function closeHealthDebts(cwd, command, resolvedBy) {
|
|
261
|
+
try {
|
|
262
|
+
const all = await readAcceptDebts(cwd);
|
|
263
|
+
const quoted = `\`${command}\``;
|
|
264
|
+
const closing = all.filter(d => d.resolvedBy === undefined
|
|
265
|
+
&& isStaticClassDebt(d.reason)
|
|
266
|
+
&& d.reason.includes(quoted));
|
|
267
|
+
if (closing.length === 0)
|
|
268
|
+
return [];
|
|
269
|
+
await ledger.write(cwd, all.map(d => (closing.includes(d) ? { ...d, resolvedBy } : d)));
|
|
270
|
+
return closing;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
237
276
|
/**
|
|
238
277
|
* STATIC-CLASS debt: one whose accepted FAIL was the deterministic whole-repo static
|
|
239
278
|
* health check (`repo health: …`, the prefix runWorkVerification's repoHealth branch
|
|
@@ -512,7 +551,10 @@ export function describeDebt(d) {
|
|
|
512
551
|
export async function deriveOpenDebts(cwd, staticOk,
|
|
513
552
|
/** The spawner for the VERIFY-COMMAND re-runs. Defaults to the real one. */
|
|
514
553
|
run = spawnCommand, signal) {
|
|
515
|
-
const
|
|
554
|
+
const all = await readAcceptDebts(cwd);
|
|
555
|
+
// Closed debts are kept as the record of what fixed them, and never re-checked.
|
|
556
|
+
const closed = all.filter(d => d.resolvedBy !== undefined);
|
|
557
|
+
const { open: openRaw, resolved, trail } = await recheckAcceptDebts(all.filter(d => d.resolvedBy === undefined), {
|
|
516
558
|
staticOk,
|
|
517
559
|
// Cross-task-deletion debts auto-close iff the deleted file is back in the
|
|
518
560
|
// tree — a deterministic existence check, corroborating the per-file
|
|
@@ -524,7 +566,7 @@ run = spawnCommand, signal) {
|
|
|
524
566
|
rerunVerify: cmd => rerunDebtVerifyCommand(cwd, cmd, run, signal)
|
|
525
567
|
});
|
|
526
568
|
if (resolved.length > 0)
|
|
527
|
-
await writeAcceptDebts(cwd, openRaw);
|
|
569
|
+
await writeAcceptDebts(cwd, [...closed, ...openRaw]);
|
|
528
570
|
// Conflicting-claim annotation: an existence-as-failure debt whose
|
|
529
571
|
// named file is another task's committed deliverable is a plan defect — surface
|
|
530
572
|
// the contradiction with the debt so nobody (human or child) treats the claim as
|
package/dist/task/auto-io.d.ts
CHANGED
|
@@ -127,6 +127,13 @@ export declare function recordTaskEnd(cwd: string, id: string, index: number, la
|
|
|
127
127
|
* cached index.
|
|
128
128
|
*/
|
|
129
129
|
export declare function insertTaskAfter(cwd: string, id: string, afterIndex: number, title: string): Promise<boolean>;
|
|
130
|
+
/**
|
|
131
|
+
* Insert a NEW unchecked entry directly BEFORE the `index`th checkbox — the
|
|
132
|
+
* position a health repair needs: the task about to run must wait until the red
|
|
133
|
+
* it would build on is fixed. Same monotonic, duplicate-refusing splice as
|
|
134
|
+
* {@link insertTaskAfter}.
|
|
135
|
+
*/
|
|
136
|
+
export declare function insertTaskBefore(cwd: string, id: string, index: number, title: string): Promise<boolean>;
|
|
130
137
|
/**
|
|
131
138
|
* Find the most-recently-updated resumable TASK_AUTO_* file, with the state and
|
|
132
139
|
* last-write time the resume banner reports (see resume-gap.ts). Null when there
|
package/dist/task/auto-io.js
CHANGED
|
@@ -294,6 +294,9 @@ export async function insertTaskAfter(cwd, id, afterIndex, title) {
|
|
|
294
294
|
if (!entry)
|
|
295
295
|
continue;
|
|
296
296
|
entries.push(entry);
|
|
297
|
+
// A negative afterIndex means "before the first entry".
|
|
298
|
+
if (entries.length === 1 && afterIndex < 0)
|
|
299
|
+
insertAt = i;
|
|
297
300
|
if (entries.length - 1 <= afterIndex)
|
|
298
301
|
insertAt = i + 1;
|
|
299
302
|
}
|
|
@@ -310,6 +313,15 @@ export async function insertTaskAfter(cwd, id, afterIndex, title) {
|
|
|
310
313
|
await setTaskSection(cwd, id, 'tasks', lines.join('\n'));
|
|
311
314
|
return true;
|
|
312
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Insert a NEW unchecked entry directly BEFORE the `index`th checkbox — the
|
|
318
|
+
* position a health repair needs: the task about to run must wait until the red
|
|
319
|
+
* it would build on is fixed. Same monotonic, duplicate-refusing splice as
|
|
320
|
+
* {@link insertTaskAfter}.
|
|
321
|
+
*/
|
|
322
|
+
export function insertTaskBefore(cwd, id, index, title) {
|
|
323
|
+
return insertTaskAfter(cwd, id, index - 1, title);
|
|
324
|
+
}
|
|
313
325
|
/**
|
|
314
326
|
* Find the most-recently-updated resumable TASK_AUTO_* file, with the state and
|
|
315
327
|
* last-write time the resume banner reports (see resume-gap.ts). Null when there
|
|
@@ -16,12 +16,12 @@ import { parseAutoAnswer, autoAnswerHasTag, deriveTitle } from './parsers.js';
|
|
|
16
16
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
17
17
|
import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT, DECOMPOSE_COVERAGE_PROMPT } from './auto-prompts.js';
|
|
18
18
|
import { GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT } from './prompts.js';
|
|
19
|
-
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, UNNAMED_COVERAGE_GAP, parseTaskList, planKeyAt, checkOffTask, stampTaskInProgress, beginTaskAttempt, recordTaskEnd, insertTaskAfter, findResumableAutoDetailed } from './auto-io.js';
|
|
19
|
+
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, UNNAMED_COVERAGE_GAP, parseTaskList, planKeyAt, checkOffTask, stampTaskInProgress, beginTaskAttempt, recordTaskEnd, insertTaskAfter, insertTaskBefore, findResumableAutoDetailed } from './auto-io.js';
|
|
20
20
|
import { decideResume, UNATTENDED_STATES } from './resume-gap.js';
|
|
21
21
|
import { ENTRY_ATTEMPT_BUDGET } from './gate-resolution.js';
|
|
22
22
|
import { recordDebt } from './accept-debt.js';
|
|
23
23
|
import { drainRepairQueue, mergeRepairCandidates, planHasRepairFor, parseRepairTitleFile, buildRepairTitle, buildRepairScopeFence, extractFailingCommand } from './root-cause-repair.js';
|
|
24
|
-
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath } from './task-io.js';
|
|
24
|
+
import { writeTaskFile, readTaskFile, readSection, updateTaskFrontMatter, taskFilePath } from './task-io.js';
|
|
25
25
|
// Re-exported as well as used: the @-mention helpers moved to their own module so
|
|
26
26
|
// the research phase can select a cited spec doc without importing this one, and
|
|
27
27
|
// the planning call sites still name them here.
|
|
@@ -37,6 +37,8 @@ import { getParentContextWindow } from './context-usage.js';
|
|
|
37
37
|
import { ChildStatus, runPlanningChild, statusCallbacks } from './child-status.js';
|
|
38
38
|
import { buildGateDeps, collectTreeChanges } from './gate-deps.js';
|
|
39
39
|
import { runGatesForTask } from './task-gates.js';
|
|
40
|
+
import { buildHealthRepairFence, buildHealthRepairTitle, healthRedSubject, parseHealthRepairTitle, planCoversHealthRed } from './health-repair.js';
|
|
41
|
+
import { HEALTH_BASELINE_SECTION, parseHealthBaseline } from './health-baseline.js';
|
|
40
42
|
import { runFinalGateStage } from './run-final-gate.js';
|
|
41
43
|
import { gitUnmergedPaths, gitStashRef } from './auto-commit.js';
|
|
42
44
|
import { runFinalIntegrationGate, deriveOpenDebts } from './final-gate.js';
|
|
@@ -324,6 +326,9 @@ export function buildScopeFence(entries, currentIndex) {
|
|
|
324
326
|
export function buildStepFence(entries, currentIndex) {
|
|
325
327
|
const base = buildScopeFence(entries, currentIndex);
|
|
326
328
|
const title = entries[currentIndex]?.title ?? '';
|
|
329
|
+
const healthRepair = parseHealthRepairTitle(title);
|
|
330
|
+
if (healthRepair)
|
|
331
|
+
return `${base}\n\n${buildHealthRepairFence(healthRepair)}`;
|
|
327
332
|
const repairFile = parseRepairTitleFile(title);
|
|
328
333
|
if (!repairFile)
|
|
329
334
|
return base;
|
|
@@ -373,6 +378,53 @@ async function schedulePendingRepairs(cwd, id, afterIndex, ctx, deps) {
|
|
|
373
378
|
// the plan is best-effort here; the underlying debt is already recorded
|
|
374
379
|
}
|
|
375
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* A red health check at the pre-task checkpoint becomes a repair entry spliced
|
|
383
|
+
* BEFORE the task about to run (health-repair.ts). Returns whether the plan
|
|
384
|
+
* changed — the caller then re-reads it and runs the repair first. No splice when
|
|
385
|
+
* the plan already covers this red (a repair that ran and failed included): the
|
|
386
|
+
* task then proceeds and inherits it, as the baseline differential intends.
|
|
387
|
+
*/
|
|
388
|
+
async function spliceHealthRepair(cwd, id, next, entries, health, ctx, deps) {
|
|
389
|
+
try {
|
|
390
|
+
const red = healthRedSubject(health, cwd, (await deps.repoFiles?.(cwd)) ?? null);
|
|
391
|
+
if (!red)
|
|
392
|
+
return false;
|
|
393
|
+
if (planCoversHealthRed(entries.map(e => e.title), red))
|
|
394
|
+
return false;
|
|
395
|
+
const owners = [];
|
|
396
|
+
for (const f of red.files) {
|
|
397
|
+
const owner = await deps.introducedBy?.(cwd, f);
|
|
398
|
+
if (owner && !owners.includes(owner))
|
|
399
|
+
owners.push(owner);
|
|
400
|
+
}
|
|
401
|
+
const title = buildHealthRepairTitle({ ...red, owners });
|
|
402
|
+
if (!(await insertTaskBefore(cwd, id, next.index, title)))
|
|
403
|
+
return false;
|
|
404
|
+
await deps.record?.(cwd, id, `plan: \`${red.command}\` is red at the checkpoint before step ${next.index + 1} — inserted repair step first: ${title}`);
|
|
405
|
+
notifyRun(ctx, `${id}: \`${red.command}\` is red before "${next.title}" — queued a repair for `
|
|
406
|
+
+ `${red.files.length > 0 ? red.files.join(', ') : 'it'} to run first.`, 'warning');
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* The health baseline a task starts from: the one its (resumed) task file already
|
|
415
|
+
* holds, else a fresh capture at the checkpoint. Null when neither is available.
|
|
416
|
+
*/
|
|
417
|
+
async function baselineAtCheckpoint(ctx, cwd, resumeId, label, deps) {
|
|
418
|
+
if (resumeId) {
|
|
419
|
+
const stored = await readSection(cwd, resumeId, HEALTH_BASELINE_SECTION).catch(() => null);
|
|
420
|
+
const parsed = parseHealthBaseline(stored);
|
|
421
|
+
if (parsed)
|
|
422
|
+
return parsed;
|
|
423
|
+
}
|
|
424
|
+
if (!deps.captureHealthBaseline)
|
|
425
|
+
return null;
|
|
426
|
+
return deps.captureHealthBaseline(ctx, cwd, label);
|
|
427
|
+
}
|
|
376
428
|
/**
|
|
377
429
|
* ORIENT — read the feature, strike what must never reach a planning child, and
|
|
378
430
|
* derive the requirement ledger. Depends on nothing but the feature and the tree,
|
|
@@ -1312,14 +1364,20 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1312
1364
|
// the tree clean: what the project's own statics say now is what this task
|
|
1313
1365
|
// INHERITED, and the verify gate attributes a red check against it instead
|
|
1314
1366
|
// of failing the task for a sibling's defect (health-baseline.ts). The
|
|
1315
|
-
// inner task file does not exist yet, so the
|
|
1367
|
+
// inner task file does not exist yet, so the result is handed to the
|
|
1316
1368
|
// runner, which writes the section once its id is allocated.
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1369
|
+
//
|
|
1370
|
+
// A RED baseline is also the one moment every way red enters the tree
|
|
1371
|
+
// is visible before anything builds on it — an accepted regression, a
|
|
1372
|
+
// leftover the checkpoint just committed, a repo red at run start — so
|
|
1373
|
+
// it is where the repair is scheduled (health-repair.ts).
|
|
1374
|
+
const baseline = await baselineAtCheckpoint(active, cwd, resumeId, next.title, deps);
|
|
1375
|
+
if (baseline
|
|
1376
|
+
&& !baseline.outcome.ok
|
|
1377
|
+
&& (await spliceHealthRepair(cwd, id, next, entries, baseline.outcome, active, deps))) {
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
const healthBaseline = baseline ? { healthBaseline: () => Promise.resolve(baseline) } : {};
|
|
1323
1381
|
// Stash ref before the task: compared after the gates so a stash pushed
|
|
1324
1382
|
// during the task (impl model or any child) and left behind is called
|
|
1325
1383
|
// out instead of silently waiting to detonate in a later task.
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -33,7 +33,7 @@ import { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
|
33
33
|
import { currentRunContext } from './run-context.js';
|
|
34
34
|
import { runGateEvidence, evidenceVerifyFindings } from './gate-evidence.js';
|
|
35
35
|
import { readContracts } from './contracts.js';
|
|
36
|
-
import { recordDebt } from './accept-debt.js';
|
|
36
|
+
import { closeHealthDebts, recordDebt } from './accept-debt.js';
|
|
37
37
|
import { recordRepairCandidate } from './root-cause-repair.js';
|
|
38
38
|
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
39
39
|
import { runFinalIntegrationGate, discoverGateCommandLabels, discoverGateCommandBodies } from './final-gate.js';
|
|
@@ -786,6 +786,7 @@ export function buildGateDeps(params) {
|
|
|
786
786
|
// origin, and the final integration gate re-checks each one at run end.
|
|
787
787
|
recordDebt,
|
|
788
788
|
recordRepairCandidate: (cwd2, candidate) => recordRepairCandidate(cwd2, candidate),
|
|
789
|
+
closeHealthDebts,
|
|
789
790
|
// file → introducing task, the provenance half of the discriminator.
|
|
790
791
|
introducedBy: (cwd2, rel) => Promise.resolve(taskThatIntroduced(cwd2, rel)),
|
|
791
792
|
// Tracked paths, used only to resolve a bare file name a FAIL text names
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* health-repair — a red static check at a pre-task checkpoint becomes a repair
|
|
3
|
+
* entry in the running plan, BEFORE the next planned task builds on it.
|
|
4
|
+
*
|
|
5
|
+
* The failure this closes: red enters the tree by three doors — a task whose
|
|
6
|
+
* regression was accepted, a leftover from an abandoned run swept in by the
|
|
7
|
+
* checkpoint commit, a repo that was red when the run began — and none of them is
|
|
8
|
+
* a gate. The health baseline (health-baseline.ts) stops the NEXT task being
|
|
9
|
+
* blamed, but blaming nobody is not fixing it: every later task inherits the red,
|
|
10
|
+
* and only the run-end gate re-checks it. All three doors pass the checkpoint,
|
|
11
|
+
* which already measures health for the baseline, so that one measurement is
|
|
12
|
+
* where the repair is scheduled.
|
|
13
|
+
*
|
|
14
|
+
* The subject of a repair is what the health output NAMES: the tracked files the
|
|
15
|
+
* failing command reported, or the command itself when it named none. The plan is
|
|
16
|
+
* the dedup ledger — a title covering the same command, or any of the same files,
|
|
17
|
+
* means no second entry, checked-off ones included, which is what stops a repair
|
|
18
|
+
* that failed from being re-spawned.
|
|
19
|
+
*/
|
|
20
|
+
import type { HealthSignal } from './health-baseline.js';
|
|
21
|
+
/** The failing check, and what its output named. */
|
|
22
|
+
export interface HealthRed {
|
|
23
|
+
command: string;
|
|
24
|
+
exitCode: number | null;
|
|
25
|
+
/** Repo-relative tracked paths the output named, in first-seen order. */
|
|
26
|
+
files: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface HealthRedOwners {
|
|
29
|
+
/** Task ids whose commits introduced the named files; empty when nothing in
|
|
30
|
+
* the run owns them (a leftover, or a red the run started on). */
|
|
31
|
+
owners: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* What a red health result is about. Null when the result records no failing
|
|
35
|
+
* command (a legacy baseline, or a signal with no per-command detail) — there is
|
|
36
|
+
* nothing a repair could be pinned to.
|
|
37
|
+
*/
|
|
38
|
+
export declare function healthRedSubject(health: HealthSignal & {
|
|
39
|
+
output?: string;
|
|
40
|
+
}, cwd: string, tracked: readonly string[] | null): HealthRed | null;
|
|
41
|
+
/**
|
|
42
|
+
* The plan title, in one of two fixed shapes the parser below recovers:
|
|
43
|
+
* `repair src/a.ts, src/b.ts: \`bun run lint\` exits 1 (introduced by TASK_0033)`
|
|
44
|
+
* `repair \`bun run lint\`: exits 1 (no task in this run owns it)`
|
|
45
|
+
* The subject sits right after the prefix so it is both the dedup key and what
|
|
46
|
+
* the scope fence pins.
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildHealthRepairTitle(red: HealthRed & HealthRedOwners): string;
|
|
49
|
+
export interface HealthRepairSubject {
|
|
50
|
+
command: string;
|
|
51
|
+
files: string[];
|
|
52
|
+
}
|
|
53
|
+
/** The command and files a health-repair title names, or null for any other title. */
|
|
54
|
+
export declare function parseHealthRepairTitle(title: string): HealthRepairSubject | null;
|
|
55
|
+
/**
|
|
56
|
+
* Does the plan already carry a repair for this red? Same command, or any of the
|
|
57
|
+
* same files — including a file-scoped root-cause repair (root-cause-repair.ts),
|
|
58
|
+
* which pins the same file. Checked-off entries count: a repair that ran and
|
|
59
|
+
* failed lands in the debt ledger, never in the plan a second time.
|
|
60
|
+
*/
|
|
61
|
+
export declare function planCoversHealthRed(titles: readonly string[], red: HealthRed): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The extra scope fence a health-repair entry carries into refine. Without it
|
|
64
|
+
* "repair src/a.ts" refines into "overhaul the client", and a fix child left free
|
|
65
|
+
* to choose greens a linter fastest by suppressing it — which is how the red in
|
|
66
|
+
* the run this closes was painted over the first time.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildHealthRepairFence(subject: HealthRepairSubject): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { parseRepairTitleFile } from './root-cause-repair.js';
|
|
2
|
+
/** A path-like token: at least one directory separator, ending in a file name. */
|
|
3
|
+
const PATH_TOKEN_RE = /(?:[\w.@-]+[\\/])+[\w.@-]+\.\w+/g;
|
|
4
|
+
function normalisePath(p) {
|
|
5
|
+
return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '').trim();
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a token the output printed to the ONE tracked file it names. Linters
|
|
9
|
+
* print absolute paths, compilers print repo-relative ones, and a stack trace
|
|
10
|
+
* prints `node_modules/...` — the tracked list is what tells a deliverable from
|
|
11
|
+
* noise. An ambiguous suffix (two tracked `src/x.ts`) resolves to nothing rather
|
|
12
|
+
* than to a guess.
|
|
13
|
+
*/
|
|
14
|
+
function resolveTracked(token, cwd, tracked) {
|
|
15
|
+
let t = normalisePath(token);
|
|
16
|
+
const root = normalisePath(cwd);
|
|
17
|
+
if (root.length > 0 && t.startsWith(`${root}/`))
|
|
18
|
+
t = t.slice(root.length + 1);
|
|
19
|
+
if (tracked.includes(t))
|
|
20
|
+
return t;
|
|
21
|
+
const bySuffix = tracked.filter(r => t.endsWith(`/${r}`));
|
|
22
|
+
return bySuffix.length === 1 ? bySuffix[0] : null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What a red health result is about. Null when the result records no failing
|
|
26
|
+
* command (a legacy baseline, or a signal with no per-command detail) — there is
|
|
27
|
+
* nothing a repair could be pinned to.
|
|
28
|
+
*/
|
|
29
|
+
export function healthRedSubject(health, cwd, tracked) {
|
|
30
|
+
const failing = (health.commands ?? []).find(c => c.outcome === 'fail');
|
|
31
|
+
if (!failing)
|
|
32
|
+
return null;
|
|
33
|
+
const files = [];
|
|
34
|
+
if (tracked) {
|
|
35
|
+
for (const m of (health.output ?? '').matchAll(PATH_TOKEN_RE)) {
|
|
36
|
+
const rel = resolveTracked(m[0], cwd, tracked);
|
|
37
|
+
if (rel !== null && !files.includes(rel))
|
|
38
|
+
files.push(rel);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { command: failing.cmd, exitCode: failing.exitCode, files };
|
|
42
|
+
}
|
|
43
|
+
// ─── Plan entry ──────────────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* The plan title, in one of two fixed shapes the parser below recovers:
|
|
46
|
+
* `repair src/a.ts, src/b.ts: \`bun run lint\` exits 1 (introduced by TASK_0033)`
|
|
47
|
+
* `repair \`bun run lint\`: exits 1 (no task in this run owns it)`
|
|
48
|
+
* The subject sits right after the prefix so it is both the dedup key and what
|
|
49
|
+
* the scope fence pins.
|
|
50
|
+
*/
|
|
51
|
+
export function buildHealthRepairTitle(red) {
|
|
52
|
+
const exits = `exits ${red.exitCode ?? '?'}`;
|
|
53
|
+
const owner = red.owners.length > 0 ?
|
|
54
|
+
`introduced by ${red.owners.join(', ')}`
|
|
55
|
+
: 'no task in this run owns it';
|
|
56
|
+
return red.files.length > 0 ?
|
|
57
|
+
`repair ${red.files.join(', ')}: \`${red.command}\` ${exits} (${owner})`
|
|
58
|
+
: `repair \`${red.command}\`: ${exits} (${owner})`;
|
|
59
|
+
}
|
|
60
|
+
const PATH = String.raw `(?:[\w.@-]+\/)*[\w.@-]+\.\w+`;
|
|
61
|
+
const HEALTH_REPAIR_TITLE_RE = new RegExp(`^repair\\s+(?:\`([^\`]+)\`\\s*:\\s*exits|(${PATH}(?:,\\s*${PATH})*)\\s*:\\s*\`([^\`]+)\`\\s+exits)\\s`);
|
|
62
|
+
/** The command and files a health-repair title names, or null for any other title. */
|
|
63
|
+
export function parseHealthRepairTitle(title) {
|
|
64
|
+
const m = HEALTH_REPAIR_TITLE_RE.exec(title.trim());
|
|
65
|
+
if (!m)
|
|
66
|
+
return null;
|
|
67
|
+
if (m[1] !== undefined)
|
|
68
|
+
return { command: m[1].trim(), files: [] };
|
|
69
|
+
return { command: m[3].trim(), files: m[2].split(',').map(f => f.trim()) };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Does the plan already carry a repair for this red? Same command, or any of the
|
|
73
|
+
* same files — including a file-scoped root-cause repair (root-cause-repair.ts),
|
|
74
|
+
* which pins the same file. Checked-off entries count: a repair that ran and
|
|
75
|
+
* failed lands in the debt ledger, never in the plan a second time.
|
|
76
|
+
*/
|
|
77
|
+
export function planCoversHealthRed(titles, red) {
|
|
78
|
+
const files = new Set(red.files.map(f => normalisePath(f).toLowerCase()));
|
|
79
|
+
return titles.some(t => {
|
|
80
|
+
const rootCause = parseRepairTitleFile(t);
|
|
81
|
+
if (rootCause !== null && files.has(normalisePath(rootCause).toLowerCase()))
|
|
82
|
+
return true;
|
|
83
|
+
const h = parseHealthRepairTitle(t);
|
|
84
|
+
if (h === null)
|
|
85
|
+
return false;
|
|
86
|
+
if (h.command === red.command)
|
|
87
|
+
return true;
|
|
88
|
+
return h.files.some(f => files.has(normalisePath(f).toLowerCase()));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The extra scope fence a health-repair entry carries into refine. Without it
|
|
93
|
+
* "repair src/a.ts" refines into "overhaul the client", and a fix child left free
|
|
94
|
+
* to choose greens a linter fastest by suppressing it — which is how the red in
|
|
95
|
+
* the run this closes was painted over the first time.
|
|
96
|
+
*/
|
|
97
|
+
export function buildHealthRepairFence(subject) {
|
|
98
|
+
const scope = subject.files.length > 0 ?
|
|
99
|
+
[
|
|
100
|
+
` - ${subject.files.map(f => `\`${f}\``).join(', ')} ${subject.files.length > 1 ? 'are' : 'is'} the ONLY`,
|
|
101
|
+
' file(s) you may modify. Do not refactor, restructure, or "improve" anything else,',
|
|
102
|
+
' and do not create new files.'
|
|
103
|
+
]
|
|
104
|
+
: [
|
|
105
|
+
' - Modify only the files the command reports. Do not refactor, restructure, or',
|
|
106
|
+
' "improve" anything else, and do not create new files.'
|
|
107
|
+
];
|
|
108
|
+
return [
|
|
109
|
+
`REPAIR TASK — this step exists ONLY to make \`${subject.command}\` pass again. It`,
|
|
110
|
+
'was created because that check was red before this step, and every later step',
|
|
111
|
+
'would otherwise build on the red; it is not a feature step and must not grow into one.',
|
|
112
|
+
'',
|
|
113
|
+
'HARD CONSTRAINTS for this step (they override any broader reading of the title):',
|
|
114
|
+
...scope,
|
|
115
|
+
' - Fix the reported findings and nothing more. Do NOT redesign the build, the',
|
|
116
|
+
' lint configuration, the schema, or any shared infrastructure — a wider change',
|
|
117
|
+
" here would silently overwrite sibling tasks' shipped work.",
|
|
118
|
+
' - Do NOT suppress, disable, ignore or weaken the check to make it pass: no',
|
|
119
|
+
' disable comments, no ignore entries, no relaxed rules, no deleted or skipped',
|
|
120
|
+
' tests. A finding is fixed in the code it reports.',
|
|
121
|
+
` - The VERIFY block MUST be exactly: \`${subject.command}\` — the check that was`,
|
|
122
|
+
' red. It passing is the whole acceptance criterion.'
|
|
123
|
+
].join('\n');
|
|
124
|
+
}
|
|
@@ -16,6 +16,10 @@ export declare function implementationGuardArmed(): boolean;
|
|
|
16
16
|
* one altered byte is a new key and a clean slate on both counters.
|
|
17
17
|
*/
|
|
18
18
|
export declare function blockedCallReason(toolName: string, count: number): string;
|
|
19
|
+
/** Every file under the task dir is host-written. MEASURED: an mx5 implementer
|
|
20
|
+
* wrote its report over TASK_AUTO_0001.md, and the run died on its front matter. */
|
|
21
|
+
export declare function targetsTaskDir(toolName: string, input: unknown): boolean;
|
|
22
|
+
export declare function taskDirWriteReason(): string;
|
|
19
23
|
/** The reason on the final block, which also ends the turn. */
|
|
20
24
|
export declare function terminalCallReason(): string;
|
|
21
25
|
export declare function consumeGuardTermination(): boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LoopDetector, loopKey, LOOP_THRESHOLD, LOOP_WINDOW, MAX_LOOP_RESTARTS } from './loop-detector.js';
|
|
2
|
+
import { TASKS_DIR_NAME } from './task-types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Runaway guard for the IMPLEMENTATION TURN — the one model surface with none.
|
|
4
5
|
*
|
|
@@ -81,6 +82,18 @@ export function blockedCallReason(toolName, count) {
|
|
|
81
82
|
return (`Blocked: this is the ${count}th identical ${toolName} call in this turn. `
|
|
82
83
|
+ `Use what you already have, or do something different, then continue the task.`);
|
|
83
84
|
}
|
|
85
|
+
/** Every file under the task dir is host-written. MEASURED: an mx5 implementer
|
|
86
|
+
* wrote its report over TASK_AUTO_0001.md, and the run died on its front matter. */
|
|
87
|
+
export function targetsTaskDir(toolName, input) {
|
|
88
|
+
if (!MUTATING_TOOLS.has(toolName))
|
|
89
|
+
return false;
|
|
90
|
+
const target = input?.path;
|
|
91
|
+
return typeof target === 'string' && target.split(/[\\/]/).includes(TASKS_DIR_NAME);
|
|
92
|
+
}
|
|
93
|
+
export function taskDirWriteReason() {
|
|
94
|
+
return (`Blocked: ${TASKS_DIR_NAME}/ belongs to pi-task and is written only by the host. `
|
|
95
|
+
+ `Put your report in your reply, not in a file, then continue the task.`);
|
|
96
|
+
}
|
|
84
97
|
/** The reason on the final block, which also ends the turn. */
|
|
85
98
|
export function terminalCallReason() {
|
|
86
99
|
return (`Blocked: this turn repeated one call past every warning, so it is being stopped `
|
|
@@ -120,6 +133,9 @@ export function registerImplementationGuards(pi) {
|
|
|
120
133
|
if (state.terminating) {
|
|
121
134
|
return { block: true, terminate: true, reason: terminalCallReason() };
|
|
122
135
|
}
|
|
136
|
+
if (targetsTaskDir(event.toolName, event.input)) {
|
|
137
|
+
return { block: true, reason: taskDirWriteReason() };
|
|
138
|
+
}
|
|
123
139
|
const call = { name: event.toolName, args: event.input };
|
|
124
140
|
const mutating = MUTATING_TOOLS.has(event.toolName);
|
|
125
141
|
const hit = mutating ? state.edits.record(call) : state.loop.record(call);
|
|
@@ -176,6 +176,16 @@ export interface GateDeps {
|
|
|
176
176
|
* queues repairs exactly like /task-auto — only the plan mutation is the loop's.
|
|
177
177
|
*/
|
|
178
178
|
recordRepairCandidate?: (cwd: string, candidate: RepairCandidate) => Promise<void>;
|
|
179
|
+
/**
|
|
180
|
+
* Close the open static debts naming a health command, stamped with the task
|
|
181
|
+
* whose verified work made that check pass — a health repair entry
|
|
182
|
+
* (health-repair.ts) going green. Returns the debts closed. Absent (tests) →
|
|
183
|
+
* nothing closes.
|
|
184
|
+
*/
|
|
185
|
+
closeHealthDebts?: (cwd: string, command: string, resolvedBy: string) => Promise<{
|
|
186
|
+
taskId: string;
|
|
187
|
+
reason: string;
|
|
188
|
+
}[]>;
|
|
179
189
|
/**
|
|
180
190
|
* Paths the CURRENT task's own work touches — the AUTHORSHIP discriminator for
|
|
181
191
|
* the root-cause channel: a FAIL blamed on a file this task edited may well be
|
|
@@ -282,7 +292,9 @@ export type GateResult = {
|
|
|
282
292
|
* clarify/grill dialog: the same SessionUI.ask races the local boxed picker against
|
|
283
293
|
* a remote answer, with the two actions also surfaced as remote buttons.
|
|
284
294
|
*/
|
|
285
|
-
export declare function askVerifyResolution(ctx: ExtensionCommandContext, title: string, failReason: string, rec: ResolutionOutcome
|
|
295
|
+
export declare function askVerifyResolution(ctx: ExtensionCommandContext, title: string, failReason: string, rec: ResolutionOutcome,
|
|
296
|
+
/** The repair ACCEPT would queue (a regressed health check), if any. */
|
|
297
|
+
acceptQueues?: string): Promise<ResolutionChoice>;
|
|
286
298
|
/**
|
|
287
299
|
* Run the verify + enforce gates against a task's just-finished implementation.
|
|
288
300
|
*
|
package/dist/task/task-gates.js
CHANGED
|
@@ -6,6 +6,7 @@ import { SessionUI, notifyBoth, notifyRun } from '../remote/bridge.js';
|
|
|
6
6
|
import { isYoloMode, YOLO_STAMP } from './yolo.js';
|
|
7
7
|
import { extractFailingCommand, findRepairCandidate, summariseDefect } from './root-cause-repair.js';
|
|
8
8
|
import { attributeEnforceFailure } from './enforce-attribution.js';
|
|
9
|
+
import { healthRedSubject, parseHealthRepairTitle } from './health-repair.js';
|
|
9
10
|
// The debt ledger is reached through the injected `recordDebt` dep (so it stays
|
|
10
11
|
// absent-in-tests); only the origin TYPE and the cross-task-deletion reason SHAPE
|
|
11
12
|
// come from accept-debt.ts directly — the latter because its writer and its
|
|
@@ -23,10 +24,13 @@ import { updateTaskFrontMatter } from './task-io.js';
|
|
|
23
24
|
* clarify/grill dialog: the same SessionUI.ask races the local boxed picker against
|
|
24
25
|
* a remote answer, with the two actions also surfaced as remote buttons.
|
|
25
26
|
*/
|
|
26
|
-
export async function askVerifyResolution(ctx, title, failReason, rec
|
|
27
|
-
|
|
27
|
+
export async function askVerifyResolution(ctx, title, failReason, rec,
|
|
28
|
+
/** The repair ACCEPT would queue (a regressed health check), if any. */
|
|
29
|
+
acceptQueues) {
|
|
30
|
+
const options = resolutionOptions(rec.recommend, acceptQueues);
|
|
28
31
|
const question = `Verification FAILED for "${title}".\n\n${failReason}\n\n`
|
|
29
|
-
+ `Recommended: ${rec.recommend.toUpperCase()} — ${rec.rationale}
|
|
32
|
+
+ `Recommended: ${rec.recommend.toUpperCase()} — ${rec.rationale}`
|
|
33
|
+
+ (acceptQueues ? `\n\nACCEPT queues ${acceptQueues} before the next task.` : '');
|
|
30
34
|
const answer = await new SessionUI(ctx).ask({
|
|
31
35
|
localTitle: 'Verification failed — how should pi proceed?',
|
|
32
36
|
displayQuestion: question,
|
|
@@ -38,6 +42,12 @@ export async function askVerifyResolution(ctx, title, failReason, rec) {
|
|
|
38
42
|
});
|
|
39
43
|
return classifyResolutionAnswer(answer);
|
|
40
44
|
}
|
|
45
|
+
/** The repair a red health check earns, as the picker and the trail name it. */
|
|
46
|
+
function describeHealthRepair(red) {
|
|
47
|
+
return red.files.length > 0 ?
|
|
48
|
+
`a repair for ${red.files.join(', ')} (\`${red.command}\`)`
|
|
49
|
+
: `a repair for \`${red.command}\``;
|
|
50
|
+
}
|
|
41
51
|
/**
|
|
42
52
|
* The VERIFY resolution loop: run the task's verification against the finished
|
|
43
53
|
* work, and carry out what gate-resolution.ts's decision table says to do with a
|
|
@@ -72,6 +82,14 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
72
82
|
// recording must never break the gate sequence
|
|
73
83
|
}
|
|
74
84
|
};
|
|
85
|
+
const healthRedOf = async (health) => {
|
|
86
|
+
try {
|
|
87
|
+
return healthRedSubject(health, p.cwd, (await deps.repoFiles?.(p.cwd)) ?? null);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
75
93
|
// GATE: actually RUN the task's verification against the just-finished work
|
|
76
94
|
// BEFORE it is checked off or committed. Whether this produced a GENUINE clean
|
|
77
95
|
// pass (a real signal ran and the work met it) also decides how the enforce pass
|
|
@@ -142,6 +160,12 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
142
160
|
await rec(`resolution: recommended ${recOutcome.recommend.toUpperCase()}`);
|
|
143
161
|
contradiction ??= recOutcome.contradiction ?? null;
|
|
144
162
|
const unattended = isYoloMode();
|
|
163
|
+
// A regressed health check is THIS task's red. Accepting it ships the
|
|
164
|
+
// red, and the next checkpoint splices a repair before anything builds
|
|
165
|
+
// on it (health-repair.ts) — named here so a human choosing ACCEPT
|
|
166
|
+
// sees what the choice queues, and the trail says the same.
|
|
167
|
+
const regression = verified.health ? await healthRedOf(verified.health) : null;
|
|
168
|
+
const acceptQueues = regression ? describeHealthRepair(regression) : undefined;
|
|
145
169
|
const disposition = resolveDisposition({
|
|
146
170
|
failClass,
|
|
147
171
|
recommend: recOutcome.recommend,
|
|
@@ -170,7 +194,7 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
170
194
|
if (disposition.rule !== 'judge-accept') {
|
|
171
195
|
await rec(`resolution: asking the human — ${disposition.reason}`);
|
|
172
196
|
}
|
|
173
|
-
choice = await askVerifyResolution(active, p.title, failReason, recOutcome);
|
|
197
|
+
choice = await askVerifyResolution(active, p.title, failReason, recOutcome, acceptQueues);
|
|
174
198
|
}
|
|
175
199
|
if (choice.action === 'cancel') {
|
|
176
200
|
await rec('resolution: user dismissed the verify-FAIL picker — paused');
|
|
@@ -193,6 +217,9 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
193
217
|
await settleDebt(origin, disposition.rule === 'spec-contradiction' ?
|
|
194
218
|
`${failReason} — ${disposition.reason}`
|
|
195
219
|
: failReason);
|
|
220
|
+
if (acceptQueues) {
|
|
221
|
+
await rec(`accept: repo health regressed by this task — ${acceptQueues} is spliced before the next task`);
|
|
222
|
+
}
|
|
196
223
|
// ROOT CAUSE: an accepted FAIL that some OTHER task's file caused is
|
|
197
224
|
// not fixed by accepting it — every later task keeps tripping over
|
|
198
225
|
// the same bug. Queue the scoped repair so the plan closes it.
|
|
@@ -283,6 +310,21 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
283
310
|
await rec(`accept-debt: inherited repo health — ${verified.inheritedHealth}`);
|
|
284
311
|
await settleDebt('inherited-health', verified.inheritedHealth);
|
|
285
312
|
}
|
|
313
|
+
// A health repair that verified CLEAN — the check it exists for ran green,
|
|
314
|
+
// nothing inherited — closes the debts that check opened, under its own id.
|
|
315
|
+
const repair = parseHealthRepairTitle(p.title);
|
|
316
|
+
if (repair && verified.ok && !verified.reason && !verified.inheritedHealth) {
|
|
317
|
+
try {
|
|
318
|
+
const closed = await deps.closeHealthDebts?.(p.cwd, repair.command, p.taskId);
|
|
319
|
+
if (closed && closed.length > 0) {
|
|
320
|
+
await rec(`accept-debt: closed ${closed.length} debt(s) on \`${repair.command}\` — `
|
|
321
|
+
+ `${closed.map(d => d.taskId).join(', ')} — repaired by ${p.taskId}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// closing debts must never break the gate sequence
|
|
326
|
+
}
|
|
327
|
+
}
|
|
286
328
|
// Loop exited because the work verified OR the user accepted the artifact. A
|
|
287
329
|
// genuine clean pass is ok===true with NO reason; a no-op pass or an
|
|
288
330
|
// accept-override (verified.ok still false at break) is NOT a guardable signal.
|
|
@@ -64,7 +64,10 @@ export interface ResolutionChoice {
|
|
|
64
64
|
* `recommended: i === 0`). Values are the bare tokens; remote buttons carry the
|
|
65
65
|
* labels — {@link classifyResolutionAnswer} accepts both.
|
|
66
66
|
*/
|
|
67
|
-
export declare function resolutionOptions(recommend: ResolutionRecommendation
|
|
67
|
+
export declare function resolutionOptions(recommend: ResolutionRecommendation,
|
|
68
|
+
/** What ACCEPT queues besides keeping the artifact — the repair a red health
|
|
69
|
+
* check earns — so the picker shows the whole consequence of the choice. */
|
|
70
|
+
acceptQueues?: string): {
|
|
68
71
|
label: string;
|
|
69
72
|
value: string;
|
|
70
73
|
recommended: boolean;
|
|
@@ -193,8 +193,14 @@ export const AUTOFIX_LABEL = 'Autofix — re-run the task to fix it, then verify
|
|
|
193
193
|
* `recommended: i === 0`). Values are the bare tokens; remote buttons carry the
|
|
194
194
|
* labels — {@link classifyResolutionAnswer} accepts both.
|
|
195
195
|
*/
|
|
196
|
-
export function resolutionOptions(recommend
|
|
197
|
-
|
|
196
|
+
export function resolutionOptions(recommend,
|
|
197
|
+
/** What ACCEPT queues besides keeping the artifact — the repair a red health
|
|
198
|
+
* check earns — so the picker shows the whole consequence of the choice. */
|
|
199
|
+
acceptQueues) {
|
|
200
|
+
const accept = {
|
|
201
|
+
label: acceptQueues ? `${ACCEPT_LABEL}; queues ${acceptQueues}` : ACCEPT_LABEL,
|
|
202
|
+
value: ACCEPT_VALUE
|
|
203
|
+
};
|
|
198
204
|
const autofix = { label: AUTOFIX_LABEL, value: AUTOFIX_VALUE };
|
|
199
205
|
return recommend === 'accept' ?
|
|
200
206
|
[
|
|
@@ -35,6 +35,7 @@ export interface VerifyPass {
|
|
|
35
35
|
unobserved?: undefined;
|
|
36
36
|
crossTaskDeletions?: undefined;
|
|
37
37
|
inheritedHealth?: string;
|
|
38
|
+
health?: undefined;
|
|
38
39
|
probes?: ProbeFindings;
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
@@ -63,6 +64,11 @@ export interface VerifyFail {
|
|
|
63
64
|
* then ships in the next commit and the final gate must re-check it. */
|
|
64
65
|
crossTaskDeletions?: CrossTaskDeletion[];
|
|
65
66
|
inheritedHealth?: string;
|
|
67
|
+
/** The health result behind a `repo-health` FAIL: which command, and what its
|
|
68
|
+
* output named. What an ACCEPT of this FAIL hands to the repair channel. */
|
|
69
|
+
health?: HealthSignal & {
|
|
70
|
+
output?: string;
|
|
71
|
+
};
|
|
66
72
|
/** What the deterministic probes found for THIS verdict, carried out so an
|
|
67
73
|
* AUTOFIX re-run is told what the gate already knows (see fix-context.ts)
|
|
68
74
|
* instead of re-deriving it from the one-line reason. */
|
|
@@ -254,6 +260,7 @@ export interface VerificationDeps {
|
|
|
254
260
|
repoHealth?: () => Promise<{
|
|
255
261
|
ok: boolean;
|
|
256
262
|
reason: string;
|
|
263
|
+
output?: string;
|
|
257
264
|
} & HealthSignal>;
|
|
258
265
|
/**
|
|
259
266
|
* What those same checks said BEFORE this task ran (see health-baseline.ts).
|
package/dist/task/verify-work.js
CHANGED
|
@@ -908,7 +908,12 @@ export async function runWorkVerification(deps) {
|
|
|
908
908
|
if (!h.ok) {
|
|
909
909
|
const baseline = deps.healthBaseline ? await deps.healthBaseline() : null;
|
|
910
910
|
if (classifyHealthDelta(baseline?.outcome ?? null, h) === 'regressed') {
|
|
911
|
-
return {
|
|
911
|
+
return {
|
|
912
|
+
ok: false,
|
|
913
|
+
failClass: 'repo-health',
|
|
914
|
+
reason: `repo health: ${h.reason}`,
|
|
915
|
+
health: h
|
|
916
|
+
};
|
|
912
917
|
}
|
|
913
918
|
pre.repoHealth = inheritedHealthFindings(h);
|
|
914
919
|
inheritedHealth = `repo health: ${h.reason} — already failing before this task`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.1",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|