@mjasnikovs/pi-task 0.25.0 → 0.27.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.
@@ -1,7 +1,8 @@
1
1
  import { resolutionOptions, classifyResolutionAnswer } from './verify-resolution.js';
2
2
  import { SessionUI } from '../remote/bridge.js';
3
3
  import { isYoloMode, yoloVerifyResolution, YOLO_STAMP } from './yolo.js';
4
- import { findRepairCandidate } from './root-cause-repair.js';
4
+ import { extractFailingCommand, findRepairCandidate, summariseDefect } from './root-cause-repair.js';
5
+ import { attributeEnforceFailure } from './enforce-attribution.js';
5
6
  /**
6
7
  * How many times a verify FAIL may be auto-fixed UNATTENDED (the research
7
8
  * recommended AUTOFIX, so pi re-runs the impl turn without prompting) before the
@@ -455,8 +456,8 @@ export async function runGatesForTask(ctxIn, deps, p) {
455
456
  const afterReason = after.reason ?? 'enforce re-verify failed';
456
457
  // PRE-EXISTING-CAUSE KEEP PATH (mx5 run 14 item 5b). Both of run
457
458
  // 14's enforce-reverts were this shape: the re-verify FAILed on
458
- // TASK_0007's `test/teardown.ts` TRUNCATE bug — a file neither the
459
- // task's work nor the enforce pass touched — and the differential
459
+ // TASK_0007's `test/teardown.ts` TRUNCATE bug — a file the enforce
460
+ // pass never touched — and the differential
460
461
  // reverted enforce's edits anyway, destroying good work over a fault
461
462
  // it did not cause AND leaving the actual cause unscheduled. When the
462
463
  // FAIL is attributed to another task's untouched file, KEEP the edits
@@ -464,16 +465,82 @@ export async function runGatesForTask(ctxIn, deps, p) {
464
465
  // unknown (git unavailable, no provenance, this task touched the file,
465
466
  // an environment-blamed FAIL) falls through to the revert below —
466
467
  // the conservative pre-existing behavior.
467
- const rootCause = after.ok ? null : await routeRootCause(afterReason, '', 'committed');
468
+ //
469
+ // The scope is `enforce-commit`, NOT the task's own commit (mx5 run
470
+ // 18 / nexttask 4). This differential decides whether to discard the
471
+ // ENFORCE COMMIT, so the causal question is "could the enforce diff
472
+ // have caused this?" — asking what the TASK touched answers a
473
+ // question nobody at this seam is asking, and in run 18 it answered
474
+ // it in a way that destroyed a correct one-line change.
475
+ const rootCause = after.ok ? null : await routeRootCause(afterReason, '', 'enforce-commit');
476
+ // ATTRIBUTION PRE-FILTER (mx5 run 18 / nexttask 4). The root-cause
477
+ // channel above needs a blame CUE, a path-separator token and known
478
+ // provenance; run 18's FAIL text carried none of the three (it named
479
+ // a bare `MyListings.spec.tsx:186`), so it fell straight through to
480
+ // the revert. This filter asks only the mechanical question: does the
481
+ // failing check name any file the ENFORCE COMMIT touched? Disjoint =>
482
+ // the revert cannot repair the failure, so keep the edits and route
483
+ // the defect. Unknown diff, or a FAIL naming no file at all, still
484
+ // reverts — never keep on ignorance.
485
+ const attribution = !after.ok && !rootCause ?
486
+ attributeEnforceFailure({
487
+ failReason: afterReason,
488
+ enforceTouched: (await deps.touchedFiles?.(p.cwd, 'enforce-commit')) ?? null,
489
+ repoFiles: (await deps.repoFiles?.(p.cwd)) ?? null
490
+ })
491
+ : null;
468
492
  if (!after.ok && rootCause) {
469
493
  await rec(`enforce: re-verify FAILED (${afterReason.slice(0, 200)}) but the failure is attributed to a PRE-EXISTING defect in \`${rootCause.file}\` `
470
- + `(${rootCause.owner}'s file, untouched by this task and by the enforce pass) — edits KEPT, not reverted; repair task queued`);
494
+ + `(${rootCause.owner}'s file, untouched by the ENFORCE COMMIT whose fate this differential decides) — edits KEPT, not reverted; repair task queued`);
471
495
  active.ui.notify(`${p.tag}: guideline fixes on "${p.title}" re-verified red on a pre-existing defect in ${rootCause.file} (${rootCause.owner}'s file) — keeping the fixes, queued a repair task.`, 'warning');
472
496
  }
497
+ else if (!after.ok && attribution?.verdict === 'keep') {
498
+ // KEEP, mechanically justified: every file the failing check named
499
+ // is outside the enforce diff (and outside its companions — a
500
+ // touched file's own spec/story counts as inside). Discarding the
501
+ // enforce commit could not repair this, and in run 18 doing so
502
+ // cost a correct change that the final gate then re-made.
503
+ await rec(`enforce: re-verify FAILED (${afterReason.slice(0, 200)}) but the failing check names only \`${attribution.named.join(', ')}\`, `
504
+ + `which the ENFORCE COMMIT does not touch (its diff: ${attribution.enforceDiff.join(', ') || '—'}) — `
505
+ + 'reverting it could not repair this, so the edits are KEPT and the defect is recorded as durable debt');
506
+ // Keeping the edits must NOT lose the finding — that was mx5 run
507
+ // 5's mistake. Same durability as the revert path; only the
508
+ // disposition of the edits differs.
509
+ await deps.recordEnforceKeptDebt?.(p.cwd, p.taskId, afterReason);
510
+ // …and, when the named file is somebody else's committed work,
511
+ // queue the scoped repair so something actually FIXES it.
512
+ if (attribution.file && deps.introducedBy && deps.recordRepairCandidate) {
513
+ try {
514
+ const owner = await deps.introducedBy(p.cwd, attribution.file);
515
+ const verifyCommand = extractFailingCommand(afterReason);
516
+ if (owner && owner !== p.taskId) {
517
+ await deps.recordRepairCandidate(p.cwd, {
518
+ file: attribution.file,
519
+ owner,
520
+ defect: summariseDefect(afterReason, attribution.file),
521
+ blamedTask: p.taskId,
522
+ ...(verifyCommand ? { verifyCommand } : {})
523
+ });
524
+ await rec(`root-cause: \`${attribution.file}\` is ${owner}'s file — scoped repair task queued`);
525
+ }
526
+ }
527
+ catch {
528
+ // queueing a repair must never break the gate sequence
529
+ }
530
+ }
531
+ active.ui.notify(`${p.tag}: guideline fixes on "${p.title}" re-verified red on ${attribution.file ?? 'a file'} — outside the enforce diff, so keeping the fixes and recording the defect.`, 'warning');
532
+ }
473
533
  else if (!after.ok) {
474
534
  if (deps.revert)
475
535
  await deps.revert(p.cwd);
476
- await rec(`enforce: fixes committed but re-verify FAILED (${(after.reason ?? 'now fails').slice(0, 200)}) — ${deps.revert ? 'REVERTED' : 'left in place (no revert available)'}`);
536
+ await rec(`enforce: fixes committed but re-verify FAILED (${(after.reason ?? 'now fails').slice(0, 200)}) — ${deps.revert ? 'REVERTED' : 'left in place (no revert available)'}`
537
+ // Why the attribution filter did NOT save the edits, so a
538
+ // revert is explainable from the trail alone.
539
+ + (attribution ?
540
+ ` [attribution: ${attribution.why}${attribution.overlap ?
541
+ ` — the check names \`${attribution.overlap.named}\`, the enforce diff touches \`${attribution.overlap.enforce}\``
542
+ : ''}]`
543
+ : ''));
477
544
  // Persist the FAIL as a durable defect (mx5 run 10 item 3). The
478
545
  // revert restores the tree the ORIGINAL verify already blessed, so
479
546
  // this re-verify caught a defect that verify's earlier PASS missed —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
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",