@mjasnikovs/pi-task 0.38.16 → 0.38.17
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/config/config.d.ts +26 -0
- package/dist/config/config.js +68 -17
- package/dist/task/accept-debt.js +2 -1
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +205 -214
- package/dist/task/boot-probe.d.ts +46 -0
- package/dist/task/boot-probe.js +41 -21
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/final-gate-fix.js +14 -24
- package/dist/task/final-gate.js +6 -1
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.js +9 -2
- package/dist/task/phases.d.ts +66 -4
- package/dist/task/phases.js +94 -34
- package/dist/task/plan-rounds.d.ts +86 -0
- package/dist/task/plan-rounds.js +105 -0
- package/dist/task/plan-session.d.ts +31 -21
- package/dist/task/plan-session.js +97 -120
- package/dist/task/qa-transcript.d.ts +100 -0
- package/dist/task/qa-transcript.js +99 -0
- package/dist/task/question-source.d.ts +117 -0
- package/dist/task/question-source.js +174 -0
- package/dist/task/serve-entry.js +6 -57
- package/dist/task/shipped-source.d.ts +67 -0
- package/dist/task/shipped-source.js +144 -0
- package/dist/task/task-gates.d.ts +1 -1
- package/dist/task/task-gates.js +4 -2
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/pi-worker-core.js +23 -8
- package/package.json +1 -1
package/dist/task/phases.js
CHANGED
|
@@ -40,7 +40,8 @@ import { detachUnsatisfiableRequirements, claimPendingRequirements, unclaimedPen
|
|
|
40
40
|
import { trackedSourceOracle } from './owned-freeze-conflict.js';
|
|
41
41
|
import { runPhaseChild, runWithEmphasisRetry, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
42
42
|
import { SessionUI } from '../remote/bridge.js';
|
|
43
|
-
import { isYoloMode, yoloPickAutoAnswer
|
|
43
|
+
import { isYoloMode, yoloPickAutoAnswer } from './yolo.js';
|
|
44
|
+
import { QaTranscript, GRILL_QA_POLICY } from './qa-transcript.js';
|
|
44
45
|
// ─── Re-export constants from their home modules ────────────────────────────
|
|
45
46
|
export { MAX_GRILL_QUESTIONS };
|
|
46
47
|
// ─── Tooling helpers ─────────────────────────────────────────────────────────
|
|
@@ -1211,8 +1212,11 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1211
1212
|
// nothing ambiguous remains. Kept in sync with /task-auto's clarify dialog.
|
|
1212
1213
|
const theme = ctx.ui.theme;
|
|
1213
1214
|
const ui = new SessionUI(ctx);
|
|
1214
|
-
|
|
1215
|
-
|
|
1215
|
+
// ONE record, two renderings (task/qa-transcript.ts): `forRecord()` is what
|
|
1216
|
+
// compose and critique are handed, `forGenerator()` is what the next grill-gen
|
|
1217
|
+
// call sees. The provenance rule below used to be a comment 12 lines under a
|
|
1218
|
+
// push that broke it.
|
|
1219
|
+
const transcript = new QaTranscript(GRILL_QA_POLICY);
|
|
1216
1220
|
const askedQuestions = []; // plain text of each question, for the dup backstop
|
|
1217
1221
|
// Deterministic backstop against a model that ignores "never re-ask": a
|
|
1218
1222
|
// near-duplicate question is reprompted (not auto-answered or shown), and after
|
|
@@ -1226,7 +1230,7 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1226
1230
|
for (let n = 0; n < MAX_GRILL_QUESTIONS; n++) {
|
|
1227
1231
|
const tGenStart = Date.now();
|
|
1228
1232
|
const genHint = dupHint;
|
|
1229
|
-
const raw = await runPhaseChild(deps, 'grill-gen', 'read', prependHint(genHint, GRILL_GEN_PROMPT(refined, research,
|
|
1233
|
+
const raw = await runPhaseChild(deps, 'grill-gen', 'read', prependHint(genHint, GRILL_GEN_PROMPT(refined, research, transcript.forGenerator())), { verb: 'restart' });
|
|
1230
1234
|
deps.recordSubStep?.('gen', Date.now() - tGenStart);
|
|
1231
1235
|
const questions = parseGrillQuestions(raw);
|
|
1232
1236
|
if (questions.length === 0)
|
|
@@ -1252,11 +1256,8 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1252
1256
|
// for the editable default and the persisted file.
|
|
1253
1257
|
const shownQ = renderInlineMarkdown(q, theme);
|
|
1254
1258
|
const plainQ = stripInlineMarkdown(q);
|
|
1255
|
-
out.push(`Q${n + 1}: ${plainQ}`);
|
|
1256
|
-
let answer;
|
|
1257
1259
|
if (auto.kind === 'answered') {
|
|
1258
|
-
|
|
1259
|
-
out.push(`A${n + 1}: ${answer} (auto)`);
|
|
1260
|
+
transcript.add('auto', plainQ, stripInlineMarkdown(auto.text));
|
|
1260
1261
|
}
|
|
1261
1262
|
else {
|
|
1262
1263
|
const plainSuggested = auto.suggested === undefined ? undefined : stripInlineMarkdown(auto.suggested);
|
|
@@ -1268,12 +1269,15 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1268
1269
|
// guard direction, promoting a hallucination is not.
|
|
1269
1270
|
const yolo = yoloPickAutoAnswer(isYoloMode(), auto);
|
|
1270
1271
|
if (yolo !== null) {
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1272
|
+
// The YOLO stamp is a RECORD fact. It used to be pushed into the
|
|
1273
|
+
// generator feedback too, against the rule stated a dozen lines
|
|
1274
|
+
// below — the policy now decides, so the two cannot disagree.
|
|
1275
|
+
if (yolo.kind === 'answer') {
|
|
1276
|
+
transcript.add('yolo', plainQ, stripInlineMarkdown(yolo.answer));
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
transcript.add('yolo-skip', plainQ, `(skipped — ${yolo.note})`);
|
|
1280
|
+
}
|
|
1277
1281
|
continue;
|
|
1278
1282
|
}
|
|
1279
1283
|
// The picker cards and the reply mapping are shared with /task-auto's
|
|
@@ -1304,17 +1308,17 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1304
1308
|
});
|
|
1305
1309
|
if (a === undefined)
|
|
1306
1310
|
throw new Error(USER_CANCELLED);
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
|
|
1311
|
-
|
|
1311
|
+
// Grill's generator sees NO provenance — its feedback is fed verbatim
|
|
1312
|
+
// into the next grill-gen prompt, where a suffix would describe how the
|
|
1313
|
+
// answer was obtained rather than what it was. That is
|
|
1314
|
+
// `GRILL_QA_POLICY.generatorSeesProvenance: false`, stated once.
|
|
1315
|
+
const resolved = resolveAnswer(pending, a);
|
|
1316
|
+
transcript.add(resolved.source === 'accepted' ? 'accepted' : 'typed', plainQ, resolved.answer);
|
|
1312
1317
|
}
|
|
1313
|
-
qa.push(`Q${n + 1}: ${plainQ}\nA${n + 1}: ${answer}`);
|
|
1314
1318
|
}
|
|
1315
|
-
if (
|
|
1319
|
+
if (transcript.length === 0)
|
|
1316
1320
|
return '(no questions produced)';
|
|
1317
|
-
return
|
|
1321
|
+
return transcript.forRecord();
|
|
1318
1322
|
}
|
|
1319
1323
|
/**
|
|
1320
1324
|
* A refutation is a DELETION. Where the run's own research explicitly says a
|
|
@@ -1340,11 +1344,32 @@ export async function dropRefutedConstraints(deps, refined, research) {
|
|
|
1340
1344
|
const refuted = applyRefutations(refined, research);
|
|
1341
1345
|
if (refuted.trail.length === 0)
|
|
1342
1346
|
return refined;
|
|
1343
|
-
|
|
1344
|
-
|
|
1347
|
+
await recordPhaseTrail(deps, 'compose', refuted.trail);
|
|
1348
|
+
return refuted.refined;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* COMPOSE's carry: the refutation drop, as a `PhaseConfig.carry`.
|
|
1352
|
+
*
|
|
1353
|
+
* Same transform as `dropRefutedConstraints` over the same pure core, minus the
|
|
1354
|
+
* recording — the caller decides whether this application is the live one or a
|
|
1355
|
+
* resume replay. `dropRefutedConstraints` stays exported and unchanged for the
|
|
1356
|
+
* harnesses under `scripts/` that drive the drop directly.
|
|
1357
|
+
*/
|
|
1358
|
+
export function composeCarry(_deps, pc) {
|
|
1359
|
+
// Not `async`, and that is the shape rather than an oversight: this carry
|
|
1360
|
+
// performs no I/O at all, which is what lets the resume path replay it.
|
|
1361
|
+
const refuted = applyRefutations(pc.refined, pc.research);
|
|
1362
|
+
if (refuted.trail.length === 0)
|
|
1363
|
+
return Promise.resolve([]);
|
|
1364
|
+
pc.refined = refuted.refined;
|
|
1365
|
+
return Promise.resolve(refuted.trail);
|
|
1366
|
+
}
|
|
1367
|
+
/** Write a carry's trail to the debug log and the task file's `## gates` section. */
|
|
1368
|
+
export async function recordPhaseTrail(deps, phaseName, trail) {
|
|
1369
|
+
for (const line of trail) {
|
|
1370
|
+
deps.logDebug?.(`${phaseName}: ${line}`);
|
|
1345
1371
|
await appendGateRecord(deps.cwd, deps.taskId, line).catch(() => { });
|
|
1346
1372
|
}
|
|
1347
|
-
return refuted.refined;
|
|
1348
1373
|
}
|
|
1349
1374
|
export async function phaseCompose(deps, refined, research, qa) {
|
|
1350
1375
|
// CLAIM before the belt is built: an obligation an earlier task had to
|
|
@@ -1558,14 +1583,14 @@ export function grillPhase(d, p) {
|
|
|
1558
1583
|
return phaseGrill(d, p.ctx, p.widgetState, p.refined, p.research);
|
|
1559
1584
|
}
|
|
1560
1585
|
/**
|
|
1561
|
-
* COMPOSE —
|
|
1586
|
+
* COMPOSE — compose the spec from the refined task, the research and the Q&A.
|
|
1562
1587
|
*
|
|
1563
|
-
* The drop
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1588
|
+
* The refutation drop that must happen first is compose's declared `carry`
|
|
1589
|
+
* (`composeCarry`), not a line at the top of this function. It settles `p.refined`,
|
|
1590
|
+
* which is not compose's own `field`, so the orchestrator has to replay it on the
|
|
1591
|
+
* resume path too — and a `run` body cannot be replayed.
|
|
1566
1592
|
*/
|
|
1567
1593
|
export async function composePhase(d, p) {
|
|
1568
|
-
p.refined = await dropRefutedConstraints(d, p.refined, p.research);
|
|
1569
1594
|
return await phaseCompose(d, p.refined, p.research, p.qa);
|
|
1570
1595
|
}
|
|
1571
1596
|
/**
|
|
@@ -1604,12 +1629,40 @@ export async function critiquePhase(d, p) {
|
|
|
1604
1629
|
* they run in is now asserted by driving the row rather than retyped in a test.
|
|
1605
1630
|
*/
|
|
1606
1631
|
export const PHASES = [
|
|
1607
|
-
{
|
|
1632
|
+
{
|
|
1633
|
+
name: 'refine',
|
|
1634
|
+
section: 'refined prompt',
|
|
1635
|
+
field: 'refined',
|
|
1636
|
+
run: refinePhase,
|
|
1637
|
+
postCommit: refinePostCommit
|
|
1638
|
+
},
|
|
1608
1639
|
{ name: 'research', section: 'research', field: 'research', run: researchPhase },
|
|
1609
1640
|
{ name: 'grill', section: 'grill Q&A', field: 'qa', run: grillPhase },
|
|
1610
|
-
{ name: 'compose', section: 'spec', field: 'spec', run: composePhase },
|
|
1641
|
+
{ name: 'compose', section: 'spec', field: 'spec', carry: composeCarry, run: composePhase },
|
|
1611
1642
|
{ name: 'critique', section: 'spec', field: 'spec', run: critiquePhase }
|
|
1612
1643
|
];
|
|
1644
|
+
/**
|
|
1645
|
+
* Run one phase row the way the orchestrator does: carry, then run.
|
|
1646
|
+
*
|
|
1647
|
+
* The row is the interface, so this is the surface a row-driving test crosses —
|
|
1648
|
+
* calling `row.run` alone tests past it and would not have caught a carry that the
|
|
1649
|
+
* resume path drops. The orchestrator adds only persistence, timings and the
|
|
1650
|
+
* checkpoint around this.
|
|
1651
|
+
*/
|
|
1652
|
+
export async function runPhaseRow(row, deps, pc) {
|
|
1653
|
+
if (row.carry)
|
|
1654
|
+
await recordPhaseTrail(deps, row.name, await row.carry(deps, pc));
|
|
1655
|
+
return await row.run(deps, pc);
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Re-apply one phase row's carry on the RESUME path, where `run` is skipped.
|
|
1659
|
+
*
|
|
1660
|
+
* The trail is discarded: the live run that produced this phase's output already
|
|
1661
|
+
* recorded it on `## gates`, and a replay must not append a second copy.
|
|
1662
|
+
*/
|
|
1663
|
+
export async function replayPhaseCarry(row, deps, pc) {
|
|
1664
|
+
await row.carry?.(deps, pc);
|
|
1665
|
+
}
|
|
1613
1666
|
// INTEGRATION-DEPTH APPEND (2026-07-27): the lever proposed for this exact site —
|
|
1614
1667
|
// deterministically append a known-runnable integration command to the VERIFY block
|
|
1615
1668
|
// whenever a task's ACCEPTANCE claims runtime behaviour — is REFUTED at STEP 0 and was
|
|
@@ -1654,9 +1707,16 @@ export const PHASES = [
|
|
|
1654
1707
|
//
|
|
1655
1708
|
// Durable assets kept: both rigs above and their unit tests. Do NOT wire an append here
|
|
1656
1709
|
// without a command source that satisfies all four properties at once.
|
|
1710
|
+
/** Dispatch a row's declared post-commit effect. Rows with none do nothing. */
|
|
1657
1711
|
export async function postCommitPhase(phase, deps, pc, out) {
|
|
1658
|
-
|
|
1659
|
-
|
|
1712
|
+
await phase.postCommit?.(deps, pc, out);
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* REFINE's post-commit: derive the task title from the refined prompt, then a short
|
|
1716
|
+
* display label. Runs after the section write, so a fault here cannot lose the
|
|
1717
|
+
* output it reads.
|
|
1718
|
+
*/
|
|
1719
|
+
export async function refinePostCommit(deps, pc, out) {
|
|
1660
1720
|
const title = deriveTitle(out);
|
|
1661
1721
|
pc.widgetState.title = title;
|
|
1662
1722
|
// Compress the (often paragraph-long) title into a short display label. This
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the PLAN-SHAPING loops record, and the decisions that record makes.
|
|
3
|
+
*
|
|
4
|
+
* `GateTally`'s and `AutofixLedger`'s twin, one phase earlier. CONTEXT.md records
|
|
5
|
+
* that shape twice already: a long loop threading mutable locals by closure, with
|
|
6
|
+
* pure helpers extracted for testability while the ORDERING and CARRY-FORWARD
|
|
7
|
+
* decisions stayed in the caller. `coverPlan` was the third instance — five locals
|
|
8
|
+
* (`planTitles`, `best`, `round`, `roundCap`, `bonusRoundUsed`) plus a
|
|
9
|
+
* snapshot-before-overwrite pair (`priorCovered`, `priorMissing`) that existed
|
|
10
|
+
* ONLY because the bonus-round decision was made downstream from the evidence it
|
|
11
|
+
* needed, so the loop had to save a copy of `best` before replacing it.
|
|
12
|
+
*
|
|
13
|
+
* The last real bug here says the shape out loud, in the loop's own comment:
|
|
14
|
+
* *"This used to be two assignments, and the second one kept the OLD plan's
|
|
15
|
+
* accounting whenever the new plan's coverage-map child faulted
|
|
16
|
+
* (`cand.accounting ?? accounting`) — binding requirements to titles they were
|
|
17
|
+
* never mapped against."* `AutofixLedger`'s indictment, verbatim: the decision was
|
|
18
|
+
* made downstream from the evidence.
|
|
19
|
+
*
|
|
20
|
+
* `consider` closes it by construction rather than by comment. It compares, it
|
|
21
|
+
* replaces the plan WHOLE (titles and accounting together, because they are one
|
|
22
|
+
* value), and it grants the bonus round IN THE SAME CALL that adopts — the way
|
|
23
|
+
* `judge(outcome, edited)` enters the demoted signature in the call that demotes.
|
|
24
|
+
* There is no window in which the snapshot and the replacement can disagree.
|
|
25
|
+
*
|
|
26
|
+
* NO I/O. No `logPlanDebug`, no notify, no child — for the same reason `GateTally`
|
|
27
|
+
* performs none: a record that performs effects cannot be driven by a test that
|
|
28
|
+
* only wants the verdict. The caller trails what the returned decision says.
|
|
29
|
+
*/
|
|
30
|
+
import { type AdoptionDecision, type ScoredPlan } from './coverage-loop.js';
|
|
31
|
+
/** What `consider` did with a candidate, and why. */
|
|
32
|
+
export interface ConsiderOutcome {
|
|
33
|
+
adopted: boolean;
|
|
34
|
+
/** The adoption verdict's own reasoning, for the caller's trail. */
|
|
35
|
+
decision: AdoptionDecision;
|
|
36
|
+
/** True when this adoption is what bought the one bonus round. */
|
|
37
|
+
grantedBonusRound: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface CoverageLedgerOptions {
|
|
40
|
+
/** The round cap before any bonus. */
|
|
41
|
+
cap: number;
|
|
42
|
+
/**
|
|
43
|
+
* Are there grounded requirements to judge against?
|
|
44
|
+
*
|
|
45
|
+
* Without them `missing` is pure holistic-judge free text that can change every
|
|
46
|
+
* round, so there is no trustworthy "grew"/"new" signal — which is why the
|
|
47
|
+
* bonus round is requirements-path only.
|
|
48
|
+
*/
|
|
49
|
+
hasRequirements: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The coverage loop's record: the best plan seen, the rounds spent, and the
|
|
53
|
+
* one-shot bonus round.
|
|
54
|
+
*
|
|
55
|
+
* Methods are named for what they MEAN, not for the field they touch.
|
|
56
|
+
*/
|
|
57
|
+
export declare class CoverageLedger {
|
|
58
|
+
private _best;
|
|
59
|
+
private readonly _opts;
|
|
60
|
+
private _round;
|
|
61
|
+
private _cap;
|
|
62
|
+
private _bonusUsed;
|
|
63
|
+
constructor(_best: ScoredPlan, _opts: CoverageLedgerOptions);
|
|
64
|
+
/** The best-covered plan seen so far — the one that reprompts, and the one that ships. */
|
|
65
|
+
best(): ScoredPlan;
|
|
66
|
+
/** Rounds spent so far. */
|
|
67
|
+
round(): number;
|
|
68
|
+
/** May another reprompt round run? */
|
|
69
|
+
mayRetry(): boolean;
|
|
70
|
+
/** Spend a round. Call once per reprompt, before the child runs. */
|
|
71
|
+
startRound(): number;
|
|
72
|
+
/** What is still uncovered in the shipping plan, or null when nothing is. */
|
|
73
|
+
unresolved(): string[] | null;
|
|
74
|
+
/**
|
|
75
|
+
* Judge one candidate against the best plan and, if it wins, adopt it.
|
|
76
|
+
*
|
|
77
|
+
* The bonus-round grant is decided HERE, against the pre-adoption plan this
|
|
78
|
+
* method still holds — not by a caller reading a snapshot it took beforehand.
|
|
79
|
+
* Two guards keep it off generic judge churn: the grounded covered-set must
|
|
80
|
+
* strictly GROW (a flaky judge relabelling the same-shaped plan's gap does not
|
|
81
|
+
* qualify), and the candidate must expose a NEW area (a gap already present is
|
|
82
|
+
* one we have reprompted against or will). Bounded to one, so a judge that
|
|
83
|
+
* flags forever still cannot loop the plan phase.
|
|
84
|
+
*/
|
|
85
|
+
consider(cand: ScoredPlan): ConsiderOutcome;
|
|
86
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the PLAN-SHAPING loops record, and the decisions that record makes.
|
|
3
|
+
*
|
|
4
|
+
* `GateTally`'s and `AutofixLedger`'s twin, one phase earlier. CONTEXT.md records
|
|
5
|
+
* that shape twice already: a long loop threading mutable locals by closure, with
|
|
6
|
+
* pure helpers extracted for testability while the ORDERING and CARRY-FORWARD
|
|
7
|
+
* decisions stayed in the caller. `coverPlan` was the third instance — five locals
|
|
8
|
+
* (`planTitles`, `best`, `round`, `roundCap`, `bonusRoundUsed`) plus a
|
|
9
|
+
* snapshot-before-overwrite pair (`priorCovered`, `priorMissing`) that existed
|
|
10
|
+
* ONLY because the bonus-round decision was made downstream from the evidence it
|
|
11
|
+
* needed, so the loop had to save a copy of `best` before replacing it.
|
|
12
|
+
*
|
|
13
|
+
* The last real bug here says the shape out loud, in the loop's own comment:
|
|
14
|
+
* *"This used to be two assignments, and the second one kept the OLD plan's
|
|
15
|
+
* accounting whenever the new plan's coverage-map child faulted
|
|
16
|
+
* (`cand.accounting ?? accounting`) — binding requirements to titles they were
|
|
17
|
+
* never mapped against."* `AutofixLedger`'s indictment, verbatim: the decision was
|
|
18
|
+
* made downstream from the evidence.
|
|
19
|
+
*
|
|
20
|
+
* `consider` closes it by construction rather than by comment. It compares, it
|
|
21
|
+
* replaces the plan WHOLE (titles and accounting together, because they are one
|
|
22
|
+
* value), and it grants the bonus round IN THE SAME CALL that adopts — the way
|
|
23
|
+
* `judge(outcome, edited)` enters the demoted signature in the call that demotes.
|
|
24
|
+
* There is no window in which the snapshot and the replacement can disagree.
|
|
25
|
+
*
|
|
26
|
+
* NO I/O. No `logPlanDebug`, no notify, no child — for the same reason `GateTally`
|
|
27
|
+
* performs none: a record that performs effects cannot be driven by a test that
|
|
28
|
+
* only wants the verdict. The caller trails what the returned decision says.
|
|
29
|
+
*/
|
|
30
|
+
import { decideAdoption, normMissingArea } from './coverage-loop.js';
|
|
31
|
+
/**
|
|
32
|
+
* The coverage loop's record: the best plan seen, the rounds spent, and the
|
|
33
|
+
* one-shot bonus round.
|
|
34
|
+
*
|
|
35
|
+
* Methods are named for what they MEAN, not for the field they touch.
|
|
36
|
+
*/
|
|
37
|
+
export class CoverageLedger {
|
|
38
|
+
_best;
|
|
39
|
+
_opts;
|
|
40
|
+
_round = 0;
|
|
41
|
+
_cap;
|
|
42
|
+
_bonusUsed = false;
|
|
43
|
+
constructor(_best, _opts) {
|
|
44
|
+
this._best = _best;
|
|
45
|
+
this._opts = _opts;
|
|
46
|
+
this._cap = _opts.cap;
|
|
47
|
+
}
|
|
48
|
+
/** The best-covered plan seen so far — the one that reprompts, and the one that ships. */
|
|
49
|
+
best() {
|
|
50
|
+
return this._best;
|
|
51
|
+
}
|
|
52
|
+
/** Rounds spent so far. */
|
|
53
|
+
round() {
|
|
54
|
+
return this._round;
|
|
55
|
+
}
|
|
56
|
+
/** May another reprompt round run? */
|
|
57
|
+
mayRetry() {
|
|
58
|
+
return this._round < this._cap;
|
|
59
|
+
}
|
|
60
|
+
/** Spend a round. Call once per reprompt, before the child runs. */
|
|
61
|
+
startRound() {
|
|
62
|
+
return ++this._round;
|
|
63
|
+
}
|
|
64
|
+
/** What is still uncovered in the shipping plan, or null when nothing is. */
|
|
65
|
+
unresolved() {
|
|
66
|
+
return this._best.plan.missing.length > 0 ? this._best.plan.missing : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Judge one candidate against the best plan and, if it wins, adopt it.
|
|
70
|
+
*
|
|
71
|
+
* The bonus-round grant is decided HERE, against the pre-adoption plan this
|
|
72
|
+
* method still holds — not by a caller reading a snapshot it took beforehand.
|
|
73
|
+
* Two guards keep it off generic judge churn: the grounded covered-set must
|
|
74
|
+
* strictly GROW (a flaky judge relabelling the same-shaped plan's gap does not
|
|
75
|
+
* qualify), and the candidate must expose a NEW area (a gap already present is
|
|
76
|
+
* one we have reprompted against or will). Bounded to one, so a judge that
|
|
77
|
+
* flags forever still cannot loop the plan phase.
|
|
78
|
+
*/
|
|
79
|
+
consider(cand) {
|
|
80
|
+
const decision = decideAdoption(this._best.plan, cand.plan, this._opts.hasRequirements);
|
|
81
|
+
if (!decision.adopt)
|
|
82
|
+
return { adopted: false, decision, grantedBonusRound: false };
|
|
83
|
+
const priorCovered = this._best.plan.covered.size;
|
|
84
|
+
const priorMissing = new Set(this._best.plan.missing.map(normMissingArea));
|
|
85
|
+
// WHOLE, titles and accounting together. They are one value; splitting them
|
|
86
|
+
// is a bug this codebase has already had.
|
|
87
|
+
this._best = cand;
|
|
88
|
+
const grant = !this._bonusUsed
|
|
89
|
+
&& this._round >= this._cap
|
|
90
|
+
&& this._opts.hasRequirements
|
|
91
|
+
&& cand.plan.covered.size > priorCovered
|
|
92
|
+
&& cand.plan.missing.some((m) => !priorMissing.has(normMissingArea(m)));
|
|
93
|
+
if (grant) {
|
|
94
|
+
this._bonusUsed = true;
|
|
95
|
+
this._cap++;
|
|
96
|
+
}
|
|
97
|
+
return { adopted: true, decision, grantedBonusRound: grant };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// DECOMPOSE's two retry budgets (`emptyAttempts`, `smallRetryUsed`) are NOT here.
|
|
101
|
+
// They look like this shape and are not: that loop keys on `isSuspectPlan`, a
|
|
102
|
+
// predicate over the SPEC LENGTH rather than a title-count floor, and its two
|
|
103
|
+
// counters already sit inside a nine-line comment explaining why a single counter
|
|
104
|
+
// was wrong. Wrapping them in a class that does not model `isSuspectPlan` would
|
|
105
|
+
// move the code without concentrating the decision.
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* is unit-testable without a TUI or a model.
|
|
31
31
|
*/
|
|
32
32
|
import type { AskSpec } from '../remote/bridge.js';
|
|
33
|
+
import { type QuestionRule } from './question-source.js';
|
|
33
34
|
import { type PlanEntry } from './plan-io.js';
|
|
34
35
|
import { type PendingQuestion } from './question-dialog.js';
|
|
35
36
|
export { resolveAnswer } from './question-dialog.js';
|
|
@@ -65,27 +66,7 @@ export declare const MAX_PLAN_QUESTIONS = 8;
|
|
|
65
66
|
* recommend.
|
|
66
67
|
*/
|
|
67
68
|
export declare const PLAN_FORMAT_HINT: string;
|
|
68
|
-
|
|
69
|
-
* opposed to output the parser simply could not read. */
|
|
70
|
-
export declare function isNoneReply(raw: string): boolean;
|
|
71
|
-
/**
|
|
72
|
-
* Which of the parsed entries is the actual question.
|
|
73
|
-
*
|
|
74
|
-
* parseClarifyList turns EVERY numbered line into an entry, and the local model
|
|
75
|
-
* sometimes writes a numbered analysis note or two before the question it was
|
|
76
|
-
* asked for (measured live: the first numbered line was a note like
|
|
77
|
-
* "1. gateDebugWriter in orchestrator.ts — wraps a raw append function"). Taking
|
|
78
|
-
* entry 0 blindly then shows the note as the question and loses the SUGGESTED
|
|
79
|
-
* line that was attached further down.
|
|
80
|
-
*
|
|
81
|
-
* The SUGGESTED line is the reliable marker of the real question — the prompt
|
|
82
|
-
* requires exactly one, and parseClarifyList attaches it to the entry it follows.
|
|
83
|
-
* So: prefer the first entry that has one; fall back to the first entry when none
|
|
84
|
-
* does, which is the case the format re-prompt then covers.
|
|
85
|
-
*/
|
|
86
|
-
export declare function pickQuestion<T extends {
|
|
87
|
-
suggested?: string;
|
|
88
|
-
}>(parsed: T[]): T | undefined;
|
|
69
|
+
export { isNoneReply, pickQuestion } from './question-source.js';
|
|
89
70
|
/**
|
|
90
71
|
* Does the question offer the user a choice between two named alternatives?
|
|
91
72
|
* Deliberately shallow — an "X or Y?" in the question's own clause.
|
|
@@ -132,6 +113,35 @@ export declare function planDecisiveHint(question: string, suggested: string): s
|
|
|
132
113
|
* subject. It costs one extra child call on the questions where it fires.
|
|
133
114
|
*/
|
|
134
115
|
export declare function planForkHint(question: string): string;
|
|
116
|
+
/**
|
|
117
|
+
* PLAN's quality rules, in order.
|
|
118
|
+
*
|
|
119
|
+
* Each is worth exactly one corrective re-prompt (the child is stateless, so each
|
|
120
|
+
* hint quotes the question back), and each DEGRADES rather than discards when the
|
|
121
|
+
* defect survives — a question with a weak default still beats no question.
|
|
122
|
+
*
|
|
123
|
+
* Only {@link CLARIFY_QUALITY_RULES} is shared with `/task-auto`, and only the
|
|
124
|
+
* deferral rule is in it. The other two were MEASURED here (10/15 fork-shaped
|
|
125
|
+
* questions shipped one option; the SUGGESTED requirement is in both prompts) but
|
|
126
|
+
* each costs one extra child call every time it fires, and clarify is the most
|
|
127
|
+
* A/B'd path in the codebase — moving them there is its own experiment, not a
|
|
128
|
+
* side effect of sharing a state machine. Recorded rather than done.
|
|
129
|
+
*/
|
|
130
|
+
export declare const PLAN_QUALITY_RULES: ReadonlyArray<QuestionRule>;
|
|
131
|
+
/**
|
|
132
|
+
* The deferral rule alone — the one clarify shares.
|
|
133
|
+
*
|
|
134
|
+
* It exists because an accepted "clarify with the user before proceeding" rode
|
|
135
|
+
* into `/task`'s handoff AS AN AUTHORITATIVE DECISION and produced a task whose
|
|
136
|
+
* ACCEPTANCE was "a planning document with placeholder sections" and whose VERIFY
|
|
137
|
+
* asserted that no source file had changed. Clarify's answers ride into the
|
|
138
|
+
* decompose prompt and the AUTO file with exactly the same authority and had no
|
|
139
|
+
* guard at all — the same bug, one command over, waiting.
|
|
140
|
+
*
|
|
141
|
+
* It is also the only one of the three that costs nothing on the happy path: a
|
|
142
|
+
* decisive default never triggers it.
|
|
143
|
+
*/
|
|
144
|
+
export declare const CLARIFY_QUALITY_RULES: ReadonlyArray<QuestionRule>;
|
|
135
145
|
/** The ask spec the session hands to the UI: an {@link AskSpec} plus the picker
|
|
136
146
|
* entries. Kept structurally identical to what phaseGrill/planAuto build so the
|
|
137
147
|
* same SessionUI.ask serves all three. */
|