@mjasnikovs/pi-task 0.42.5 → 0.42.7
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/deferred-breakage.js +36 -17
- package/dist/task/health-baseline.d.ts +4 -1
- package/dist/task/health-baseline.js +6 -3
- package/dist/task/health-repair.d.ts +6 -2
- package/dist/task/health-repair.js +7 -2
- package/dist/task/repo-health-check.d.ts +2 -0
- package/dist/task/repo-health-check.js +5 -1
- package/dist/task/task-gates.js +9 -4
- package/dist/task/verify-work.d.ts +4 -1
- package/dist/task/verify-work.js +5 -1
- package/package.json +1 -1
|
@@ -31,7 +31,12 @@ const NOT_A_DECISION = /\b(?:not|never|no|don't|do not|doesn't|does not|rather t
|
|
|
31
31
|
* be rephrased away.
|
|
32
32
|
*/
|
|
33
33
|
const MODAL = /\b(?:would|could|might)\b/i;
|
|
34
|
-
|
|
34
|
+
/** A condition governs its own half of a semicolon and reaches no further. */
|
|
35
|
+
const CONDITION = /\b(?:if|unless)\b/i;
|
|
36
|
+
/** Options are posed once and weighed in any later clause of the sentence. */
|
|
37
|
+
const OPTION = /\b(?:either|whether|option|alternative|otherwise)\b/i;
|
|
38
|
+
/** Whatever the sentence weighs, "I would …" is the answer's own decision. */
|
|
39
|
+
const FIRST_PERSON = /\b(?:i|we)\s+(?:would|could|might)\b/i;
|
|
35
40
|
/** Where one clause ends and the next begins. A semicolon joins clauses of ONE
|
|
36
41
|
* thought, so the breakage a clause defers may sit in the other half. */
|
|
37
42
|
const CLAUSE_BOUNDARY = /[,:;()]|\s[—–-]\s|\b(?:and|but|so|then|while|whereas|although|though|because|since|however)\b/gi;
|
|
@@ -54,7 +59,14 @@ const PHRASES = [
|
|
|
54
59
|
// Handing the work to an unnamed someone is the deferral itself, whatever the
|
|
55
60
|
// clause is about; bare `whoever` below still needs a check to be one.
|
|
56
61
|
{ re: /\bwhoever\s+(?:owns|revisits|maintains|touches)\b/i, needs: 'alone' },
|
|
57
|
-
|
|
62
|
+
// Ownership handed to a ROLE is handed to nobody: no release manager exists in
|
|
63
|
+
// a /task-auto run. A named team is someone, so the lookahead hands those to
|
|
64
|
+
// the `check` rule below rather than listing every job title here.
|
|
65
|
+
{
|
|
66
|
+
re: /\bownership\s+(?:belongs|lies|rests)\s+(?:to|with)\b(?!.*\b(?:teams?|groups?|squads?|guilds?|crews?)\b)/i,
|
|
67
|
+
needs: 'alone'
|
|
68
|
+
},
|
|
69
|
+
{ re: /\bownership\s+(?:belongs|lies|rests)\s+(?:to|with)\b/i, needs: 'check' },
|
|
58
70
|
{ re: /\bwhoever\b/i, needs: 'check' },
|
|
59
71
|
{ re: /\bowned\s+(?:by|follow[- ]?up)\b/i, needs: 'check' },
|
|
60
72
|
{ re: /\bleft\s+for\b/i, needs: 'check' },
|
|
@@ -94,21 +106,28 @@ function clauses(sentence) {
|
|
|
94
106
|
export function defersBreakage(answer) {
|
|
95
107
|
for (const sentence of prose(answer).split(SENTENCE_BOUNDARY)) {
|
|
96
108
|
const sentenceBreaks = aboutACheck(sentence) && FAILURE.test(sentence);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
const posesOptions = OPTION.test(sentence);
|
|
110
|
+
// A semicolon joins two independent clauses, so "the test fails only if X;
|
|
111
|
+
// it could be flagged as known" states a condition and then decides. The
|
|
112
|
+
// options the sentence posed reach across it; the condition does not.
|
|
113
|
+
for (const half of sentence.split(';')) {
|
|
114
|
+
const weighsOptions = posesOptions || CONDITION.test(half);
|
|
115
|
+
for (const clause of clauses(half)) {
|
|
116
|
+
for (const { re, needs } of PHRASES) {
|
|
117
|
+
const hit = re.exec(clause);
|
|
118
|
+
if (!hit)
|
|
119
|
+
continue;
|
|
120
|
+
const before = clause.slice(0, hit.index);
|
|
121
|
+
if (NOT_A_DECISION.test(before))
|
|
122
|
+
continue;
|
|
123
|
+
if (MODAL.test(before) && weighsOptions && !FIRST_PERSON.test(before))
|
|
124
|
+
continue;
|
|
125
|
+
if (needs === 'check' && !aboutACheck(clause))
|
|
126
|
+
continue;
|
|
127
|
+
if (needs === 'breakage' && !sentenceBreaks)
|
|
128
|
+
continue;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
112
131
|
}
|
|
113
132
|
}
|
|
114
133
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
73
|
-
return (after.commands ?? []).filter(c => c.outcome === 'skip' && c.gap === 'empty-suite' &&
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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`)
|
package/dist/task/task-gates.js
CHANGED
|
@@ -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 (${
|
|
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 (${
|
|
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 (${
|
|
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)
|
package/dist/task/verify-work.js
CHANGED
|
@@ -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]} ${
|
|
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.
|
|
3
|
+
"version": "0.42.7",
|
|
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",
|