@mjasnikovs/pi-task 0.18.39 → 0.18.41
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 +18 -1
- package/dist/task/accept-debt.js +20 -1
- package/dist/task/auto-io.d.ts +16 -0
- package/dist/task/auto-io.js +54 -0
- package/dist/task/auto-orchestrator.js +93 -5
- package/dist/task/auto-prompts.d.ts +5 -1
- package/dist/task/auto-prompts.js +7 -2
- package/dist/task/batch-test-task.d.ts +71 -0
- package/dist/task/batch-test-task.js +320 -0
- package/dist/task/gate-deps.js +36 -1
- package/dist/task/root-cause-repair.d.ts +122 -0
- package/dist/task/root-cause-repair.js +378 -0
- package/dist/task/task-gates.d.ts +31 -0
- package/dist/task/task-gates.js +58 -1
- package/package.json +1 -1
|
@@ -29,8 +29,16 @@
|
|
|
29
29
|
* `lsof` and the sandbox had neither) and no further attempt can move it. The
|
|
30
30
|
* run is allowed to converge on the REMAINING checks, carrying this one here
|
|
31
31
|
* so the next run's gate re-checks and re-surfaces it rather than losing it.
|
|
32
|
+
* - 'root-cause' — the task's verify FAILed because of a PRE-EXISTING defect in a
|
|
33
|
+
* file a DIFFERENT task created, which this task's own work never touched (mx5
|
|
34
|
+
* run 14: TASK_0007's `test/teardown.ts` TRUNCATE bug FAILed TASK_0013 and
|
|
35
|
+
* TASK_0019). The current task is not at fault, so its work — and, at the
|
|
36
|
+
* enforce site, the enforce pass's edits — are KEPT rather than reverted; the
|
|
37
|
+
* defect is recorded here and a scoped repair task is queued into the plan
|
|
38
|
+
* (root-cause-repair.ts). Before this class existed the ledger recorded the same
|
|
39
|
+
* root cause twice and nothing ever scheduled a fix, so it survived ~24h.
|
|
32
40
|
*/
|
|
33
|
-
export type DebtOrigin = 'accepted' | 'enforce-revert' | 'frozen-blocked' | 'cross-task-deletion' | 'yolo-accepted' | 'final-gate';
|
|
41
|
+
export type DebtOrigin = 'accepted' | 'enforce-revert' | 'frozen-blocked' | 'cross-task-deletion' | 'yolo-accepted' | 'final-gate' | 'root-cause';
|
|
34
42
|
/** One recorded defect: the task, why its VERIFY failed, and how it was recorded. */
|
|
35
43
|
export interface AcceptDebt {
|
|
36
44
|
taskId: string;
|
|
@@ -105,6 +113,15 @@ export declare function recordYoloAcceptDebt(cwd: string, taskId: string, reason
|
|
|
105
113
|
* The taskId is the run's parent id — the demotion is a run-level decision.
|
|
106
114
|
*/
|
|
107
115
|
export declare function recordFinalGateUnobservedDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Record a ROOT-CAUSE debt (mx5 run 14 / PROMPT item 5): this task's verify FAILed
|
|
118
|
+
* on a pre-existing defect in a file ANOTHER task created and this task never
|
|
119
|
+
* touched. The current task is not at fault — its work (and, at the enforce site,
|
|
120
|
+
* the enforce pass's edits) is KEPT — but the defect is real and still in the tree,
|
|
121
|
+
* so it is recorded here and a scoped repair task is queued (root-cause-repair.ts).
|
|
122
|
+
* Behavioral/model-judged, so the final gate surfaces it rather than auto-closing it.
|
|
123
|
+
*/
|
|
124
|
+
export declare function recordRootCauseDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
108
125
|
/**
|
|
109
126
|
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
110
127
|
* recordCrossTaskDeletionDebt writes). Null on any other reason text — an
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -78,7 +78,8 @@ export function parseAcceptDebts(raw) {
|
|
|
78
78
|
|| origin === 'frozen-blocked'
|
|
79
79
|
|| origin === 'cross-task-deletion'
|
|
80
80
|
|| origin === 'yolo-accepted'
|
|
81
|
-
|| origin === 'final-gate'
|
|
81
|
+
|| origin === 'final-gate'
|
|
82
|
+
|| origin === 'root-cause') ?
|
|
82
83
|
{ origin: origin }
|
|
83
84
|
: {})
|
|
84
85
|
});
|
|
@@ -203,6 +204,21 @@ export async function recordFinalGateUnobservedDebt(cwd, taskId, reason) {
|
|
|
203
204
|
origin: 'final-gate'
|
|
204
205
|
});
|
|
205
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Record a ROOT-CAUSE debt (mx5 run 14 / PROMPT item 5): this task's verify FAILed
|
|
209
|
+
* on a pre-existing defect in a file ANOTHER task created and this task never
|
|
210
|
+
* touched. The current task is not at fault — its work (and, at the enforce site,
|
|
211
|
+
* the enforce pass's edits) is KEPT — but the defect is real and still in the tree,
|
|
212
|
+
* so it is recorded here and a scoped repair task is queued (root-cause-repair.ts).
|
|
213
|
+
* Behavioral/model-judged, so the final gate surfaces it rather than auto-closing it.
|
|
214
|
+
*/
|
|
215
|
+
export async function recordRootCauseDebt(cwd, taskId, reason) {
|
|
216
|
+
await appendDebt(cwd, {
|
|
217
|
+
taskId: taskId.trim(),
|
|
218
|
+
reason: normaliseReason(reason),
|
|
219
|
+
origin: 'root-cause'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
206
222
|
/**
|
|
207
223
|
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
208
224
|
* recordCrossTaskDeletionDebt writes). Null on any other reason text — an
|
|
@@ -357,6 +373,9 @@ export function describeDebt(d) {
|
|
|
357
373
|
if (d.origin === 'yolo-accepted') {
|
|
358
374
|
return 'auto-ACCEPTED by YOLO mode despite verify-FAIL (unattended — no human weighed this)';
|
|
359
375
|
}
|
|
376
|
+
if (d.origin === 'root-cause') {
|
|
377
|
+
return "verify FAILed on a PRE-EXISTING defect in another task's file that this task never touched (this task's work was kept; a scoped repair task was queued for the root cause)";
|
|
378
|
+
}
|
|
360
379
|
if (d.origin === 'final-gate') {
|
|
361
380
|
return 'final-gate check DEMOTED to UNOBSERVED (identical failure across two tree-changing fix attempts — unfalsifiable in that environment, never proven passing)';
|
|
362
381
|
}
|
package/dist/task/auto-io.d.ts
CHANGED
|
@@ -42,5 +42,21 @@ export declare function checkOffTask(cwd: string, id: string, index: number, pro
|
|
|
42
42
|
* of starting a brand-new task — matching how /task-resume behaves.
|
|
43
43
|
*/
|
|
44
44
|
export declare function stampTaskInProgress(cwd: string, id: string, index: number, producedId: string, title: string): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Insert a NEW unchecked entry directly after the `afterIndex`th checkbox — the
|
|
47
|
+
* mid-run plan mutation the root-cause repair channel needs (mx5 run 14 item 5:
|
|
48
|
+
* a repair task must land BEFORE the next dependent task, not at the end of the
|
|
49
|
+
* plan, or the defect keeps failing everything in between).
|
|
50
|
+
*
|
|
51
|
+
* MONOTONIC by construction (the run-12 replacement lesson): this only ever
|
|
52
|
+
* SPLICES a line in. No existing entry is rewritten, reordered, or dropped, and
|
|
53
|
+
* an already-present title is a no-op — so a plan can grow mid-run but never
|
|
54
|
+
* shrink, and a retried insert cannot duplicate. Returns whether a line was added.
|
|
55
|
+
*
|
|
56
|
+
* Later entries shift down by one, which is safe because the /task-auto loop
|
|
57
|
+
* re-reads and re-parses the plan at the top of every iteration and locates its
|
|
58
|
+
* next step by "first unchecked" rather than by a cached index.
|
|
59
|
+
*/
|
|
60
|
+
export declare function insertTaskAfter(cwd: string, id: string, afterIndex: number, title: string): Promise<boolean>;
|
|
45
61
|
/** Find the most-recently-updated resumable TASK_AUTO_* file, or null. */
|
|
46
62
|
export declare function findResumableAuto(cwd: string): Promise<string | null>;
|
package/dist/task/auto-io.js
CHANGED
|
@@ -140,6 +140,60 @@ export async function checkOffTask(cwd, id, index, producedId, title) {
|
|
|
140
140
|
export async function stampTaskInProgress(cwd, id, index, producedId, title) {
|
|
141
141
|
await rewriteTaskLine(cwd, id, index, () => `- [ ] ${producedId} ${title}`, 'stampTaskInProgress');
|
|
142
142
|
}
|
|
143
|
+
/** The bare title of a checkbox line (id stamp stripped), or null if not one. */
|
|
144
|
+
function entryTitle(line) {
|
|
145
|
+
const m = CHECKBOX_RE.exec(line.trim());
|
|
146
|
+
if (!m)
|
|
147
|
+
return null;
|
|
148
|
+
const rest = m[2].trim();
|
|
149
|
+
const idm = PRODUCED_ID_RE.exec(rest);
|
|
150
|
+
return idm ? idm[2].trim() : rest;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Insert a NEW unchecked entry directly after the `afterIndex`th checkbox — the
|
|
154
|
+
* mid-run plan mutation the root-cause repair channel needs (mx5 run 14 item 5:
|
|
155
|
+
* a repair task must land BEFORE the next dependent task, not at the end of the
|
|
156
|
+
* plan, or the defect keeps failing everything in between).
|
|
157
|
+
*
|
|
158
|
+
* MONOTONIC by construction (the run-12 replacement lesson): this only ever
|
|
159
|
+
* SPLICES a line in. No existing entry is rewritten, reordered, or dropped, and
|
|
160
|
+
* an already-present title is a no-op — so a plan can grow mid-run but never
|
|
161
|
+
* shrink, and a retried insert cannot duplicate. Returns whether a line was added.
|
|
162
|
+
*
|
|
163
|
+
* Later entries shift down by one, which is safe because the /task-auto loop
|
|
164
|
+
* re-reads and re-parses the plan at the top of every iteration and locates its
|
|
165
|
+
* next step by "first unchecked" rather than by a cached index.
|
|
166
|
+
*/
|
|
167
|
+
export async function insertTaskAfter(cwd, id, afterIndex, title) {
|
|
168
|
+
const clean = title.trim();
|
|
169
|
+
if (clean.length === 0)
|
|
170
|
+
return false;
|
|
171
|
+
const { body } = await readTaskFile(cwd, id);
|
|
172
|
+
const section = extractSection(body, 'tasks') ?? '';
|
|
173
|
+
const lines = section.split('\n');
|
|
174
|
+
// Duplicate check scans the WHOLE list first: an existing entry with this exact
|
|
175
|
+
// title (checked or not) means the plan already carries this step, wherever it
|
|
176
|
+
// sits relative to afterIndex — never add a second one.
|
|
177
|
+
if (lines.some(l => entryTitle(l) === clean))
|
|
178
|
+
return false;
|
|
179
|
+
let seen = -1;
|
|
180
|
+
let insertAt = -1;
|
|
181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
182
|
+
if (!CHECKBOX_RE.test(lines[i].trim()))
|
|
183
|
+
continue;
|
|
184
|
+
seen++;
|
|
185
|
+
insertAt = i + 1;
|
|
186
|
+
if (seen === afterIndex)
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
// An out-of-range index appends after the LAST checkbox rather than throwing:
|
|
190
|
+
// a plan that grew underneath the caller must still receive the entry.
|
|
191
|
+
if (insertAt === -1)
|
|
192
|
+
return false;
|
|
193
|
+
lines.splice(insertAt, 0, `- [ ] ${clean}`);
|
|
194
|
+
await setTaskSection(cwd, id, 'tasks', lines.join('\n'));
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
143
197
|
/** Find the most-recently-updated resumable TASK_AUTO_* file, or null. */
|
|
144
198
|
export async function findResumableAuto(cwd) {
|
|
145
199
|
await ensureTasksDir(cwd);
|
|
@@ -14,7 +14,8 @@ import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js'
|
|
|
14
14
|
import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT, DECOMPOSE_COVERAGE_PROMPT } from './auto-prompts.js';
|
|
15
15
|
import { GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT } from './prompts.js';
|
|
16
16
|
import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './question-dedup.js';
|
|
17
|
-
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
|
|
17
|
+
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, parseTaskList, checkOffTask, stampTaskInProgress, insertTaskAfter, findResumableAuto } from './auto-io.js';
|
|
18
|
+
import { drainRepairQueue, mergeRepairCandidates, planHasRepairFor, parseRepairTitleFile, buildRepairTitle, buildRepairScopeFence, extractFailingCommand } from './root-cause-repair.js';
|
|
18
19
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
|
|
19
20
|
import { readTextFile } from '../shared/fs-text.js';
|
|
20
21
|
import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
@@ -38,6 +39,7 @@ import { isYoloMode, yoloPickAnswer, yoloFinalGateChoice, YOLO_STAMP } from './y
|
|
|
38
39
|
import { configureResearchRun, resumeResearchRun } from '../workers/research-cache.js';
|
|
39
40
|
import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, appendContracts } from './contracts.js';
|
|
40
41
|
import { reconcileTitleSources } from './decompose-fidelity.js';
|
|
42
|
+
import { mandatesTestsInSameChange, rewriteBatchTestPlan } from './batch-test-task.js';
|
|
41
43
|
import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, isCrossCuttingRequirement, appendCarriedRequirements, buildRequirementsLedger } from './requirements.js';
|
|
42
44
|
import { decideAdoption, groundedCoverage } from './coverage-loop.js';
|
|
43
45
|
import { findSpecDanglingArtifacts, titlesCoverArtifact, danglingMissingText, danglingCarryText } from './artifact-closure.js';
|
|
@@ -320,6 +322,63 @@ export function buildScopeFence(titles, currentIndex) {
|
|
|
320
322
|
+ `components, or flows owned by a later step.\n\n`
|
|
321
323
|
+ `The full plan (these run separately — do NOT implement them here):\n${listing}`);
|
|
322
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* The scope fence for step `currentIndex`, plus the REPAIR fence when that step is
|
|
327
|
+
* a queued root-cause repair. A repair title ("repair test/teardown.ts: …") reads
|
|
328
|
+
* to refine like any other feature step, and refine's job is to expand a title into
|
|
329
|
+
* a full spec — which is exactly how "repair the teardown" becomes "overhaul the
|
|
330
|
+
* test infrastructure" (the /task-auto drift lesson). The extra fence pins the one
|
|
331
|
+
* editable file and pins VERIFY to the command the defect was failing.
|
|
332
|
+
*/
|
|
333
|
+
function buildStepFence(titles, currentIndex) {
|
|
334
|
+
const base = buildScopeFence(titles, currentIndex);
|
|
335
|
+
const repairFile = parseRepairTitleFile(titles[currentIndex] ?? '');
|
|
336
|
+
if (!repairFile)
|
|
337
|
+
return base;
|
|
338
|
+
return `${base}\n\n${buildRepairScopeFence(repairFile, extractFailingCommand(titles[currentIndex] ?? ''))}`;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Drain the gate's root-cause repair queue into the running plan: one scoped
|
|
342
|
+
* repair step per accused FILE, spliced in directly after the step that just
|
|
343
|
+
* finished.
|
|
344
|
+
*
|
|
345
|
+
* Three bounds, all mandatory (mx5 run 14 item 5 gray areas):
|
|
346
|
+
* - DEDUP by file — run 14's two `test/teardown.ts` debts must yield ONE repair
|
|
347
|
+
* step, not two. mergeRepairCandidates collapses the drained queue, and
|
|
348
|
+
* planHasRepairFor rejects a file the plan already carries a repair for.
|
|
349
|
+
* - CAP 1 per file per RUN — planHasRepairFor counts CHECKED-OFF entries too, so
|
|
350
|
+
* a repair step that itself failed is never re-spawned; it lands in the
|
|
351
|
+
* accept-debt ledger like any other task. That is what stops a repair loop.
|
|
352
|
+
* - MONOTONIC — insertTaskAfter only splices; no existing entry is rewritten,
|
|
353
|
+
* reordered or dropped (the run-12 replacement lesson).
|
|
354
|
+
*
|
|
355
|
+
* Best-effort throughout: a fault here must never fail the run that produced the
|
|
356
|
+
* finding — the debt is already durably recorded either way.
|
|
357
|
+
*/
|
|
358
|
+
async function schedulePendingRepairs(cwd, id, afterIndex, ctx, deps) {
|
|
359
|
+
try {
|
|
360
|
+
const pending = await drainRepairQueue(cwd);
|
|
361
|
+
if (pending.length === 0)
|
|
362
|
+
return;
|
|
363
|
+
const { body } = await readTaskFile(cwd, id);
|
|
364
|
+
const titles = parseTaskList(body).map(e => e.title);
|
|
365
|
+
let at = afterIndex;
|
|
366
|
+
for (const repair of mergeRepairCandidates(pending)) {
|
|
367
|
+
if (planHasRepairFor(titles, repair.file))
|
|
368
|
+
continue;
|
|
369
|
+
const title = buildRepairTitle(repair);
|
|
370
|
+
if (!(await insertTaskAfter(cwd, id, at, title)))
|
|
371
|
+
continue;
|
|
372
|
+
titles.splice(at + 1, 0, title);
|
|
373
|
+
at += 1;
|
|
374
|
+
await deps.record?.(cwd, id, `plan: inserted scoped repair step after step ${afterIndex + 1} — ${title}`);
|
|
375
|
+
ctx.ui.notify(`${id}: queued a scoped repair for ${repair.file} (${repair.owner}'s file — root cause of ${repair.blamed.join(', ')}).`, 'warning');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// the plan is best-effort here; the underlying debt is already recorded
|
|
380
|
+
}
|
|
381
|
+
}
|
|
323
382
|
/** Plan phase: clarify → decompose → write AUTO file. Returns the new id, or null. */
|
|
324
383
|
export async function planAuto(ctx, cwd, feature, deps) {
|
|
325
384
|
// clarify — sequential & adaptive: ask one question at a time, feeding every
|
|
@@ -532,8 +591,18 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
532
591
|
catch {
|
|
533
592
|
// best-effort channel
|
|
534
593
|
}
|
|
594
|
+
// Tests-in-the-same-change cadence (mx5 run 14, PROMPT item 6): when the
|
|
595
|
+
// decisions mandate it, a whole-project batch test task contradicts them —
|
|
596
|
+
// run 14 shipped one anyway (TASK_0037, 4.7h, yolo-accepted FAIL) because
|
|
597
|
+
// decompose mirrors the spec's milestone shape. The decisions channel
|
|
598
|
+
// OVERRIDES the spec doc, so this resolves toward the decision without asking.
|
|
599
|
+
const noBatchTests = mandatesTestsInSameChange(clarifications, featureForModel);
|
|
600
|
+
if (noBatchTests) {
|
|
601
|
+
logPlanDebug(cwd, 'decisions mandate tests-in-the-same-change — batch test tasks are banned '
|
|
602
|
+
+ 'from this plan (prompt rule + host rewrite)');
|
|
603
|
+
}
|
|
535
604
|
// decompose
|
|
536
|
-
const decomposePrompt = AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications, buildRequirementsLedger(reqEntries));
|
|
605
|
+
const decomposePrompt = AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications, buildRequirementsLedger(reqEntries), noBatchTests);
|
|
537
606
|
// Parse + FIDELITY RECONCILIATION (mx5 run 11, goal B): ground each title's
|
|
538
607
|
// [source: "…"] citation against the doc, strip the clause, and re-attach any
|
|
539
608
|
// `+`-joined constraint fragment the paraphrased title dropped (the silently
|
|
@@ -548,7 +617,18 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
548
617
|
.map(r => ` [task ${r.index + 1}: ${r.fragments.join(', ')}]`)
|
|
549
618
|
.join(''));
|
|
550
619
|
}
|
|
551
|
-
|
|
620
|
+
// Batch-test ban (item 6): drop or scope a whole-project "write all the
|
|
621
|
+
// tests" task. Identity unless the cadence decision is present, and the
|
|
622
|
+
// sweep replacement re-grounds every requirement the drop would cost — so
|
|
623
|
+
// planned coverage cannot fall (run 12's lesson).
|
|
624
|
+
const debatched = rewriteBatchTestPlan(plan.titles, clarifications, featureForModel, reqEntries.map(e => e.quote), isCrossCuttingRequirement);
|
|
625
|
+
for (const a of debatched.actions) {
|
|
626
|
+
logPlanDebug(cwd, `batch test task ${a.kind} (tests-in-same-change decision): "${a.title}"`
|
|
627
|
+
+ (a.kind === 'scoped' ?
|
|
628
|
+
` → scoped sweep over ${a.orphaned.length} orphaned requirement(s)`
|
|
629
|
+
: ' — every requirement it touched is owned by another task'));
|
|
630
|
+
}
|
|
631
|
+
return debatched.titles;
|
|
552
632
|
};
|
|
553
633
|
const listRaw = await deps.runChild('auto-decompose', 'read', decomposePrompt);
|
|
554
634
|
let planTitles = parsePlan(listRaw);
|
|
@@ -1341,7 +1421,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1341
1421
|
// matters when refine runs fresh (a resumed task past refine ignores
|
|
1342
1422
|
// it), but always supplied so a resume that restarts at refine is
|
|
1343
1423
|
// fenced too.
|
|
1344
|
-
planContext:
|
|
1424
|
+
planContext: buildStepFence(entries.map(e => e.title), next.index),
|
|
1345
1425
|
onStart: resumeId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
|
|
1346
1426
|
});
|
|
1347
1427
|
active = res.ctx ?? active;
|
|
@@ -1396,7 +1476,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1396
1476
|
title: next.title,
|
|
1397
1477
|
tag: id,
|
|
1398
1478
|
// Fence an AUTOFIX re-run against re-expanding the whole spec.
|
|
1399
|
-
planContext:
|
|
1479
|
+
planContext: buildStepFence(entries.map(e => e.title), next.index),
|
|
1400
1480
|
// res.ok === true means runner.run() completed, so res.taskId is the
|
|
1401
1481
|
// allocated TASK_NNNN id (never empty here). The parent task-list
|
|
1402
1482
|
// check-off runs after verify passes/accepts and before the commit,
|
|
@@ -1426,6 +1506,14 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1426
1506
|
announceDone(active, `${id} stopped at "${next.title}"${why} — fix and run /task-auto-resume.`, 'error');
|
|
1427
1507
|
return;
|
|
1428
1508
|
}
|
|
1509
|
+
// ROOT-CAUSE REPAIR (mx5 run 14 item 5): the gate may have attributed a
|
|
1510
|
+
// FAIL to a pre-existing defect in a file some OTHER task created. It can
|
|
1511
|
+
// only QUEUE that finding — mutating the plan is this loop's job. Drain
|
|
1512
|
+
// the queue and splice a scoped repair step in right after the step that
|
|
1513
|
+
// just finished, so the defect is fixed BEFORE the next dependent task
|
|
1514
|
+
// trips over it too (run 14 recorded the same `test/teardown.ts` cause
|
|
1515
|
+
// twice, scheduled nothing, and the bug outlived ~24h of the run).
|
|
1516
|
+
await schedulePendingRepairs(cwd, id, next.index, active, deps);
|
|
1429
1517
|
// gate.kind === 'done' → fall through to the next task, after checking
|
|
1430
1518
|
// no landmine stash was left behind by anything that ran in between.
|
|
1431
1519
|
if (deps.stashRef && stashBefore !== undefined) {
|
|
@@ -12,8 +12,12 @@ export declare const AUTO_CLARIFY_PROMPT: (feature: string, priorQA: string) =>
|
|
|
12
12
|
* belt): the spec's obligations ride into decompose explicitly, so mirroring the
|
|
13
13
|
* spec's own milestone/section structure cannot silently discharge them. '' ⇒
|
|
14
14
|
* the prompt is unchanged.
|
|
15
|
+
*
|
|
16
|
+
* `noBatchTests` adds the anti-batch-test rule (batch-test-task.ts) — emitted ONLY
|
|
17
|
+
* when the decisions mandate tests-in-the-same-change, so every other run sees the
|
|
18
|
+
* prompt it always saw. It is the belt; the host-side rewrite is the lever.
|
|
15
19
|
*/
|
|
16
|
-
export declare const AUTO_DECOMPOSE_PROMPT: (feature: string, clarifications: string, requirementsLedger?: string) => string;
|
|
20
|
+
export declare const AUTO_DECOMPOSE_PROMPT: (feature: string, clarifications: string, requirementsLedger?: string, noBatchTests?: boolean) => string;
|
|
17
21
|
/**
|
|
18
22
|
* Coverage triage: judge whether a decomposed task list covers the whole
|
|
19
23
|
* feature. Guards the plan — the highest-leverage artifact in /task-auto —
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* LIST only; all research/spec depth is /task's job, run per-title later.
|
|
4
4
|
*/
|
|
5
5
|
import { DECOMPOSE_SOURCE_RULE } from './decompose-fidelity.js';
|
|
6
|
+
import { DECOMPOSE_NO_BATCH_TESTS_RULE } from './batch-test-task.js';
|
|
6
7
|
/**
|
|
7
8
|
* Clarify: asks ONE question at a time. Output MUST match parseClarifyList — a
|
|
8
9
|
* single numbered question followed by a "SUGGESTED: <default>" line, an optional
|
|
@@ -71,8 +72,12 @@ NONE`;
|
|
|
71
72
|
* belt): the spec's obligations ride into decompose explicitly, so mirroring the
|
|
72
73
|
* spec's own milestone/section structure cannot silently discharge them. '' ⇒
|
|
73
74
|
* the prompt is unchanged.
|
|
75
|
+
*
|
|
76
|
+
* `noBatchTests` adds the anti-batch-test rule (batch-test-task.ts) — emitted ONLY
|
|
77
|
+
* when the decisions mandate tests-in-the-same-change, so every other run sees the
|
|
78
|
+
* prompt it always saw. It is the belt; the host-side rewrite is the lever.
|
|
74
79
|
*/
|
|
75
|
-
export const AUTO_DECOMPOSE_PROMPT = (feature, clarifications, requirementsLedger = '') => `Split this feature into an ordered list of implementation tasks. Each task
|
|
80
|
+
export const AUTO_DECOMPOSE_PROMPT = (feature, clarifications, requirementsLedger = '', noBatchTests = false) => `Split this feature into an ordered list of implementation tasks. Each task
|
|
76
81
|
will be handed, by its title, to a separate pipeline that does its own research
|
|
77
82
|
and writes its own spec — so here you produce TITLES ONLY, not specs.
|
|
78
83
|
|
|
@@ -94,7 +99,7 @@ RULES:
|
|
|
94
99
|
none. These are explicit user choices that may contradict the referenced spec
|
|
95
100
|
doc; phrase them as imperative directives (e.g. "use Bun's built-in bundler, do
|
|
96
101
|
not add vite"). Do NOT invent decisions — only restate ones from CLARIFICATIONS.
|
|
97
|
-
${DECOMPOSE_SOURCE_RULE}
|
|
102
|
+
${DECOMPOSE_SOURCE_RULE}${noBatchTests ? `\n${DECOMPOSE_NO_BATCH_TESTS_RULE}` : ''}
|
|
98
103
|
- Output the checkbox list and NOTHING else (no preamble, no numbering).`;
|
|
99
104
|
/**
|
|
100
105
|
* Coverage triage: judge whether a decomposed task list covers the whole
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does the decisions/spec text mandate tests-in-the-same-change?
|
|
3
|
+
*
|
|
4
|
+
* Scans sentence by sentence and requires BOTH signals in the SAME sentence, so
|
|
5
|
+
* a testing section that happens to sit near an unrelated "don't defer" line
|
|
6
|
+
* cannot trigger the ban. `decisions` is checked first and alone is sufficient;
|
|
7
|
+
* the spec is scanned too because run 14's cadence rule is stated in §10 of the
|
|
8
|
+
* doc and only echoed into the decisions channel.
|
|
9
|
+
*/
|
|
10
|
+
export declare function mandatesTestsInSameChange(decisions: string, spec?: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Indices of titles that are whole-project BATCH test tasks.
|
|
13
|
+
*
|
|
14
|
+
* Three conditions must all hold — the title's deliverable is tests, its scope is
|
|
15
|
+
* the whole project, and it is not test INFRASTRUCTURE. A per-feature task that
|
|
16
|
+
* carries "+ tests" never matches (its head names the feature), which is the
|
|
17
|
+
* point: those are the cadence the decision asks for.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findBatchTestTitles(titles: string[]): number[];
|
|
20
|
+
/**
|
|
21
|
+
* The spec's own coverage source — the command whose report scopes the sweep.
|
|
22
|
+
*
|
|
23
|
+
* Only backticked spans are considered, so the result is a command the spec
|
|
24
|
+
* actually writes down rather than a phrase inferred from prose; a span carrying
|
|
25
|
+
* a coverage flag wins over a plain test run. Falls back to a generic phrase when
|
|
26
|
+
* the spec names no command, which keeps the sweep title well-formed either way.
|
|
27
|
+
*/
|
|
28
|
+
export declare function coverageSourceFromSpec(spec: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* The scoped replacement: a gap-filling sweep, bounded by what the coverage
|
|
31
|
+
* source reports and by the requirements that lost their only owner. It states
|
|
32
|
+
* the ban explicitly, because the child that receives this title sees nothing
|
|
33
|
+
* else.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildSweepTitle(orphaned: string[], coverageSource: string): string;
|
|
36
|
+
export interface BatchTestAction {
|
|
37
|
+
/** Index of the offending title in the INPUT list. */
|
|
38
|
+
index: number;
|
|
39
|
+
/** The offending title, verbatim. */
|
|
40
|
+
title: string;
|
|
41
|
+
kind: 'dropped' | 'scoped';
|
|
42
|
+
/** The sweep title that replaced it (`kind: 'scoped'` only). */
|
|
43
|
+
replacement?: string;
|
|
44
|
+
/** Requirement quotes that no OTHER title grounds — what the sweep must cover. */
|
|
45
|
+
orphaned: string[];
|
|
46
|
+
}
|
|
47
|
+
export interface BatchTestRewrite {
|
|
48
|
+
titles: string[];
|
|
49
|
+
actions: BatchTestAction[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Rewrite a decomposed plan so it carries no whole-project batch test task when
|
|
53
|
+
* the decisions mandate tests-in-the-same-change.
|
|
54
|
+
*
|
|
55
|
+
* No mandate, or no batch title ⇒ the plan is returned untouched (identity), so
|
|
56
|
+
* every non-cadence run behaves exactly as before.
|
|
57
|
+
*
|
|
58
|
+
* With a mandate, ALL batch titles are removed at once before coverage is
|
|
59
|
+
* re-measured — otherwise two batch tasks would each mask the other's orphans and
|
|
60
|
+
* both would look droppable. The orphaned set is then whatever grounded coverage
|
|
61
|
+
* the removal costs; it is non-empty only when no other task's title claims that
|
|
62
|
+
* requirement, and it becomes the sweep's scope. At most ONE sweep is emitted (in
|
|
63
|
+
* the first batch title's position, so plan order is preserved).
|
|
64
|
+
*/
|
|
65
|
+
export declare function rewriteBatchTestPlan(titles: string[], decisions: string, spec: string, requirementQuotes: string[], isCrossCutting: (quote: string) => boolean): BatchTestRewrite;
|
|
66
|
+
/**
|
|
67
|
+
* The decompose-prompt rule (the belt; the host rewrite above is the lever).
|
|
68
|
+
* Emitted ONLY when the decisions mandate the cadence, so ordinary runs see the
|
|
69
|
+
* prompt they always saw. Kept next to the detector so the two cannot drift.
|
|
70
|
+
*/
|
|
71
|
+
export declare const DECOMPOSE_NO_BATCH_TESTS_RULE: string;
|