@mjasnikovs/pi-task 0.42.2 → 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 +13 -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 +23 -0
- package/dist/task/deferred-breakage.js +110 -0
- package/dist/task/final-gate.js +15 -52
- package/dist/task/gate-deps.js +37 -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/parsers.d.ts +1 -1
- package/dist/task/phases.js +76 -44
- package/dist/task/prompts.d.ts +9 -0
- package/dist/task/prompts.js +14 -3
- package/dist/task/repo-health-check.d.ts +34 -7
- package/dist/task/repo-health-check.js +95 -19
- 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 +10 -5
- package/dist/task/yolo.js +19 -7
- 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 [];
|
|
@@ -15,11 +15,12 @@ import { RUN_END_POLICY, runSucceeded } from './run-end.js';
|
|
|
15
15
|
import { parseAutoAnswer, autoAnswerHasTag, deriveTitle } from './parsers.js';
|
|
16
16
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
17
17
|
import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT, DECOMPOSE_COVERAGE_PROMPT } from './auto-prompts.js';
|
|
18
|
+
import { defersBreakage } from './deferred-breakage.js';
|
|
18
19
|
import { GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT } from './prompts.js';
|
|
19
20
|
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, UNNAMED_COVERAGE_GAP, parseTaskList, planKeyAt, checkOffTask, stampTaskInProgress, beginTaskAttempt, recordTaskEnd, insertTaskAfter, insertTaskBefore, findResumableAutoDetailed } from './auto-io.js';
|
|
20
21
|
import { decideResume, UNATTENDED_STATES } from './resume-gap.js';
|
|
21
22
|
import { ENTRY_ATTEMPT_BUDGET } from './gate-resolution.js';
|
|
22
|
-
import { recordDebt } from './accept-debt.js';
|
|
23
|
+
import { readOpenAcceptDebts, recordDebt } from './accept-debt.js';
|
|
23
24
|
import { drainRepairQueue, mergeRepairCandidates, planHasRepairFor, parseRepairTitleFile, buildRepairTitle, buildRepairScopeFence, extractFailingCommand } from './root-cause-repair.js';
|
|
24
25
|
import { writeTaskFile, readTaskFile, readSection, updateTaskFrontMatter, taskFilePath } from './task-io.js';
|
|
25
26
|
// Re-exported as well as used: the @-mention helpers moved to their own module so
|
|
@@ -37,7 +38,7 @@ import { getParentContextWindow } from './context-usage.js';
|
|
|
37
38
|
import { ChildStatus, runPlanningChild, statusCallbacks } from './child-status.js';
|
|
38
39
|
import { buildGateDeps, collectTreeChanges } from './gate-deps.js';
|
|
39
40
|
import { runGatesForTask } from './task-gates.js';
|
|
40
|
-
import { buildHealthRepairFence, buildHealthRepairTitle, healthRedSubject, parseHealthRepairTitle, planCoversHealthRed } from './health-repair.js';
|
|
41
|
+
import { buildHealthRepairFence, buildHealthRepairTitle, healthRedSubject, suiteRegressionOwed, parseHealthRepairTitle, planCoversHealthRed } from './health-repair.js';
|
|
41
42
|
import { HEALTH_BASELINE_SECTION, parseHealthBaseline } from './health-baseline.js';
|
|
42
43
|
import { runFinalGateStage } from './run-final-gate.js';
|
|
43
44
|
import { gitUnmergedPaths, gitStashRef } from './auto-commit.js';
|
|
@@ -220,6 +221,10 @@ async function triageClarifyQuestion(deps, cwd, featureForModel, existingFilesBl
|
|
|
220
221
|
text = await deps.runChild('clarify-triage', 'read', prependHint(GRILL_AUTO_FORMAT_HINT, basePrompt));
|
|
221
222
|
}
|
|
222
223
|
const parsed = parseAutoAnswer(text);
|
|
224
|
+
if (parsed.kind === 'answered' && defersBreakage(parsed.text)) {
|
|
225
|
+
logPlanDebug(cwd, `clarify-triage answer defers a breakage to a nonexistent owner — surfacing: ${parsed.text.replace(/\s+/g, ' ').slice(0, 100)}`);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
223
228
|
if (parsed.kind === 'answered') {
|
|
224
229
|
logPlanDebug(cwd, `clarify-triage auto-resolved (spec-settled): ${question.replace(/\s+/g, ' ').slice(0, 100)}`
|
|
225
230
|
+ ` → ${parsed.text.replace(/\s+/g, ' ').slice(0, 100)}`);
|
|
@@ -387,7 +392,8 @@ async function schedulePendingRepairs(cwd, id, afterIndex, ctx, deps) {
|
|
|
387
392
|
*/
|
|
388
393
|
async function spliceHealthRepair(cwd, id, next, entries, health, ctx, deps) {
|
|
389
394
|
try {
|
|
390
|
-
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));
|
|
391
397
|
if (!red)
|
|
392
398
|
return false;
|
|
393
399
|
if (planCoversHealthRed(entries.map(e => e.title), red))
|
|
@@ -604,8 +610,9 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
604
610
|
}
|
|
605
611
|
// YOLO: take the recommended option (index 0 / the green card) without ever
|
|
606
612
|
// building the prompt. Clarify has no anti-synthesis channel — it runs before
|
|
607
|
-
// any research — so the
|
|
608
|
-
// 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.
|
|
609
616
|
const outcome = await settleQuestion({
|
|
610
617
|
ui,
|
|
611
618
|
transcript,
|
|
@@ -1361,7 +1368,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1361
1368
|
notifyRun(active, `${id}: checkpointed uncommitted work before "${next.title}".`, 'info');
|
|
1362
1369
|
}
|
|
1363
1370
|
// REPO-HEALTH BASELINE, taken here because the checkpoint above just made
|
|
1364
|
-
// the tree clean: what the project's own
|
|
1371
|
+
// the tree clean: what the project's own checks say now is what this task
|
|
1365
1372
|
// INHERITED, and the verify gate attributes a red check against it instead
|
|
1366
1373
|
// of failing the task for a sibling's defect (health-baseline.ts). The
|
|
1367
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
|
}
|
|
@@ -0,0 +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
|
+
*/
|
|
21
|
+
export declare function defersBreakage(answer: string): boolean;
|
|
22
|
+
/** The one re-ask a deferring answer gets before it is surfaced instead of promoted. */
|
|
23
|
+
export declare function deferredBreakageReaskHint(answer: string): string;
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
*/
|
|
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
|
+
}
|
|
62
|
+
];
|
|
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
|
+
}
|
|
83
|
+
export function defersBreakage(answer) {
|
|
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;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/** The one re-ask a deferring answer gets before it is surfaced instead of promoted. */
|
|
104
|
+
export function deferredBreakageReaskHint(answer) {
|
|
105
|
+
return ('[SYSTEM NOTE: Your previous answer deferred a breakage to someone who does not '
|
|
106
|
+
+ `exist — "${answer.replace(/\s+/g, ' ').slice(0, 160)}". No later step owns a `
|
|
107
|
+
+ 'failing test, lint, or build; each one inherits it and is told not to touch it. '
|
|
108
|
+
+ 'Re-run the GREEN-SUITE CHECK: answer with the option that keeps the suite green, '
|
|
109
|
+
+ 'naming the test file this task must also update. Output ONLY the tagged lines.]');
|
|
110
|
+
}
|
package/dist/task/final-gate.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
*/
|
|
47
47
|
import { existsSync, readFileSync } from 'node:fs';
|
|
48
48
|
import * as path from 'node:path';
|
|
49
|
-
import { runRepoHealthCheck, discoverHealthCommands } from './repo-health-check.js';
|
|
49
|
+
import { runRepoHealthCheck, discoverHealthCommands, discoverTestCommands } from './repo-health-check.js';
|
|
50
50
|
import { deriveOpenDebts, rerunDebtVerifyCommand } from './accept-debt.js';
|
|
51
51
|
import { readDeclaredScripts, missingDeclaredScripts, runnableDeclaredScripts } from './launch-contract.js';
|
|
52
52
|
import { readLaunchManifest, inertLaunchContractNote, packageScripts, makeHasTarget } from './launch-manifest.js';
|
|
@@ -83,58 +83,21 @@ import { VERIFY_FAIL_PREFIX } from './verify-work.js';
|
|
|
83
83
|
* tool-by-tool is not the fix either.
|
|
84
84
|
*/
|
|
85
85
|
export function discoverIntegrationCommands(cwd) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// project's only browser-executing suite is often `test:ct` or similar, and
|
|
91
|
-
// looking for `test` alone never runs it. Plain `test` leads, then every
|
|
92
|
-
// `test:`/`test_`/`test-` prefixed name in declaration order (Array#sort is
|
|
93
|
-
// stable), then `build`. Measured on a manifest declaring test:ct, build,
|
|
94
|
-
// test_unit, test-e2e, test, testing and pretest, the result is exactly
|
|
95
|
-
// test, test:ct, test_unit, test-e2e, build — `testing` and `pretest` do
|
|
96
|
-
// not match. Env-gap SKIP still applies per command: a suite whose browser
|
|
97
|
-
// or runtime is absent skips rather than fails (see runGateCommand).
|
|
98
|
-
const testNames = Object.keys(s).filter(n => n === 'test' || /^test[:_-]/.test(n));
|
|
99
|
-
testNames.sort((a, b) => a === 'test' ? -1
|
|
100
|
-
: b === 'test' ? 1
|
|
101
|
-
: 0);
|
|
102
|
-
for (const name of testNames)
|
|
103
|
-
cmds.push(['bun', ['run', name]]);
|
|
104
|
-
if (s.build)
|
|
105
|
-
cmds.push(['bun', ['run', 'build']]);
|
|
106
|
-
return { ecosystem: 'package.json', cmds };
|
|
107
|
-
}
|
|
108
|
-
if (existsSync(path.join(cwd, 'Makefile'))) {
|
|
109
|
-
const cmds = [];
|
|
110
|
-
for (const target of ['test', 'build']) {
|
|
111
|
-
if (makeHasTarget(cwd, target))
|
|
112
|
-
cmds.push(['make', [target]]);
|
|
86
|
+
const tests = discoverTestCommands(cwd);
|
|
87
|
+
const build = (() => {
|
|
88
|
+
if (existsSync(path.join(cwd, 'package.json'))) {
|
|
89
|
+
return packageScripts(cwd).build ? ['bun', ['run', 'build']] : null;
|
|
113
90
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
if (existsSync(path.join(cwd, 'go.mod'))) {
|
|
126
|
-
return {
|
|
127
|
-
ecosystem: 'go.mod',
|
|
128
|
-
cmds: [
|
|
129
|
-
['go', ['test', './...']],
|
|
130
|
-
['go', ['build', './...']]
|
|
131
|
-
]
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
if (existsSync(path.join(cwd, 'pyproject.toml'))) {
|
|
135
|
-
return { ecosystem: 'pyproject.toml', cmds: [['pytest', ['-q']]] };
|
|
136
|
-
}
|
|
137
|
-
return { ecosystem: null, cmds: [] };
|
|
91
|
+
if (existsSync(path.join(cwd, 'Makefile'))) {
|
|
92
|
+
return makeHasTarget(cwd, 'build') ? ['make', ['build']] : null;
|
|
93
|
+
}
|
|
94
|
+
if (existsSync(path.join(cwd, 'Cargo.toml')))
|
|
95
|
+
return ['cargo', ['build', '--quiet']];
|
|
96
|
+
if (existsSync(path.join(cwd, 'go.mod')))
|
|
97
|
+
return ['go', ['build', './...']];
|
|
98
|
+
return null;
|
|
99
|
+
})();
|
|
100
|
+
return { ecosystem: tests.ecosystem, cmds: build ? [...tests.cmds, build] : tests.cmds };
|
|
138
101
|
}
|
|
139
102
|
/**
|
|
140
103
|
* Per-ecosystem lockfile↔manifest consistency checks. A check applies only when
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -537,6 +537,9 @@ export async function healthBaselineFor(cwd, taskId, signal) {
|
|
|
537
537
|
return stored;
|
|
538
538
|
const fresh = await lazyHealthBaseline({
|
|
539
539
|
git: makeGit(cwd, signal),
|
|
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.
|
|
540
543
|
runHealthIn: dir => runRepoHealthCheck(dir, { signal })
|
|
541
544
|
});
|
|
542
545
|
if (fresh) {
|
|
@@ -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,11 +764,8 @@ export function buildGateDeps(params) {
|
|
|
737
764
|
startedAt,
|
|
738
765
|
lastLine: running ? `repo health · ${running}` : 'repo health'
|
|
739
766
|
}));
|
|
740
|
-
return
|
|
741
|
-
|
|
742
|
-
onCommand: c => {
|
|
743
|
-
running = c;
|
|
744
|
-
}
|
|
767
|
+
return gateHealth(cwd2, c => {
|
|
768
|
+
running = c;
|
|
745
769
|
}).finally(stop);
|
|
746
770
|
};
|
|
747
771
|
// Adapter onto the shared gate-child runner (gate-child.ts). What survives
|
|
@@ -934,20 +958,19 @@ export function buildGateDeps(params) {
|
|
|
934
958
|
onStage: label => {
|
|
935
959
|
stageLine = label;
|
|
936
960
|
},
|
|
937
|
-
// Deterministic whole-repo
|
|
938
|
-
//
|
|
939
|
-
//
|
|
961
|
+
// Deterministic whole-repo gate — runs the project's own
|
|
962
|
+
// lint/typecheck AND its test suite, independent of the
|
|
963
|
+
// model-authored VERIFY block, which may not lint at all. The suite
|
|
964
|
+
// is judged only against the baseline below (a task that turned a
|
|
965
|
+
// green suite red is this task's FAIL, whatever its spec says). ASYNC,
|
|
940
966
|
// so the lint does not starve pi-tui's nextTick-scheduled renders.
|
|
941
967
|
//
|
|
942
968
|
// ONE call, both arms. A baseline arm that dropped the signal or the
|
|
943
969
|
// progress hook here would be uncancellable and mute for reasons
|
|
944
970
|
// unrelated to the thing under test. The arm's only difference is
|
|
945
971
|
// the LOADER, above.
|
|
946
|
-
repoHealth: () =>
|
|
947
|
-
|
|
948
|
-
onCommand: c => {
|
|
949
|
-
stageLine = `repo health · ${c}`;
|
|
950
|
-
}
|
|
972
|
+
repoHealth: () => gateHealth(cwd2, c => {
|
|
973
|
+
stageLine = `repo health · ${c}`;
|
|
951
974
|
}),
|
|
952
975
|
// What those checks said before the task started, so a red one
|
|
953
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)`
|