@mjasnikovs/pi-task 0.42.5 → 0.42.6

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.
@@ -24,11 +24,11 @@ const FAILURE = /\b(?:fail\w*|red|broken|breaks?|breakage|errors?)\b/i;
24
24
  /** Before a phrase in its clause: the phrase is rejected. */
25
25
  const NOT_A_DECISION = /\b(?:not|never|no|don't|do not|doesn't|does not|rather than|instead of|isn't|is not|without|avoid|avoiding)\b/i;
26
26
  /**
27
- * A modal cancels a phrase only where the sentence poses an option for it to
28
- * weigh: "IF NOT EXISTS would still leave the test failing" describes what a
29
- * rejected option does. A bare hedge does not — "I would flag it as a known
30
- * issue" is the decision, and treating every modal as hypothetical let the guard
31
- * be rephrased away.
27
+ * A modal cancels a phrase only where the condition's own half of the sentence
28
+ * poses an option for it to weigh: "IF NOT EXISTS would still leave the test
29
+ * failing" describes what a rejected option does. A bare hedge does not — "I
30
+ * would flag it as a known issue" is the decision, and treating every modal as
31
+ * hypothetical let the guard be rephrased away.
32
32
  */
33
33
  const MODAL = /\b(?:would|could|might)\b/i;
34
34
  const HYPOTHETICAL = /\b(?:if|unless|either|whether|option|alternative|otherwise)\b/i;
@@ -54,7 +54,13 @@ const PHRASES = [
54
54
  // Handing the work to an unnamed someone is the deferral itself, whatever the
55
55
  // clause is about; bare `whoever` below still needs a check to be one.
56
56
  { re: /\bwhoever\s+(?:owns|revisits|maintains|touches)\b/i, needs: 'alone' },
57
- { re: /\bownership\s+(?:belongs|lies|rests)\s+(?:to|with)\b/i, needs: 'alone' },
57
+ // Ownership handed to a ROLE is handed to nobody. Handed to a named team it is
58
+ // handed to someone, so it counts only where the clause is about a check.
59
+ {
60
+ re: /\bownership\s+(?:belongs|lies|rests)\s+(?:to|with)\s+(?:whoever|someone|somebody|another\b|a\s+later\b|the\s+(?:\w+\s+)?(?:owners?|maintainers?)\b)/i,
61
+ needs: 'alone'
62
+ },
63
+ { re: /\bownership\s+(?:belongs|lies|rests)\s+(?:to|with)\b/i, needs: 'check' },
58
64
  { re: /\bwhoever\b/i, needs: 'check' },
59
65
  { re: /\bowned\s+(?:by|follow[- ]?up)\b/i, needs: 'check' },
60
66
  { re: /\bleft\s+for\b/i, needs: 'check' },
@@ -94,21 +100,27 @@ function clauses(sentence) {
94
100
  export function defersBreakage(answer) {
95
101
  for (const sentence of prose(answer).split(SENTENCE_BOUNDARY)) {
96
102
  const sentenceBreaks = aboutACheck(sentence) && FAILURE.test(sentence);
97
- for (const clause of clauses(sentence)) {
98
- for (const { re, needs } of PHRASES) {
99
- const hit = re.exec(clause);
100
- if (!hit)
101
- continue;
102
- const before = clause.slice(0, hit.index);
103
- if (NOT_A_DECISION.test(before))
104
- continue;
105
- if (MODAL.test(before) && HYPOTHETICAL.test(sentence))
106
- continue;
107
- if (needs === 'check' && !aboutACheck(clause))
108
- continue;
109
- if (needs === 'breakage' && !sentenceBreaks)
110
- continue;
111
- return true;
103
+ // A condition governs its own half of a semicolon. It joins two independent
104
+ // clauses, so "the test fails only if X; I would flag it as known" states a
105
+ // condition and then decides — the decision is not one of X's options.
106
+ for (const half of sentence.split(';')) {
107
+ const weighsOptions = HYPOTHETICAL.test(half);
108
+ for (const clause of clauses(half)) {
109
+ for (const { re, needs } of PHRASES) {
110
+ const hit = re.exec(clause);
111
+ if (!hit)
112
+ continue;
113
+ const before = clause.slice(0, hit.index);
114
+ if (NOT_A_DECISION.test(before))
115
+ continue;
116
+ if (MODAL.test(before) && weighsOptions)
117
+ continue;
118
+ if (needs === 'check' && !aboutACheck(clause))
119
+ continue;
120
+ if (needs === 'breakage' && !sentenceBreaks)
121
+ continue;
122
+ return true;
123
+ }
112
124
  }
113
125
  }
114
126
  }
@@ -39,13 +39,16 @@ export type HealthDelta = 'clean' | 'regressed' | 'pre-existing';
39
39
  */
40
40
  export declare function classifyHealthDelta(baseline: HealthSignal | null, after: HealthSignal): HealthDelta;
41
41
  /**
42
- * Test commands the baseline saw PASS that now find no tests to run.
42
+ * Test commands the baseline RAN that now find no tests to run.
43
43
  *
44
44
  * A runner that found nothing observed nothing, which is a gap — in isolation. A
45
45
  * task that deleted the test directory, renamed it, or broke the config's glob
46
46
  * leaves the same gap, and the check reports the repo healthy because a gap never
47
47
  * fails. Against a baseline that ran the suite, the suite is gone: this task's
48
48
  * regression, and the largest one it can hide behind a green.
49
+ *
50
+ * Ran, not passed. A baseline that ran the suite RED ran it, and deleting a red
51
+ * suite is the same move with a larger payoff: the whole check turns green.
49
52
  */
50
53
  export declare function vanishedSuites(baseline: HealthSignal | null, after: HealthSignal): HealthCommandResult[];
51
54
  /** The failing commands the baseline did not have failing the same way — what a
@@ -58,19 +58,22 @@ export function classifyHealthDelta(baseline, after) {
58
58
  }
59
59
  const failureKey = (c) => JSON.stringify([c.cmd, c.exitCode]);
60
60
  /**
61
- * Test commands the baseline saw PASS that now find no tests to run.
61
+ * Test commands the baseline RAN that now find no tests to run.
62
62
  *
63
63
  * A runner that found nothing observed nothing, which is a gap — in isolation. A
64
64
  * task that deleted the test directory, renamed it, or broke the config's glob
65
65
  * leaves the same gap, and the check reports the repo healthy because a gap never
66
66
  * fails. Against a baseline that ran the suite, the suite is gone: this task's
67
67
  * regression, and the largest one it can hide behind a green.
68
+ *
69
+ * Ran, not passed. A baseline that ran the suite RED ran it, and deleting a red
70
+ * suite is the same move with a larger payoff: the whole check turns green.
68
71
  */
69
72
  export function vanishedSuites(baseline, after) {
70
73
  if (!baseline)
71
74
  return [];
72
- const passed = new Set((baseline.commands ?? []).filter(c => c.outcome === 'pass').map(c => c.cmd));
73
- return (after.commands ?? []).filter(c => c.outcome === 'skip' && c.gap === 'empty-suite' && passed.has(c.cmd));
75
+ const ran = new Set((baseline.commands ?? []).filter(c => c.outcome !== 'skip').map(c => c.cmd));
76
+ return (after.commands ?? []).filter(c => c.outcome === 'skip' && c.gap === 'empty-suite' && ran.has(c.cmd));
74
77
  }
75
78
  /** The failing commands the baseline did not have failing the same way — what a
76
79
  * `regressed` verdict is about. Every failing command when there is no baseline. */
@@ -22,7 +22,7 @@
22
22
  * because its script is a placeholder `exit 1`, and no repair task can fix either.
23
23
  */
24
24
  import type { HealthSignal } from './health-baseline.js';
25
- import type { HealthCommandResult } from './repo-health-check.js';
25
+ import { type HealthCommandResult } from './repo-health-check.js';
26
26
  /** The failing check, and what its output named. */
27
27
  export interface HealthRed {
28
28
  command: string;
@@ -36,9 +36,13 @@ export interface HealthRedOwners {
36
36
  owners: string[];
37
37
  }
38
38
  /**
39
- * What a red health result is about: its first failing command that `mayRepair`
39
+ * What a red health result is about: its first red command that `mayRepair`
40
40
  * admits. Null when there is none — a legacy baseline, a signal with no
41
41
  * per-command detail, or only reds no repair can fix — so nothing to pin to.
42
+ *
43
+ * A vanished suite is red here though it failed nothing and left `ok` true. It
44
+ * is the regression the differential exists to catch, and without a subject
45
+ * ACCEPT queued no repair for it and the checkpoint spliced none.
42
46
  */
43
47
  export declare function healthRedSubject(health: HealthSignal & {
44
48
  output?: string;
@@ -1,3 +1,4 @@
1
+ import { isHealthRed } from './repo-health-check.js';
1
2
  import { parseRepairTitleFile } from './root-cause-repair.js';
2
3
  import { failClassOfReason } from './verify-work.js';
3
4
  /** A path-like token: at least one directory separator, ending in a file name. */
@@ -23,12 +24,16 @@ function resolveTracked(token, cwd, tracked) {
23
24
  return bySuffix.length === 1 ? bySuffix[0] : null;
24
25
  }
25
26
  /**
26
- * What a red health result is about: its first failing command that `mayRepair`
27
+ * What a red health result is about: its first red command that `mayRepair`
27
28
  * admits. Null when there is none — a legacy baseline, a signal with no
28
29
  * per-command detail, or only reds no repair can fix — so nothing to pin to.
30
+ *
31
+ * A vanished suite is red here though it failed nothing and left `ok` true. It
32
+ * is the regression the differential exists to catch, and without a subject
33
+ * ACCEPT queued no repair for it and the checkpoint spliced none.
29
34
  */
30
35
  export function healthRedSubject(health, cwd, tracked, mayRepair = () => true) {
31
- const failing = (health.commands ?? []).find(c => c.outcome === 'fail' && mayRepair(c));
36
+ const failing = (health.commands ?? []).find(c => isHealthRed(c) && mayRepair(c));
32
37
  if (!failing)
33
38
  return null;
34
39
  const files = [];
@@ -115,5 +115,7 @@ export declare function runRepoHealthCheck(cwd: string, opts?: {
115
115
  * that will judge the result DIFFERENTIALLY may set this — see the header. */
116
116
  withTests?: boolean;
117
117
  }): Promise<HealthOutcome>;
118
+ /** A command the repo owes an answer for: it failed, or its suite went missing. */
119
+ export declare function isHealthRed(c: HealthCommandResult): boolean;
118
120
  /** "`bun run lint` exited 1; `bun run test` exited 1" — every failing command. */
119
121
  export declare function describeHealthFailures(commands: readonly HealthCommandResult[]): string;
@@ -280,10 +280,14 @@ export async function runRepoHealthCheck(cwd, opts = {}) {
280
280
  output: ''
281
281
  };
282
282
  }
283
+ /** A command the repo owes an answer for: it failed, or its suite went missing. */
284
+ export function isHealthRed(c) {
285
+ return c.outcome === 'fail' || c.gap === 'empty-suite';
286
+ }
283
287
  /** "`bun run lint` exited 1; `bun run test` exited 1" — every failing command. */
284
288
  export function describeHealthFailures(commands) {
285
289
  return commands
286
- .filter(c => c.outcome === 'fail' || c.gap === 'empty-suite')
290
+ .filter(isHealthRed)
287
291
  .map(c => c.outcome === 'fail' ?
288
292
  `\`${c.cmd}\` exited ${c.exitCode}`
289
293
  : `\`${c.cmd}\` found no tests to run`)
@@ -1,11 +1,12 @@
1
1
  import { verifyFailClass } from './verify-work.js';
2
2
  import { resolutionOptions, classifyResolutionAnswer } from './verify-resolution.js';
3
3
  import { resolveDisposition } from './gate-resolution.js';
4
- import { classifyHealthDelta } from './health-baseline.js';
4
+ import { classifyHealthDelta, regressedCommands } from './health-baseline.js';
5
5
  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 { describeHealthFailures } from './repo-health-check.js';
9
10
  import { healthRedSubject, parseHealthRepairTitle } from './health-repair.js';
10
11
  // The debt ledger is reached through the injected `recordDebt` dep (so it stays
11
12
  // absent-in-tests); only the origin TYPE and the cross-task-deletion reason SHAPE
@@ -417,15 +418,19 @@ export async function runEnforcePass(active, deps, p, rec, routeRootCause, args)
417
418
  const delta = classifyHealthDelta(healthBefore ?? null, after);
418
419
  if (delta === 'regressed') {
419
420
  enforceEditsBlocked = true;
421
+ // What REGRESSED, not the check's own verdict: a vanished suite fails
422
+ // nothing, so `after.reason` reads "tests passed" under this discard.
423
+ const regressed = regressedCommands(healthBefore ?? null, after);
424
+ const why = regressed.length > 0 ? describeHealthFailures(regressed) : after.reason;
420
425
  const outputTail = after.output ? ` — output:\n${clampOutput(after.output)}` : '';
421
426
  if (deps.discardEdits) {
422
427
  await deps.discardEdits(p.cwd);
423
- await rec(`enforce: edits discarded pre-commit — REGRESSED repo health (${after.reason})${outputTail}`);
428
+ await rec(`enforce: edits discarded pre-commit — REGRESSED repo health (${why})${outputTail}`);
424
429
  }
425
430
  else {
426
- await rec(`enforce: edits REGRESSED repo health pre-commit (${after.reason}) — no discard available, left uncommitted${outputTail}`);
431
+ await rec(`enforce: edits REGRESSED repo health pre-commit (${why}) — no discard available, left uncommitted${outputTail}`);
427
432
  }
428
- notifyRun(active, `${p.tag}: guideline edits on "${p.title}" regressed repo health (${after.reason.slice(0, 120)}) — discarded before commit.`, 'warning');
433
+ notifyRun(active, `${p.tag}: guideline edits on "${p.title}" regressed repo health (${why.slice(0, 120)}) — discarded before commit.`, 'warning');
429
434
  }
430
435
  else if (delta === 'pre-existing') {
431
436
  // Failing both before and after → not enforce's fault. Keep the edits;
@@ -65,9 +65,12 @@ export interface VerifyFail {
65
65
  crossTaskDeletions?: CrossTaskDeletion[];
66
66
  inheritedHealth?: string;
67
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. */
68
+ * output named. What an ACCEPT of this FAIL hands to the repair channel.
69
+ * `reason` describes what REGRESSED, which is not what the check itself said:
70
+ * a vanished suite fails nothing, so the check's verdict reads "tests passed". */
69
71
  health?: HealthSignal & {
70
72
  output?: string;
73
+ reason?: string;
71
74
  };
72
75
  /** What the deterministic probes found for THIS verdict, carried out so an
73
76
  * AUTOFIX re-run is told what the gate already knows (see fix-context.ts)
@@ -939,13 +939,17 @@ export async function runWorkVerification(deps) {
939
939
  };
940
940
  }
941
941
  const failClass = healthFailClass(regressed);
942
+ // Re-minted, never `h.reason`: a vanished suite fails nothing, so the
943
+ // check's own reason reads "tests passed" under a REGRESSED verdict.
944
+ const why = describeHealthFailures(regressed);
942
945
  return {
943
946
  ok: false,
944
947
  failClass,
945
- reason: `${VERIFY_FAIL_PREFIX[failClass]} ${describeHealthFailures(regressed)}`,
948
+ reason: `${VERIFY_FAIL_PREFIX[failClass]} ${why}`,
946
949
  health: {
947
950
  ...h,
948
951
  ok: false,
952
+ reason: why,
949
953
  commands: regressed,
950
954
  output: regressed[0].output ?? h.output
951
955
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.42.5",
3
+ "version": "0.42.6",
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",