@mjasnikovs/pi-task 0.42.4 → 0.42.5
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.js +45 -10
- package/dist/task/deferred-breakage.d.ts +5 -6
- package/dist/task/deferred-breakage.js +28 -14
- package/dist/task/gate-deps.d.ts +11 -0
- package/dist/task/gate-deps.js +18 -2
- package/dist/task/health-baseline.d.ts +10 -0
- package/dist/task/health-baseline.js +21 -1
- package/dist/task/repo-health-check.d.ts +4 -1
- package/dist/task/repo-health-check.js +15 -5
- package/dist/task/verify-work.js +20 -8
- package/package.json +1 -1
package/dist/task/accept-debt.js
CHANGED
|
@@ -36,6 +36,7 @@ import * as path from 'node:path';
|
|
|
36
36
|
import * as fsp from 'node:fs/promises';
|
|
37
37
|
import { runVerifyCommandLine, spawnCommand } from './command-run.js';
|
|
38
38
|
import { failClassOfReason, isHealthClass, isStaticClass } from './verify-work.js';
|
|
39
|
+
import { discoverTestCommands } from './repo-health-check.js';
|
|
39
40
|
import { taskThatIntroduced } from './task-provenance.js';
|
|
40
41
|
import { makeLedger } from './ledger.js';
|
|
41
42
|
import { parseVerifyBlockStrict } from './spec-validation.js';
|
|
@@ -341,6 +342,9 @@ function isStorableCommand(cmd) {
|
|
|
341
342
|
* command with fabricated provenance, which is the one thing this class may not do.
|
|
342
343
|
*/
|
|
343
344
|
export async function classifyVerifyCommand(cwd, taskId, reason) {
|
|
345
|
+
const suite = suiteCommandFromReason(cwd, reason);
|
|
346
|
+
if (suite !== null)
|
|
347
|
+
return suite;
|
|
344
348
|
if (taskId.trim().length === 0)
|
|
345
349
|
return null;
|
|
346
350
|
try {
|
|
@@ -355,6 +359,23 @@ export async function classifyVerifyCommand(cwd, taskId, reason) {
|
|
|
355
359
|
return null;
|
|
356
360
|
}
|
|
357
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* The command a `test suite:` reason names, when the repo's own check is what
|
|
364
|
+
* discovers it. The suite is not a task's VERIFY line — it belongs to the repo —
|
|
365
|
+
* so without this the class has NO closure path at all: a passing lint may not
|
|
366
|
+
* close it (that is why the class exists), the run-end static check does not run
|
|
367
|
+
* the suite, and a repair splices only for a task's own regression. An inherited
|
|
368
|
+
* red suite was then reported open for the rest of the run after it went green.
|
|
369
|
+
*
|
|
370
|
+
* Provenance is the manifest: the span must equal a command `discoverTestCommands`
|
|
371
|
+
* produced, exactly as the VERIFY-block match must equal a parsed line.
|
|
372
|
+
*/
|
|
373
|
+
function suiteCommandFromReason(cwd, reason) {
|
|
374
|
+
if (failClassOfReason(reason) !== 'test-suite')
|
|
375
|
+
return null;
|
|
376
|
+
const hit = verifyCommandFromReason(reason, discoverTestCommands(cwd).cmds.map(([bin, args]) => `${bin} ${args.join(' ')}`));
|
|
377
|
+
return hit !== null && isStorableCommand(hit) ? hit : null;
|
|
378
|
+
}
|
|
358
379
|
export function verifyCommandFromReason(reason, verifyCommands) {
|
|
359
380
|
const byText = new Map();
|
|
360
381
|
for (const c of verifyCommands) {
|
|
@@ -395,6 +416,19 @@ export async function recheckAcceptDebts(debts, opts) {
|
|
|
395
416
|
const resolved = [];
|
|
396
417
|
const trail = [];
|
|
397
418
|
let rerunsLeft = MAX_VERIFY_RERUNS;
|
|
419
|
+
const ran = new Map();
|
|
420
|
+
const settle = (d, cmd, r) => {
|
|
421
|
+
if (r.outcome === 'pass') {
|
|
422
|
+
resolved.push(d);
|
|
423
|
+
trail.push(`${d.taskId}: RESOLVED — re-ran \`${cmd}\` and it exited 0`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
trail.push(`${d.taskId}: still open — re-ran \`${cmd}\`: `
|
|
427
|
+
+ (r.outcome === 'fail' ?
|
|
428
|
+
`it FAILED${r.detail ? ` (${r.detail})` : ''}`
|
|
429
|
+
: `INCONCLUSIVE${r.detail ? ` (${r.detail})` : ''}, nothing was observed`));
|
|
430
|
+
open.push(d);
|
|
431
|
+
};
|
|
398
432
|
for (const d of debts) {
|
|
399
433
|
if (d.origin === 'cross-task-deletion') {
|
|
400
434
|
const p = extractDeletedDebtPath(d.reason);
|
|
@@ -423,6 +457,15 @@ export async function recheckAcceptDebts(debts, opts) {
|
|
|
423
457
|
open.push(d);
|
|
424
458
|
continue;
|
|
425
459
|
}
|
|
460
|
+
// One command, one run. A run that inherits a red suite records the same
|
|
461
|
+
// `bun run test` against every task in it, and re-running it once per debt
|
|
462
|
+
// would spend the whole budget proving the same thing and leave the rest
|
|
463
|
+
// open. The budget counts commands, which is what it was for.
|
|
464
|
+
const already = ran.get(cmd);
|
|
465
|
+
if (already !== undefined) {
|
|
466
|
+
settle(d, cmd, already);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
426
469
|
if (rerunsLeft <= 0) {
|
|
427
470
|
trail.push(`${d.taskId}: NOT re-checked — the per-run re-run budget `
|
|
428
471
|
+ `(${MAX_VERIFY_RERUNS}) is spent; the debt stays open`);
|
|
@@ -438,16 +481,8 @@ export async function recheckAcceptDebts(debts, opts) {
|
|
|
438
481
|
// A harness fault observes nothing, so it proves nothing.
|
|
439
482
|
r = { outcome: 'gap', detail: 're-run harness fault' };
|
|
440
483
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
trail.push(`${d.taskId}: RESOLVED — re-ran \`${cmd}\` and it exited 0`);
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
trail.push(`${d.taskId}: still open — re-ran \`${cmd}\`: `
|
|
447
|
-
+ (r.outcome === 'fail' ?
|
|
448
|
-
`it FAILED${r.detail ? ` (${r.detail})` : ''}`
|
|
449
|
-
: `INCONCLUSIVE${r.detail ? ` (${r.detail})` : ''}, nothing was observed`));
|
|
450
|
-
open.push(d);
|
|
484
|
+
ran.set(cmd, r);
|
|
485
|
+
settle(d, cmd, r);
|
|
451
486
|
}
|
|
452
487
|
return { open, resolved, trail };
|
|
453
488
|
}
|
|
@@ -11,12 +11,11 @@
|
|
|
11
11
|
* The phrases are the ones a model reaches for when it wants to defer, not the
|
|
12
12
|
* word "test" — "add a test later" is a plan, not a deferral.
|
|
13
13
|
*
|
|
14
|
-
* SCOPE IS GRAMMATICAL, never a character count. A negation
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* about a check.
|
|
14
|
+
* SCOPE IS GRAMMATICAL, never a character count. A negation cancels a phrase only
|
|
15
|
+
* inside the phrase's own clause: "rather than flag it as a known issue" rejects
|
|
16
|
+
* the phrase, and "without touching the test file, accepting that it fails" does
|
|
17
|
+
* not. "Known issue", "follow-up" and "a later step" also name legitimate plans —
|
|
18
|
+
* an upstream bug, a scope cut — so they count only in a clause about a check.
|
|
20
19
|
*/
|
|
21
20
|
export declare function defersBreakage(answer: string): boolean;
|
|
22
21
|
/** The one re-ask a deferring answer gets before it is surfaced instead of promoted. */
|
|
@@ -11,23 +11,31 @@
|
|
|
11
11
|
* The phrases are the ones a model reaches for when it wants to defer, not the
|
|
12
12
|
* word "test" — "add a test later" is a plan, not a deferral.
|
|
13
13
|
*
|
|
14
|
-
* SCOPE IS GRAMMATICAL, never a character count. A negation
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* about a check.
|
|
14
|
+
* SCOPE IS GRAMMATICAL, never a character count. A negation cancels a phrase only
|
|
15
|
+
* inside the phrase's own clause: "rather than flag it as a known issue" rejects
|
|
16
|
+
* the phrase, and "without touching the test file, accepting that it fails" does
|
|
17
|
+
* not. "Known issue", "follow-up" and "a later step" also name legitimate plans —
|
|
18
|
+
* an upstream bug, a scope cut — so they count only in a clause about a check.
|
|
20
19
|
*/
|
|
21
20
|
/** A test, or a static check that the same clause calls broken. */
|
|
22
21
|
const TEST_NOUN = /\b(?:tests?|suites?|assertions?)\b/i;
|
|
23
22
|
const BUILD_NOUN = /\b(?:lint|linter|typecheck|build|ci)\b/i;
|
|
24
23
|
const FAILURE = /\b(?:fail\w*|red|broken|breaks?|breakage|errors?)\b/i;
|
|
25
|
-
/** Before a phrase in its clause: the phrase is rejected
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
/** Before a phrase in its clause: the phrase is rejected. */
|
|
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
|
+
/**
|
|
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.
|
|
32
|
+
*/
|
|
33
|
+
const MODAL = /\b(?:would|could|might)\b/i;
|
|
34
|
+
const HYPOTHETICAL = /\b(?:if|unless|either|whether|option|alternative|otherwise)\b/i;
|
|
35
|
+
/** Where one clause ends and the next begins. A semicolon joins clauses of ONE
|
|
36
|
+
* thought, so the breakage a clause defers may sit in the other half. */
|
|
37
|
+
const CLAUSE_BOUNDARY = /[,:;()]|\s[—–-]\s|\b(?:and|but|so|then|while|whereas|although|though|because|since|however)\b/gi;
|
|
38
|
+
const SENTENCE_BOUNDARY = /[.!?](?=\s|$)|\n/;
|
|
31
39
|
const PHRASES = [
|
|
32
40
|
{ re: /\b(?:test|suite)[- ]owners?\b/i, needs: 'alone' },
|
|
33
41
|
{
|
|
@@ -43,9 +51,12 @@ const PHRASES = [
|
|
|
43
51
|
needs: 'alone'
|
|
44
52
|
},
|
|
45
53
|
{ re: /\baccept(?:s|ed|ing)?\b.*?\b(?:fail\w*|red|broken)\b/i, needs: 'check' },
|
|
54
|
+
// Handing the work to an unnamed someone is the deferral itself, whatever the
|
|
55
|
+
// clause is about; bare `whoever` below still needs a check to be one.
|
|
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' },
|
|
46
58
|
{ re: /\bwhoever\b/i, needs: 'check' },
|
|
47
59
|
{ re: /\bowned\s+(?:by|follow[- ]?up)\b/i, needs: 'check' },
|
|
48
|
-
{ re: /\bownership\s+(?:belongs|lies|rests)\s+(?:to|with)\b/i, needs: 'check' },
|
|
49
60
|
{ re: /\bleft\s+for\b/i, needs: 'check' },
|
|
50
61
|
{ re: /\bknown[- ]issues?\b/i, needs: 'check' },
|
|
51
62
|
{ re: /\bfollow[- ]?ups?\b/i, needs: 'check' },
|
|
@@ -88,7 +99,10 @@ export function defersBreakage(answer) {
|
|
|
88
99
|
const hit = re.exec(clause);
|
|
89
100
|
if (!hit)
|
|
90
101
|
continue;
|
|
91
|
-
|
|
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))
|
|
92
106
|
continue;
|
|
93
107
|
if (needs === 'check' && !aboutACheck(clause))
|
|
94
108
|
continue;
|
package/dist/task/gate-deps.d.ts
CHANGED
|
@@ -27,6 +27,17 @@ export type FinalGateFixFn = (ctx: ExtensionCommandContext, cwd: string, failRea
|
|
|
27
27
|
* FinalFixDeps.ignoredKnown) — a failed attempt's ignored writes survive its
|
|
28
28
|
* discard and can green a later attempt. */
|
|
29
29
|
ignoredKnown?: string[]) => Promise<FinalFixResult>;
|
|
30
|
+
/**
|
|
31
|
+
* A file the suite wrote as the repo's OWN record, not as a report about the run.
|
|
32
|
+
* A test added this task and never run locally generates its snapshot on the
|
|
33
|
+
* gate's run; deleting it commits a snapshot test with no snapshot, and the next
|
|
34
|
+
* suite — or CI — fails on a file the task was supposed to carry.
|
|
35
|
+
*
|
|
36
|
+
* A named set, not a shape test: the alternative is an allowlist of throwaway
|
|
37
|
+
* artefacts, and anything it misses rides into the commit, which is the problem
|
|
38
|
+
* the cleanup exists for. A stray snapshot is the smaller error.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isSuiteRecord(rel: string): boolean;
|
|
30
41
|
/**
|
|
31
42
|
* Collect the task's changed files as pure GIT SHAPE — path + added-line count,
|
|
32
43
|
* no content, no language parsing — for the self-verification probe. Before the
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -82,6 +82,21 @@ export function truncateToolResult(text, limit = TOOL_RESULT_LOG_LIMIT) {
|
|
|
82
82
|
}
|
|
83
83
|
/** Keep the gate machinery's own artifacts out of every git pathspec below. */
|
|
84
84
|
const EXCLUDE_TASKS_DIR = ':(exclude).pi-tasks';
|
|
85
|
+
/**
|
|
86
|
+
* A file the suite wrote as the repo's OWN record, not as a report about the run.
|
|
87
|
+
* A test added this task and never run locally generates its snapshot on the
|
|
88
|
+
* gate's run; deleting it commits a snapshot test with no snapshot, and the next
|
|
89
|
+
* suite — or CI — fails on a file the task was supposed to carry.
|
|
90
|
+
*
|
|
91
|
+
* A named set, not a shape test: the alternative is an allowlist of throwaway
|
|
92
|
+
* artefacts, and anything it misses rides into the commit, which is the problem
|
|
93
|
+
* the cleanup exists for. A stray snapshot is the smaller error.
|
|
94
|
+
*/
|
|
95
|
+
export function isSuiteRecord(rel) {
|
|
96
|
+
return (/(?:^|[\\/])__(?:snapshots|image_snapshots)__[\\/]/.test(rel)
|
|
97
|
+
|| /\.snap$/.test(rel)
|
|
98
|
+
|| /\.approved\.[^.\\/]+$/.test(rel));
|
|
99
|
+
}
|
|
85
100
|
/**
|
|
86
101
|
* Pin the diff header prefixes on any command whose output we PARSE for paths.
|
|
87
102
|
*
|
|
@@ -730,7 +745,8 @@ export function buildGateDeps(params) {
|
|
|
730
745
|
// The project's own checks, suite included, once per tree for the run. A suite
|
|
731
746
|
// writes coverage, reports and databases into the tree; left there, they ride
|
|
732
747
|
// into the task's commit and read as enforce edits, so what the check created
|
|
733
|
-
// is removed before the tree is hashed again
|
|
748
|
+
// is removed before the tree is hashed again — except what it wrote as the
|
|
749
|
+
// repo's own record (see isSuiteRecord).
|
|
734
750
|
const gateHealth = (cwd2, onCommand) => currentRunContext(cwd2).healthFor(async () => {
|
|
735
751
|
const before = await untrackedFiles(cwd2);
|
|
736
752
|
try {
|
|
@@ -739,7 +755,7 @@ export function buildGateDeps(params) {
|
|
|
739
755
|
finally {
|
|
740
756
|
const after = before ? await untrackedFiles(cwd2) : null;
|
|
741
757
|
for (const rel of after ?? []) {
|
|
742
|
-
if (!before?.has(rel)) {
|
|
758
|
+
if (!before?.has(rel) && !isSuiteRecord(rel)) {
|
|
743
759
|
await fsp
|
|
744
760
|
.rm(path.join(cwd2, rel), { recursive: true, force: true })
|
|
745
761
|
.catch(() => { });
|
|
@@ -38,6 +38,16 @@ export type HealthDelta = 'clean' | 'regressed' | 'pre-existing';
|
|
|
38
38
|
* default the enforce site used to carry.
|
|
39
39
|
*/
|
|
40
40
|
export declare function classifyHealthDelta(baseline: HealthSignal | null, after: HealthSignal): HealthDelta;
|
|
41
|
+
/**
|
|
42
|
+
* Test commands the baseline saw PASS that now find no tests to run.
|
|
43
|
+
*
|
|
44
|
+
* A runner that found nothing observed nothing, which is a gap — in isolation. A
|
|
45
|
+
* task that deleted the test directory, renamed it, or broke the config's glob
|
|
46
|
+
* leaves the same gap, and the check reports the repo healthy because a gap never
|
|
47
|
+
* fails. Against a baseline that ran the suite, the suite is gone: this task's
|
|
48
|
+
* regression, and the largest one it can hide behind a green.
|
|
49
|
+
*/
|
|
50
|
+
export declare function vanishedSuites(baseline: HealthSignal | null, after: HealthSignal): HealthCommandResult[];
|
|
41
51
|
/** The failing commands the baseline did not have failing the same way — what a
|
|
42
52
|
* `regressed` verdict is about. Every failing command when there is no baseline. */
|
|
43
53
|
export declare function regressedCommands(baseline: HealthSignal | null, after: HealthSignal): HealthCommandResult[];
|
|
@@ -41,6 +41,8 @@ function failures(signal) {
|
|
|
41
41
|
* default the enforce site used to carry.
|
|
42
42
|
*/
|
|
43
43
|
export function classifyHealthDelta(baseline, after) {
|
|
44
|
+
if (vanishedSuites(baseline, after).length > 0)
|
|
45
|
+
return 'regressed';
|
|
44
46
|
if (after.ok)
|
|
45
47
|
return 'clean';
|
|
46
48
|
if (!baseline)
|
|
@@ -55,11 +57,29 @@ export function classifyHealthDelta(baseline, after) {
|
|
|
55
57
|
return regressedCommands(baseline, after).length > 0 ? 'regressed' : 'pre-existing';
|
|
56
58
|
}
|
|
57
59
|
const failureKey = (c) => JSON.stringify([c.cmd, c.exitCode]);
|
|
60
|
+
/**
|
|
61
|
+
* Test commands the baseline saw PASS that now find no tests to run.
|
|
62
|
+
*
|
|
63
|
+
* A runner that found nothing observed nothing, which is a gap — in isolation. A
|
|
64
|
+
* task that deleted the test directory, renamed it, or broke the config's glob
|
|
65
|
+
* leaves the same gap, and the check reports the repo healthy because a gap never
|
|
66
|
+
* fails. Against a baseline that ran the suite, the suite is gone: this task's
|
|
67
|
+
* regression, and the largest one it can hide behind a green.
|
|
68
|
+
*/
|
|
69
|
+
export function vanishedSuites(baseline, after) {
|
|
70
|
+
if (!baseline)
|
|
71
|
+
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));
|
|
74
|
+
}
|
|
58
75
|
/** The failing commands the baseline did not have failing the same way — what a
|
|
59
76
|
* `regressed` verdict is about. Every failing command when there is no baseline. */
|
|
60
77
|
export function regressedCommands(baseline, after) {
|
|
61
78
|
const wasFailing = new Set(baseline ? failures(baseline).map(failureKey) : []);
|
|
62
|
-
return
|
|
79
|
+
return [
|
|
80
|
+
...failures(after).filter(c => !wasFailing.has(failureKey(c))),
|
|
81
|
+
...vanishedSuites(baseline, after)
|
|
82
|
+
];
|
|
63
83
|
}
|
|
64
84
|
/**
|
|
65
85
|
* The failing commands, as prompt/trail lines naming the exit code. A test
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CommandRunner } from './command-run.js';
|
|
1
|
+
import { type CommandGapId, type CommandRunner } from './command-run.js';
|
|
2
2
|
/**
|
|
3
3
|
* What ONE discovered command did. `outcome` is `classifyCommandRun`'s verdict, so
|
|
4
4
|
* a tool that could not run at all is `skip` rather than a zero-exit pass.
|
|
@@ -18,6 +18,9 @@ export interface HealthCommandResult {
|
|
|
18
18
|
/** Absent on a record written before the suite joined the check, which ran
|
|
19
19
|
* statics only. A test red is judged, owed and repaired differently. */
|
|
20
20
|
kind?: 'static' | 'test';
|
|
21
|
+
/** Why nothing was observed, on a `skip`. The differential reads it: a runner
|
|
22
|
+
* that found no tests is a gap alone and a regression against a suite. */
|
|
23
|
+
gap?: CommandGapId;
|
|
21
24
|
/** This command's own captured output, on a `fail` only. */
|
|
22
25
|
output?: string;
|
|
23
26
|
}
|
|
@@ -125,9 +125,16 @@ export function discoverHealthCommands(cwd) {
|
|
|
125
125
|
}
|
|
126
126
|
return { ecosystem: null, cmds: [] };
|
|
127
127
|
}
|
|
128
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* `test:watch`, `jest --watchAll`, `vitest watch`, `bun test --watch`.
|
|
130
|
+
*
|
|
131
|
+
* The flag is read by its VALUE, not its presence: `--watchAll=false` is how a CI
|
|
132
|
+
* script turns watch off, and excluding it drops the only `test` script such a
|
|
133
|
+
* repo has.
|
|
134
|
+
*/
|
|
129
135
|
function isWatchScript(name, body) {
|
|
130
|
-
return (/watch/i.test(name)
|
|
136
|
+
return (/watch/i.test(name)
|
|
137
|
+
|| /(?:^|\s)--watch(?:All)?(?:=(?:true|1))?(?=\s|$)|(?:^|\s)watch(?=\s|$)/.test(body));
|
|
131
138
|
}
|
|
132
139
|
/**
|
|
133
140
|
* The project's OWN test commands, in the order the run-end gate runs them. One
|
|
@@ -242,7 +249,8 @@ export async function runRepoHealthCheck(cwd, opts = {}) {
|
|
|
242
249
|
cmd,
|
|
243
250
|
outcome: passed ? 'pass' : 'skip',
|
|
244
251
|
exitCode: passed ? 0 : null,
|
|
245
|
-
kind
|
|
252
|
+
kind,
|
|
253
|
+
...(verdict.outcome === 'gap' ? { gap: verdict.gap } : {})
|
|
246
254
|
});
|
|
247
255
|
continue;
|
|
248
256
|
}
|
|
@@ -275,7 +283,9 @@ export async function runRepoHealthCheck(cwd, opts = {}) {
|
|
|
275
283
|
/** "`bun run lint` exited 1; `bun run test` exited 1" — every failing command. */
|
|
276
284
|
export function describeHealthFailures(commands) {
|
|
277
285
|
return commands
|
|
278
|
-
.filter(c => c.outcome === 'fail')
|
|
279
|
-
.map(c =>
|
|
286
|
+
.filter(c => c.outcome === 'fail' || c.gap === 'empty-suite')
|
|
287
|
+
.map(c => c.outcome === 'fail' ?
|
|
288
|
+
`\`${c.cmd}\` exited ${c.exitCode}`
|
|
289
|
+
: `\`${c.cmd}\` found no tests to run`)
|
|
280
290
|
.join('; ');
|
|
281
291
|
}
|
package/dist/task/verify-work.js
CHANGED
|
@@ -918,7 +918,12 @@ export async function runWorkVerification(deps) {
|
|
|
918
918
|
if (deps.repoHealth) {
|
|
919
919
|
stage('repo health');
|
|
920
920
|
const h = await deps.repoHealth();
|
|
921
|
-
|
|
921
|
+
// `ok` is not the whole verdict: a suite this tree no longer finds observes
|
|
922
|
+
// nothing, so nothing fails, and only the differential sees it went away.
|
|
923
|
+
// Establishing a baseline can cost a worktree health run, so it is asked for
|
|
924
|
+
// only in the two shapes it can speak to.
|
|
925
|
+
const suiteGone = (h.commands ?? []).some(c => c.gap === 'empty-suite');
|
|
926
|
+
if (!h.ok || suiteGone) {
|
|
922
927
|
const baseline = deps.healthBaseline ? await deps.healthBaseline() : null;
|
|
923
928
|
const before = baseline?.outcome ?? null;
|
|
924
929
|
if (classifyHealthDelta(before, h) === 'regressed') {
|
|
@@ -938,15 +943,22 @@ export async function runWorkVerification(deps) {
|
|
|
938
943
|
ok: false,
|
|
939
944
|
failClass,
|
|
940
945
|
reason: `${VERIFY_FAIL_PREFIX[failClass]} ${describeHealthFailures(regressed)}`,
|
|
941
|
-
health: {
|
|
946
|
+
health: {
|
|
947
|
+
...h,
|
|
948
|
+
ok: false,
|
|
949
|
+
commands: regressed,
|
|
950
|
+
output: regressed[0].output ?? h.output
|
|
951
|
+
}
|
|
942
952
|
};
|
|
943
953
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
954
|
+
if (!h.ok) {
|
|
955
|
+
pre.repoHealth = inheritedHealthFindings(h);
|
|
956
|
+
const failing = h.commands?.filter(c => c.outcome === 'fail') ?? [];
|
|
957
|
+
inheritedHealth =
|
|
958
|
+
failing.length > 0 ?
|
|
959
|
+
`${VERIFY_FAIL_PREFIX[healthFailClass(failing)]} ${describeHealthFailures(failing)} — already failing before this task`
|
|
960
|
+
: `repo health: ${h.reason} — already failing before this task`;
|
|
961
|
+
}
|
|
950
962
|
}
|
|
951
963
|
}
|
|
952
964
|
const inherited = inheritedHealth === undefined ? {} : { inheritedHealth };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.5",
|
|
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",
|