@mjasnikovs/pi-task 0.42.3 → 0.42.4
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 +5 -4
- 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 +20 -0
- package/dist/task/deferred-breakage.js +84 -22
- package/dist/task/gate-deps.js +33 -14
- package/dist/task/gate-resolution.js +1 -0
- package/dist/task/health-baseline.d.ts +11 -4
- package/dist/task/health-baseline.js +21 -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 +17 -8
- package/dist/task/repo-health-check.js +39 -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 +41 -11
- package/dist/task/yolo.d.ts +8 -4
- package/dist/task/yolo.js +17 -9
- package/package.json +1 -1
|
@@ -155,11 +155,12 @@ export declare function writeAcceptDebts(cwd: string, debts: AcceptDebt[]): Prom
|
|
|
155
155
|
/** The debts nothing has closed yet — the only ones a re-check may read. */
|
|
156
156
|
export declare function readOpenAcceptDebts(cwd: string): Promise<AcceptDebt[]>;
|
|
157
157
|
/**
|
|
158
|
-
* Close every open
|
|
158
|
+
* Close every open health-class debt that names `command`, stamping the task
|
|
159
159
|
* whose verified work made that check pass again. Returns the debts closed.
|
|
160
160
|
* A reason that quotes the command is the whole match: the health-check reason
|
|
161
161
|
* (`repo health: \`bun run lint\` exited 1`) and its inherited form both do,
|
|
162
|
-
* and nothing else in the ledger quotes a health command.
|
|
162
|
+
* and nothing else in the ledger quotes a health command. A suite debt closes
|
|
163
|
+
* here too: the repair verified clean, and that check runs the suite. Best-effort.
|
|
163
164
|
*/
|
|
164
165
|
export declare function closeHealthDebts(cwd: string, command: string, resolvedBy: string): Promise<AcceptDebt[]>;
|
|
165
166
|
/**
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -35,7 +35,7 @@ import { existsSync } from 'node:fs';
|
|
|
35
35
|
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
|
-
import { failClassOfReason, isStaticClass } from './verify-work.js';
|
|
38
|
+
import { failClassOfReason, isHealthClass, isStaticClass } from './verify-work.js';
|
|
39
39
|
import { taskThatIntroduced } from './task-provenance.js';
|
|
40
40
|
import { makeLedger } from './ledger.js';
|
|
41
41
|
import { parseVerifyBlockStrict } from './spec-validation.js';
|
|
@@ -251,18 +251,19 @@ export async function readOpenAcceptDebts(cwd) {
|
|
|
251
251
|
return (await readAcceptDebts(cwd)).filter(d => d.resolvedBy === undefined);
|
|
252
252
|
}
|
|
253
253
|
/**
|
|
254
|
-
* Close every open
|
|
254
|
+
* Close every open health-class debt that names `command`, stamping the task
|
|
255
255
|
* whose verified work made that check pass again. Returns the debts closed.
|
|
256
256
|
* A reason that quotes the command is the whole match: the health-check reason
|
|
257
257
|
* (`repo health: \`bun run lint\` exited 1`) and its inherited form both do,
|
|
258
|
-
* and nothing else in the ledger quotes a health command.
|
|
258
|
+
* and nothing else in the ledger quotes a health command. A suite debt closes
|
|
259
|
+
* here too: the repair verified clean, and that check runs the suite. Best-effort.
|
|
259
260
|
*/
|
|
260
261
|
export async function closeHealthDebts(cwd, command, resolvedBy) {
|
|
261
262
|
try {
|
|
262
263
|
const all = await readAcceptDebts(cwd);
|
|
263
264
|
const quoted = `\`${command}\``;
|
|
264
265
|
const closing = all.filter(d => d.resolvedBy === undefined
|
|
265
|
-
&&
|
|
266
|
+
&& isHealthClass(failClassOfReason(d.reason))
|
|
266
267
|
&& d.reason.includes(quoted));
|
|
267
268
|
if (closing.length === 0)
|
|
268
269
|
return [];
|
|
@@ -20,7 +20,7 @@ import { GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT } from './prompts.js';
|
|
|
20
20
|
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, UNNAMED_COVERAGE_GAP, parseTaskList, planKeyAt, checkOffTask, stampTaskInProgress, beginTaskAttempt, recordTaskEnd, insertTaskAfter, insertTaskBefore, findResumableAutoDetailed } from './auto-io.js';
|
|
21
21
|
import { decideResume, UNATTENDED_STATES } from './resume-gap.js';
|
|
22
22
|
import { ENTRY_ATTEMPT_BUDGET } from './gate-resolution.js';
|
|
23
|
-
import { recordDebt } from './accept-debt.js';
|
|
23
|
+
import { readOpenAcceptDebts, recordDebt } from './accept-debt.js';
|
|
24
24
|
import { drainRepairQueue, mergeRepairCandidates, planHasRepairFor, parseRepairTitleFile, buildRepairTitle, buildRepairScopeFence, extractFailingCommand } from './root-cause-repair.js';
|
|
25
25
|
import { writeTaskFile, readTaskFile, readSection, updateTaskFrontMatter, taskFilePath } from './task-io.js';
|
|
26
26
|
// Re-exported as well as used: the @-mention helpers moved to their own module so
|
|
@@ -38,7 +38,7 @@ import { getParentContextWindow } from './context-usage.js';
|
|
|
38
38
|
import { ChildStatus, runPlanningChild, statusCallbacks } from './child-status.js';
|
|
39
39
|
import { buildGateDeps, collectTreeChanges } from './gate-deps.js';
|
|
40
40
|
import { runGatesForTask } from './task-gates.js';
|
|
41
|
-
import { buildHealthRepairFence, buildHealthRepairTitle, healthRedSubject, parseHealthRepairTitle, planCoversHealthRed } from './health-repair.js';
|
|
41
|
+
import { buildHealthRepairFence, buildHealthRepairTitle, healthRedSubject, suiteRegressionOwed, parseHealthRepairTitle, planCoversHealthRed } from './health-repair.js';
|
|
42
42
|
import { HEALTH_BASELINE_SECTION, parseHealthBaseline } from './health-baseline.js';
|
|
43
43
|
import { runFinalGateStage } from './run-final-gate.js';
|
|
44
44
|
import { gitUnmergedPaths, gitStashRef } from './auto-commit.js';
|
|
@@ -392,7 +392,8 @@ async function schedulePendingRepairs(cwd, id, afterIndex, ctx, deps) {
|
|
|
392
392
|
*/
|
|
393
393
|
async function spliceHealthRepair(cwd, id, next, entries, health, ctx, deps) {
|
|
394
394
|
try {
|
|
395
|
-
const
|
|
395
|
+
const debts = await readOpenAcceptDebts(cwd);
|
|
396
|
+
const red = healthRedSubject(health, cwd, (await deps.repoFiles?.(cwd)) ?? null, c => c.kind !== 'test' || suiteRegressionOwed(c.cmd, debts));
|
|
396
397
|
if (!red)
|
|
397
398
|
return false;
|
|
398
399
|
if (planCoversHealthRed(entries.map(e => e.title), red))
|
|
@@ -609,8 +610,9 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
609
610
|
}
|
|
610
611
|
// YOLO: take the recommended option (index 0 / the green card) without ever
|
|
611
612
|
// building the prompt. Clarify has no anti-synthesis channel — it runs before
|
|
612
|
-
// any research — so the
|
|
613
|
-
// recommendation to take
|
|
613
|
+
// any research — so the step-asides here are a question with no
|
|
614
|
+
// recommendation to take, and one whose every option defers a breakage the
|
|
615
|
+
// triage just refused; each is skipped rather than guessed.
|
|
614
616
|
const outcome = await settleQuestion({
|
|
615
617
|
ui,
|
|
616
618
|
transcript,
|
|
@@ -1366,7 +1368,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1366
1368
|
notifyRun(active, `${id}: checkpointed uncommitted work before "${next.title}".`, 'info');
|
|
1367
1369
|
}
|
|
1368
1370
|
// REPO-HEALTH BASELINE, taken here because the checkpoint above just made
|
|
1369
|
-
// the tree clean: what the project's own
|
|
1371
|
+
// the tree clean: what the project's own checks say now is what this task
|
|
1370
1372
|
// INHERITED, and the verify gate attributes a red check against it instead
|
|
1371
1373
|
// of failing the task for a sibling's defect (health-baseline.ts). The
|
|
1372
1374
|
// inner task file does not exist yet, so the result is handed to the
|
|
@@ -109,8 +109,14 @@ export declare const ENV_GAP_OUTPUT_RE: RegExp;
|
|
|
109
109
|
* `gapPatterns`, which is why that parameter exists rather than a boolean.
|
|
110
110
|
*/
|
|
111
111
|
export declare const INFRA_GAP_OUTPUT_RE: RegExp;
|
|
112
|
+
/**
|
|
113
|
+
* A test runner that found no tests to run: bun, jest, vitest, mocha and pytest
|
|
114
|
+
* each exit non-zero on it. A suite that ran nothing observed nothing, which is the
|
|
115
|
+
* contract's definition of a gap. Test commands only (see `emptySuite`).
|
|
116
|
+
*/
|
|
117
|
+
export declare const EMPTY_SUITE_OUTPUT_RE: RegExp;
|
|
112
118
|
/** Which way a command failed to tell us anything. */
|
|
113
|
-
export type CommandGapId = 'spawn-failed' | 'killed' | 'command-not-found' | 'missing-runtime' | 'infrastructure';
|
|
119
|
+
export type CommandGapId = 'spawn-failed' | 'killed' | 'command-not-found' | 'missing-runtime' | 'infrastructure' | 'empty-suite';
|
|
114
120
|
export type CommandVerdict =
|
|
115
121
|
/** Nothing was observed. Never fails a gate, never closes a debt. */
|
|
116
122
|
{
|
|
@@ -145,6 +151,11 @@ export interface ClassifyOptions {
|
|
|
145
151
|
* and tell the gate the repo is healthy.
|
|
146
152
|
*/
|
|
147
153
|
runtimeGap?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* May this command's output claim it found NO TESTS? False by default: the
|
|
156
|
+
* wording is only a gap when a test runner printed it.
|
|
157
|
+
*/
|
|
158
|
+
emptySuite?: boolean;
|
|
148
159
|
}
|
|
149
160
|
/**
|
|
150
161
|
* Decide what one finished command proved. Pure — no spawning, no filesystem, no
|
package/dist/task/command-run.js
CHANGED
|
@@ -233,6 +233,12 @@ export const ENV_GAP_OUTPUT_RE = /Executable doesn't exist|playwright install|br
|
|
|
233
233
|
* `gapPatterns`, which is why that parameter exists rather than a boolean.
|
|
234
234
|
*/
|
|
235
235
|
export const INFRA_GAP_OUTPUT_RE = /ECONNREFUSED|connection refused|ENOTFOUND|EAI_AGAIN|is the server running|could not connect|cannot connect to the docker daemon|connect: connection|no such host/i;
|
|
236
|
+
/**
|
|
237
|
+
* A test runner that found no tests to run: bun, jest, vitest, mocha and pytest
|
|
238
|
+
* each exit non-zero on it. A suite that ran nothing observed nothing, which is the
|
|
239
|
+
* contract's definition of a gap. Test commands only (see `emptySuite`).
|
|
240
|
+
*/
|
|
241
|
+
export const EMPTY_SUITE_OUTPUT_RE = /\b0 test files matching\b|\bNo tests found\b|\bNo test files found\b|\bno tests ran\b|\bcollected 0 items\b/i;
|
|
236
242
|
/**
|
|
237
243
|
* The gap ladder, in order. FIRST MATCH WINS.
|
|
238
244
|
*
|
|
@@ -268,6 +274,11 @@ const GAP_RULES = [
|
|
|
268
274
|
id: 'infrastructure',
|
|
269
275
|
detail: () => 'external infrastructure unreachable',
|
|
270
276
|
applies: (_run, output, gapPatterns) => gapPatterns.some(re => re.test(output))
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'empty-suite',
|
|
280
|
+
detail: () => 'no tests found',
|
|
281
|
+
applies: (_run, output) => EMPTY_SUITE_OUTPUT_RE.test(output)
|
|
271
282
|
}
|
|
272
283
|
];
|
|
273
284
|
/** Last ~`limit` chars of the command's combined output, one line, for the reason. */
|
|
@@ -296,6 +307,8 @@ export function classifyCommandRun(run, gapPatterns = [], opts = {}) {
|
|
|
296
307
|
for (const rule of GAP_RULES) {
|
|
297
308
|
if (rule.id === 'missing-runtime' && !runtimeGap)
|
|
298
309
|
continue;
|
|
310
|
+
if (rule.id === 'empty-suite' && opts.emptySuite !== true)
|
|
311
|
+
continue;
|
|
299
312
|
if (rule.applies(run, output, gapPatterns)) {
|
|
300
313
|
return { outcome: 'gap', gap: rule.id, detail: rule.detail(run) };
|
|
301
314
|
}
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does an auto-answer hand a breakage to someone who does not exist?
|
|
3
|
+
*
|
|
4
|
+
* MEASURED (mx5-n TASK_0004, 2026-09-17): "flag the test/migrate.test.ts breakage
|
|
5
|
+
* as a known issue for the test owner". Nothing in a /task-auto run owns a test:
|
|
6
|
+
* the answer was stamped `(auto)`, verify passed the task with the suite red, and
|
|
7
|
+
* four tasks later an unsatisfiable spec looped until the runaway guard fired.
|
|
8
|
+
*
|
|
9
|
+
* This is the deterministic backstop behind the prompt's GREEN-SUITE CHECK: a
|
|
10
|
+
* model that ignores the rule still cannot promote a deferral into a decision.
|
|
11
|
+
* The phrases are the ones a model reaches for when it wants to defer, not the
|
|
12
|
+
* word "test" — "add a test later" is a plan, not a deferral.
|
|
13
|
+
*
|
|
14
|
+
* SCOPE IS GRAMMATICAL, never a character count. A negation or a conditional
|
|
15
|
+
* cancels a phrase only inside the phrase's own clause: "rather than flag it as a
|
|
16
|
+
* known issue" rejects the phrase, and "without touching the test file, accepting
|
|
17
|
+
* that it fails" does not. "Known issue", "follow-up" and "a later step" also name
|
|
18
|
+
* legitimate plans — an upstream bug, a scope cut — so they count only in a clause
|
|
19
|
+
* about a check.
|
|
20
|
+
*/
|
|
1
21
|
export declare function defersBreakage(answer: string): boolean;
|
|
2
22
|
/** The one re-ask a deferring answer gets before it is surfaced instead of promoted. */
|
|
3
23
|
export declare function deferredBreakageReaskHint(answer: string): string;
|
|
@@ -10,33 +10,95 @@
|
|
|
10
10
|
* model that ignores the rule still cannot promote a deferral into a decision.
|
|
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
|
+
*
|
|
14
|
+
* SCOPE IS GRAMMATICAL, never a character count. A negation or a conditional
|
|
15
|
+
* cancels a phrase only inside the phrase's own clause: "rather than flag it as a
|
|
16
|
+
* known issue" rejects the phrase, and "without touching the test file, accepting
|
|
17
|
+
* that it fails" does not. "Known issue", "follow-up" and "a later step" also name
|
|
18
|
+
* legitimate plans — an upstream bug, a scope cut — so they count only in a clause
|
|
19
|
+
* about a check.
|
|
13
20
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/\
|
|
21
|
+
/** A test, or a static check that the same clause calls broken. */
|
|
22
|
+
const TEST_NOUN = /\b(?:tests?|suites?|assertions?)\b/i;
|
|
23
|
+
const BUILD_NOUN = /\b(?:lint|linter|typecheck|build|ci)\b/i;
|
|
24
|
+
const FAILURE = /\b(?:fail\w*|red|broken|breaks?|breakage|errors?)\b/i;
|
|
25
|
+
/** Before a phrase in its clause: the phrase is rejected, or it is what an option
|
|
26
|
+
* WOULD do — "IF NOT EXISTS would still leave the test failing" weighs an option. */
|
|
27
|
+
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|would|could|might)\b/i;
|
|
28
|
+
/** Where one clause ends and the next begins. */
|
|
29
|
+
const CLAUSE_BOUNDARY = /[,:()]|\s[—–-]\s|\b(?:and|but|so|then|while|whereas|although|though|because|since|however)\b/gi;
|
|
30
|
+
const SENTENCE_BOUNDARY = /[.!?](?=\s|$)|;|\n/;
|
|
31
|
+
const PHRASES = [
|
|
32
|
+
{ re: /\b(?:test|suite)[- ]owners?\b/i, needs: 'alone' },
|
|
33
|
+
{
|
|
34
|
+
re: /\bflag(?:s|ged|ging)?\b.*?\b(?:as\s+(?:an?\s+|the\s+)?(?:known|owned)\b|for\s+(?:whoever|later|a\s+later)\b)/i,
|
|
35
|
+
needs: 'alone'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
re: /\bleav(?:e|es|ing)\b.*?\b(?:tests?|suites?|assertions?|lint|build|checks?|ci)\b.*?\b(?:failing|red|broken|as[- ]is)\b/i,
|
|
39
|
+
needs: 'alone'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
re: /\bskip(?:s|ping)?\s+(?:updating|fixing|adjusting|changing|touching)\b.*?\b(?:tests?|suites?|assertions?)\b/i,
|
|
43
|
+
needs: 'alone'
|
|
44
|
+
},
|
|
45
|
+
{ re: /\baccept(?:s|ed|ing)?\b.*?\b(?:fail\w*|red|broken)\b/i, needs: 'check' },
|
|
46
|
+
{ re: /\bwhoever\b/i, needs: 'check' },
|
|
47
|
+
{ 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
|
+
{ re: /\bleft\s+for\b/i, needs: 'check' },
|
|
50
|
+
{ re: /\bknown[- ]issues?\b/i, needs: 'check' },
|
|
51
|
+
{ re: /\bfollow[- ]?ups?\b/i, needs: 'check' },
|
|
52
|
+
{
|
|
53
|
+
re: /\b(?:a|the|another|some)\s+(?:later|future|subsequent|separate)\s+(?:step|task|change|pr)\b/i,
|
|
54
|
+
needs: 'check'
|
|
55
|
+
},
|
|
56
|
+
{ re: /\bdefer(?:s|red|ring)?\b/i, needs: 'check' },
|
|
57
|
+
{ re: /\bout\s+of\s+scope\b/i, needs: 'check' },
|
|
58
|
+
{
|
|
59
|
+
re: /\b(?:that|this|which|it|they|those)\s+(?:is|are|remains?)\s+out\s+of\s+scope\b/i,
|
|
60
|
+
needs: 'breakage'
|
|
61
|
+
}
|
|
26
62
|
];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
63
|
+
function aboutACheck(text) {
|
|
64
|
+
return TEST_NOUN.test(text) || (BUILD_NOUN.test(text) && FAILURE.test(text));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parenthetical asides go, and a code span keeps its words but loses the
|
|
68
|
+
* punctuation that would split a clause in two: `toEqual([{filename: X}])` is
|
|
69
|
+
* one token of the sentence around it, not three clauses.
|
|
70
|
+
*/
|
|
71
|
+
function prose(answer) {
|
|
72
|
+
let text = answer.replace(/`([^`]*)`/g, (_m, code) => code.replace(/[,;:()[\]{}]/g, ' '));
|
|
73
|
+
let before;
|
|
74
|
+
do {
|
|
75
|
+
before = text;
|
|
76
|
+
text = text.replace(/\([^()]*\)/g, ' ');
|
|
77
|
+
} while (text !== before);
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
function clauses(sentence) {
|
|
81
|
+
return sentence.split(CLAUSE_BOUNDARY).filter(c => c.trim().length > 0);
|
|
82
|
+
}
|
|
30
83
|
export function defersBreakage(answer) {
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
for (const
|
|
34
|
-
const
|
|
35
|
-
|
|
84
|
+
for (const sentence of prose(answer).split(SENTENCE_BOUNDARY)) {
|
|
85
|
+
const sentenceBreaks = aboutACheck(sentence) && FAILURE.test(sentence);
|
|
86
|
+
for (const clause of clauses(sentence)) {
|
|
87
|
+
for (const { re, needs } of PHRASES) {
|
|
88
|
+
const hit = re.exec(clause);
|
|
89
|
+
if (!hit)
|
|
90
|
+
continue;
|
|
91
|
+
if (NOT_A_DECISION.test(clause.slice(0, hit.index)))
|
|
92
|
+
continue;
|
|
93
|
+
if (needs === 'check' && !aboutACheck(clause))
|
|
94
|
+
continue;
|
|
95
|
+
if (needs === 'breakage' && !sentenceBreaks)
|
|
96
|
+
continue;
|
|
36
97
|
return true;
|
|
98
|
+
}
|
|
37
99
|
}
|
|
38
|
-
|
|
39
|
-
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
40
102
|
}
|
|
41
103
|
/** The one re-ask a deferring answer gets before it is surfaced instead of promoted. */
|
|
42
104
|
export function deferredBreakageReaskHint(answer) {
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -537,7 +537,10 @@ export async function healthBaselineFor(cwd, taskId, signal) {
|
|
|
537
537
|
return stored;
|
|
538
538
|
const fresh = await lazyHealthBaseline({
|
|
539
539
|
git: makeGit(cwd, signal),
|
|
540
|
-
|
|
540
|
+
// Statics only. The worktree has none of the tree's ignored files (the
|
|
541
|
+
// installed dependencies, a `.env`), so its suite fails for that, and a
|
|
542
|
+
// red recorded here would excuse the real regression it matches.
|
|
543
|
+
runHealthIn: dir => runRepoHealthCheck(dir, { signal })
|
|
541
544
|
});
|
|
542
545
|
if (fresh) {
|
|
543
546
|
await setTaskSection(cwd, taskId, HEALTH_BASELINE_SECTION, formatHealthBaseline(fresh)).catch(() => { });
|
|
@@ -720,7 +723,31 @@ export function buildGateDeps(params) {
|
|
|
720
723
|
await git(cwd2, ['checkout', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
721
724
|
await git(cwd2, ['clean', '-fd', '-e', '.pi-tasks'], signal);
|
|
722
725
|
};
|
|
723
|
-
|
|
726
|
+
const untrackedFiles = async (cwd2) => {
|
|
727
|
+
const r = await git(cwd2, ['ls-files', '--others', '--exclude-standard', '-z', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
728
|
+
return r.exitCode === 0 ? new Set(r.stdout.split('\u0000').filter(f => f.length > 0)) : null;
|
|
729
|
+
};
|
|
730
|
+
// The project's own checks, suite included, once per tree for the run. A suite
|
|
731
|
+
// writes coverage, reports and databases into the tree; left there, they ride
|
|
732
|
+
// into the task's commit and read as enforce edits, so what the check created
|
|
733
|
+
// is removed before the tree is hashed again.
|
|
734
|
+
const gateHealth = (cwd2, onCommand) => currentRunContext(cwd2).healthFor(async () => {
|
|
735
|
+
const before = await untrackedFiles(cwd2);
|
|
736
|
+
try {
|
|
737
|
+
return await runRepoHealthCheck(cwd2, { signal, withTests: true, onCommand });
|
|
738
|
+
}
|
|
739
|
+
finally {
|
|
740
|
+
const after = before ? await untrackedFiles(cwd2) : null;
|
|
741
|
+
for (const rel of after ?? []) {
|
|
742
|
+
if (!before?.has(rel)) {
|
|
743
|
+
await fsp
|
|
744
|
+
.rm(path.join(cwd2, rel), { recursive: true, force: true })
|
|
745
|
+
.catch(() => { });
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
// The project's own checks, under a live loader naming the running command.
|
|
724
751
|
// Each run is as long as that command, and a gate step that long with no widget
|
|
725
752
|
// is indistinguishable from a hang. Shared by the enforce pre-commit gate (a
|
|
726
753
|
// baseline before the edit pass, a differential after it) and by the
|
|
@@ -737,12 +764,8 @@ export function buildGateDeps(params) {
|
|
|
737
764
|
startedAt,
|
|
738
765
|
lastLine: running ? `repo health · ${running}` : 'repo health'
|
|
739
766
|
}));
|
|
740
|
-
return
|
|
741
|
-
|
|
742
|
-
withTests: true,
|
|
743
|
-
onCommand: c => {
|
|
744
|
-
running = c;
|
|
745
|
-
}
|
|
767
|
+
return gateHealth(cwd2, c => {
|
|
768
|
+
running = c;
|
|
746
769
|
}).finally(stop);
|
|
747
770
|
};
|
|
748
771
|
// Adapter onto the shared gate-child runner (gate-child.ts). What survives
|
|
@@ -946,12 +969,8 @@ export function buildGateDeps(params) {
|
|
|
946
969
|
// progress hook here would be uncancellable and mute for reasons
|
|
947
970
|
// unrelated to the thing under test. The arm's only difference is
|
|
948
971
|
// the LOADER, above.
|
|
949
|
-
repoHealth: () =>
|
|
950
|
-
|
|
951
|
-
withTests: true,
|
|
952
|
-
onCommand: c => {
|
|
953
|
-
stageLine = `repo health · ${c}`;
|
|
954
|
-
}
|
|
972
|
+
repoHealth: () => gateHealth(cwd2, c => {
|
|
973
|
+
stageLine = `repo health · ${c}`;
|
|
955
974
|
}),
|
|
956
975
|
// What those checks said before the task started, so a red one
|
|
957
976
|
// is attributed rather than absolutely failed. Read only when
|
|
@@ -11,6 +11,7 @@ const REATTEMPTABLE_BUDGET = 3;
|
|
|
11
11
|
export const AUTOFIX_BUDGET = {
|
|
12
12
|
'repo-health': REATTEMPTABLE_BUDGET,
|
|
13
13
|
'static-checks': REATTEMPTABLE_BUDGET,
|
|
14
|
+
'test-suite': REATTEMPTABLE_BUDGET,
|
|
14
15
|
'model-verdict': REATTEMPTABLE_BUDGET,
|
|
15
16
|
unobserved: 0,
|
|
16
17
|
'harness-fault': 0
|
|
@@ -38,7 +38,14 @@ 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
|
-
/** The failing commands
|
|
41
|
+
/** The failing commands the baseline did not have failing the same way — what a
|
|
42
|
+
* `regressed` verdict is about. Every failing command when there is no baseline. */
|
|
43
|
+
export declare function regressedCommands(baseline: HealthSignal | null, after: HealthSignal): HealthCommandResult[];
|
|
44
|
+
/**
|
|
45
|
+
* The failing commands, as prompt/trail lines naming the exit code. A test
|
|
46
|
+
* runner exits 1 for one failing test or for fifty, so for a suite the line
|
|
47
|
+
* claims only the exit code: which tests fail was not compared.
|
|
48
|
+
*/
|
|
42
49
|
export declare function inheritedHealthFindings(after: HealthSignal): string[];
|
|
43
50
|
export declare const HEALTH_BASELINE_SECTION = "health baseline";
|
|
44
51
|
/**
|
|
@@ -46,9 +53,9 @@ export declare const HEALTH_BASELINE_SECTION = "health baseline";
|
|
|
46
53
|
* grammar: this round-trips through a committed file that a later run parses, and
|
|
47
54
|
* a second grammar is a second thing to drift.
|
|
48
55
|
*
|
|
49
|
-
* The captured
|
|
50
|
-
* file committed with every task, for a field the
|
|
51
|
-
* live run's own trail already carries it.
|
|
56
|
+
* The captured output is dropped, the outcome's and each command's — up to 40
|
|
57
|
+
* lines of a linter's report, in a file committed with every task, for a field the
|
|
58
|
+
* differential never reads. The live run's own trail already carries it.
|
|
52
59
|
*/
|
|
53
60
|
export declare function formatHealthBaseline(b: HealthBaseline): string;
|
|
54
61
|
/** Parse a `## health baseline` section back. Null on anything unreadable — an
|
|
@@ -52,13 +52,24 @@ export function classifyHealthDelta(baseline, after) {
|
|
|
52
52
|
const detailed = now.length > 0 && (baseline.ok || before.length > 0);
|
|
53
53
|
if (!detailed)
|
|
54
54
|
return baseline.ok ? 'regressed' : 'pre-existing';
|
|
55
|
-
|
|
56
|
-
const wasFailing = new Set(before.map(key));
|
|
57
|
-
return now.every(c => wasFailing.has(key(c))) ? 'pre-existing' : 'regressed';
|
|
55
|
+
return regressedCommands(baseline, after).length > 0 ? 'regressed' : 'pre-existing';
|
|
58
56
|
}
|
|
59
|
-
|
|
57
|
+
const failureKey = (c) => JSON.stringify([c.cmd, c.exitCode]);
|
|
58
|
+
/** The failing commands the baseline did not have failing the same way — what a
|
|
59
|
+
* `regressed` verdict is about. Every failing command when there is no baseline. */
|
|
60
|
+
export function regressedCommands(baseline, after) {
|
|
61
|
+
const wasFailing = new Set(baseline ? failures(baseline).map(failureKey) : []);
|
|
62
|
+
return failures(after).filter(c => !wasFailing.has(failureKey(c)));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The failing commands, as prompt/trail lines naming the exit code. A test
|
|
66
|
+
* runner exits 1 for one failing test or for fifty, so for a suite the line
|
|
67
|
+
* claims only the exit code: which tests fail was not compared.
|
|
68
|
+
*/
|
|
60
69
|
export function inheritedHealthFindings(after) {
|
|
61
|
-
return failures(after).map(c =>
|
|
70
|
+
return failures(after).map(c => c.kind === 'test' ?
|
|
71
|
+
`\`${c.cmd}\` exits ${c.exitCode}, as it did before this task — the same exit code, not proof the same tests fail`
|
|
72
|
+
: `\`${c.cmd}\` exits ${c.exitCode} (and did before this task)`);
|
|
62
73
|
}
|
|
63
74
|
// ─── The task-file section ───────────────────────────────────────────────────
|
|
64
75
|
export const HEALTH_BASELINE_SECTION = 'health baseline';
|
|
@@ -67,12 +78,13 @@ export const HEALTH_BASELINE_SECTION = 'health baseline';
|
|
|
67
78
|
* grammar: this round-trips through a committed file that a later run parses, and
|
|
68
79
|
* a second grammar is a second thing to drift.
|
|
69
80
|
*
|
|
70
|
-
* The captured
|
|
71
|
-
* file committed with every task, for a field the
|
|
72
|
-
* live run's own trail already carries it.
|
|
81
|
+
* The captured output is dropped, the outcome's and each command's — up to 40
|
|
82
|
+
* lines of a linter's report, in a file committed with every task, for a field the
|
|
83
|
+
* differential never reads. The live run's own trail already carries it.
|
|
73
84
|
*/
|
|
74
85
|
export function formatHealthBaseline(b) {
|
|
75
|
-
const { output: _output, ...
|
|
86
|
+
const { output: _output, commands, ...rest } = b.outcome;
|
|
87
|
+
const outcome = { ...rest, commands: commands.map(({ output: _o, ...c }) => c) };
|
|
76
88
|
return ['```json', JSON.stringify({ ...b, outcome }, null, 2), '```'].join('\n');
|
|
77
89
|
}
|
|
78
90
|
/** Parse a `## health baseline` section back. Null on anything unreadable — an
|
|
@@ -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
|
|
@@ -15,20 +15,26 @@ 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
|
+
/** This command's own captured output, on a `fail` only. */
|
|
22
|
+
output?: string;
|
|
18
23
|
}
|
|
19
24
|
export interface HealthOutcome {
|
|
20
|
-
/** true → every discovered
|
|
21
|
-
* false → a discovered command actually ran and exited non-zero. */
|
|
25
|
+
/** true → every discovered check passed or could not run, or there was nothing
|
|
26
|
+
* to run. false → a discovered command actually ran and exited non-zero. */
|
|
22
27
|
ok: boolean;
|
|
23
|
-
/** Human-readable reason. On a fail, names
|
|
28
|
+
/** Human-readable reason. On a fail, names every failing command and its exit code. */
|
|
24
29
|
reason: string;
|
|
25
30
|
/** Which manifest drove discovery, or null when none was found. */
|
|
26
31
|
ecosystem: string | null;
|
|
27
|
-
/** Every command
|
|
28
|
-
*
|
|
32
|
+
/** Every discovered command, in run order. A red one does not stop the run: a
|
|
33
|
+
* command it skipped would be absent from both sides of the differential, which
|
|
34
|
+
* then cannot see that command break. */
|
|
29
35
|
commands: HealthCommandResult[];
|
|
30
36
|
/**
|
|
31
|
-
* First lines of the failing command's combined stderr+stdout — captured so a
|
|
37
|
+
* First lines of the first failing command's combined stderr+stdout — captured so a
|
|
32
38
|
* FAIL is explainable from artifacts alone. The exit code alone does not say
|
|
33
39
|
* what happened: eslint exits 1 for findings and 2 when it could not run at
|
|
34
40
|
* all (a missing config, say), so "`bun run lint` exited 2" is unreproducible
|
|
@@ -65,7 +71,8 @@ export declare function discoverHealthCommands(cwd: string): {
|
|
|
65
71
|
* Every test-shaped script, not just the one literally named `test`: a project's
|
|
66
72
|
* only browser-executing suite is often `test:ct`, and looking for `test` alone
|
|
67
73
|
* never runs it. Plain `test` leads, then every `test:`/`test_`/`test-` name in
|
|
68
|
-
* declaration order (Array#sort is stable).
|
|
74
|
+
* declaration order (Array#sort is stable). A watch-mode script is left out: it
|
|
75
|
+
* never exits, so all it can add is a timeout.
|
|
69
76
|
*/
|
|
70
77
|
export declare function discoverTestCommands(cwd: string): {
|
|
71
78
|
ecosystem: string | null;
|
|
@@ -80,7 +87,7 @@ export type HealthProgress = (command: string) => void;
|
|
|
80
87
|
* - No manifest / no static command → ok (nothing can regress).
|
|
81
88
|
* - A command that CANNOT run (ENOENT / null exit / 127 inside the chain) → skipped,
|
|
82
89
|
* treated as an environment gap, not a fault.
|
|
83
|
-
* - A command that ran and exited non-zero →
|
|
90
|
+
* - A command that ran and exited non-zero → red. Every command still runs.
|
|
84
91
|
*
|
|
85
92
|
* This module owns DISCOVERY and its own output policy. Running a command and
|
|
86
93
|
* deciding what its ending MEANS is `command-run.ts`'s — one statement of the
|
|
@@ -105,3 +112,5 @@ export declare function runRepoHealthCheck(cwd: string, opts?: {
|
|
|
105
112
|
* that will judge the result DIFFERENTIALLY may set this — see the header. */
|
|
106
113
|
withTests?: boolean;
|
|
107
114
|
}): Promise<HealthOutcome>;
|
|
115
|
+
/** "`bun run lint` exited 1; `bun run test` exited 1" — every failing command. */
|
|
116
|
+
export declare function describeHealthFailures(commands: readonly HealthCommandResult[]): string;
|
|
@@ -125,6 +125,10 @@ export function discoverHealthCommands(cwd) {
|
|
|
125
125
|
}
|
|
126
126
|
return { ecosystem: null, cmds: [] };
|
|
127
127
|
}
|
|
128
|
+
/** `test:watch`, `jest --watchAll`, `vitest watch`, `bun test --watch`. */
|
|
129
|
+
function isWatchScript(name, body) {
|
|
130
|
+
return (/watch/i.test(name) || /(?:^|\s)--watch(?:All)?(?=[\s=]|$)|(?:^|\s)watch(?=\s|$)/.test(body));
|
|
131
|
+
}
|
|
128
132
|
/**
|
|
129
133
|
* The project's OWN test commands, in the order the run-end gate runs them. One
|
|
130
134
|
* statement for both gates: final-gate.ts appends `build` to this list for the
|
|
@@ -134,12 +138,13 @@ export function discoverHealthCommands(cwd) {
|
|
|
134
138
|
* Every test-shaped script, not just the one literally named `test`: a project's
|
|
135
139
|
* only browser-executing suite is often `test:ct`, and looking for `test` alone
|
|
136
140
|
* never runs it. Plain `test` leads, then every `test:`/`test_`/`test-` name in
|
|
137
|
-
* declaration order (Array#sort is stable).
|
|
141
|
+
* declaration order (Array#sort is stable). A watch-mode script is left out: it
|
|
142
|
+
* never exits, so all it can add is a timeout.
|
|
138
143
|
*/
|
|
139
144
|
export function discoverTestCommands(cwd) {
|
|
140
145
|
if (existsSync(path.join(cwd, 'package.json'))) {
|
|
141
146
|
const s = packageScripts(cwd);
|
|
142
|
-
const names = Object.keys(s).filter(n => n === 'test' || /^test[:_-]/.test(n));
|
|
147
|
+
const names = Object.keys(s).filter(n => (n === 'test' || /^test[:_-]/.test(n)) && !isWatchScript(n, s[n]));
|
|
143
148
|
names.sort((a, b) => a === 'test' ? -1
|
|
144
149
|
: b === 'test' ? 1
|
|
145
150
|
: 0);
|
|
@@ -178,7 +183,7 @@ function noCommandOutcome(ecosystem) {
|
|
|
178
183
|
* - No manifest / no static command → ok (nothing can regress).
|
|
179
184
|
* - A command that CANNOT run (ENOENT / null exit / 127 inside the chain) → skipped,
|
|
180
185
|
* treated as an environment gap, not a fault.
|
|
181
|
-
* - A command that ran and exited non-zero →
|
|
186
|
+
* - A command that ran and exited non-zero → red. Every command still runs.
|
|
182
187
|
*
|
|
183
188
|
* This module owns DISCOVERY and its own output policy. Running a command and
|
|
184
189
|
* deciding what its ending MEANS is `command-run.ts`'s — one statement of the
|
|
@@ -225,23 +230,38 @@ export async function runRepoHealthCheck(cwd, opts = {}) {
|
|
|
225
230
|
// ladder's `tail` keeps 400 characters, and that difference is real — a
|
|
226
231
|
// truncated lint report is unactionable. So the run is classified, not
|
|
227
232
|
// 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 });
|
|
233
|
+
// `runtimeGap` and `emptySuite` only for a TEST command. Both rows read the
|
|
234
|
+
// command's output, and on lint and typecheck a genuine report quoting
|
|
235
|
+
// "browsers are not installed" would skip the static check and certify
|
|
236
|
+
// the repo healthy.
|
|
237
|
+
const verdict = classifyCommandRun(r, [], { runtimeGap: test, emptySuite: test });
|
|
238
|
+
const kind = test ? 'test' : 'static';
|
|
233
239
|
if (verdict.outcome !== 'fail') {
|
|
234
240
|
const passed = verdict.outcome === 'pass';
|
|
235
|
-
commands.push({
|
|
241
|
+
commands.push({
|
|
242
|
+
cmd,
|
|
243
|
+
outcome: passed ? 'pass' : 'skip',
|
|
244
|
+
exitCode: passed ? 0 : null,
|
|
245
|
+
kind
|
|
246
|
+
});
|
|
236
247
|
continue;
|
|
237
248
|
}
|
|
238
|
-
commands.push({
|
|
249
|
+
commands.push({
|
|
250
|
+
cmd,
|
|
251
|
+
outcome: 'fail',
|
|
252
|
+
exitCode: verdict.status,
|
|
253
|
+
kind,
|
|
254
|
+
output: captureHealthOutput(r.stdout, r.stderr)
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const firstFail = commands.find(c => c.outcome === 'fail');
|
|
258
|
+
if (firstFail) {
|
|
239
259
|
return {
|
|
240
260
|
ok: false,
|
|
241
|
-
reason:
|
|
261
|
+
reason: describeHealthFailures(commands),
|
|
242
262
|
ecosystem,
|
|
243
263
|
commands,
|
|
244
|
-
output:
|
|
264
|
+
output: firstFail.output ?? ''
|
|
245
265
|
};
|
|
246
266
|
}
|
|
247
267
|
return {
|
|
@@ -252,3 +272,10 @@ export async function runRepoHealthCheck(cwd, opts = {}) {
|
|
|
252
272
|
output: ''
|
|
253
273
|
};
|
|
254
274
|
}
|
|
275
|
+
/** "`bun run lint` exited 1; `bun run test` exited 1" — every failing command. */
|
|
276
|
+
export function describeHealthFailures(commands) {
|
|
277
|
+
return commands
|
|
278
|
+
.filter(c => c.outcome === 'fail')
|
|
279
|
+
.map(c => `\`${c.cmd}\` exited ${c.exitCode}`)
|
|
280
|
+
.join('; ');
|
|
281
|
+
}
|
|
@@ -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
|
package/dist/task/verify-work.js
CHANGED
|
@@ -42,7 +42,8 @@ import { buildEnvNotesBlock, ENV_NOTE_EMIT_INSTRUCTION, extractEnvNotes } from '
|
|
|
42
42
|
import { buildContractsVerifyBlock } from './contracts.js';
|
|
43
43
|
import { findSkipEscapes, skipEscapeVerifyFindings } from './skip-escape.js';
|
|
44
44
|
import { crossTaskDeletionVerifyFindings } from './task-provenance.js';
|
|
45
|
-
import { classifyHealthDelta, inheritedHealthFindings } from './health-baseline.js';
|
|
45
|
+
import { classifyHealthDelta, inheritedHealthFindings, regressedCommands } from './health-baseline.js';
|
|
46
|
+
import { describeHealthFailures } from './repo-health-check.js';
|
|
46
47
|
import { parseSpec, sliceSpecSection } from './spec-model.js';
|
|
47
48
|
import { qaKindsFromRecord } from './qa-transcript.js';
|
|
48
49
|
import { annotateConstraints, anyBinding, renderConstraintPolicy } from './constraint-policy.js';
|
|
@@ -72,6 +73,7 @@ const VERIFY_TOOLS = 'read,bash';
|
|
|
72
73
|
export const VERIFY_FAIL_PREFIX = {
|
|
73
74
|
'repo-health': 'repo health:',
|
|
74
75
|
'static-checks': 'static checks:',
|
|
76
|
+
'test-suite': 'test suite:',
|
|
75
77
|
unobserved: 'work unobserved:',
|
|
76
78
|
'model-verdict': 'work did not verify:',
|
|
77
79
|
'harness-fault': 'verification pass could not run:'
|
|
@@ -103,6 +105,14 @@ export function failClassOfReason(reason) {
|
|
|
103
105
|
export function isStaticClass(cls) {
|
|
104
106
|
return cls === 'repo-health' || cls === 'static-checks';
|
|
105
107
|
}
|
|
108
|
+
/** The class a red health result is minted under: the suite's when any test is red. */
|
|
109
|
+
function healthFailClass(failing) {
|
|
110
|
+
return failing.some(c => c.kind === 'test') ? 'test-suite' : 'repo-health';
|
|
111
|
+
}
|
|
112
|
+
/** Does this class name the deterministic whole-repo check, suite included? */
|
|
113
|
+
export function isHealthClass(cls) {
|
|
114
|
+
return isStaticClass(cls) || cls === 'test-suite';
|
|
115
|
+
}
|
|
106
116
|
/**
|
|
107
117
|
* The delivered spec's TEXT, for the children that must read its prose verbatim.
|
|
108
118
|
* The slicing itself lives in spec-model.ts beside the parser, so "the spec
|
|
@@ -182,8 +192,8 @@ const PROBE_ADAPTERS = [
|
|
|
182
192
|
]
|
|
183
193
|
}),
|
|
184
194
|
/**
|
|
185
|
-
* PRE-EXISTING repo health (see health-baseline.ts):
|
|
186
|
-
*
|
|
195
|
+
* PRE-EXISTING repo health (see health-baseline.ts): checks that were ALREADY
|
|
196
|
+
* failing, with the same exit code, before this task started. They used to be an
|
|
187
197
|
* absolute FAIL that short-circuited the whole pass, so the task answered for
|
|
188
198
|
* a sibling's defect and its own probe findings were never computed. As a row
|
|
189
199
|
* they are stated to the child instead: judge this task's work, and do not
|
|
@@ -197,7 +207,7 @@ const PROBE_ADAPTERS = [
|
|
|
197
207
|
ruleId: '4h',
|
|
198
208
|
block: findings => [
|
|
199
209
|
'INHERITED REPO-HEALTH NOTICE (deterministic, computed by the orchestrator by',
|
|
200
|
-
"re-running the project's own
|
|
210
|
+
"re-running the project's own checks and comparing their exit codes against the",
|
|
201
211
|
'baseline taken before this task started): these checks were ALREADY failing,',
|
|
202
212
|
'with the same exit code, before any of this work existed:',
|
|
203
213
|
...findings.map(f => `- ${f}`),
|
|
@@ -208,13 +218,16 @@ const PROBE_ADAPTERS = [
|
|
|
208
218
|
],
|
|
209
219
|
rule: [
|
|
210
220
|
"4h. AN INHERITED RED CHECK IS NOT THIS TASK'S FAIL, AND NOT ITS PROOF — when the",
|
|
211
|
-
' INHERITED REPO-HEALTH NOTICE above names a check, that check
|
|
221
|
+
' INHERITED REPO-HEALTH NOTICE above names a check, that check exited the same way',
|
|
212
222
|
' before this task ran. Do NOT fail this work for it: the defect belongs to',
|
|
213
223
|
' whatever put it there, it is recorded as durable debt, and the run-end gate',
|
|
214
224
|
' re-checks it. Do NOT lean on it either — a command that was already exiting',
|
|
215
225
|
" non-zero tells you nothing about this task's behavior, so verify that behavior",
|
|
216
226
|
' another way. A check that is failing DIFFERENTLY, or one absent from the notice,',
|
|
217
|
-
" is this task's to answer for in the ordinary way."
|
|
227
|
+
" is this task's to answer for in the ordinary way. A TEST command is the",
|
|
228
|
+
' exception to the exit code: it exits the same way for one failing test or',
|
|
229
|
+
" fifty. Run it, and a test that fails because of THIS work is this task's FAIL,",
|
|
230
|
+
' whatever the spec says about it.'
|
|
218
231
|
]
|
|
219
232
|
}),
|
|
220
233
|
/**
|
|
@@ -907,16 +920,33 @@ export async function runWorkVerification(deps) {
|
|
|
907
920
|
const h = await deps.repoHealth();
|
|
908
921
|
if (!h.ok) {
|
|
909
922
|
const baseline = deps.healthBaseline ? await deps.healthBaseline() : null;
|
|
910
|
-
|
|
923
|
+
const before = baseline?.outcome ?? null;
|
|
924
|
+
if (classifyHealthDelta(before, h) === 'regressed') {
|
|
925
|
+
// Named after what REGRESSED, not after whatever failed first: a lint
|
|
926
|
+
// red on arrival would otherwise stand in for the suite this task broke.
|
|
927
|
+
const regressed = regressedCommands(before, h);
|
|
928
|
+
if (regressed.length === 0) {
|
|
929
|
+
return {
|
|
930
|
+
ok: false,
|
|
931
|
+
failClass: 'repo-health',
|
|
932
|
+
reason: `repo health: ${h.reason}`,
|
|
933
|
+
health: h
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
const failClass = healthFailClass(regressed);
|
|
911
937
|
return {
|
|
912
938
|
ok: false,
|
|
913
|
-
failClass
|
|
914
|
-
reason:
|
|
915
|
-
health: h
|
|
939
|
+
failClass,
|
|
940
|
+
reason: `${VERIFY_FAIL_PREFIX[failClass]} ${describeHealthFailures(regressed)}`,
|
|
941
|
+
health: { ...h, commands: regressed, output: regressed[0].output ?? h.output }
|
|
916
942
|
};
|
|
917
943
|
}
|
|
918
944
|
pre.repoHealth = inheritedHealthFindings(h);
|
|
919
|
-
|
|
945
|
+
const failing = h.commands?.filter(c => c.outcome === 'fail') ?? [];
|
|
946
|
+
inheritedHealth =
|
|
947
|
+
failing.length > 0 ?
|
|
948
|
+
`${VERIFY_FAIL_PREFIX[healthFailClass(failing)]} ${describeHealthFailures(failing)} — already failing before this task`
|
|
949
|
+
: `repo health: ${h.reason} — already failing before this task`;
|
|
920
950
|
}
|
|
921
951
|
}
|
|
922
952
|
const inherited = inheritedHealth === undefined ? {} : { inheritedHealth };
|
package/dist/task/yolo.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export type YoloPick = {
|
|
|
36
36
|
* demotion — an answer proven to name an unverified API identifier. Auto-accepting
|
|
37
37
|
* that would re-promote exactly the invention the demotion exists to stop, so a
|
|
38
38
|
* machine may never take it; a human still can.
|
|
39
|
+
*
|
|
40
|
+
* An option that defers a breakage to an owner nobody is, is passed over here
|
|
41
|
+
* rather than at each caller: the clarify generator, the plan review and an
|
|
42
|
+
* UNKNOWN's own suggestion reach a machine with no guard in front of them.
|
|
39
43
|
*/
|
|
40
44
|
export declare function yoloPickAnswer(enabled: boolean, opts: {
|
|
41
45
|
suggested?: string;
|
|
@@ -45,11 +49,11 @@ export declare function yoloPickAnswer(enabled: boolean, opts: {
|
|
|
45
49
|
/**
|
|
46
50
|
* The same policy expressed over an {@link AutoAnswer}, for the grill site. Of the
|
|
47
51
|
* five `reason` tags an unknown can carry — `api-synthesis`, `deferred-breakage`,
|
|
48
|
-
* `integration`, `threw`, `model-unknown` —
|
|
49
|
-
*
|
|
52
|
+
* `integration`, `threw`, `model-unknown` — `api-synthesis` is unsafe by its tag:
|
|
53
|
+
* the answer names an API nobody verified. A `deferred-breakage` suggestion is the
|
|
54
|
+
* deferral itself, which `yoloPickAnswer` passes over for every site. The other
|
|
50
55
|
* three carry an ordinary best-effort recommendation, which is precisely what a
|
|
51
|
-
* human would be shown as the green card.
|
|
52
|
-
* never by pattern-matching the answer text.
|
|
56
|
+
* human would be shown as the green card.
|
|
53
57
|
*/
|
|
54
58
|
export declare function yoloPickAutoAnswer(enabled: boolean, auto: AutoAnswer): YoloPick;
|
|
55
59
|
/**
|
package/dist/task/yolo.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* the config.
|
|
30
30
|
*/
|
|
31
31
|
import { getConfig } from '../config/config.js';
|
|
32
|
+
import { defersBreakage } from './deferred-breakage.js';
|
|
32
33
|
/**
|
|
33
34
|
* Visible provenance marker on the artifacts an auto-pick writes: gate trail lines
|
|
34
35
|
* and the task file's Q&A record (qa-transcript.ts `QA_PROVENANCE`). A later audit
|
|
@@ -53,6 +54,10 @@ export function isYoloMode() {
|
|
|
53
54
|
* demotion — an answer proven to name an unverified API identifier. Auto-accepting
|
|
54
55
|
* that would re-promote exactly the invention the demotion exists to stop, so a
|
|
55
56
|
* machine may never take it; a human still can.
|
|
57
|
+
*
|
|
58
|
+
* An option that defers a breakage to an owner nobody is, is passed over here
|
|
59
|
+
* rather than at each caller: the clarify generator, the plan review and an
|
|
60
|
+
* UNKNOWN's own suggestion reach a machine with no guard in front of them.
|
|
56
61
|
*/
|
|
57
62
|
export function yoloPickAnswer(enabled, opts) {
|
|
58
63
|
if (!enabled)
|
|
@@ -60,20 +65,26 @@ export function yoloPickAnswer(enabled, opts) {
|
|
|
60
65
|
if (opts.unsafe !== undefined && opts.unsafe.length > 0) {
|
|
61
66
|
return { kind: 'skip', note: opts.unsafe };
|
|
62
67
|
}
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
68
|
+
const offered = [opts.suggested, opts.alt].filter((o) => o !== undefined && o.trim().length > 0);
|
|
69
|
+
if (offered.length === 0)
|
|
65
70
|
return { kind: 'skip', note: 'no recommended option to take' };
|
|
71
|
+
const pick = offered.find(o => !defersBreakage(o));
|
|
72
|
+
if (pick === undefined) {
|
|
73
|
+
return {
|
|
74
|
+
kind: 'skip',
|
|
75
|
+
note: 'the recommended answer leaves a test or build failing for an owner that does not exist — needs a human'
|
|
76
|
+
};
|
|
66
77
|
}
|
|
67
78
|
return { kind: 'answer', answer: pick };
|
|
68
79
|
}
|
|
69
80
|
/**
|
|
70
81
|
* The same policy expressed over an {@link AutoAnswer}, for the grill site. Of the
|
|
71
82
|
* five `reason` tags an unknown can carry — `api-synthesis`, `deferred-breakage`,
|
|
72
|
-
* `integration`, `threw`, `model-unknown` —
|
|
73
|
-
*
|
|
83
|
+
* `integration`, `threw`, `model-unknown` — `api-synthesis` is unsafe by its tag:
|
|
84
|
+
* the answer names an API nobody verified. A `deferred-breakage` suggestion is the
|
|
85
|
+
* deferral itself, which `yoloPickAnswer` passes over for every site. The other
|
|
74
86
|
* three carry an ordinary best-effort recommendation, which is precisely what a
|
|
75
|
-
* human would be shown as the green card.
|
|
76
|
-
* never by pattern-matching the answer text.
|
|
87
|
+
* human would be shown as the green card.
|
|
77
88
|
*/
|
|
78
89
|
export function yoloPickAutoAnswer(enabled, auto) {
|
|
79
90
|
if (!enabled)
|
|
@@ -85,9 +96,6 @@ export function yoloPickAutoAnswer(enabled, auto) {
|
|
|
85
96
|
...(auto.alt !== undefined && { alt: auto.alt }),
|
|
86
97
|
...(auto.reason === 'api-synthesis' && {
|
|
87
98
|
unsafe: 'the suggested answer names an unverified API identifier — needs a human'
|
|
88
|
-
}),
|
|
89
|
-
...(auto.reason === 'deferred-breakage' && {
|
|
90
|
-
unsafe: 'the suggested answer leaves a test or build failing for an owner that does not exist — needs a human'
|
|
91
99
|
})
|
|
92
100
|
});
|
|
93
101
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.4",
|
|
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",
|