@mjasnikovs/pi-task 0.38.15 → 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/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +18 -14
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +211 -218
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +109 -1
- package/dist/task/boot-probe.js +139 -23
- package/dist/task/child-runner.d.ts +50 -6
- package/dist/task/child-runner.js +48 -69
- package/dist/task/command-run.d.ts +49 -6
- package/dist/task/command-run.js +154 -18
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/external-context.d.ts +9 -12
- package/dist/task/external-context.js +5 -5
- package/dist/task/failure-classifier.d.ts +9 -1
- package/dist/task/failure-classifier.js +9 -0
- package/dist/task/final-gate-fix.d.ts +22 -26
- package/dist/task/final-gate-fix.js +16 -31
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +55 -89
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +75 -46
- package/dist/task/phases.d.ts +120 -34
- package/dist/task/phases.js +221 -134
- package/dist/task/plan-orchestrator.js +2 -2
- 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/repo-health-check.d.ts +21 -21
- package/dist/task/repo-health-check.js +43 -112
- package/dist/task/run-end.d.ts +77 -0
- package/dist/task/run-end.js +37 -0
- package/dist/task/run-final-gate.js +71 -79
- 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 +9 -1
- package/dist/task/task-gates.js +27 -6
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/workers/brave-search.d.ts +7 -0
- package/dist/workers/brave-search.js +36 -55
- package/dist/workers/ddg-search.d.ts +1 -1
- package/dist/workers/ddg-search.js +27 -47
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/exa-search.d.ts +2 -2
- package/dist/workers/exa-search.js +53 -68
- package/dist/workers/html-clean.js +67 -88
- package/dist/workers/http-request.d.ts +74 -0
- package/dist/workers/http-request.js +103 -0
- package/dist/workers/npm-version.js +37 -42
- package/dist/workers/pi-worker-core.d.ts +13 -2
- package/dist/workers/pi-worker-core.js +35 -25
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +49 -68
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +20 -21
- package/dist/workers/pi-worker-search.js +6 -4
- package/dist/workers/pi-worker.js +5 -4
- package/dist/workers/search-core.d.ts +1 -1
- package/dist/workers/search-core.js +36 -42
- package/dist/workers/search-types.d.ts +13 -0
- package/dist/workers/search-types.js +27 -0
- package/dist/workers/shared.d.ts +51 -11
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-channels.d.ts +60 -0
- package/dist/workers/worker-channels.js +98 -0
- package/package.json +1 -1
package/dist/task/phases.js
CHANGED
|
@@ -8,6 +8,8 @@ import { fetchFocused } from '../workers/fetch-core.js';
|
|
|
8
8
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
9
9
|
import { classifyWorkerFailure } from '../workers/worker-failure.js';
|
|
10
10
|
import { findPhantomImports, formatApiCorrections, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
11
|
+
import { searchProviderKey } from '../workers/search-types.js';
|
|
12
|
+
import { channelSet } from '../workers/worker-channels.js';
|
|
11
13
|
import { fanoutTimeoutPolicy, workerCarryForward, workerProgressCeilingMs, projectDocsBudget, projectDocsBudgetNotice } from './research-fanout-budget.js';
|
|
12
14
|
import { isIntegrationUnknown } from './unknown-routing.js';
|
|
13
15
|
import { extractUserDirectives, preserveDirectivesBlock, enforceDirectives } from './user-directives.js';
|
|
@@ -36,9 +38,10 @@ import { readContracts, buildContractsBlock, buildContractsVerifyBlock } from '.
|
|
|
36
38
|
import { readRequirements, buildRequirementsBlock, buildOwnedRequirementsBlock, readOwnedRequirements, writeOwnedRequirements, ownedForTitle, appendOwnedConstraints } from './requirements.js';
|
|
37
39
|
import { detachUnsatisfiableRequirements, claimPendingRequirements, unclaimedPendingRequirements, formatReassignActions } from './owned-freeze-reassign.js';
|
|
38
40
|
import { trackedSourceOracle } from './owned-freeze-conflict.js';
|
|
39
|
-
import { runPhaseChild,
|
|
41
|
+
import { runPhaseChild, runWithEmphasisRetry, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
40
42
|
import { SessionUI } from '../remote/bridge.js';
|
|
41
|
-
import { isYoloMode, yoloPickAutoAnswer
|
|
43
|
+
import { isYoloMode, yoloPickAutoAnswer } from './yolo.js';
|
|
44
|
+
import { QaTranscript, GRILL_QA_POLICY } from './qa-transcript.js';
|
|
42
45
|
// ─── Re-export constants from their home modules ────────────────────────────
|
|
43
46
|
export { MAX_GRILL_QUESTIONS };
|
|
44
47
|
// ─── Tooling helpers ─────────────────────────────────────────────────────────
|
|
@@ -262,7 +265,7 @@ export const phaseRefine = async (deps, raw, planContext) => {
|
|
|
262
265
|
// re-check the output below (lever). Empty on an ordinary prompt → refine unchanged.
|
|
263
266
|
const directives = extractUserDirectives(raw);
|
|
264
267
|
const directivesBlock = preserveDirectivesBlock(directives);
|
|
265
|
-
const refined = await
|
|
268
|
+
const refined = await runPhaseChild(deps, 'refine', 'read', appendNoThink(REFINE_PROMPT(raw, planContext, existingFiles, contracts, directivesBlock)),
|
|
266
269
|
// refine's deliverable is a 4-section text rewrite that never strictly
|
|
267
270
|
// needs a successful read — on a test-writing task against a large
|
|
268
271
|
// existing codebase the model over-explores (re-reads source hunting for
|
|
@@ -270,7 +273,7 @@ export const phaseRefine = async (deps, raw, planContext) => {
|
|
|
270
273
|
// attempt instead of hard-failing the whole run. See TASK_0016 (mx5):
|
|
271
274
|
// refine looped 3×/resume forever; the deliverable was always producible
|
|
272
275
|
// from the title + design doc alone.
|
|
273
|
-
{ degradeOnExhaustion: true });
|
|
276
|
+
{ degradeOnExhaustion: true, verb: 'restart' });
|
|
274
277
|
// Deterministic backstop: if the refined spec still dropped a directive, append
|
|
275
278
|
// it verbatim rather than trusting the paraphrase. No model in this path.
|
|
276
279
|
const { text, appended } = enforceDirectives(refined, directives);
|
|
@@ -300,21 +303,34 @@ export async function phaseVerifyTooling(deps, research) {
|
|
|
300
303
|
await setTaskSection(deps.cwd, deps.taskId, 'verified tooling', verifiedSection);
|
|
301
304
|
return replaceToolingWithVerified(research, parsed.verified);
|
|
302
305
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
|
|
306
|
+
/**
|
|
307
|
+
* The worker channels the APIS research worker is given.
|
|
308
|
+
*
|
|
309
|
+
* `pi-worker-search` + `pi-worker-fetch` ride along only when the configured
|
|
310
|
+
* engine is usable (a keyless engine always is; brave needs its key). A tool
|
|
311
|
+
* without a key just errors, and a weak model burns calls on it — while search
|
|
312
|
+
* being ABSENT was structural in the other direction: three consecutive audited
|
|
313
|
+
* runs made 0 search calls because the child literally did not have the tool.
|
|
314
|
+
*
|
|
315
|
+
* Both halves of "given a channel" — the tools string and the `-e` path — come
|
|
316
|
+
* from the same rows, so they cannot disagree.
|
|
317
|
+
*/
|
|
318
|
+
function apisWorkerChannels() {
|
|
319
|
+
return channelSet([
|
|
320
|
+
'pi-worker-docs',
|
|
321
|
+
...(searchConfigured() ? ['pi-worker-search', 'pi-worker-fetch'] : [])
|
|
322
|
+
]);
|
|
323
|
+
}
|
|
310
324
|
/**
|
|
311
325
|
* Is live web search configured for this process? The keyless providers (exa,
|
|
312
|
-
* ddg) always are; only brave needs its API key
|
|
326
|
+
* ddg) always are; only brave needs its API key.
|
|
313
327
|
*/
|
|
314
328
|
export function searchConfigured(getEnv = k => process.env[k], provider = getConfig().searchProvider) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
329
|
+
// Asks the SAME row `search()` asks. This used to re-state brave's env pair
|
|
330
|
+
// under a comment saying it "mirrors search-core's lookup" — two statements of
|
|
331
|
+
// one fact, and the one that decides whether the APIS worker is even handed the
|
|
332
|
+
// search tool.
|
|
333
|
+
return searchProviderKey(provider, getEnv) !== null;
|
|
318
334
|
}
|
|
319
335
|
/** Extra prompt block for the APIS worker when search is available — trigger-framed
|
|
320
336
|
* (the validated shape for getting a local model to actually reach for search). */
|
|
@@ -604,10 +620,10 @@ const CONTEXT_SILENT_RETRY_PREAMBLE = 'STOP. Your previous attempt at this task
|
|
|
604
620
|
+ 'nothing else. Keep the same rules as before: state an external library/API behaviour as fact '
|
|
605
621
|
+ 'ONLY when quoting an EXTERNAL CONTEXT block; otherwise write it as an "unverified:" open '
|
|
606
622
|
+ 'question. One claim per bullet. Better to emit three sharp sourced bullets than to say nothing.';
|
|
607
|
-
export async function phaseResearch(deps, refined
|
|
608
|
-
const fileInventoryFn =
|
|
609
|
-
const runWorkerFn =
|
|
610
|
-
const externalContext = await gatherExternalContext(refined, deps
|
|
623
|
+
export async function phaseResearch(deps, refined) {
|
|
624
|
+
const fileInventoryFn = deps.getFileInventory ?? getFileInventory;
|
|
625
|
+
const runWorkerFn = deps.runWorker ?? ((_label, input) => runWorker(input));
|
|
626
|
+
const externalContext = await gatherExternalContext(refined, deps);
|
|
611
627
|
// Pre-compute the project file inventory once and hand it to every worker.
|
|
612
628
|
// Workers can then jump straight to targeted read/grep on known paths
|
|
613
629
|
// instead of each spawning its own discovery loop (find/ls). A '' result
|
|
@@ -721,6 +737,9 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
721
737
|
// The worker still calls as many tools as it wants; it just stops narrating
|
|
722
738
|
// between them. See appendNoThink. Result order (files, apis, context,
|
|
723
739
|
// tooling) is preserved for assembly.
|
|
740
|
+
// Resolved once: `searchConfigured()` reads the environment, and the tools
|
|
741
|
+
// string and the `-e` paths must be derived from the SAME answer.
|
|
742
|
+
const apisChannels = apisWorkerChannels();
|
|
724
743
|
const workerSpecs = [
|
|
725
744
|
{
|
|
726
745
|
section: 'FILES',
|
|
@@ -746,13 +765,12 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
746
765
|
// budget enforced without being announced would just read
|
|
747
766
|
// to the worker as a broken tool.
|
|
748
767
|
+ (fanoutBudget === null ? '' : projectDocsBudgetNotice(fanoutBudget))),
|
|
749
|
-
tools
|
|
750
|
-
|
|
768
|
+
// The tools string and the `-e` paths are ONE fact — which worker
|
|
769
|
+
// channels this research worker is given — and used to be two literals
|
|
770
|
+
// kept in step by eye. `channelSet` derives both from the same rows.
|
|
771
|
+
tools: `read,grep,find,ls,${apisChannels.tools}`,
|
|
751
772
|
fanoutBounded: true,
|
|
752
|
-
extensions:
|
|
753
|
-
DOCS_EXTENSION_PATH,
|
|
754
|
-
...(searchConfigured() ? [SEARCH_EXTENSION_PATH] : [])
|
|
755
|
-
],
|
|
773
|
+
extensions: apisChannels.extensions,
|
|
756
774
|
// ZERO-RETRIEVAL GATE (mx5 run-15 F-1, distinct from the STAGE 1-3 stopping-point
|
|
757
775
|
// thread). In a MINORITY of reps worker:apis emits a complete, plausible APIS section
|
|
758
776
|
// having made ZERO retrieval tool calls — the whole thing recalled from memory.
|
|
@@ -1069,9 +1087,9 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
1069
1087
|
}
|
|
1070
1088
|
return sections.map(({ name, text }) => `${name}\n${text}`).join('\n\n');
|
|
1071
1089
|
}
|
|
1072
|
-
export async function phaseAutoAnswer(deps, refined, research, question
|
|
1073
|
-
const docsFocusedFn =
|
|
1074
|
-
const fetchFocusedFn =
|
|
1090
|
+
export async function phaseAutoAnswer(deps, refined, research, question) {
|
|
1091
|
+
const docsFocusedFn = deps.docsFocused ?? docsFocused;
|
|
1092
|
+
const fetchFocusedFn = deps.fetchFocused ?? fetchFocused;
|
|
1075
1093
|
try {
|
|
1076
1094
|
// Same assembly as the research phase (see external-context.ts); what
|
|
1077
1095
|
// differs is POLICY and the worker variant, and both are arguments now.
|
|
@@ -1109,7 +1127,7 @@ export async function phaseAutoAnswer(deps, refined, research, question, autoDep
|
|
|
1109
1127
|
});
|
|
1110
1128
|
return { body: countBody(r.answer || undefined) };
|
|
1111
1129
|
},
|
|
1112
|
-
search:
|
|
1130
|
+
search: deps.searchFn
|
|
1113
1131
|
}, { targetCap: 2, serviceCap: 2 });
|
|
1114
1132
|
const basePrompt = externalContext + GRILL_AUTO_ANSWER_PROMPT(refined, research, question);
|
|
1115
1133
|
let text = await runPhaseChild(deps, 'grill-auto', 'read', basePrompt);
|
|
@@ -1194,8 +1212,11 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1194
1212
|
// nothing ambiguous remains. Kept in sync with /task-auto's clarify dialog.
|
|
1195
1213
|
const theme = ctx.ui.theme;
|
|
1196
1214
|
const ui = new SessionUI(ctx);
|
|
1197
|
-
|
|
1198
|
-
|
|
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);
|
|
1199
1220
|
const askedQuestions = []; // plain text of each question, for the dup backstop
|
|
1200
1221
|
// Deterministic backstop against a model that ignores "never re-ask": a
|
|
1201
1222
|
// near-duplicate question is reprompted (not auto-answered or shown), and after
|
|
@@ -1209,7 +1230,7 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1209
1230
|
for (let n = 0; n < MAX_GRILL_QUESTIONS; n++) {
|
|
1210
1231
|
const tGenStart = Date.now();
|
|
1211
1232
|
const genHint = dupHint;
|
|
1212
|
-
const raw = await
|
|
1233
|
+
const raw = await runPhaseChild(deps, 'grill-gen', 'read', prependHint(genHint, GRILL_GEN_PROMPT(refined, research, transcript.forGenerator())), { verb: 'restart' });
|
|
1213
1234
|
deps.recordSubStep?.('gen', Date.now() - tGenStart);
|
|
1214
1235
|
const questions = parseGrillQuestions(raw);
|
|
1215
1236
|
if (questions.length === 0)
|
|
@@ -1235,11 +1256,8 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1235
1256
|
// for the editable default and the persisted file.
|
|
1236
1257
|
const shownQ = renderInlineMarkdown(q, theme);
|
|
1237
1258
|
const plainQ = stripInlineMarkdown(q);
|
|
1238
|
-
out.push(`Q${n + 1}: ${plainQ}`);
|
|
1239
|
-
let answer;
|
|
1240
1259
|
if (auto.kind === 'answered') {
|
|
1241
|
-
|
|
1242
|
-
out.push(`A${n + 1}: ${answer} (auto)`);
|
|
1260
|
+
transcript.add('auto', plainQ, stripInlineMarkdown(auto.text));
|
|
1243
1261
|
}
|
|
1244
1262
|
else {
|
|
1245
1263
|
const plainSuggested = auto.suggested === undefined ? undefined : stripInlineMarkdown(auto.suggested);
|
|
@@ -1251,12 +1269,15 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1251
1269
|
// guard direction, promoting a hallucination is not.
|
|
1252
1270
|
const yolo = yoloPickAutoAnswer(isYoloMode(), auto);
|
|
1253
1271
|
if (yolo !== null) {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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
|
+
}
|
|
1260
1281
|
continue;
|
|
1261
1282
|
}
|
|
1262
1283
|
// The picker cards and the reply mapping are shared with /task-auto's
|
|
@@ -1287,17 +1308,17 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
1287
1308
|
});
|
|
1288
1309
|
if (a === undefined)
|
|
1289
1310
|
throw new Error(USER_CANCELLED);
|
|
1290
|
-
//
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1293
|
-
|
|
1294
|
-
|
|
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);
|
|
1295
1317
|
}
|
|
1296
|
-
qa.push(`Q${n + 1}: ${plainQ}\nA${n + 1}: ${answer}`);
|
|
1297
1318
|
}
|
|
1298
|
-
if (
|
|
1319
|
+
if (transcript.length === 0)
|
|
1299
1320
|
return '(no questions produced)';
|
|
1300
|
-
return
|
|
1321
|
+
return transcript.forRecord();
|
|
1301
1322
|
}
|
|
1302
1323
|
/**
|
|
1303
1324
|
* A refutation is a DELETION. Where the run's own research explicitly says a
|
|
@@ -1323,11 +1344,32 @@ export async function dropRefutedConstraints(deps, refined, research) {
|
|
|
1323
1344
|
const refuted = applyRefutations(refined, research);
|
|
1324
1345
|
if (refuted.trail.length === 0)
|
|
1325
1346
|
return refined;
|
|
1326
|
-
|
|
1327
|
-
|
|
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}`);
|
|
1328
1371
|
await appendGateRecord(deps.cwd, deps.taskId, line).catch(() => { });
|
|
1329
1372
|
}
|
|
1330
|
-
return refuted.refined;
|
|
1331
1373
|
}
|
|
1332
1374
|
export async function phaseCompose(deps, refined, research, qa) {
|
|
1333
1375
|
// CLAIM before the belt is built: an obligation an earlier task had to
|
|
@@ -1497,92 +1539,130 @@ export async function critiqueWithFallback(d, p) {
|
|
|
1497
1539
|
}
|
|
1498
1540
|
}
|
|
1499
1541
|
// ─── Phase config table ──────────────────────────────────────────────────────
|
|
1542
|
+
/**
|
|
1543
|
+
* REFINE — restate the raw prompt as a bounded 4-section spec, then subtractively
|
|
1544
|
+
* strike any phantom runtime specifier (`bun:sql`) it carried up verbatim from the
|
|
1545
|
+
* spec doc, BEFORE it flows to research/grill/compose. An appended correction alone
|
|
1546
|
+
* loses: the affirmative survives into the composed GOAL and on to the implementer
|
|
1547
|
+
* (proven: compose re-leaks it 4/4). Rewriting the source so compose has nothing to
|
|
1548
|
+
* contradict is the fix. Silent + no-op when nothing is wrong or the runtime's types
|
|
1549
|
+
* aren't installed.
|
|
1550
|
+
*/
|
|
1551
|
+
export async function refinePhase(d, p) {
|
|
1552
|
+
const refined = await phaseRefine(d, p.rawPrompt, p.planContext);
|
|
1553
|
+
const phantoms = await findPhantomImports(refined, d.cwd);
|
|
1554
|
+
if (phantoms.length === 0)
|
|
1555
|
+
return refined;
|
|
1556
|
+
d.logDebug?.(`phantom specifiers rewritten in refined: ${phantoms.map(x => x.spec).join(', ')}`);
|
|
1557
|
+
return rewritePhantomSpecifiers(refined, phantoms);
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* RESEARCH — the four workers, then the TOOLING verification pass, then a
|
|
1561
|
+
* deterministic check of every runtime builtin specifier the refined task names
|
|
1562
|
+
* (`bun:sql`, `node:…`) against the installed types. A doc can confidently name a
|
|
1563
|
+
* module that does not exist; left unchecked it rides through every phase and the
|
|
1564
|
+
* implementer fabricates a `declare module` shim to compile it. The corrections are
|
|
1565
|
+
* APPENDED so compose folds them into CONSTRAINTS. No LLM cost, silent when clean.
|
|
1566
|
+
*/
|
|
1567
|
+
export async function researchPhase(d, p) {
|
|
1568
|
+
const tResearch = Date.now();
|
|
1569
|
+
const rawResearch = await phaseResearch(d, p.refined);
|
|
1570
|
+
d.recordSubStep?.('workers', Date.now() - tResearch);
|
|
1571
|
+
const tVerify = Date.now();
|
|
1572
|
+
const out = await phaseVerifyTooling(d, rawResearch);
|
|
1573
|
+
d.recordSubStep?.('verify-tooling', Date.now() - tVerify);
|
|
1574
|
+
const corrections = formatApiCorrections(await findPhantomImports(p.refined, d.cwd));
|
|
1575
|
+
if (corrections) {
|
|
1576
|
+
d.logDebug?.(`phantom imports flagged:\n${corrections}`);
|
|
1577
|
+
return `${out}\n\n${corrections}`;
|
|
1578
|
+
}
|
|
1579
|
+
return out;
|
|
1580
|
+
}
|
|
1581
|
+
/** GRILL — the adaptive question loop, and the only phase that talks to the user. */
|
|
1582
|
+
export function grillPhase(d, p) {
|
|
1583
|
+
return phaseGrill(d, p.ctx, p.widgetState, p.refined, p.research);
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* COMPOSE — compose the spec from the refined task, the research and the Q&A.
|
|
1587
|
+
*
|
|
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.
|
|
1592
|
+
*/
|
|
1593
|
+
export async function composePhase(d, p) {
|
|
1594
|
+
return await phaseCompose(d, p.refined, p.research, p.qa);
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* CRITIQUE — the last spec-producing step, and the two host-side corrections that
|
|
1598
|
+
* must run after it in THIS ORDER.
|
|
1599
|
+
*
|
|
1600
|
+
* BRACES (mx5 run 16): append any owned design obligation the spec still omits as a
|
|
1601
|
+
* CONSTRAINTS bullet. The belt block upstream is obeyed ~25% (measured); a host-side
|
|
1602
|
+
* append is obeyed by construction. Idempotent — quotes the spec already carries
|
|
1603
|
+
* (belt-obeying reps) are skipped.
|
|
1604
|
+
*
|
|
1605
|
+
* Then DETACH: an owned obligation whose only file this spec also FREEZES is
|
|
1606
|
+
* unsatisfiable here, so it moves to the pending task that writes that file rather
|
|
1607
|
+
* than shipping a requirement no one can meet. It MUST run after the append, because
|
|
1608
|
+
* the append is what writes the stamp the detach reads — a critique-time probe
|
|
1609
|
+
* measured 0/40 because the stamp did not exist yet.
|
|
1610
|
+
*/
|
|
1611
|
+
export async function critiquePhase(d, p) {
|
|
1612
|
+
const spec = await critiqueWithFallback(d, p);
|
|
1613
|
+
const owned = await ownedForThisTask(d);
|
|
1614
|
+
if (owned.length === 0)
|
|
1615
|
+
return spec;
|
|
1616
|
+
const out = appendOwnedConstraints(spec, owned);
|
|
1617
|
+
if (out !== spec) {
|
|
1618
|
+
d.logDebug?.('owned-requirements braces: appended omitted design obligation(s) to CONSTRAINTS');
|
|
1619
|
+
}
|
|
1620
|
+
return await resolveOwnedFreezeForThisTask(d, out);
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* The pipeline, as a table with no bodies.
|
|
1624
|
+
*
|
|
1625
|
+
* Every row's `run` is a named exported function, so the COMPOSITION inside a
|
|
1626
|
+
* step — which is where this codebase's recorded phase defects have lived, not in
|
|
1627
|
+
* the parts — is drivable directly instead of only through a whole TaskRunner run.
|
|
1628
|
+
* The parts stay exported and separately covered; what changed is that the ORDER
|
|
1629
|
+
* they run in is now asserted by driving the row rather than retyped in a test.
|
|
1630
|
+
*/
|
|
1500
1631
|
export const PHASES = [
|
|
1501
1632
|
{
|
|
1502
1633
|
name: 'refine',
|
|
1503
1634
|
section: 'refined prompt',
|
|
1504
1635
|
field: 'refined',
|
|
1505
|
-
run:
|
|
1506
|
-
|
|
1507
|
-
// Subtractively strike any phantom runtime specifier (`bun:sql`) the
|
|
1508
|
-
// refine carried up verbatim from the spec doc, BEFORE it flows to
|
|
1509
|
-
// research/grill/compose. An appended correction alone loses: the
|
|
1510
|
-
// affirmative survives into the composed GOAL and on to the implementer
|
|
1511
|
-
// (proven: compose re-leaks it 4/4). Rewriting the source so compose has
|
|
1512
|
-
// nothing to contradict is the fix. Silent + no-op when nothing is wrong
|
|
1513
|
-
// or the runtime's types aren't installed.
|
|
1514
|
-
const phantoms = await findPhantomImports(refined, d.cwd);
|
|
1515
|
-
if (phantoms.length === 0)
|
|
1516
|
-
return refined;
|
|
1517
|
-
d.logDebug?.(`phantom specifiers rewritten in refined: ${phantoms.map(x => x.spec).join(', ')}`);
|
|
1518
|
-
return rewritePhantomSpecifiers(refined, phantoms);
|
|
1519
|
-
}
|
|
1520
|
-
},
|
|
1521
|
-
{
|
|
1522
|
-
name: 'research',
|
|
1523
|
-
section: 'research',
|
|
1524
|
-
field: 'research',
|
|
1525
|
-
run: async (d, p) => {
|
|
1526
|
-
const tResearch = Date.now();
|
|
1527
|
-
const rawResearch = await phaseResearch(d, p.refined);
|
|
1528
|
-
d.recordSubStep?.('workers', Date.now() - tResearch);
|
|
1529
|
-
const tVerify = Date.now();
|
|
1530
|
-
const out = await phaseVerifyTooling(d, rawResearch);
|
|
1531
|
-
d.recordSubStep?.('verify-tooling', Date.now() - tVerify);
|
|
1532
|
-
// Deterministically verify every runtime builtin specifier the refined
|
|
1533
|
-
// task names (`bun:sql`, `node:…`) against the installed types. A doc can
|
|
1534
|
-
// confidently name a module that does not exist; left unchecked it rides
|
|
1535
|
-
// through every phase and the implementer fabricates a `declare module`
|
|
1536
|
-
// shim to compile it. Append the corrections so compose folds them into
|
|
1537
|
-
// CONSTRAINTS. No LLM cost and silent when nothing is wrong.
|
|
1538
|
-
const corrections = formatApiCorrections(await findPhantomImports(p.refined, d.cwd));
|
|
1539
|
-
if (corrections) {
|
|
1540
|
-
d.logDebug?.(`phantom imports flagged:\n${corrections}`);
|
|
1541
|
-
return `${out}\n\n${corrections}`;
|
|
1542
|
-
}
|
|
1543
|
-
return out;
|
|
1544
|
-
}
|
|
1545
|
-
},
|
|
1546
|
-
{
|
|
1547
|
-
name: 'grill',
|
|
1548
|
-
section: 'grill Q&A',
|
|
1549
|
-
field: 'qa',
|
|
1550
|
-
run: (d, p) => phaseGrill(d, p.ctx, p.widgetState, p.refined, p.research)
|
|
1551
|
-
},
|
|
1552
|
-
{
|
|
1553
|
-
name: 'compose',
|
|
1554
|
-
section: 'spec',
|
|
1555
|
-
field: 'spec',
|
|
1556
|
-
run: async (d, p) => {
|
|
1557
|
-
p.refined = await dropRefutedConstraints(d, p.refined, p.research);
|
|
1558
|
-
return await phaseCompose(d, p.refined, p.research, p.qa);
|
|
1559
|
-
}
|
|
1636
|
+
run: refinePhase,
|
|
1637
|
+
postCommit: refinePostCommit
|
|
1560
1638
|
},
|
|
1561
|
-
{
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
run: async (d, p) => {
|
|
1566
|
-
const spec = await critiqueWithFallback(d, p);
|
|
1567
|
-
// BRACES (mx5 run 16): after the LAST spec-producing step, append any
|
|
1568
|
-
// owned design obligation the spec still omits as a CONSTRAINTS
|
|
1569
|
-
// bullet. The belt block upstream is obeyed ~25% (measured); a
|
|
1570
|
-
// host-side append is obeyed by construction. Idempotent: quotes the
|
|
1571
|
-
// spec already carries (belt-obeying reps) are skipped.
|
|
1572
|
-
const owned = await ownedForThisTask(d);
|
|
1573
|
-
if (owned.length === 0)
|
|
1574
|
-
return spec;
|
|
1575
|
-
const out = appendOwnedConstraints(spec, owned);
|
|
1576
|
-
if (out !== spec) {
|
|
1577
|
-
d.logDebug?.('owned-requirements braces: appended omitted design obligation(s) to CONSTRAINTS');
|
|
1578
|
-
}
|
|
1579
|
-
// An owned obligation whose only file this spec also FREEZES is
|
|
1580
|
-
// unsatisfiable here; move it to the pending task that writes that
|
|
1581
|
-
// file rather than shipping a requirement no one can meet.
|
|
1582
|
-
return await resolveOwnedFreezeForThisTask(d, out);
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1639
|
+
{ name: 'research', section: 'research', field: 'research', run: researchPhase },
|
|
1640
|
+
{ name: 'grill', section: 'grill Q&A', field: 'qa', run: grillPhase },
|
|
1641
|
+
{ name: 'compose', section: 'spec', field: 'spec', carry: composeCarry, run: composePhase },
|
|
1642
|
+
{ name: 'critique', section: 'spec', field: 'spec', run: critiquePhase }
|
|
1585
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
|
+
}
|
|
1586
1666
|
// INTEGRATION-DEPTH APPEND (2026-07-27): the lever proposed for this exact site —
|
|
1587
1667
|
// deterministically append a known-runnable integration command to the VERIFY block
|
|
1588
1668
|
// whenever a task's ACCEPTANCE claims runtime behaviour — is REFUTED at STEP 0 and was
|
|
@@ -1627,9 +1707,16 @@ export const PHASES = [
|
|
|
1627
1707
|
//
|
|
1628
1708
|
// Durable assets kept: both rigs above and their unit tests. Do NOT wire an append here
|
|
1629
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. */
|
|
1630
1711
|
export async function postCommitPhase(phase, deps, pc, out) {
|
|
1631
|
-
|
|
1632
|
-
|
|
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) {
|
|
1633
1720
|
const title = deriveTitle(out);
|
|
1634
1721
|
pc.widgetState.title = title;
|
|
1635
1722
|
// Compress the (often paragraph-long) title into a short display label. This
|
|
@@ -187,8 +187,8 @@ async function defaultHandoff(ctx, cwd, prompt) {
|
|
|
187
187
|
await runGatedTask(ctx, cwd, prompt);
|
|
188
188
|
return undefined;
|
|
189
189
|
}
|
|
190
|
-
const { taskId,
|
|
191
|
-
if (
|
|
190
|
+
const { taskId, end } = await runSingleTask(ctx, cwd, prompt, { notifyFinish: true });
|
|
191
|
+
if (end.kind === 'no-session') {
|
|
192
192
|
ctx.ui.notify('Could not start a fresh session for /task-plan.', 'warning');
|
|
193
193
|
return undefined;
|
|
194
194
|
}
|
|
@@ -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
|
+
}
|