@mjasnikovs/pi-task 0.42.3 → 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.d.ts +3 -2
- package/dist/task/accept-debt.js +50 -14
- package/dist/task/auto-orchestrator.js +8 -6
- package/dist/task/command-run.d.ts +12 -1
- package/dist/task/command-run.js +13 -0
- package/dist/task/deferred-breakage.d.ts +19 -0
- package/dist/task/deferred-breakage.js +98 -22
- package/dist/task/gate-deps.d.ts +11 -0
- package/dist/task/gate-deps.js +49 -14
- package/dist/task/gate-resolution.js +1 -0
- package/dist/task/health-baseline.d.ts +21 -4
- package/dist/task/health-baseline.js +41 -9
- package/dist/task/health-repair.d.ts +19 -4
- package/dist/task/health-repair.js +18 -6
- package/dist/task/phases.js +75 -75
- package/dist/task/repo-health-check.d.ts +21 -9
- package/dist/task/repo-health-check.js +49 -12
- package/dist/task/run-context.d.ts +14 -0
- package/dist/task/run-context.js +29 -2
- package/dist/task/verify-work.d.ts +7 -1
- package/dist/task/verify-work.js +55 -13
- package/dist/task/yolo.d.ts +8 -4
- package/dist/task/yolo.js +17 -9
- package/package.json +1 -1
|
@@ -16,8 +16,13 @@
|
|
|
16
16
|
* the dedup ledger — a title covering the same command, or any of the same files,
|
|
17
17
|
* means no second entry, checked-off ones included, which is what stops a repair
|
|
18
18
|
* that failed from being re-spawned.
|
|
19
|
+
*
|
|
20
|
+
* A red TEST command is repaired only when a task's regression of it is on the
|
|
21
|
+
* debt ledger. A suite can also be red because a database is not up here, or
|
|
22
|
+
* because its script is a placeholder `exit 1`, and no repair task can fix either.
|
|
19
23
|
*/
|
|
20
24
|
import type { HealthSignal } from './health-baseline.js';
|
|
25
|
+
import type { HealthCommandResult } from './repo-health-check.js';
|
|
21
26
|
/** The failing check, and what its output named. */
|
|
22
27
|
export interface HealthRed {
|
|
23
28
|
command: string;
|
|
@@ -31,13 +36,23 @@ export interface HealthRedOwners {
|
|
|
31
36
|
owners: string[];
|
|
32
37
|
}
|
|
33
38
|
/**
|
|
34
|
-
* What a red health result is about
|
|
35
|
-
*
|
|
36
|
-
*
|
|
39
|
+
* What a red health result is about: its first failing command that `mayRepair`
|
|
40
|
+
* admits. Null when there is none — a legacy baseline, a signal with no
|
|
41
|
+
* per-command detail, or only reds no repair can fix — so nothing to pin to.
|
|
37
42
|
*/
|
|
38
43
|
export declare function healthRedSubject(health: HealthSignal & {
|
|
39
44
|
output?: string;
|
|
40
|
-
}, cwd: string, tracked: readonly string[] | null): HealthRed | null;
|
|
45
|
+
}, cwd: string, tracked: readonly string[] | null, mayRepair?: (c: HealthCommandResult) => boolean): HealthRed | null;
|
|
46
|
+
/**
|
|
47
|
+
* Is a red TEST command owed? True when an open debt records a task's regression
|
|
48
|
+
* of it — an accepted `test suite:` FAIL naming the command. An inherited-health
|
|
49
|
+
* debt does not count: every task in a run whose suite needs a missing database
|
|
50
|
+
* records one.
|
|
51
|
+
*/
|
|
52
|
+
export declare function suiteRegressionOwed(cmd: string, openDebts: readonly {
|
|
53
|
+
reason: string;
|
|
54
|
+
origin?: string;
|
|
55
|
+
}[]): boolean;
|
|
41
56
|
/**
|
|
42
57
|
* The plan title, in one of two fixed shapes the parser below recovers:
|
|
43
58
|
* `repair src/a.ts, src/b.ts: \`bun run lint\` exits 1 (introduced by TASK_0033)`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseRepairTitleFile } from './root-cause-repair.js';
|
|
2
|
+
import { failClassOfReason } from './verify-work.js';
|
|
2
3
|
/** A path-like token: at least one directory separator, ending in a file name. */
|
|
3
4
|
const PATH_TOKEN_RE = /(?:[\w.@-]+[\\/])+[\w.@-]+\.\w+/g;
|
|
4
5
|
function normalisePath(p) {
|
|
@@ -22,17 +23,17 @@ function resolveTracked(token, cwd, tracked) {
|
|
|
22
23
|
return bySuffix.length === 1 ? bySuffix[0] : null;
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
|
-
* What a red health result is about
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* What a red health result is about: its first failing command that `mayRepair`
|
|
27
|
+
* admits. Null when there is none — a legacy baseline, a signal with no
|
|
28
|
+
* per-command detail, or only reds no repair can fix — so nothing to pin to.
|
|
28
29
|
*/
|
|
29
|
-
export function healthRedSubject(health, cwd, tracked) {
|
|
30
|
-
const failing = (health.commands ?? []).find(c => c.outcome === 'fail');
|
|
30
|
+
export function healthRedSubject(health, cwd, tracked, mayRepair = () => true) {
|
|
31
|
+
const failing = (health.commands ?? []).find(c => c.outcome === 'fail' && mayRepair(c));
|
|
31
32
|
if (!failing)
|
|
32
33
|
return null;
|
|
33
34
|
const files = [];
|
|
34
35
|
if (tracked) {
|
|
35
|
-
for (const m of (health.output ?? '').matchAll(PATH_TOKEN_RE)) {
|
|
36
|
+
for (const m of (failing.output ?? health.output ?? '').matchAll(PATH_TOKEN_RE)) {
|
|
36
37
|
const rel = resolveTracked(m[0], cwd, tracked);
|
|
37
38
|
if (rel !== null && !files.includes(rel))
|
|
38
39
|
files.push(rel);
|
|
@@ -40,6 +41,17 @@ export function healthRedSubject(health, cwd, tracked) {
|
|
|
40
41
|
}
|
|
41
42
|
return { command: failing.cmd, exitCode: failing.exitCode, files };
|
|
42
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Is a red TEST command owed? True when an open debt records a task's regression
|
|
46
|
+
* of it — an accepted `test suite:` FAIL naming the command. An inherited-health
|
|
47
|
+
* debt does not count: every task in a run whose suite needs a missing database
|
|
48
|
+
* records one.
|
|
49
|
+
*/
|
|
50
|
+
export function suiteRegressionOwed(cmd, openDebts) {
|
|
51
|
+
return openDebts.some(d => d.origin !== 'inherited-health'
|
|
52
|
+
&& failClassOfReason(d.reason) === 'test-suite'
|
|
53
|
+
&& d.reason.includes(`\`${cmd}\``));
|
|
54
|
+
}
|
|
43
55
|
// ─── Plan entry ──────────────────────────────────────────────────────────────
|
|
44
56
|
/**
|
|
45
57
|
* The plan title, in one of two fixed shapes the parser below recovers:
|
package/dist/task/phases.js
CHANGED
|
@@ -673,6 +673,55 @@ export async function phaseResearch(deps, refined, rawPrompt = '') {
|
|
|
673
673
|
}
|
|
674
674
|
return sections.map(({ name, text }) => `${name}\n${text}`).join('\n\n');
|
|
675
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Hold an answer to every guard. Each guard re-asks ONCE, and the answer that
|
|
678
|
+
* comes back faces every guard again: one that fixed a deferral by inventing an
|
|
679
|
+
* API is still caught. An answer that trips a guard it was already re-asked for,
|
|
680
|
+
* or a re-ask that produced no tagged answer, is surfaced as an unknown carrying
|
|
681
|
+
* that guard's reason. yolo.ts skips those, and a human sees them.
|
|
682
|
+
*/
|
|
683
|
+
async function guardAutoAnswer(deps, first, guards) {
|
|
684
|
+
const reasked = new Set();
|
|
685
|
+
let parsed = first;
|
|
686
|
+
while (parsed.kind === 'answered') {
|
|
687
|
+
const answer = parsed.text;
|
|
688
|
+
let tripped;
|
|
689
|
+
for (const g of guards) {
|
|
690
|
+
const prompt = g.reask(answer);
|
|
691
|
+
if (prompt !== null) {
|
|
692
|
+
tripped = { reason: g.reason, prompt };
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (!tripped)
|
|
697
|
+
return parsed;
|
|
698
|
+
const surfaced = {
|
|
699
|
+
kind: 'unknown',
|
|
700
|
+
suggested: answer,
|
|
701
|
+
raw: parsed.raw,
|
|
702
|
+
reason: tripped.reason
|
|
703
|
+
};
|
|
704
|
+
if (reasked.has(tripped.reason)) {
|
|
705
|
+
deps.logDebug?.(`grill-auto: ${tripped.reason} survived its re-ask — surfacing to user`);
|
|
706
|
+
return surfaced;
|
|
707
|
+
}
|
|
708
|
+
reasked.add(tripped.reason);
|
|
709
|
+
let again = null;
|
|
710
|
+
try {
|
|
711
|
+
const text = await runPhaseChild(deps, 'grill-auto', 'read', tripped.prompt);
|
|
712
|
+
if (autoAnswerHasTag(text))
|
|
713
|
+
again = parseAutoAnswer(text);
|
|
714
|
+
}
|
|
715
|
+
catch (e) {
|
|
716
|
+
if (isFatalChildCause(e))
|
|
717
|
+
throw e;
|
|
718
|
+
}
|
|
719
|
+
if (again === null)
|
|
720
|
+
return surfaced;
|
|
721
|
+
parsed = again;
|
|
722
|
+
}
|
|
723
|
+
return parsed;
|
|
724
|
+
}
|
|
676
725
|
export async function phaseAutoAnswer(deps, refined, research, question) {
|
|
677
726
|
const docsFocusedFn = deps.docsFocused ?? docsFocused;
|
|
678
727
|
const fetchFocusedFn = deps.fetchFocused ?? fetchFocused;
|
|
@@ -724,84 +773,35 @@ export async function phaseAutoAnswer(deps, refined, research, question) {
|
|
|
724
773
|
// otherwise a preamble line leaks out as the recommended answer.
|
|
725
774
|
text = await runPhaseChild(deps, 'grill-auto', 'read', prependHint(GRILL_AUTO_FORMAT_HINT, basePrompt));
|
|
726
775
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
let reasked = null;
|
|
743
|
-
try {
|
|
744
|
-
const text2 = await runPhaseChild(deps, 'grill-auto', 'read', prependHint(synthesizedApiReaskHint(synth, research), basePrompt));
|
|
745
|
-
if (autoAnswerHasTag(text2))
|
|
746
|
-
reasked = parseAutoAnswer(text2);
|
|
747
|
-
}
|
|
748
|
-
catch (e) {
|
|
749
|
-
if (isFatalChildCause(e))
|
|
750
|
-
throw e;
|
|
751
|
-
reasked = null;
|
|
776
|
+
const parsed = await guardAutoAnswer(deps, parseAutoAnswer(text), [
|
|
777
|
+
// Anti-synthesis: the auto-answer invented `Bun.mkdirSync` while
|
|
778
|
+
// research's APIS section carried the correct list, and the invention
|
|
779
|
+
// was promoted into requirements + VERIFY. An API-shaped identifier
|
|
780
|
+
// absent from the research AND the question, in a namespace the
|
|
781
|
+
// research claims to cover, is re-asked with the verified lines injected.
|
|
782
|
+
{
|
|
783
|
+
reason: 'api-synthesis',
|
|
784
|
+
reask: answer => {
|
|
785
|
+
const synth = findSynthesizedApis(answer, question, research);
|
|
786
|
+
if (synth.length === 0)
|
|
787
|
+
return null;
|
|
788
|
+
deps.logDebug?.('grill-auto: unverified API identifier(s) in answer — '
|
|
789
|
+
+ synth.map(f => f.identifier).join(', '));
|
|
790
|
+
return prependHint(synthesizedApiReaskHint(synth, research), basePrompt);
|
|
752
791
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
// Tagged so a call site can tell this producer from the other
|
|
764
|
-
// two: the suggestion is PROVEN to name an unverified API, so
|
|
765
|
-
// it may only be judged by a human (yolo.ts must not take it).
|
|
766
|
-
reason: 'api-synthesis'
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
else {
|
|
770
|
-
parsed = reasked;
|
|
792
|
+
},
|
|
793
|
+
// Behind the prompt's GREEN-SUITE CHECK: promoting "flag it for the test
|
|
794
|
+
// owner" is how mx5-n TASK_0004 turned the suite red for the rest of the run.
|
|
795
|
+
{
|
|
796
|
+
reason: 'deferred-breakage',
|
|
797
|
+
reask: answer => {
|
|
798
|
+
if (!defersBreakage(answer))
|
|
799
|
+
return null;
|
|
800
|
+
deps.logDebug?.('grill-auto: answer defers a breakage to a nonexistent owner');
|
|
801
|
+
return prependHint(deferredBreakageReaskHint(answer), basePrompt);
|
|
771
802
|
}
|
|
772
803
|
}
|
|
773
|
-
|
|
774
|
-
// Deterministic backstop behind the prompt's GREEN-SUITE CHECK: an answer
|
|
775
|
-
// that defers a breakage to "the test owner" gets ONE re-ask, and a second
|
|
776
|
-
// deferral is surfaced as an unsafe unknown — yolo.ts skips it, a human
|
|
777
|
-
// sees it. Promoting it is how mx5-n TASK_0004 turned the suite red for
|
|
778
|
-
// the rest of the run.
|
|
779
|
-
if (parsed.kind === 'answered' && defersBreakage(parsed.text)) {
|
|
780
|
-
deps.logDebug?.('grill-auto: answer defers a breakage to a nonexistent owner — re-asking once');
|
|
781
|
-
let reasked = null;
|
|
782
|
-
try {
|
|
783
|
-
const text2 = await runPhaseChild(deps, 'grill-auto', 'read', prependHint(deferredBreakageReaskHint(parsed.text), basePrompt));
|
|
784
|
-
if (autoAnswerHasTag(text2))
|
|
785
|
-
reasked = parseAutoAnswer(text2);
|
|
786
|
-
}
|
|
787
|
-
catch (e) {
|
|
788
|
-
if (isFatalChildCause(e))
|
|
789
|
-
throw e;
|
|
790
|
-
reasked = null;
|
|
791
|
-
}
|
|
792
|
-
if (reasked !== null && reasked.kind === 'answered' && !defersBreakage(reasked.text)) {
|
|
793
|
-
parsed = reasked;
|
|
794
|
-
}
|
|
795
|
-
else {
|
|
796
|
-
deps.logDebug?.('grill-auto: answer still defers the breakage — surfacing to user');
|
|
797
|
-
parsed = {
|
|
798
|
-
kind: 'unknown',
|
|
799
|
-
suggested: reasked?.kind === 'answered' ? reasked.text : parsed.text,
|
|
800
|
-
raw: (reasked ?? parsed).raw,
|
|
801
|
-
reason: 'deferred-breakage'
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
}
|
|
804
|
+
]);
|
|
805
805
|
// Surviving-unknown routing: an integration / build-wiring unknown whose
|
|
806
806
|
// wrong guess is a structural landmine must NOT be silently auto-answered.
|
|
807
807
|
// We first try to ground it from fetched docs (the enrichment fan-out
|
|
@@ -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.
|
|
@@ -15,20 +15,29 @@ export interface HealthCommandResult {
|
|
|
15
15
|
outcome: 'pass' | 'fail' | 'skip';
|
|
16
16
|
/** Real exit status on a `fail`; null when nothing conclusive ran. */
|
|
17
17
|
exitCode: number | null;
|
|
18
|
+
/** Absent on a record written before the suite joined the check, which ran
|
|
19
|
+
* statics only. A test red is judged, owed and repaired differently. */
|
|
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;
|
|
24
|
+
/** This command's own captured output, on a `fail` only. */
|
|
25
|
+
output?: string;
|
|
18
26
|
}
|
|
19
27
|
export interface HealthOutcome {
|
|
20
|
-
/** true → every discovered
|
|
21
|
-
* false → a discovered command actually ran and exited non-zero. */
|
|
28
|
+
/** true → every discovered check passed or could not run, or there was nothing
|
|
29
|
+
* to run. false → a discovered command actually ran and exited non-zero. */
|
|
22
30
|
ok: boolean;
|
|
23
|
-
/** Human-readable reason. On a fail, names
|
|
31
|
+
/** Human-readable reason. On a fail, names every failing command and its exit code. */
|
|
24
32
|
reason: string;
|
|
25
33
|
/** Which manifest drove discovery, or null when none was found. */
|
|
26
34
|
ecosystem: string | null;
|
|
27
|
-
/** Every command
|
|
28
|
-
*
|
|
35
|
+
/** Every discovered command, in run order. A red one does not stop the run: a
|
|
36
|
+
* command it skipped would be absent from both sides of the differential, which
|
|
37
|
+
* then cannot see that command break. */
|
|
29
38
|
commands: HealthCommandResult[];
|
|
30
39
|
/**
|
|
31
|
-
* First lines of the failing command's combined stderr+stdout — captured so a
|
|
40
|
+
* First lines of the first failing command's combined stderr+stdout — captured so a
|
|
32
41
|
* FAIL is explainable from artifacts alone. The exit code alone does not say
|
|
33
42
|
* what happened: eslint exits 1 for findings and 2 when it could not run at
|
|
34
43
|
* all (a missing config, say), so "`bun run lint` exited 2" is unreproducible
|
|
@@ -65,7 +74,8 @@ export declare function discoverHealthCommands(cwd: string): {
|
|
|
65
74
|
* Every test-shaped script, not just the one literally named `test`: a project's
|
|
66
75
|
* only browser-executing suite is often `test:ct`, and looking for `test` alone
|
|
67
76
|
* never runs it. Plain `test` leads, then every `test:`/`test_`/`test-` name in
|
|
68
|
-
* declaration order (Array#sort is stable).
|
|
77
|
+
* declaration order (Array#sort is stable). A watch-mode script is left out: it
|
|
78
|
+
* never exits, so all it can add is a timeout.
|
|
69
79
|
*/
|
|
70
80
|
export declare function discoverTestCommands(cwd: string): {
|
|
71
81
|
ecosystem: string | null;
|
|
@@ -80,7 +90,7 @@ export type HealthProgress = (command: string) => void;
|
|
|
80
90
|
* - No manifest / no static command → ok (nothing can regress).
|
|
81
91
|
* - A command that CANNOT run (ENOENT / null exit / 127 inside the chain) → skipped,
|
|
82
92
|
* treated as an environment gap, not a fault.
|
|
83
|
-
* - A command that ran and exited non-zero →
|
|
93
|
+
* - A command that ran and exited non-zero → red. Every command still runs.
|
|
84
94
|
*
|
|
85
95
|
* This module owns DISCOVERY and its own output policy. Running a command and
|
|
86
96
|
* deciding what its ending MEANS is `command-run.ts`'s — one statement of the
|
|
@@ -105,3 +115,5 @@ export declare function runRepoHealthCheck(cwd: string, opts?: {
|
|
|
105
115
|
* that will judge the result DIFFERENTIALLY may set this — see the header. */
|
|
106
116
|
withTests?: boolean;
|
|
107
117
|
}): Promise<HealthOutcome>;
|
|
118
|
+
/** "`bun run lint` exited 1; `bun run test` exited 1" — every failing command. */
|
|
119
|
+
export declare function describeHealthFailures(commands: readonly HealthCommandResult[]): string;
|
|
@@ -125,6 +125,17 @@ export function discoverHealthCommands(cwd) {
|
|
|
125
125
|
}
|
|
126
126
|
return { ecosystem: null, cmds: [] };
|
|
127
127
|
}
|
|
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
|
+
*/
|
|
135
|
+
function isWatchScript(name, body) {
|
|
136
|
+
return (/watch/i.test(name)
|
|
137
|
+
|| /(?:^|\s)--watch(?:All)?(?:=(?:true|1))?(?=\s|$)|(?:^|\s)watch(?=\s|$)/.test(body));
|
|
138
|
+
}
|
|
128
139
|
/**
|
|
129
140
|
* The project's OWN test commands, in the order the run-end gate runs them. One
|
|
130
141
|
* statement for both gates: final-gate.ts appends `build` to this list for the
|
|
@@ -134,12 +145,13 @@ export function discoverHealthCommands(cwd) {
|
|
|
134
145
|
* Every test-shaped script, not just the one literally named `test`: a project's
|
|
135
146
|
* only browser-executing suite is often `test:ct`, and looking for `test` alone
|
|
136
147
|
* never runs it. Plain `test` leads, then every `test:`/`test_`/`test-` name in
|
|
137
|
-
* declaration order (Array#sort is stable).
|
|
148
|
+
* declaration order (Array#sort is stable). A watch-mode script is left out: it
|
|
149
|
+
* never exits, so all it can add is a timeout.
|
|
138
150
|
*/
|
|
139
151
|
export function discoverTestCommands(cwd) {
|
|
140
152
|
if (existsSync(path.join(cwd, 'package.json'))) {
|
|
141
153
|
const s = packageScripts(cwd);
|
|
142
|
-
const names = Object.keys(s).filter(n => n === 'test' || /^test[:_-]/.test(n));
|
|
154
|
+
const names = Object.keys(s).filter(n => (n === 'test' || /^test[:_-]/.test(n)) && !isWatchScript(n, s[n]));
|
|
143
155
|
names.sort((a, b) => a === 'test' ? -1
|
|
144
156
|
: b === 'test' ? 1
|
|
145
157
|
: 0);
|
|
@@ -178,7 +190,7 @@ function noCommandOutcome(ecosystem) {
|
|
|
178
190
|
* - No manifest / no static command → ok (nothing can regress).
|
|
179
191
|
* - A command that CANNOT run (ENOENT / null exit / 127 inside the chain) → skipped,
|
|
180
192
|
* treated as an environment gap, not a fault.
|
|
181
|
-
* - A command that ran and exited non-zero →
|
|
193
|
+
* - A command that ran and exited non-zero → red. Every command still runs.
|
|
182
194
|
*
|
|
183
195
|
* This module owns DISCOVERY and its own output policy. Running a command and
|
|
184
196
|
* deciding what its ending MEANS is `command-run.ts`'s — one statement of the
|
|
@@ -225,23 +237,39 @@ export async function runRepoHealthCheck(cwd, opts = {}) {
|
|
|
225
237
|
// ladder's `tail` keeps 400 characters, and that difference is real — a
|
|
226
238
|
// truncated lint report is unactionable. So the run is classified, not
|
|
227
239
|
// consumed: the verdict decides, the raw streams are what we show.
|
|
228
|
-
// `runtimeGap` only for a TEST command.
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
const verdict = classifyCommandRun(r, [], { runtimeGap: test });
|
|
240
|
+
// `runtimeGap` and `emptySuite` only for a TEST command. Both rows read the
|
|
241
|
+
// command's output, and on lint and typecheck a genuine report quoting
|
|
242
|
+
// "browsers are not installed" would skip the static check and certify
|
|
243
|
+
// the repo healthy.
|
|
244
|
+
const verdict = classifyCommandRun(r, [], { runtimeGap: test, emptySuite: test });
|
|
245
|
+
const kind = test ? 'test' : 'static';
|
|
233
246
|
if (verdict.outcome !== 'fail') {
|
|
234
247
|
const passed = verdict.outcome === 'pass';
|
|
235
|
-
commands.push({
|
|
248
|
+
commands.push({
|
|
249
|
+
cmd,
|
|
250
|
+
outcome: passed ? 'pass' : 'skip',
|
|
251
|
+
exitCode: passed ? 0 : null,
|
|
252
|
+
kind,
|
|
253
|
+
...(verdict.outcome === 'gap' ? { gap: verdict.gap } : {})
|
|
254
|
+
});
|
|
236
255
|
continue;
|
|
237
256
|
}
|
|
238
|
-
commands.push({
|
|
257
|
+
commands.push({
|
|
258
|
+
cmd,
|
|
259
|
+
outcome: 'fail',
|
|
260
|
+
exitCode: verdict.status,
|
|
261
|
+
kind,
|
|
262
|
+
output: captureHealthOutput(r.stdout, r.stderr)
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const firstFail = commands.find(c => c.outcome === 'fail');
|
|
266
|
+
if (firstFail) {
|
|
239
267
|
return {
|
|
240
268
|
ok: false,
|
|
241
|
-
reason:
|
|
269
|
+
reason: describeHealthFailures(commands),
|
|
242
270
|
ecosystem,
|
|
243
271
|
commands,
|
|
244
|
-
output:
|
|
272
|
+
output: firstFail.output ?? ''
|
|
245
273
|
};
|
|
246
274
|
}
|
|
247
275
|
return {
|
|
@@ -252,3 +280,12 @@ export async function runRepoHealthCheck(cwd, opts = {}) {
|
|
|
252
280
|
output: ''
|
|
253
281
|
};
|
|
254
282
|
}
|
|
283
|
+
/** "`bun run lint` exited 1; `bun run test` exited 1" — every failing command. */
|
|
284
|
+
export function describeHealthFailures(commands) {
|
|
285
|
+
return commands
|
|
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`)
|
|
290
|
+
.join('; ');
|
|
291
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { SpawnFn } from '../shared/child-process.js';
|
|
|
2
2
|
import { type EcosystemId } from '../workers/docs-ecosystems.js';
|
|
3
3
|
import type { GateEvidence } from './gate-evidence.js';
|
|
4
4
|
import { type OrientationResult } from './orientation.js';
|
|
5
|
+
import { type HealthOutcome } from './repo-health-check.js';
|
|
5
6
|
/**
|
|
6
7
|
* What a verified command is FOR, and the only column that decides whether the
|
|
7
8
|
* gate-evidence runner may execute it: `check` and `build` terminate on their own,
|
|
@@ -79,6 +80,8 @@ export declare class RunContext {
|
|
|
79
80
|
private _toolingHash;
|
|
80
81
|
private _evidence;
|
|
81
82
|
private _evidenceQueue;
|
|
83
|
+
private _health;
|
|
84
|
+
private _healthQueue;
|
|
82
85
|
constructor(opts: RunContextOptions);
|
|
83
86
|
/** `git ls-files` for this run; '' outside a git tree (see file-inventory.ts). */
|
|
84
87
|
inventory(): Promise<string>;
|
|
@@ -134,6 +137,17 @@ export declare class RunContext {
|
|
|
134
137
|
*/
|
|
135
138
|
gateEvidenceFor(produce: EvidenceRunner): Promise<GateEvidence>;
|
|
136
139
|
private freshEvidence;
|
|
140
|
+
/**
|
|
141
|
+
* The repo-health check with its suite, at most once per tree, on the same terms
|
|
142
|
+
* as {@link gateEvidenceFor}. A verify, the enforce baseline on the commit it
|
|
143
|
+
* just judged and the next task's checkpoint all measure one tree; each running
|
|
144
|
+
* the whole suite again is the cost this removes.
|
|
145
|
+
*
|
|
146
|
+
* Stored under the tree the check LEFT, not the one it found: a `--fix` lint
|
|
147
|
+
* moves the tree, and the result describes the fixed one.
|
|
148
|
+
*/
|
|
149
|
+
healthFor(produce: () => Promise<HealthOutcome>): Promise<HealthOutcome>;
|
|
150
|
+
private freshHealth;
|
|
137
151
|
}
|
|
138
152
|
/** Open a run: every task inside it now shares one context. Nests — an inner
|
|
139
153
|
* bracket returns the outer run's context untouched. */
|
package/dist/task/run-context.js
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* run found it and is not re-read: a mid-run inventory refresh would hand two
|
|
20
20
|
* tasks different orientation cores for the same question.
|
|
21
21
|
*
|
|
22
|
-
* TREE HASH is the other half, used by the gate-evidence
|
|
23
|
-
* implementation lives in tree-hash.ts.
|
|
22
|
+
* TREE HASH is the other half, used by the gate-evidence and repo-health caches;
|
|
23
|
+
* its one implementation lives in tree-hash.ts.
|
|
24
24
|
*/
|
|
25
25
|
import { createHash } from 'node:crypto';
|
|
26
26
|
import * as fsp from 'node:fs/promises';
|
|
@@ -90,6 +90,8 @@ export class RunContext {
|
|
|
90
90
|
_toolingHash;
|
|
91
91
|
_evidence;
|
|
92
92
|
_evidenceQueue = Promise.resolve();
|
|
93
|
+
_health;
|
|
94
|
+
_healthQueue = Promise.resolve();
|
|
93
95
|
constructor(opts) {
|
|
94
96
|
this.cwd = opts.cwd;
|
|
95
97
|
this.runId = opts.runId ?? newRunToken();
|
|
@@ -238,6 +240,31 @@ export class RunContext {
|
|
|
238
240
|
this._evidence = { hash, value };
|
|
239
241
|
return value;
|
|
240
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* The repo-health check with its suite, at most once per tree, on the same terms
|
|
245
|
+
* as {@link gateEvidenceFor}. A verify, the enforce baseline on the commit it
|
|
246
|
+
* just judged and the next task's checkpoint all measure one tree; each running
|
|
247
|
+
* the whole suite again is the cost this removes.
|
|
248
|
+
*
|
|
249
|
+
* Stored under the tree the check LEFT, not the one it found: a `--fix` lint
|
|
250
|
+
* moves the tree, and the result describes the fixed one.
|
|
251
|
+
*/
|
|
252
|
+
healthFor(produce) {
|
|
253
|
+
const next = this._healthQueue.then(() => this.freshHealth(produce));
|
|
254
|
+
this._healthQueue = next.catch(() => { });
|
|
255
|
+
return next;
|
|
256
|
+
}
|
|
257
|
+
async freshHealth(produce) {
|
|
258
|
+
const opts = this._signal ? { signal: this._signal } : {};
|
|
259
|
+
const found = await treeHash(this.cwd, opts);
|
|
260
|
+
if (found !== null && this._health?.hash === found)
|
|
261
|
+
return this._health.value;
|
|
262
|
+
const value = await produce();
|
|
263
|
+
const left = await treeHash(this.cwd, opts);
|
|
264
|
+
if (left !== null)
|
|
265
|
+
this._health = { hash: left, value };
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
241
268
|
}
|
|
242
269
|
/**
|
|
243
270
|
* The context of the run that owns the session right now, set by the run bracket.
|
|
@@ -90,8 +90,12 @@ export type VerifyOutcome = VerifyPass | VerifyFail;
|
|
|
90
90
|
* `static-checks` is the RUN-level twin of `repo-health`: final-gate.ts mints
|
|
91
91
|
* `VERIFY_FAIL_PREFIX['static-checks']` for the same concept at the other
|
|
92
92
|
* altitude, and `isStaticClass` answers true for both.
|
|
93
|
+
*
|
|
94
|
+
* `test-suite` is the same deterministic check when a TEST command regressed. It
|
|
95
|
+
* is its own class because a passing lint proves nothing about a suite: a static
|
|
96
|
+
* debt closes when the statics pass, and a lint fix cannot green a test.
|
|
93
97
|
*/
|
|
94
|
-
export type VerifyFailClass = 'repo-health' | 'static-checks' | 'unobserved' | 'model-verdict' | 'harness-fault';
|
|
98
|
+
export type VerifyFailClass = 'repo-health' | 'static-checks' | 'test-suite' | 'unobserved' | 'model-verdict' | 'harness-fault';
|
|
95
99
|
/**
|
|
96
100
|
* The prefix each class MINTS, stated once.
|
|
97
101
|
*
|
|
@@ -117,6 +121,8 @@ export declare function verifyFailClass(o: {
|
|
|
117
121
|
export declare function failClassOfReason(reason: string): VerifyFailClass | undefined;
|
|
118
122
|
/** Does this class name a deterministic whole-repo static check, at either altitude? */
|
|
119
123
|
export declare function isStaticClass(cls: VerifyFailClass | undefined): boolean;
|
|
124
|
+
/** Does this class name the deterministic whole-repo check, suite included? */
|
|
125
|
+
export declare function isHealthClass(cls: VerifyFailClass | undefined): boolean;
|
|
120
126
|
/**
|
|
121
127
|
* The delivered spec's TEXT, for the children that must read its prose verbatim.
|
|
122
128
|
* The slicing itself lives in spec-model.ts beside the parser, so "the spec
|