@mjasnikovs/pi-task 0.18.14 → 0.18.16
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/README.md +2 -2
- package/dist/task/accept-debt.d.ts +28 -1
- package/dist/task/accept-debt.js +61 -3
- package/dist/task/auto-io.d.ts +4 -2
- package/dist/task/auto-io.js +6 -3
- package/dist/task/auto-orchestrator.d.ts +1 -0
- package/dist/task/auto-orchestrator.js +135 -19
- package/dist/task/auto-prompts.d.ts +5 -5
- package/dist/task/auto-prompts.js +9 -2
- package/dist/task/contracts.d.ts +8 -0
- package/dist/task/contracts.js +4 -2
- package/dist/task/decompose-fidelity.d.ts +47 -0
- package/dist/task/decompose-fidelity.js +132 -0
- package/dist/task/final-gate-fix.d.ts +22 -3
- package/dist/task/final-gate-fix.js +72 -7
- package/dist/task/final-gate.d.ts +48 -1
- package/dist/task/final-gate.js +182 -34
- package/dist/task/gate-deps.d.ts +7 -0
- package/dist/task/gate-deps.js +37 -1
- package/dist/task/launch-contract.d.ts +36 -1
- package/dist/task/launch-contract.js +80 -2
- package/dist/task/phases.d.ts +13 -1
- package/dist/task/phases.js +50 -11
- package/dist/task/prompts.js +2 -0
- package/dist/task/render-check.d.ts +32 -0
- package/dist/task/render-check.js +186 -0
- package/dist/task/requirements.d.ts +88 -0
- package/dist/task/requirements.js +331 -0
- package/dist/task/verify-reconcile.d.ts +36 -0
- package/dist/task/verify-reconcile.js +203 -0
- package/dist/task/write-guard.d.ts +52 -0
- package/dist/task/write-guard.js +112 -0
- package/package.json +1 -1
- package/dist/task/_ab.d.ts +0 -1
- package/dist/task/_ab.js +0 -68
- package/dist/task/task-file.d.ts +0 -14
- package/dist/task/task-file.js +0 -15
- package/dist/think-test/cli.d.ts +0 -1
- package/dist/think-test/cli.js +0 -98
- package/dist/think-test/client.d.ts +0 -26
- package/dist/think-test/client.js +0 -37
- package/dist/think-test/compressor.d.ts +0 -5
- package/dist/think-test/compressor.js +0 -25
- package/dist/think-test/judge.d.ts +0 -4
- package/dist/think-test/judge.js +0 -11
- package/dist/think-test/score.d.ts +0 -8
- package/dist/think-test/score.js +0 -22
- package/dist/think-test/serialize.d.ts +0 -19
- package/dist/think-test/serialize.js +0 -41
- package/dist/think-test/transcript.d.ts +0 -7
- package/dist/think-test/transcript.js +0 -41
- package/dist/think-test/transform.d.ts +0 -6
- package/dist/think-test/transform.js +0 -24
- package/dist/think-test/types.d.ts +0 -45
- package/dist/think-test/types.js +0 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/@mjasnikovs/pi-task)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
[](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
|
|
12
|
-
[](#development)
|
|
13
13
|
[](./tsconfig.json)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
@@ -175,7 +175,7 @@ Tasks are persisted to `<cwd>/.pi-tasks/TASK_NNNN.md`. Add `.pi-tasks/` to your
|
|
|
175
175
|
|
|
176
176
|
```sh
|
|
177
177
|
bun install
|
|
178
|
-
bun test src/ #
|
|
178
|
+
bun test src/ # 1617 tests across 106 files
|
|
179
179
|
bun run lint # prettier + eslint + tsc --noEmit
|
|
180
180
|
bun run build # tsc → dist/
|
|
181
181
|
```
|
|
@@ -14,6 +14,14 @@ export interface AcceptDebt {
|
|
|
14
14
|
reason: string;
|
|
15
15
|
/** Absent in legacy 2-field records → treated as 'accepted'. */
|
|
16
16
|
origin?: DebtOrigin;
|
|
17
|
+
/**
|
|
18
|
+
* Set (never serialized) when the recorded reason CONFLICTS with the run itself:
|
|
19
|
+
* it asserts a file's existence is the failure, but that file is another task's
|
|
20
|
+
* committed deliverable (see annotateDebtConflicts). A conflicting debt is a
|
|
21
|
+
* PLAN defect to surface, never an instruction to act on — mx5 run 11's autofix
|
|
22
|
+
* child `rm`'d TASK_0008's verified admin page to satisfy exactly such a claim.
|
|
23
|
+
*/
|
|
24
|
+
conflict?: string;
|
|
17
25
|
}
|
|
18
26
|
export declare function acceptDebtFile(cwd: string): string;
|
|
19
27
|
/** The raw stored ledger ('' when none recorded yet). Parse with parseAcceptDebts. */
|
|
@@ -61,10 +69,29 @@ export declare function recheckAcceptDebts(debts: AcceptDebt[], opts: {
|
|
|
61
69
|
open: AcceptDebt[];
|
|
62
70
|
resolved: AcceptDebt[];
|
|
63
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* Extract the paths whose EXISTENCE the reason asserts as the failure — a path
|
|
74
|
+
* token immediately followed by "exists" / "still exists", or by "must/should not
|
|
75
|
+
* exist". Only this narrow shape qualifies: a reason that merely MENTIONS a path
|
|
76
|
+
* (a prohibition violation, a broken import) is an ordinary defect claim, not an
|
|
77
|
+
* existence assertion, and must never be flagged (run 11's T1/T7 debts name paths
|
|
78
|
+
* this way and are genuine).
|
|
79
|
+
*/
|
|
80
|
+
export declare function extractExistenceClaims(reason: string): string[];
|
|
81
|
+
/**
|
|
82
|
+
* Annotate each debt whose existence-as-failure claim names a file INTRODUCED by a
|
|
83
|
+
* different task's commit (per `introducedBy`, typically git history) with a
|
|
84
|
+
* human-readable conflict statement. Everything degrades to no annotation: no
|
|
85
|
+
* existence claim, an unknown introducer, or the debt's own task introducing the
|
|
86
|
+
* file (then the claim is at least self-consistent) all pass through unchanged.
|
|
87
|
+
*/
|
|
88
|
+
export declare function annotateDebtConflicts(debts: AcceptDebt[], introducedBy: (path: string) => string | null): AcceptDebt[];
|
|
64
89
|
/**
|
|
65
90
|
* A one-line-per-debt suffix appended to the final gate's report reason so the still
|
|
66
91
|
* -open accepted defects surface in the gate outcome the user sees (and in the fail
|
|
67
|
-
* picker). Empty when nothing is open.
|
|
92
|
+
* picker). Empty when nothing is open. The header states the records' status
|
|
93
|
+
* explicitly: they are claims for the HUMAN, re-stated by the gate, never
|
|
94
|
+
* instructions — and a conflicting claim carries its contradiction inline.
|
|
68
95
|
*/
|
|
69
96
|
export declare function buildAcceptDebtNote(open: AcceptDebt[]): string;
|
|
70
97
|
/** One-line provenance label for a debt, for the surfaced report. */
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -182,17 +182,75 @@ export function recheckAcceptDebts(debts, opts) {
|
|
|
182
182
|
}
|
|
183
183
|
return { open, resolved };
|
|
184
184
|
}
|
|
185
|
+
// ─── Conflicting-claim classification (mx5 run 11) ──────────────────────────
|
|
186
|
+
//
|
|
187
|
+
// A recorded debt is a CLAIM about the tree, not an instruction to change it. The
|
|
188
|
+
// one class a deterministic check can prove SELF-CONTRADICTORY is an
|
|
189
|
+
// existence-as-failure claim ("<path> exists" fails the verify) whose named path is
|
|
190
|
+
// a DIFFERENT task's committed deliverable: the plan shipped the file on purpose
|
|
191
|
+
// and a sibling's verify indicts it — a plan defect (sibling scope-fence leaked
|
|
192
|
+
// into a verify assertion), not a fixable fault. Run 11's TASK_0009 debt read
|
|
193
|
+
// "src/client/pages/admin.tsx exists (introduced by prior TASK_0008…)" and the
|
|
194
|
+
// final-gate autofix child, seeded with it, ran `rm` on the verified page.
|
|
195
|
+
/** A path-like token: at least one directory separator, ending in a file name. */
|
|
196
|
+
const PATH_TOKEN_RE = /(?:[\w.@-]+\/)+[\w.@-]+\.\w+/g;
|
|
197
|
+
/**
|
|
198
|
+
* Extract the paths whose EXISTENCE the reason asserts as the failure — a path
|
|
199
|
+
* token immediately followed by "exists" / "still exists", or by "must/should not
|
|
200
|
+
* exist". Only this narrow shape qualifies: a reason that merely MENTIONS a path
|
|
201
|
+
* (a prohibition violation, a broken import) is an ordinary defect claim, not an
|
|
202
|
+
* existence assertion, and must never be flagged (run 11's T1/T7 debts name paths
|
|
203
|
+
* this way and are genuine).
|
|
204
|
+
*/
|
|
205
|
+
export function extractExistenceClaims(reason) {
|
|
206
|
+
const out = [];
|
|
207
|
+
for (const m of reason.matchAll(PATH_TOKEN_RE)) {
|
|
208
|
+
const after = reason.slice(m.index + m[0].length);
|
|
209
|
+
if (/^\s+(?:still\s+)?exists\b/.test(after)
|
|
210
|
+
|| /^\s+(?:must|should)\s+not\s+exist\b/.test(after)) {
|
|
211
|
+
out.push(m[0]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return [...new Set(out)];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Annotate each debt whose existence-as-failure claim names a file INTRODUCED by a
|
|
218
|
+
* different task's commit (per `introducedBy`, typically git history) with a
|
|
219
|
+
* human-readable conflict statement. Everything degrades to no annotation: no
|
|
220
|
+
* existence claim, an unknown introducer, or the debt's own task introducing the
|
|
221
|
+
* file (then the claim is at least self-consistent) all pass through unchanged.
|
|
222
|
+
*/
|
|
223
|
+
export function annotateDebtConflicts(debts, introducedBy) {
|
|
224
|
+
return debts.map(d => {
|
|
225
|
+
for (const p of extractExistenceClaims(d.reason)) {
|
|
226
|
+
const producer = introducedBy(p);
|
|
227
|
+
if (producer && producer !== d.taskId) {
|
|
228
|
+
return {
|
|
229
|
+
...d,
|
|
230
|
+
conflict: `\`${p}\` is ${producer}'s committed deliverable — this assertion `
|
|
231
|
+
+ `contradicts a sibling task's shipped work (a plan defect, not a fix `
|
|
232
|
+
+ `instruction); do NOT delete or rewrite that deliverable to satisfy it`
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return d;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
185
239
|
/**
|
|
186
240
|
* A one-line-per-debt suffix appended to the final gate's report reason so the still
|
|
187
241
|
* -open accepted defects surface in the gate outcome the user sees (and in the fail
|
|
188
|
-
* picker). Empty when nothing is open.
|
|
242
|
+
* picker). Empty when nothing is open. The header states the records' status
|
|
243
|
+
* explicitly: they are claims for the HUMAN, re-stated by the gate, never
|
|
244
|
+
* instructions — and a conflicting claim carries its contradiction inline.
|
|
189
245
|
*/
|
|
190
246
|
export function buildAcceptDebtNote(open) {
|
|
191
247
|
if (open.length === 0)
|
|
192
248
|
return '';
|
|
193
|
-
const items = open.map(d => `${d.taskId || '(unknown task)'} — ${describeDebt(d)}: ${d.reason}`
|
|
249
|
+
const items = open.map(d => `${d.taskId || '(unknown task)'} — ${describeDebt(d)}: ${d.reason}`
|
|
250
|
+
+ (d.conflict ? `\n ⚠ CONFLICTING CLAIM — ${d.conflict}` : ''));
|
|
194
251
|
return (`\n\nUNRESOLVED VERIFY-FAIL DEBT still open (${open.length}) — `
|
|
195
|
-
+ 'these defects were recorded during the run and are NOT re-verified by this gate
|
|
252
|
+
+ 'these defects were recorded during the run and are NOT re-verified by this gate. '
|
|
253
|
+
+ 'They are records for a human decision, not instructions to edit code:\n'
|
|
196
254
|
+ items.map(i => ` - ${i}`).join('\n'));
|
|
197
255
|
}
|
|
198
256
|
/** One-line provenance label for a debt, for the surfaced report. */
|
package/dist/task/auto-io.d.ts
CHANGED
|
@@ -20,8 +20,10 @@ export interface CoverageVerdict {
|
|
|
20
20
|
export declare function parseCoverageVerdict(raw: string): CoverageVerdict | null;
|
|
21
21
|
/** Parse the "## tasks" checkbox list. */
|
|
22
22
|
export declare function parseTaskList(body: string): TaskEntry[];
|
|
23
|
-
/** Build the initial AUTO-file body.
|
|
24
|
-
|
|
23
|
+
/** Build the initial AUTO-file body. `coverage` is the requirement-level
|
|
24
|
+
* accounting summary (goal A(c) — a durable, user-visible record of what was
|
|
25
|
+
* carried cross-cutting and what stayed unowned); '' omits the section. */
|
|
26
|
+
export declare function buildAutoBody(feature: string, clarifications: string, titles: string[], coverage?: string): string;
|
|
25
27
|
/** Check off the Nth checkbox line, stamping the produced TASK_NNNN id. */
|
|
26
28
|
export declare function checkOffTask(cwd: string, id: string, index: number, producedId: string, title: string): Promise<void>;
|
|
27
29
|
/**
|
package/dist/task/auto-io.js
CHANGED
|
@@ -91,12 +91,15 @@ export function parseTaskList(body) {
|
|
|
91
91
|
}
|
|
92
92
|
return entries;
|
|
93
93
|
}
|
|
94
|
-
/** Build the initial AUTO-file body.
|
|
95
|
-
|
|
94
|
+
/** Build the initial AUTO-file body. `coverage` is the requirement-level
|
|
95
|
+
* accounting summary (goal A(c) — a durable, user-visible record of what was
|
|
96
|
+
* carried cross-cutting and what stayed unowned); '' omits the section. */
|
|
97
|
+
export function buildAutoBody(feature, clarifications, titles, coverage = '') {
|
|
96
98
|
const tasks = titles.map(t => `- [ ] ${t}`).join('\n');
|
|
97
99
|
return (`\n## feature prompt\n\n${feature.trim() || '(none)'}\n\n`
|
|
98
100
|
+ `## clarifications\n\n${clarifications.trim() || '(none)'}\n\n`
|
|
99
|
-
+ `## tasks\n\n${tasks}\n`
|
|
101
|
+
+ `## tasks\n\n${tasks}\n`
|
|
102
|
+
+ (coverage.trim().length > 0 ? `\n## coverage\n\n${coverage.trim()}\n` : ''));
|
|
100
103
|
}
|
|
101
104
|
/** Rewrite the Nth checkbox line of the "## tasks" section in place. */
|
|
102
105
|
async function rewriteTaskLine(cwd, id, index, render, label) {
|
|
@@ -32,7 +32,9 @@ import { classifyFinalGateAnswer, MAX_FINAL_GATE_AUTOFIX, FINAL_LEAVE_LABEL, FIN
|
|
|
32
32
|
import { getConfig } from '../config/config.js';
|
|
33
33
|
import { configureResearchRun } from '../workers/research-cache.js';
|
|
34
34
|
import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, appendContracts } from './contracts.js';
|
|
35
|
-
import {
|
|
35
|
+
import { reconcileTitleSources } from './decompose-fidelity.js';
|
|
36
|
+
import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, appendCarriedRequirements, buildRequirementsLedger } from './requirements.js';
|
|
37
|
+
import { LAUNCH_EXTRACT_PROMPT, enumerateScriptCandidates, parseScriptLines, keepGroundedScripts, appendDeclaredScripts } from './launch-contract.js';
|
|
36
38
|
// Hard ceiling on clarify questions per feature. The loop is open-ended (it stops
|
|
37
39
|
// when the model emits NONE), but a model that never says NONE would otherwise
|
|
38
40
|
// barrage the user — the real mx5 run asked 10, several of them redundant.
|
|
@@ -436,10 +438,57 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
436
438
|
ctx.ui.notify('No clarifying questions needed — planning tasks…', 'info');
|
|
437
439
|
}
|
|
438
440
|
const clarifications = answers.join('\n');
|
|
441
|
+
// Requirement extraction (mx5 run 11, goal A): grounded requirement units,
|
|
442
|
+
// extracted from whatever structure the spec has, BEFORE decompose — they ride
|
|
443
|
+
// into the decompose prompt as a ledger (structure-mirroring can't discharge
|
|
444
|
+
// them) and drive the per-requirement coverage accounting below. Best-effort:
|
|
445
|
+
// a fault leaves reqEntries empty and the whole channel degrades to the old
|
|
446
|
+
// behavior (one-liners / doc-less features naturally yield few or none).
|
|
447
|
+
let reqEntries = [];
|
|
448
|
+
try {
|
|
449
|
+
// Recall floor: the obligation-marked passages ride into the prompt as a
|
|
450
|
+
// checklist, and a marked passage that produced NO quote is hard evidence
|
|
451
|
+
// for one forced re-extraction (measured live: 1/5 extractions missed the
|
|
452
|
+
// entire marked testing section without this).
|
|
453
|
+
const passages = enumerateObligationPassages(featureForModel);
|
|
454
|
+
const extractOnce = async (hint) => keepGroundedRequirements(parseRequirementLines(await deps.runChild('requirement-extract', '', prependHint(hint, REQUIREMENT_EXTRACT_PROMPT(featureForModel, passages)))), featureForModel);
|
|
455
|
+
reqEntries = await extractOnce(null);
|
|
456
|
+
const uncovered = uncoveredPassages(passages, reqEntries);
|
|
457
|
+
if (uncovered.length > 0) {
|
|
458
|
+
logPlanDebug(cwd, `requirement extraction: ${uncovered.length} obligation-marked passage(s) `
|
|
459
|
+
+ 'uncovered — forcing one re-extraction');
|
|
460
|
+
const retry = await extractOnce(extractionRetryHint(uncovered));
|
|
461
|
+
// Union of both grounded passes (keepGrounded dedupes).
|
|
462
|
+
reqEntries = keepGroundedRequirements([...reqEntries, ...retry], featureForModel);
|
|
463
|
+
}
|
|
464
|
+
// Bound with marked-passage priority — a plain first-N cap truncates the
|
|
465
|
+
// doc's tail sections (measured live: an eager model fills 40 top-down).
|
|
466
|
+
reqEntries = capRequirements(reqEntries, passages);
|
|
467
|
+
logPlanDebug(cwd, `requirement extraction: ${reqEntries.length} grounded requirement(s) kept`);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// best-effort channel
|
|
471
|
+
}
|
|
439
472
|
// decompose
|
|
440
|
-
const decomposePrompt = AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications);
|
|
473
|
+
const decomposePrompt = AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications, buildRequirementsLedger(reqEntries));
|
|
474
|
+
// Parse + FIDELITY RECONCILIATION (mx5 run 11, goal B): ground each title's
|
|
475
|
+
// [source: "…"] citation against the doc, strip the clause, and re-attach any
|
|
476
|
+
// `+`-joined constraint fragment the paraphrased title dropped (the silently
|
|
477
|
+
// stripped "+ tests" class). Applied to EVERY decompose output — initial,
|
|
478
|
+
// suspect-retry, coverage-retry — so no path ships an unreconciled list.
|
|
479
|
+
const parsePlan = (raw) => {
|
|
480
|
+
const plan = reconcileTitleSources(parseDecomposeList(raw), featureForModel);
|
|
481
|
+
if (plan.sourced > 0 || plan.restored.length > 0) {
|
|
482
|
+
logPlanDebug(cwd, `decompose fidelity: ${plan.sourced}/${plan.titles.length} titles cited a grounded source; `
|
|
483
|
+
+ `${plan.restored.length} restoration(s)`
|
|
484
|
+
+ plan.restored
|
|
485
|
+
.map(r => ` [task ${r.index + 1}: ${r.fragments.join(', ')}]`)
|
|
486
|
+
.join(''));
|
|
487
|
+
}
|
|
488
|
+
return plan.titles;
|
|
489
|
+
};
|
|
441
490
|
const listRaw = await deps.runChild('auto-decompose', 'read', decomposePrompt);
|
|
442
|
-
let planTitles =
|
|
491
|
+
let planTitles = parsePlan(listRaw);
|
|
443
492
|
logPlanDebug(cwd, `decompose produced ${planTitles.length} title(s)`);
|
|
444
493
|
// Distrust floor (see isSuspectPlan): a ≤2-title plan for a multi-KB spec is
|
|
445
494
|
// regenerated once BEFORE the judge runs — the judge cannot be trusted to
|
|
@@ -450,7 +499,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
450
499
|
logPlanDebug(cwd, `decompose suspect (${planTitles.length} title(s) for a ${featureForModel.length}-char spec)`
|
|
451
500
|
+ ` — raw output: ${listRaw.trim().slice(0, 300)}`);
|
|
452
501
|
const retryRaw = await deps.runChild('auto-decompose', 'read', prependHint(suspectPlanHint(planTitles.length), decomposePrompt));
|
|
453
|
-
const retryTitles =
|
|
502
|
+
const retryTitles = parsePlan(retryRaw);
|
|
454
503
|
logPlanDebug(cwd, `decompose suspect-retry produced ${retryTitles.length} title(s)`);
|
|
455
504
|
if (retryTitles.length > planTitles.length)
|
|
456
505
|
planTitles = retryTitles;
|
|
@@ -472,20 +521,48 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
472
521
|
// re-judged the same unchanged list, and the known-incomplete plan shipped
|
|
473
522
|
// with no warning.
|
|
474
523
|
let unresolvedMissing = null;
|
|
524
|
+
// The last per-requirement accounting (goal A): completeness is computed
|
|
525
|
+
// HOST-SIDE from it — a holistic "COMPLETE" alone can no longer pass a plan
|
|
526
|
+
// while grounded requirements sit unowned (run 11: milestone-parity satisfied
|
|
527
|
+
// the judge while §10 Testing had zero tasks). Null when no requirements were
|
|
528
|
+
// extracted or every mapping call faulted (⇒ old judge-only behavior).
|
|
529
|
+
let accounting = null;
|
|
475
530
|
for (let round = 0; round < MAX_COVERAGE_ROUNDS && planTitles.length > 0; round++) {
|
|
531
|
+
// Signal 1 — the holistic judge (kept as the belt; catches feature areas
|
|
532
|
+
// the requirement extraction itself missed). A fault yields no signal.
|
|
476
533
|
let verdict;
|
|
477
534
|
try {
|
|
478
535
|
verdict = parseCoverageVerdict(await deps.runChild('decompose-coverage', '', DECOMPOSE_COVERAGE_PROMPT(featureForModel, clarifications, planTitles)));
|
|
479
536
|
}
|
|
480
537
|
catch {
|
|
481
|
-
|
|
482
|
-
unresolvedMissing = null;
|
|
483
|
-
break;
|
|
538
|
+
verdict = null;
|
|
484
539
|
}
|
|
485
|
-
|
|
540
|
+
const verdictMissing = verdict?.kind === 'incomplete' ? verdict.missing : [];
|
|
541
|
+
// Signal 2 — the per-requirement map (the lever): every grounded
|
|
542
|
+
// requirement gets a falsifiable verdict (TASK n / CROSS-CUTTING / NONE);
|
|
543
|
+
// the host, not the model, decides what is uncovered. A fault keeps the
|
|
544
|
+
// previous round's accounting.
|
|
545
|
+
if (reqEntries.length > 0) {
|
|
546
|
+
try {
|
|
547
|
+
const mapRaw = await deps.runChild('coverage-map', '', COVERAGE_MAP_PROMPT(reqEntries, planTitles));
|
|
548
|
+
accounting = accountCoverage(reqEntries, parseCoverageMap(mapRaw, reqEntries.length, planTitles.length));
|
|
549
|
+
logPlanDebug(cwd, `coverage-map round ${round + 1}: ${accounting.mapped.length} task-mapped, `
|
|
550
|
+
+ `${accounting.crossCutting.length} cross-cutting, `
|
|
551
|
+
+ `${accounting.unmapped.length} unmapped`);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// mapping fault — keep whatever accounting an earlier round produced
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const unmappedQuotes = (accounting?.unmapped ?? []).map(e => `"${e.quote}"`);
|
|
558
|
+
const missing = [...verdictMissing, ...unmappedQuotes];
|
|
559
|
+
if (missing.length === 0) {
|
|
486
560
|
unresolvedMissing = null;
|
|
487
561
|
logPlanDebug(cwd, `decompose-coverage round ${round + 1}: `
|
|
488
|
-
+ (verdict === null ? 'no verdict
|
|
562
|
+
+ (verdict === null ? 'no judge verdict' : 'judge COMPLETE')
|
|
563
|
+
+ (reqEntries.length > 0 ?
|
|
564
|
+
' and every grounded requirement is task-mapped or cross-cutting'
|
|
565
|
+
: ' — accepting list'));
|
|
489
566
|
// A COMPLETE on a still-suspect plan is the judge's known live
|
|
490
567
|
// false-pass mode (bare verdict, indistinguishable from a real one).
|
|
491
568
|
// The plan still ships — the floor never rejects on count — but
|
|
@@ -496,11 +573,11 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
496
573
|
}
|
|
497
574
|
break;
|
|
498
575
|
}
|
|
499
|
-
unresolvedMissing =
|
|
576
|
+
unresolvedMissing = missing;
|
|
500
577
|
logPlanDebug(cwd, `decompose-coverage round ${round + 1}: INCOMPLETE — missing: `
|
|
501
|
-
+
|
|
502
|
-
const retryRaw = await deps.runChild('auto-decompose', 'read', prependHint(coverageRepromptHint(
|
|
503
|
-
const retryTitles =
|
|
578
|
+
+ missing.join('; ').slice(0, 300));
|
|
579
|
+
const retryRaw = await deps.runChild('auto-decompose', 'read', prependHint(coverageRepromptHint(missing), decomposePrompt));
|
|
580
|
+
const retryTitles = parsePlan(retryRaw);
|
|
504
581
|
logPlanDebug(cwd, `decompose retry produced ${retryTitles.length} title(s)`);
|
|
505
582
|
if (retryTitles.length > 0 && retryTitles.length * 2 >= planTitles.length) {
|
|
506
583
|
planTitles = retryTitles;
|
|
@@ -517,6 +594,22 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
517
594
|
+ unresolvedMissing.join('; ').slice(0, 300));
|
|
518
595
|
ctx.ui.notify(`/task-auto: plan may be missing coverage — ${unresolvedMissing.join('; ').slice(0, 200)} — review the plan before running.`, 'warning');
|
|
519
596
|
}
|
|
597
|
+
// Carry what no single task owns (goal A(b)/(c)): cross-cutting requirements
|
|
598
|
+
// become `.pi-tasks/requirements.md`, injected VERBATIM into every task's
|
|
599
|
+
// refine/compose (run 11: §10's test-first cadence had no carrier — the "spec
|
|
600
|
+
// is authoritative" pointer recovered it in 1 of ~6 tasks; content travels,
|
|
601
|
+
// pointers don't). Requirements still unmapped after the rounds are carried
|
|
602
|
+
// too — marked — and recorded user-visibly in the plan file, never dropped.
|
|
603
|
+
if (accounting !== null) {
|
|
604
|
+
await appendCarriedRequirements(cwd, accounting.crossCutting, accounting.unmapped);
|
|
605
|
+
if (accounting.crossCutting.length > 0 || accounting.unmapped.length > 0) {
|
|
606
|
+
ctx.ui.notify(`/task-auto: carrying ${accounting.crossCutting.length} cross-cutting`
|
|
607
|
+
+ (accounting.unmapped.length > 0 ?
|
|
608
|
+
` and ${accounting.unmapped.length} unowned`
|
|
609
|
+
: '')
|
|
610
|
+
+ ' requirement(s) into every task — see .pi-tasks/requirements.md.', 'info');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
520
613
|
// Cross-slice contract registry (mx5 run 8, F3): now that the plan is settled,
|
|
521
614
|
// extract the interface facts MORE THAN ONE slice must agree on — endpoint paths,
|
|
522
615
|
// exported signatures, file layouts, env var names the DESIGN pins — into a
|
|
@@ -540,9 +633,14 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
540
633
|
// declares the project must expose (`migrate`/`seed` fell through decompose and
|
|
541
634
|
// shipped missing, unchecked). Each emitted name is re-grounded against the design
|
|
542
635
|
// (keepGroundedScripts — kept only if the design backticks it), so the final gate's
|
|
543
|
-
// manifest diff can never false-flag a hallucinated script.
|
|
636
|
+
// manifest diff can never false-flag a hallucinated script. Recall is mechanical
|
|
637
|
+
// (mx5 run 11): enumerateScriptCandidates hands the child every backticked
|
|
638
|
+
// script-shaped token near the word "script" as a checklist, so a script declared
|
|
639
|
+
// far from the design's summary list (`test:ct` in §2 vs §9's five) can't be
|
|
640
|
+
// missed by a weak model's recall — the child classifies, it no longer recalls.
|
|
641
|
+
// Best-effort.
|
|
544
642
|
try {
|
|
545
|
-
const scriptRaw = await deps.runChild('launch-extract', '', LAUNCH_EXTRACT_PROMPT(featureForModel));
|
|
643
|
+
const scriptRaw = await deps.runChild('launch-extract', '', LAUNCH_EXTRACT_PROMPT(featureForModel, enumerateScriptCandidates(featureForModel)));
|
|
546
644
|
const grounded = keepGroundedScripts(parseScriptLines(scriptRaw), featureForModel);
|
|
547
645
|
logPlanDebug(cwd, `launch-contract extraction: ${grounded.length} grounded script(s) kept`
|
|
548
646
|
+ ` from ${parseScriptLines(scriptRaw).length} emitted`);
|
|
@@ -571,7 +669,16 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
571
669
|
updated_at: now,
|
|
572
670
|
title: deriveTitle(feature)
|
|
573
671
|
};
|
|
574
|
-
|
|
672
|
+
// Durable, user-visible coverage record (goal A(c)): what was carried and what
|
|
673
|
+
// stayed unowned lives in the plan file itself, not only in a transient toast.
|
|
674
|
+
const coverageNote = accounting === null ? '' : ([
|
|
675
|
+
`${reqEntries.length} grounded requirement(s): ${accounting.mapped.length} task-mapped, `
|
|
676
|
+
+ `${accounting.crossCutting.length} cross-cutting (carried into every task via `
|
|
677
|
+
+ `.pi-tasks/requirements.md), ${accounting.unmapped.length} unowned`,
|
|
678
|
+
...accounting.crossCutting.map(e => `- carried: "${e.quote}"`),
|
|
679
|
+
...accounting.unmapped.map(e => `- UNOWNED (no task covers this): "${e.quote}"`)
|
|
680
|
+
].join('\n'));
|
|
681
|
+
await writeTaskFile(cwd, fm, buildAutoBody(feature, clarifications, titles, coverageNote));
|
|
575
682
|
return id;
|
|
576
683
|
}
|
|
577
684
|
/** The two feature-level planning children, shown as steps in the loader. */
|
|
@@ -714,7 +821,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
714
821
|
// already a human decision, so this reports, it does not re-fail.
|
|
715
822
|
if (fin.openDebts && fin.openDebts.length > 0) {
|
|
716
823
|
for (const d of fin.openDebts) {
|
|
717
|
-
await recGate(`defect STILL OPEN — ${d.taskId || '(unknown task)'}: ${describeDebt(d)}: ${d.reason.slice(0, 240)}`);
|
|
824
|
+
await recGate(`defect STILL OPEN — ${d.taskId || '(unknown task)'}: ${describeDebt(d)}: ${d.reason.slice(0, 240)}${d.conflict ? ` [CONFLICTING CLAIM — ${d.conflict}]` : ''}`);
|
|
718
825
|
}
|
|
719
826
|
active.ui.notify(`${id}: ${fin.openDebts.length} recorded verify-FAIL defect(s) are STILL unresolved at run end — see the gate trail.`, 'warning');
|
|
720
827
|
}
|
|
@@ -726,7 +833,10 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
726
833
|
let fixAttempts = 0;
|
|
727
834
|
while (!fin.ok) {
|
|
728
835
|
const canAutofix = deps.finalGateFix !== undefined && fixAttempts < MAX_FINAL_GATE_AUTOFIX;
|
|
729
|
-
|
|
836
|
+
// The picker question shows the debts (the HUMAN weighs them);
|
|
837
|
+
// the autofix seed below deliberately does not — mx5 run 11's
|
|
838
|
+
// fix child executed a debt claim as an `rm` instruction.
|
|
839
|
+
const question = `Final integration gate FAILED for ${id}.\n\n${fin.reason}${fin.debtNote ?? ''}\n\n`
|
|
730
840
|
+ 'All tasks are checked off — this is the whole-repo check '
|
|
731
841
|
+ '(the project’s own test/build/static commands, run unaided).'
|
|
732
842
|
+ (fixAttempts > 0 ?
|
|
@@ -772,7 +882,13 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
772
882
|
active.ui.notify(`${id}: final-gate autofix did not converge — ${fix.reason.slice(0, 140)}`, 'warning');
|
|
773
883
|
// Work from the FRESH gate failure when the fix pass got
|
|
774
884
|
// as far as re-running the gate; otherwise keep the last.
|
|
775
|
-
|
|
885
|
+
// The debt note is carried so the next picker still shows
|
|
886
|
+
// the open claims (the seed never includes it).
|
|
887
|
+
fin = {
|
|
888
|
+
ok: false,
|
|
889
|
+
reason: fix.gateReason ?? fin.reason,
|
|
890
|
+
debtNote: fin.debtNote
|
|
891
|
+
};
|
|
776
892
|
continue;
|
|
777
893
|
}
|
|
778
894
|
// Leave failed — the dismissal default, unchanged from the
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prompts for /task-auto's two feature-level child calls. These produce a task
|
|
3
|
-
* LIST only; all research/spec depth is /task's job, run per-title later.
|
|
4
|
-
*/
|
|
5
1
|
/**
|
|
6
2
|
* Clarify: asks ONE question at a time. Output MUST match parseClarifyList — a
|
|
7
3
|
* single numbered question followed by a "SUGGESTED: <default>" line, an optional
|
|
@@ -12,8 +8,12 @@
|
|
|
12
8
|
export declare const AUTO_CLARIFY_PROMPT: (feature: string, priorQA: string) => string;
|
|
13
9
|
/**
|
|
14
10
|
* Decompose: output a markdown checkbox list of task titles (one line each).
|
|
11
|
+
* `requirementsLedger` is buildRequirementsLedger's grounded quote list (goal E's
|
|
12
|
+
* belt): the spec's obligations ride into decompose explicitly, so mirroring the
|
|
13
|
+
* spec's own milestone/section structure cannot silently discharge them. '' ⇒
|
|
14
|
+
* the prompt is unchanged.
|
|
15
15
|
*/
|
|
16
|
-
export declare const AUTO_DECOMPOSE_PROMPT: (feature: string, clarifications: string) => string;
|
|
16
|
+
export declare const AUTO_DECOMPOSE_PROMPT: (feature: string, clarifications: string, requirementsLedger?: string) => string;
|
|
17
17
|
/**
|
|
18
18
|
* Coverage triage: judge whether a decomposed task list covers the whole
|
|
19
19
|
* feature. Guards the plan — the highest-leverage artifact in /task-auto —
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Prompts for /task-auto's two feature-level child calls. These produce a task
|
|
3
3
|
* LIST only; all research/spec depth is /task's job, run per-title later.
|
|
4
4
|
*/
|
|
5
|
+
import { DECOMPOSE_SOURCE_RULE } from './decompose-fidelity.js';
|
|
5
6
|
/**
|
|
6
7
|
* Clarify: asks ONE question at a time. Output MUST match parseClarifyList — a
|
|
7
8
|
* single numbered question followed by a "SUGGESTED: <default>" line, an optional
|
|
@@ -34,6 +35,7 @@ fork the breakdown). Account for the answers so far:
|
|
|
34
35
|
real-time vs polling transport, search, deployment).
|
|
35
36
|
- Skip anything /task will naturally resolve per-task during its own research.
|
|
36
37
|
- Stay grounded in the referenced spec. If a design/spec doc is included above, do NOT propose a new subsystem, dependency, or requirement it does not call for, and do NOT re-ask a choice the spec already settles. Ask only about genuine forks the spec leaves open.
|
|
38
|
+
- If the question or your SUGGESTED default locks the task breakdown to ONE part or structure of the spec (e.g. "follow the milestone list", "one task per section"), the SUGGESTED line must ALSO name the spec's other REQUIRED content that structure does not represent (a testing/security/quality section, cross-cutting rules) and state how each is carried — folded into every applicable task, or as its own task. Never present a structure-lock as settling the whole spec.
|
|
37
39
|
|
|
38
40
|
YOU MUST propose a default answer for the question — every question you emit
|
|
39
41
|
carries exactly one SUGGESTED line. Never omit it, never leave it blank, never
|
|
@@ -65,8 +67,12 @@ No question remains:
|
|
|
65
67
|
NONE`;
|
|
66
68
|
/**
|
|
67
69
|
* Decompose: output a markdown checkbox list of task titles (one line each).
|
|
70
|
+
* `requirementsLedger` is buildRequirementsLedger's grounded quote list (goal E's
|
|
71
|
+
* belt): the spec's obligations ride into decompose explicitly, so mirroring the
|
|
72
|
+
* spec's own milestone/section structure cannot silently discharge them. '' ⇒
|
|
73
|
+
* the prompt is unchanged.
|
|
68
74
|
*/
|
|
69
|
-
export const AUTO_DECOMPOSE_PROMPT = (feature, clarifications) => `Split this feature into an ordered list of implementation tasks. Each task
|
|
75
|
+
export const AUTO_DECOMPOSE_PROMPT = (feature, clarifications, requirementsLedger = '') => `Split this feature into an ordered list of implementation tasks. Each task
|
|
70
76
|
will be handed, by its title, to a separate pipeline that does its own research
|
|
71
77
|
and writes its own spec — so here you produce TITLES ONLY, not specs.
|
|
72
78
|
|
|
@@ -75,7 +81,7 @@ ${feature.trim()}
|
|
|
75
81
|
|
|
76
82
|
CLARIFICATIONS:
|
|
77
83
|
${clarifications.trim() || '(none)'}
|
|
78
|
-
|
|
84
|
+
${requirementsLedger.trim().length > 0 ? `\n${requirementsLedger.trim()}\n` : ''}
|
|
79
85
|
RULES:
|
|
80
86
|
- One task per line, as a markdown checkbox: "- [ ] <title>".
|
|
81
87
|
- Each title is a short imperative phrase; optionally add " — <one key detail>".
|
|
@@ -88,6 +94,7 @@ RULES:
|
|
|
88
94
|
none. These are explicit user choices that may contradict the referenced spec
|
|
89
95
|
doc; phrase them as imperative directives (e.g. "use Bun's built-in bundler, do
|
|
90
96
|
not add vite"). Do NOT invent decisions — only restate ones from CLARIFICATIONS.
|
|
97
|
+
${DECOMPOSE_SOURCE_RULE}
|
|
91
98
|
- Output the checkbox list and NOTHING else (no preamble, no numbering).`;
|
|
92
99
|
/**
|
|
93
100
|
* Coverage triage: judge whether a decomposed task list covers the whole
|
package/dist/task/contracts.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ export interface ContractEntry {
|
|
|
7
7
|
export declare function contractsFile(cwd: string): string;
|
|
8
8
|
/** The stored registry text ('' when none recorded yet). */
|
|
9
9
|
export declare function readContracts(cwd: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Normalise for substring matching: collapse all whitespace runs to one space and
|
|
12
|
+
* lowercase. Quoting across a line wrap or with reflowed spacing still matches the
|
|
13
|
+
* source; casing differences do not defeat the anti-synthesis guard. Exported as
|
|
14
|
+
* the ONE grounding normaliser every "verbatim quote from the source doc" guard
|
|
15
|
+
* shares (contracts, decompose source anchors), so the rule can't drift.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalise(s: string): string;
|
|
10
18
|
/**
|
|
11
19
|
* Parse `CONTRACT:` lines out of a child's answer text into entries. The line shape
|
|
12
20
|
* the extraction prompt asks for is:
|
package/dist/task/contracts.js
CHANGED
|
@@ -52,9 +52,11 @@ export async function readContracts(cwd) {
|
|
|
52
52
|
/**
|
|
53
53
|
* Normalise for substring matching: collapse all whitespace runs to one space and
|
|
54
54
|
* lowercase. Quoting across a line wrap or with reflowed spacing still matches the
|
|
55
|
-
* source; casing differences do not defeat the anti-synthesis guard.
|
|
55
|
+
* source; casing differences do not defeat the anti-synthesis guard. Exported as
|
|
56
|
+
* the ONE grounding normaliser every "verbatim quote from the source doc" guard
|
|
57
|
+
* shares (contracts, decompose source anchors), so the rule can't drift.
|
|
56
58
|
*/
|
|
57
|
-
function normalise(s) {
|
|
59
|
+
export function normalise(s) {
|
|
58
60
|
return s.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
59
61
|
}
|
|
60
62
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface SourcedTitle {
|
|
2
|
+
/** The title with any source clause stripped. */
|
|
3
|
+
base: string;
|
|
4
|
+
/** The cited spec line, when present and grounded in the source doc. */
|
|
5
|
+
source?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Split a decompose title into its base and its GROUNDED source citation.
|
|
8
|
+
* An absent clause yields no source; a fabricated (ungrounded) one is stripped
|
|
9
|
+
* and dropped — exactly like keepGroundedContracts rejects a paraphrased quote. */
|
|
10
|
+
export declare function extractTitleSource(title: string, sourceDoc: string): SourcedTitle;
|
|
11
|
+
/**
|
|
12
|
+
* The `+`-joined trailing constraint fragments of `sourceLine` whose words are
|
|
13
|
+
* absent from `title`. "2. **Auth** — sessions, login/logout/me, guards + tests."
|
|
14
|
+
* yields the fragment "tests"; a title that never mentions tests gets it back.
|
|
15
|
+
* Fragments before the first `+` are the task's body — a title paraphrases those
|
|
16
|
+
* freely and they are never judged here. A `+`-part is further split on commas
|
|
17
|
+
* ("+ Tailwind v4 tokens, nav, router" is three constraints), so a title missing
|
|
18
|
+
* one of them gets ONLY that one restored, not the whole phrase (measured live:
|
|
19
|
+
* whole-phrase restoration re-attached text the title already carried).
|
|
20
|
+
*/
|
|
21
|
+
export declare function findDroppedPlusFragments(sourceLine: string, title: string): string[];
|
|
22
|
+
export interface TitleRestoration {
|
|
23
|
+
/** Index into the reconciled titles array. */
|
|
24
|
+
index: number;
|
|
25
|
+
/** The verbatim fragments re-attached to the title. */
|
|
26
|
+
fragments: string[];
|
|
27
|
+
/** The grounded source line they came from. */
|
|
28
|
+
source: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ReconciledPlan {
|
|
31
|
+
titles: string[];
|
|
32
|
+
restored: TitleRestoration[];
|
|
33
|
+
/** How many titles carried a GROUNDED source citation (adoption signal). */
|
|
34
|
+
sourced: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Reconcile decompose output against the source doc: ground each citation, strip
|
|
38
|
+
* the clause (its job ends here), and re-attach any dropped `+`-fragments to the
|
|
39
|
+
* title verbatim so downstream refine/compose — which see ONLY the title — get
|
|
40
|
+
* the constraint back. Titles without a citation pass through unchanged, so a
|
|
41
|
+
* model that never cites degrades to exactly the old behavior.
|
|
42
|
+
*/
|
|
43
|
+
export declare function reconcileTitleSources(titles: string[], sourceDoc: string): ReconciledPlan;
|
|
44
|
+
/** The decompose-prompt rule that makes titles citable (the belt half; the host
|
|
45
|
+
* grounding + restoration above is the lever). Kept here so prompt and parser
|
|
46
|
+
* can't drift apart. */
|
|
47
|
+
export declare const DECOMPOSE_SOURCE_RULE: string;
|