@mjasnikovs/pi-task 0.40.50 → 0.41.0
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 +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +15 -1
- package/dist/task/accept-debt.js +4 -0
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +59 -7
- package/dist/task/auto-io.js +140 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +126 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +199 -38
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +39 -57
- package/dist/task/task-gates.js +115 -195
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +28 -5
- package/dist/task/verify-resolution.js +70 -13
- package/dist/task/verify-work.d.ts +98 -35
- package/dist/task/verify-work.js +227 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
package/dist/task/phases.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { docsFocused } from '../workers/docs-core.js';
|
|
7
|
-
import { detectEcosystems } from '../workers/docs-ecosystems.js';
|
|
8
7
|
import { fetchFocused } from '../workers/fetch-core.js';
|
|
9
8
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
10
9
|
import { findPhantomImports, formatApiCorrections, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
@@ -14,14 +13,14 @@ import { snapshotLeverEnv, workerProgressCeilingMs, projectDocsBudget, projectDo
|
|
|
14
13
|
import { isIntegrationUnknown } from './unknown-routing.js';
|
|
15
14
|
import { extractUserDirectives, preserveDirectivesBlock, enforceDirectives } from './user-directives.js';
|
|
16
15
|
import { demoteUnsourcedAttributions } from './context-attribution.js';
|
|
17
|
-
import {
|
|
18
|
-
import { buildOrientation, orientationTier } from './orientation.js';
|
|
16
|
+
import { ORIENTATION_TIERS, orientationTier } from './orientation.js';
|
|
19
17
|
import { getConfig } from '../config/config.js';
|
|
20
|
-
import { readFile } from 'node:fs/promises';
|
|
21
|
-
import { resolve } from 'node:path';
|
|
22
18
|
import { buildExternalContext, gatherExternalContext } from './external-context.js';
|
|
19
|
+
import { currentRunContext } from './run-context.js';
|
|
20
|
+
import { readableMentions } from './mentions.js';
|
|
23
21
|
import { REFINE_PROMPT, RESEARCH_FILES_PROMPT, RESEARCH_APIS_PROMPT, RESEARCH_CONTEXT_PROMPT, RESEARCH_TOOLING_PROMPT, GRILL_GEN_PROMPT, GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT, COMPOSE_PROMPT, CRITIQUE_PROMPT, CRITIQUE_TRIAGE_PROMPT, VERIFY_TOOLING_PROMPT, MAX_GRILL_QUESTIONS } from './prompts.js';
|
|
24
|
-
import { appendGateRecord, readSection, removeTaskSection, setTaskSection, updateTaskFrontMatter } from './task-io.js';
|
|
22
|
+
import { appendGateRecord, readSection, readTaskFile, removeTaskSection, setTaskSection, updateTaskFrontMatter } from './task-io.js';
|
|
23
|
+
import { extractSection } from './task-parsers.js';
|
|
25
24
|
import { applyDeprecations } from './deprecated-constraint.js';
|
|
26
25
|
import { applyRefutations } from './refuted-constraint.js';
|
|
27
26
|
import { spawnSync } from 'node:child_process';
|
|
@@ -37,16 +36,24 @@ import { settleQuestion } from './question-dialog.js';
|
|
|
37
36
|
import { findSynthesizedApis, synthesizedApiReaskHint } from './api-synthesis.js';
|
|
38
37
|
import { GREP_THEATER_RETRY_HINT } from './verify-quality.js';
|
|
39
38
|
import { readContracts, buildContractsBlock, buildContractsVerifyBlock } from './contracts.js';
|
|
40
|
-
import { readRequirements, buildRequirementsBlock, buildOwnedRequirementsBlock, readOwnedRequirements, writeOwnedRequirements,
|
|
39
|
+
import { readRequirements, buildRequirementsBlock, buildOwnedRequirementsBlock, readOwnedRequirements, writeOwnedRequirements, ownedForTask, appendOwnedConstraints } from './requirements.js';
|
|
41
40
|
import { detachUnsatisfiableRequirements, claimPendingRequirements, unclaimedPendingRequirements, formatReassignActions } from './owned-freeze-reassign.js';
|
|
42
41
|
import { trackedSourceOracle } from './owned-freeze-conflict.js';
|
|
43
42
|
import { groupArgsForChild, runPhaseChild, runWithEmphasisRetry, prependHint, USER_CANCELLED, ChildFailureError, isFatalChildCause } from './child-runner.js';
|
|
44
|
-
import { runResearchWorker, researchWorkerCacheHeading } from './research-worker.js';
|
|
43
|
+
import { runResearchStage, runResearchWorker, researchWorkerCacheHeading } from './research-worker.js';
|
|
45
44
|
import { SessionUI, notifyRun } from '../remote/bridge.js';
|
|
46
45
|
import { isYoloMode, yoloPickAutoAnswer } from './yolo.js';
|
|
47
46
|
import { QaTranscript, GRILL_QA_POLICY } from './qa-transcript.js';
|
|
48
47
|
// ─── Re-export constants from their home modules ────────────────────────────
|
|
49
48
|
export { MAX_GRILL_QUESTIONS };
|
|
49
|
+
/**
|
|
50
|
+
* This run's context, or a throwaway one for a caller outside a run bracket (a
|
|
51
|
+
* direct phase call in a test). Every per-project fact a phase asks for goes
|
|
52
|
+
* through it, so a /task-auto run derives each one once instead of per task.
|
|
53
|
+
*/
|
|
54
|
+
function runContextFor(deps) {
|
|
55
|
+
return deps.runContext ?? currentRunContext(deps.cwd);
|
|
56
|
+
}
|
|
50
57
|
// ─── Tooling helpers ─────────────────────────────────────────────────────────
|
|
51
58
|
/** Extract the TOOLING section commands from a research output string. */
|
|
52
59
|
export function extractToolingCommands(research) {
|
|
@@ -116,23 +123,16 @@ const REFINE_PRESERVE_DIRECTIVE = 'EXISTING FILES ON DISK — AUTHORITATIVE (ove
|
|
|
116
123
|
export async function refineExistingFilesBlock(deps) {
|
|
117
124
|
if (!getConfig().orientation)
|
|
118
125
|
return '';
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
return '';
|
|
122
|
-
const paths = inventoryRaw
|
|
123
|
-
.split('\n')
|
|
126
|
+
const runContext = runContextFor(deps);
|
|
127
|
+
const paths = (await runContext.inventoryPaths())
|
|
124
128
|
.map(l => l.trim())
|
|
125
|
-
.filter(l =>
|
|
129
|
+
.filter(l => {
|
|
130
|
+
const tier = orientationTier(l);
|
|
131
|
+
return tier !== null && tier <= ORIENTATION_TIERS.config;
|
|
132
|
+
});
|
|
126
133
|
if (paths.length === 0)
|
|
127
134
|
return '';
|
|
128
|
-
const { block } = await
|
|
129
|
-
try {
|
|
130
|
-
return await readFile(resolve(deps.cwd, p), 'utf8');
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
135
|
+
const { block } = await runContext.orientationOf(paths);
|
|
136
136
|
return block.trim().length === 0 ? '' : `${REFINE_PRESERVE_DIRECTIVE}\n\n${block.trim()}`;
|
|
137
137
|
}
|
|
138
138
|
/**
|
|
@@ -163,9 +163,8 @@ export async function phaseCarriedBlocks(deps) {
|
|
|
163
163
|
return [contracts, requirements, owned].filter(b => b.length > 0).join('\n');
|
|
164
164
|
}
|
|
165
165
|
/**
|
|
166
|
-
* The owned (task-mapped) requirements for THIS task
|
|
167
|
-
*
|
|
168
|
-
* `raw prompt` section verbatim. Empty outside /task-auto runs, for spliced
|
|
166
|
+
* The owned (task-mapped) requirements for THIS task, joined on the plan key the
|
|
167
|
+
* coverage map assigned them to. Empty outside /task-auto runs, for spliced
|
|
169
168
|
* repair tasks, and when the plan recorded no mapping — all of which make this a
|
|
170
169
|
* no-op. This is the BELT (a prompt block, into refine and compose);
|
|
171
170
|
* `appendOwnedConstraints` on the final spec is the BRACES. The belt is a request
|
|
@@ -173,20 +172,27 @@ export async function phaseCarriedBlocks(deps) {
|
|
|
173
172
|
*/
|
|
174
173
|
async function ownedForThisTask(deps) {
|
|
175
174
|
try {
|
|
176
|
-
|
|
177
|
-
return ownedForTitle(await readOwnedRequirements(deps.cwd), title.trim());
|
|
175
|
+
return ownedForTask(await readOwnedRequirements(deps.cwd), await planJoin(deps));
|
|
178
176
|
}
|
|
179
177
|
catch {
|
|
180
178
|
return [];
|
|
181
179
|
}
|
|
182
180
|
}
|
|
183
|
-
/**
|
|
184
|
-
|
|
181
|
+
/**
|
|
182
|
+
* How this task addresses the owned ledger: its plan key, and its plan title —
|
|
183
|
+
* the stored `raw prompt` section verbatim — for a ledger written before keys.
|
|
184
|
+
*/
|
|
185
|
+
async function planJoin(deps) {
|
|
185
186
|
try {
|
|
186
|
-
|
|
187
|
+
const { frontMatter, body } = await readTaskFile(deps.cwd, deps.taskId);
|
|
188
|
+
const title = extractSection(body, 'raw prompt') ?? '';
|
|
189
|
+
return {
|
|
190
|
+
...(frontMatter.plan_key !== undefined && { key: frontMatter.plan_key }),
|
|
191
|
+
title: title.trim()
|
|
192
|
+
};
|
|
187
193
|
}
|
|
188
194
|
catch {
|
|
189
|
-
return '';
|
|
195
|
+
return { title: '' };
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
/** The `isSource` oracle production uses: git tracks the path in this tree. */
|
|
@@ -214,12 +220,12 @@ export async function resolveOwnedFreezeForThisTask(deps, spec) {
|
|
|
214
220
|
const ledger = await readOwnedRequirements(deps.cwd).catch(() => []);
|
|
215
221
|
if (ledger.length === 0)
|
|
216
222
|
return spec;
|
|
217
|
-
const
|
|
218
|
-
if (title.length === 0)
|
|
223
|
+
const join = await planJoin(deps);
|
|
224
|
+
if (join.title.length === 0)
|
|
219
225
|
return spec;
|
|
220
226
|
const res = detachUnsatisfiableRequirements({
|
|
221
227
|
spec,
|
|
222
|
-
|
|
228
|
+
...join,
|
|
223
229
|
ledger,
|
|
224
230
|
isSource: repoSourceOracle(deps.cwd)
|
|
225
231
|
});
|
|
@@ -247,10 +253,10 @@ export async function claimOwnedFreezeForThisTask(deps, refined) {
|
|
|
247
253
|
const ledger = await readOwnedRequirements(deps.cwd).catch(() => []);
|
|
248
254
|
if (unclaimedPendingRequirements(ledger).length === 0)
|
|
249
255
|
return;
|
|
250
|
-
const
|
|
251
|
-
if (title.length === 0)
|
|
256
|
+
const join = await planJoin(deps);
|
|
257
|
+
if (join.title.length === 0)
|
|
252
258
|
return;
|
|
253
|
-
const res = claimPendingRequirements({ intent: refined,
|
|
259
|
+
const res = claimPendingRequirements({ intent: refined, ...join, ledger });
|
|
254
260
|
if (res.actions.length === 0)
|
|
255
261
|
return;
|
|
256
262
|
deps.logDebug?.(formatReassignActions(res.actions));
|
|
@@ -298,29 +304,43 @@ export async function phaseVerifyTooling(deps, research) {
|
|
|
298
304
|
if (!commands || commands.length === 0) {
|
|
299
305
|
return replaceToolingWithVerified(research, []);
|
|
300
306
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
307
|
+
// UNVERIFIED is the degrade for a child that merely failed, and it is decided
|
|
308
|
+
// here rather than inside the run context so a failure is never cached as a
|
|
309
|
+
// verdict: the next task asks again.
|
|
310
|
+
let unverified = false;
|
|
311
|
+
let inconclusive = false;
|
|
312
|
+
const verified = await runContextFor(deps).verifiedToolingFor(commands, async (unknown) => {
|
|
313
|
+
let output;
|
|
314
|
+
try {
|
|
315
|
+
output = await runPhaseChild(deps, 'verify-tooling', 'read,bash', VERIFY_TOOLING_PROMPT(unknown.join('\n')));
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
if (isFatalChildCause(e))
|
|
319
|
+
throw e;
|
|
320
|
+
// A hung command is the one cause worth a trail line: it cost the
|
|
321
|
+
// ceiling on every strike and says the SPEC named something unbounded.
|
|
322
|
+
if (e instanceof ChildFailureError && e.failure.kind === 'command-timeout') {
|
|
323
|
+
deps.logDebug?.(`verify-tooling: ${e.message} — shipping the list unverified`);
|
|
324
|
+
}
|
|
325
|
+
unverified = true;
|
|
326
|
+
return { verified: [], rejected: [] };
|
|
315
327
|
}
|
|
328
|
+
const parsed = parseVerifyToolingOutput(output);
|
|
329
|
+
inconclusive = parsed.verified.length === 0 && parsed.rejected.length === 0;
|
|
330
|
+
return {
|
|
331
|
+
verified: parsed.verified.map(v => ({ cmd: v.cmd, class: v.class })),
|
|
332
|
+
rejected: parsed.rejected.map(r => r.cmd)
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
if (unverified)
|
|
316
336
|
return replaceToolingWithVerified(research, commands);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return replaceToolingWithVerified(research,
|
|
337
|
+
const cmds = verified.map(v => v.cmd);
|
|
338
|
+
// Written per task for provenance even though the verdicts are the run's: the
|
|
339
|
+
// task file has to say what THIS task was told its tooling was.
|
|
340
|
+
await setTaskSection(deps.cwd, deps.taskId, 'verified tooling', cmds.length > 0 ? verified.map(v => `${v.cmd} ${v.class}`).join('\n')
|
|
341
|
+
: inconclusive ? '(verification inconclusive)'
|
|
342
|
+
: '(none verified)');
|
|
343
|
+
return replaceToolingWithVerified(research, cmds);
|
|
324
344
|
}
|
|
325
345
|
/**
|
|
326
346
|
* The worker channels the APIS research worker is given.
|
|
@@ -389,22 +409,6 @@ export function scopedToolingGoal(refined) {
|
|
|
389
409
|
const firstBullet = goal.search(/\n[ \t]*[-*]\s/);
|
|
390
410
|
return firstBullet === -1 ? goal : goal.slice(0, firstBullet).trim();
|
|
391
411
|
}
|
|
392
|
-
/**
|
|
393
|
-
* Dependency names declared by the project manifest, used by the CONTEXT post-check to
|
|
394
|
-
* tell "this bullet is about an external library" from "this bullet is about our source".
|
|
395
|
-
* A missing or malformed package.json yields none, which makes the post-check a no-op
|
|
396
|
-
* rather than an error — a non-node project must still be able to run research.
|
|
397
|
-
*/
|
|
398
|
-
async function manifestDependencyNames(cwd) {
|
|
399
|
-
try {
|
|
400
|
-
const raw = await readFile(resolve(cwd, 'package.json'), 'utf8');
|
|
401
|
-
const pkg = JSON.parse(raw);
|
|
402
|
-
return [...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})];
|
|
403
|
-
}
|
|
404
|
-
catch {
|
|
405
|
-
return [];
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
412
|
/**
|
|
409
413
|
* Prepended to worker:apis's prompt on the ONE retry the zero-retrieval gate triggers. It
|
|
410
414
|
* names the exact failure (a section written with no retrieval) so the correction is concrete,
|
|
@@ -436,42 +440,43 @@ const CONTEXT_SILENT_RETRY_PREAMBLE = 'STOP. Your previous attempt at this task
|
|
|
436
440
|
+ 'nothing else. Keep the same rules as before: state an external library/API behaviour as fact '
|
|
437
441
|
+ 'ONLY when quoting an EXTERNAL CONTEXT block; otherwise write it as an "unverified:" open '
|
|
438
442
|
+ 'question. One claim per bullet. Better to emit three sharp sourced bullets than to say nothing.';
|
|
439
|
-
export async function phaseResearch(deps, refined) {
|
|
440
|
-
const
|
|
443
|
+
export async function phaseResearch(deps, refined, rawPrompt = '') {
|
|
444
|
+
const runContext = runContextFor(deps);
|
|
441
445
|
const runWorkerFn = deps.runWorker ?? ((_label, input) => runWorker(input));
|
|
442
446
|
const externalContext = await gatherExternalContext(refined, deps);
|
|
443
|
-
//
|
|
447
|
+
// The project file inventory, derived ONCE PER RUN and handed to every worker.
|
|
444
448
|
// Workers can then jump straight to targeted read/grep on known paths
|
|
445
449
|
// instead of each spawning its own discovery loop (find/ls). A '' result
|
|
446
450
|
// (non-git repo, git missing, abort) silently falls back to the original
|
|
447
|
-
// behavior.
|
|
448
|
-
const inventoryRaw =
|
|
451
|
+
// behavior. The seam wins when a caller supplied one.
|
|
452
|
+
const inventoryRaw = deps.getFileInventory ?
|
|
453
|
+
await deps.getFileInventory(deps.cwd, deps.signal).catch(() => '')
|
|
454
|
+
: await runContext.inventory();
|
|
449
455
|
const inventoryHeader = inventoryRaw.length > 0 ? `PROJECT FILE INVENTORY\n${inventoryRaw}\n\n` : '';
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
//
|
|
456
|
+
// The spec document(s) this task's own prompt points at. A cited doc is the
|
|
457
|
+
// one file orientation cannot select by convention and the one every worker
|
|
458
|
+
// needs — in the mx5 run it was read 50 times — so the mentions are threaded
|
|
459
|
+
// here and the `cited` rule (orientation.ts) puts it first in line for the
|
|
460
|
+
// byte budget.
|
|
461
|
+
const cited = await readableMentions(deps.cwd, rawPrompt);
|
|
462
|
+
// Pre-read the project's orientation core (the cited doc, manifest, config,
|
|
463
|
+
// project rules, domain types, schema, entrypoints, API surface) ONCE and hand
|
|
464
|
+
// the full contents to the EXPLORING workers in their header. The workers run
|
|
465
|
+
// as separate child processes, so without this each one that explores re-reads
|
|
466
|
+
// the same hot files cold. Bounded by a hard byte budget so it can't overflow
|
|
467
|
+
// on a large repo; purely additive (nothing is blocked) so it can only remove
|
|
468
|
+
// a redundant read, never hide a file.
|
|
457
469
|
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
try {
|
|
469
|
-
return await readFile(resolve(deps.cwd, path), 'utf8');
|
|
470
|
-
}
|
|
471
|
-
catch {
|
|
472
|
-
return null;
|
|
473
|
-
}
|
|
474
|
-
}).catch(() => ({ block: '', supplied: new Set() }));
|
|
470
|
+
// All but TOOLING, and the split is by whether the worker reads at all. FILES,
|
|
471
|
+
// APIS and CONTEXT all explore — CONTEXT with `read,grep` — so pre-supplying
|
|
472
|
+
// the core replaces reads they would otherwise make. TOOLING is scoped to the
|
|
473
|
+
// GOAL prose and single-read-guarded, so for it the block is pure prefill with
|
|
474
|
+
// no read to displace.
|
|
475
|
+
const orientation = getConfig().orientation && inventoryRaw.length > 0 ?
|
|
476
|
+
deps.getFileInventory ?
|
|
477
|
+
await runContext.orientationOf(inventoryRaw.split('\n').filter(l => l.trim().length > 0), cited)
|
|
478
|
+
: await runContext.orientation(cited)
|
|
479
|
+
: { block: '', supplied: new Set() };
|
|
475
480
|
if (orientation.supplied.size > 0) {
|
|
476
481
|
deps.logDebug?.(`orientation: pre-supplied ${orientation.supplied.size} core files`);
|
|
477
482
|
}
|
|
@@ -479,7 +484,7 @@ export async function phaseResearch(deps, refined) {
|
|
|
479
484
|
// Braces for the CONTEXT worker's LIVE-DATA RULE (the belt is the prompt itself).
|
|
480
485
|
// Judged against the EXTERNAL CONTEXT this run actually gathered — the same string
|
|
481
486
|
// the worker is handed below — and the manifest's dependency names.
|
|
482
|
-
const manifestPackages =
|
|
487
|
+
const manifestPackages = runContext.manifestDeps();
|
|
483
488
|
// The spec-cited-URL lever from ./spec-urls.ts is built and unit-tested but is
|
|
484
489
|
// NOT WIRED HERE: pointing the worker at a page it should have read did not
|
|
485
490
|
// change what it produced, so the block would be prefill on every APIS prompt
|
|
@@ -522,14 +527,11 @@ export async function phaseResearch(deps, refined) {
|
|
|
522
527
|
}
|
|
523
528
|
return r;
|
|
524
529
|
});
|
|
525
|
-
//
|
|
526
|
-
// the
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
// reasoning level (config/reasoning.ts `research` group) changes decode length
|
|
531
|
-
// and so changes the answer too, which is why this is a config knob and not a
|
|
532
|
-
// constant.
|
|
530
|
+
// The four workers and the ONE dependency between them: APIS is written
|
|
531
|
+
// against the FILES map, and everything else is independent. `runResearchStage`
|
|
532
|
+
// reads that off `after`, so FILES runs, then APIS, with CONTEXT and TOOLING
|
|
533
|
+
// alongside both — and `researchConcurrency: 'serial'` still runs them in order
|
|
534
|
+
// for a single local GPU, where concurrent streams share one device.
|
|
533
535
|
//
|
|
534
536
|
// Result order (files, apis, context, tooling) is preserved for assembly.
|
|
535
537
|
// Resolved once: `searchConfigured()` reads the environment, and the tools
|
|
@@ -545,14 +547,15 @@ export async function phaseResearch(deps, refined) {
|
|
|
545
547
|
{
|
|
546
548
|
section: 'APIS',
|
|
547
549
|
label: 'worker:apis',
|
|
550
|
+
// The one declared edge: FILES' finished map rides along, so the worker
|
|
551
|
+
// doesn't re-derive where-things-live through project-docs queries the
|
|
552
|
+
// FILES worker just answered.
|
|
553
|
+
after: ['FILES'],
|
|
548
554
|
// Read-heavy: gets the orientation core (see note above). Search/fetch
|
|
549
555
|
// ride along only when a Brave key exists — see SEARCH_EXTENSION_PATH.
|
|
550
|
-
// FILES' finished map rides along when available (serial default), so
|
|
551
|
-
// the worker doesn't re-derive where-things-live through project-docs
|
|
552
|
-
// queries the FILES worker just answered.
|
|
553
556
|
prompt: prior => orientation.block
|
|
554
557
|
+ promptHeader
|
|
555
|
-
+ RESEARCH_APIS_PROMPT(refined, prior.find(s => s.name === 'FILES')?.text || undefined,
|
|
558
|
+
+ RESEARCH_APIS_PROMPT(refined, prior.find(s => s.name === 'FILES')?.text || undefined, runContext.ecosystems())
|
|
556
559
|
+ (searchConfigured() ? RESEARCH_SEARCH_HINT : '')
|
|
557
560
|
// Empty unless PI_TASK_PROJECT_DOCS_BUDGET is set. The tool-side
|
|
558
561
|
// half lives in pi-worker-docs.ts; a budget enforced without being
|
|
@@ -585,7 +588,9 @@ export async function phaseResearch(deps, refined) {
|
|
|
585
588
|
{
|
|
586
589
|
section: 'CONTEXT',
|
|
587
590
|
label: 'worker:context',
|
|
588
|
-
|
|
591
|
+
// Read-heavy too: it holds `read,grep` and reads to understand, so the
|
|
592
|
+
// core displaces reads here exactly as it does for FILES and APIS.
|
|
593
|
+
prompt: orientation.block + promptHeader + RESEARCH_CONTEXT_PROMPT(refined),
|
|
589
594
|
// Context owns architectural understanding, not path discovery —
|
|
590
595
|
// FILES handles that. Dropping `find`/`ls` keeps the worker from
|
|
591
596
|
// spawning long enumeration loops whose output then inflates
|
|
@@ -632,15 +637,7 @@ export async function phaseResearch(deps, refined) {
|
|
|
632
637
|
extensions: [SINGLE_READ_EXTENSION_PATH]
|
|
633
638
|
}
|
|
634
639
|
];
|
|
635
|
-
|
|
636
|
-
// file, so writes are chained through one lock — a no-op in serial mode,
|
|
637
|
-
// load-bearing in parallel mode where two workers can settle together.
|
|
638
|
-
let persistChain = Promise.resolve();
|
|
639
|
-
const persistSection = (heading, text) => {
|
|
640
|
-
const next = persistChain.then(() => setTaskSection(deps.cwd, deps.taskId, heading, text));
|
|
641
|
-
persistChain = next.catch(() => { });
|
|
642
|
-
return next;
|
|
643
|
-
};
|
|
640
|
+
const persistSection = (heading, text) => setTaskSection(deps.cwd, deps.taskId, heading, text);
|
|
644
641
|
/**
|
|
645
642
|
* This phase's binding of the research-worker driver: everything about THIS
|
|
646
643
|
* RUN, gathered once, so each of the four rows is plain data.
|
|
@@ -665,32 +662,7 @@ export async function phaseResearch(deps, refined) {
|
|
|
665
662
|
persistSection,
|
|
666
663
|
leverEnv
|
|
667
664
|
}, prior);
|
|
668
|
-
const sections =
|
|
669
|
-
if (!getConfig().parallelResearchWorkers) {
|
|
670
|
-
// Default: ONE AT A TIME (see the note above the worker specs) — a fatal
|
|
671
|
-
// failure throws before later workers run, and each worker can see the
|
|
672
|
-
// finished sections before it (APIS builds on the FILES map).
|
|
673
|
-
for (const spec of workerSpecs) {
|
|
674
|
-
sections.push(await drive(spec, sections));
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
// Opt-in for parallel-capable backends. allSettled (not all): every
|
|
679
|
-
// worker runs to its own outcome first, so one fatal failure cannot
|
|
680
|
-
// orphan the others' output — their sections persist for the resume
|
|
681
|
-
// before the failure is thrown. Assembly order stays the spec order
|
|
682
|
-
// regardless of completion order. No prior sections exist here, so prompt
|
|
683
|
-
// builders get none — APIS runs without the FILES map it gets when serial.
|
|
684
|
-
const settled = await Promise.allSettled(workerSpecs.map(spec => drive(spec, [])));
|
|
685
|
-
for (const s of settled) {
|
|
686
|
-
if (s.status === 'rejected')
|
|
687
|
-
throw s.reason;
|
|
688
|
-
}
|
|
689
|
-
for (const s of settled) {
|
|
690
|
-
if (s.status === 'fulfilled')
|
|
691
|
-
sections.push(s.value);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
665
|
+
const sections = await runResearchStage(workerSpecs, drive, getConfig().researchConcurrency);
|
|
694
666
|
// All workers succeeded — the assembled output below becomes the canonical
|
|
695
667
|
// 'research' section (written by the orchestrator). The per-worker caches
|
|
696
668
|
// exist only to survive a mid-phase failure, so drop them now to avoid
|
|
@@ -1162,7 +1134,7 @@ export async function refinePhase(d, p) {
|
|
|
1162
1134
|
*/
|
|
1163
1135
|
export async function researchPhase(d, p) {
|
|
1164
1136
|
const tResearch = Date.now();
|
|
1165
|
-
const rawResearch = await phaseResearch(d, p.refined);
|
|
1137
|
+
const rawResearch = await phaseResearch(d, p.refined, p.rawPrompt);
|
|
1166
1138
|
d.recordSubStep?.('workers', Date.now() - tResearch);
|
|
1167
1139
|
const tVerify = Date.now();
|
|
1168
1140
|
const out = await phaseVerifyTooling(d, rawResearch);
|
|
@@ -21,12 +21,11 @@
|
|
|
21
21
|
* receives beyond a bare /task is HANDOFF_DELIVERABLE_RULE, which rides on every
|
|
22
22
|
* handoff, and the decisions block when anything was settled.
|
|
23
23
|
*/
|
|
24
|
-
import * as path from 'node:path';
|
|
25
24
|
import { prependHint, USER_CANCELLED } from './child-runner.js';
|
|
26
25
|
import { PLAN_QUESTION_PROMPT, PLAN_ANSWER_PROMPT } from './plan-prompts.js';
|
|
27
26
|
import { runPlanSession, ASK_TITLE } from './plan-session.js';
|
|
28
27
|
import { allocatePlanId, buildPlanBody, buildHandoffPrompt, formatPlanDecisions } from './plan-io.js';
|
|
29
|
-
import { writeTaskFile, readTaskFile, setTaskSection, readSection, updateTaskFrontMatter, taskFilePath
|
|
28
|
+
import { writeTaskFile, readTaskFile, setTaskSection, readSection, updateTaskFrontMatter, taskFilePath } from './task-io.js';
|
|
30
29
|
import { extractSection } from './task-parsers.js';
|
|
31
30
|
import { collectTreeChanges } from './gate-deps.js';
|
|
32
31
|
import { PLAN_TOOLS, newTreeChanges, isEmptyChange, formatReadOnlyViolation } from './plan-readonly.js';
|
|
@@ -38,7 +37,8 @@ import { SessionUI, registerBridgeCommand, notifyBoth, notifyRun, isRemoteOrigin
|
|
|
38
37
|
import { withRun, announceTerminal } from './run-bracket.js';
|
|
39
38
|
import { getConfig } from '../config/config.js';
|
|
40
39
|
import { isYoloMode } from './yolo.js';
|
|
41
|
-
import { gateDebugWriter } from './debug-log.js';
|
|
40
|
+
import { appendDebugLine, gateDebugWriter } from './debug-log.js';
|
|
41
|
+
import { runLogPath } from './state-dir.js';
|
|
42
42
|
import { getParentContextWindow } from './context-usage.js';
|
|
43
43
|
import { ChildStatus, runPlanningChild, statusCallbacks } from './child-status.js';
|
|
44
44
|
import * as fsp from 'node:fs/promises';
|
|
@@ -59,8 +59,7 @@ export function buildPlanDeps(ctx, cwd, planId, task, signal) {
|
|
|
59
59
|
const childStatus = new ChildStatus({ parentContextWindow: getParentContextWindow(ctx) });
|
|
60
60
|
const title = deriveTitle(task);
|
|
61
61
|
const logDebug = gateDebugWriter((msg) => {
|
|
62
|
-
|
|
63
|
-
void fsp.appendFile(path.join(tasksDir(cwd), `${planId}-debug.log`), line).catch(() => { });
|
|
62
|
+
appendDebugLine(runLogPath(cwd, `${planId}-debug.log`), msg);
|
|
64
63
|
});
|
|
65
64
|
const phaseDeps = {
|
|
66
65
|
cwd,
|
|
@@ -225,7 +224,7 @@ export async function discardEmptyPlanFile(cwd, planId) {
|
|
|
225
224
|
if (extractSection(body, 'notes') !== null)
|
|
226
225
|
return;
|
|
227
226
|
await fsp.rm(taskFilePath(cwd, planId), { force: true });
|
|
228
|
-
await fsp.rm(
|
|
227
|
+
await fsp.rm(runLogPath(cwd, `${planId}-debug.log`), { force: true });
|
|
229
228
|
}
|
|
230
229
|
catch {
|
|
231
230
|
/* best-effort: an unreadable file is left exactly where it is */
|
|
@@ -48,6 +48,7 @@ export declare class CoverageLedger {
|
|
|
48
48
|
private _round;
|
|
49
49
|
private _cap;
|
|
50
50
|
private _bonusUsed;
|
|
51
|
+
private _terminal;
|
|
51
52
|
constructor(_best: ScoredPlan, _opts: CoverageLedgerOptions);
|
|
52
53
|
/** The best-covered plan seen so far — the one that reprompts, and the one that ships. */
|
|
53
54
|
best(): ScoredPlan;
|
package/dist/task/plan-rounds.js
CHANGED
|
@@ -27,6 +27,7 @@ export class CoverageLedger {
|
|
|
27
27
|
_round = 0;
|
|
28
28
|
_cap;
|
|
29
29
|
_bonusUsed = false;
|
|
30
|
+
_terminal = false;
|
|
30
31
|
constructor(_best, _opts) {
|
|
31
32
|
this._best = _best;
|
|
32
33
|
this._opts = _opts;
|
|
@@ -42,7 +43,7 @@ export class CoverageLedger {
|
|
|
42
43
|
}
|
|
43
44
|
/** May another reprompt round run? */
|
|
44
45
|
mayRetry() {
|
|
45
|
-
return this._round < this._cap;
|
|
46
|
+
return !this._terminal && this._round < this._cap;
|
|
46
47
|
}
|
|
47
48
|
/** Spend a round. Call once per reprompt, before the child runs. */
|
|
48
49
|
startRound() {
|
|
@@ -65,6 +66,11 @@ export class CoverageLedger {
|
|
|
65
66
|
*/
|
|
66
67
|
consider(cand) {
|
|
67
68
|
const decision = decideAdoption(this._best.plan, cand.plan, this._opts.hasRequirements);
|
|
69
|
+
// A terminal verdict ends the loop HERE, before the bonus round can extend
|
|
70
|
+
// it: the bonus exists to buy one more draw, and the whole finding of a
|
|
71
|
+
// terminal verdict is that another draw answers the same question again.
|
|
72
|
+
if (decision.terminal === true)
|
|
73
|
+
this._terminal = true;
|
|
68
74
|
if (!decision.adopt)
|
|
69
75
|
return { adopted: false, decision, grantedBonusRound: false };
|
|
70
76
|
const priorCovered = this._best.plan.covered.size;
|
|
@@ -73,6 +79,7 @@ export class CoverageLedger {
|
|
|
73
79
|
// never come from different rounds.
|
|
74
80
|
this._best = cand;
|
|
75
81
|
const grant = !this._bonusUsed
|
|
82
|
+
&& !this._terminal
|
|
76
83
|
&& this._round >= this._cap
|
|
77
84
|
&& this._opts.hasRequirements
|
|
78
85
|
&& cand.plan.covered.size > priorCovered
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* violated.
|
|
27
27
|
*/
|
|
28
28
|
import type { ChangedFile } from './substitution-probe.js';
|
|
29
|
+
import { type ConstraintProvenance, type QaResolver } from './spec-model.js';
|
|
29
30
|
/** One "do not modify X" constraint extracted from the spec text. */
|
|
30
31
|
export interface Prohibition {
|
|
31
32
|
/** The forbidden path exactly as the spec spells it (file or directory). */
|
|
@@ -33,6 +34,10 @@ export interface Prohibition {
|
|
|
33
34
|
/** The full spec line carrying the prohibition, so the verify child judges
|
|
34
35
|
* against the EXACT wording — including any exception clause it states. */
|
|
35
36
|
constraint: string;
|
|
37
|
+
/** Where the constraint carrying this ban came from. An advisory one is
|
|
38
|
+
* reported and does not decide the verdict alone — 0043 failed on a ban a
|
|
39
|
+
* grill auto-answer invented. */
|
|
40
|
+
provenance: ConstraintProvenance;
|
|
36
41
|
}
|
|
37
42
|
/**
|
|
38
43
|
* Does this line express a modification ban? Matches the active forms ("do not
|
|
@@ -47,7 +52,7 @@ export declare const PROHIBITION_RE: RegExp;
|
|
|
47
52
|
* Prose-only prohibitions ("do not modify server-side code" with no path named)
|
|
48
53
|
* extract nothing — the prompt-level rule still covers them.
|
|
49
54
|
*/
|
|
50
|
-
export declare function extractProhibitions(spec: string): Prohibition[];
|
|
55
|
+
export declare function extractProhibitions(spec: string, resolve?: QaResolver): Prohibition[];
|
|
51
56
|
/**
|
|
52
57
|
* Intersect the spec's prohibitions with the task's changed files (git shape —
|
|
53
58
|
* the same collector the substitution probe uses). A prohibition matches a
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseSpec } from './spec-model.js';
|
|
2
|
+
import { weightTag } from './constraint-policy.js';
|
|
1
3
|
/**
|
|
2
4
|
* Does this line express a modification ban? Matches the active forms ("do not
|
|
3
5
|
* modify", "must not touch", "never edit", "don't change") and the passive form
|
|
@@ -23,22 +25,39 @@ function looksLikePath(token) {
|
|
|
23
25
|
* Prose-only prohibitions ("do not modify server-side code" with no path named)
|
|
24
26
|
* extract nothing — the prompt-level rule still covers them.
|
|
25
27
|
*/
|
|
26
|
-
export function extractProhibitions(spec) {
|
|
28
|
+
export function extractProhibitions(spec, resolve) {
|
|
27
29
|
const out = [];
|
|
28
30
|
const seen = new Set();
|
|
29
|
-
for (const
|
|
30
|
-
if (!PROHIBITION_RE.test(
|
|
31
|
+
for (const { text, provenance } of prohibitionCandidates(spec, resolve)) {
|
|
32
|
+
if (!PROHIBITION_RE.test(text))
|
|
31
33
|
continue;
|
|
32
|
-
for (const m of
|
|
34
|
+
for (const m of text.matchAll(/`([^`]+)`/g)) {
|
|
33
35
|
const token = m[1].trim();
|
|
34
36
|
if (!looksLikePath(token) || seen.has(token))
|
|
35
37
|
continue;
|
|
36
38
|
seen.add(token);
|
|
37
|
-
out.push({ path: token, constraint:
|
|
39
|
+
out.push({ path: token, constraint: text.trim(), provenance });
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
return out;
|
|
41
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The lines a ban can live on: the spec's parsed CONSTRAINTS, each with its own
|
|
46
|
+
* provenance — or, for text that parses to no constraints at all, every line at
|
|
47
|
+
* `derived`.
|
|
48
|
+
*
|
|
49
|
+
* The fallback is not a courtesy to malformed specs. Half this probe's callers
|
|
50
|
+
* hand it a bare constraint list or a fragment rather than a four-section spec,
|
|
51
|
+
* and a probe that saw nothing in those would silently stop finding the
|
|
52
|
+
* violations it was built for.
|
|
53
|
+
*/
|
|
54
|
+
function prohibitionCandidates(spec, resolve) {
|
|
55
|
+
const parsed = parseSpec(spec, resolve);
|
|
56
|
+
if (parsed.constraints.length > 0) {
|
|
57
|
+
return parsed.constraints.flatMap(c => c.text.split('\n').map(text => ({ text, provenance: c.provenance })));
|
|
58
|
+
}
|
|
59
|
+
return spec.split('\n').map(text => ({ text, provenance: 'derived' }));
|
|
60
|
+
}
|
|
42
61
|
/** Normalise a path for comparison: strip leading ./ and trailing /. */
|
|
43
62
|
const norm = (p) => p.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
44
63
|
/**
|
|
@@ -58,7 +77,8 @@ export function findProhibitionViolations(prohibitions, files) {
|
|
|
58
77
|
});
|
|
59
78
|
if (!hit)
|
|
60
79
|
continue;
|
|
61
|
-
findings.push(`${f.path} — modified by this task, but the spec
|
|
80
|
+
findings.push(`${f.path} ${weightTag(hit.provenance)} — modified by this task, but the spec `
|
|
81
|
+
+ `forbids it: "${hit.constraint}"`);
|
|
62
82
|
}
|
|
63
83
|
return findings;
|
|
64
84
|
}
|