@mjasnikovs/pi-task 0.17.5 → 0.17.7
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.
|
@@ -17,6 +17,7 @@ import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, check
|
|
|
17
17
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
|
|
18
18
|
import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
19
19
|
import { runPhaseChild, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
20
|
+
import { refineExistingFilesBlock } from './phases.js';
|
|
20
21
|
import { SessionUI, registerBridgeCommand, publishLifecycleNotice } from '../remote/bridge.js';
|
|
21
22
|
import { pushNotify } from '../remote/push.js';
|
|
22
23
|
import { startAutoLoader } from './widget.js';
|
|
@@ -80,9 +81,19 @@ function logPlanDebug(cwd, msg) {
|
|
|
80
81
|
* throw or an untagged reply falls back to surfacing the question (the prior
|
|
81
82
|
* behavior), never dropping it silently.
|
|
82
83
|
*/
|
|
83
|
-
async function triageClarifyQuestion(deps, cwd, featureForModel, question) {
|
|
84
|
+
async function triageClarifyQuestion(deps, cwd, featureForModel, existingFilesBlock, question) {
|
|
84
85
|
try {
|
|
85
|
-
|
|
86
|
+
// Prepend the existing-files block (refine's REFINE_PRESERVE_DIRECTIVE +
|
|
87
|
+
// manifest/config content) so a "scaffold/create/from scratch" question is
|
|
88
|
+
// auto-resolved as an in-place UPDATE that PRESERVES what is on disk —
|
|
89
|
+
// instead of "greenfield, from scratch", which the spec-only triage emitted
|
|
90
|
+
// 13/15 of the time and would mint a destructive decompose decision that can
|
|
91
|
+
// outrank refine's preserve directive (A/B live: 2/15 → 14/15 preserve).
|
|
92
|
+
// Empty (greenfield repo / orientation off) → byte-identical to before.
|
|
93
|
+
const source = existingFilesBlock.length > 0 ?
|
|
94
|
+
`${existingFilesBlock}\n\n${featureForModel}`
|
|
95
|
+
: featureForModel;
|
|
96
|
+
const basePrompt = GRILL_AUTO_ANSWER_PROMPT(source, '(none — clarify runs before decomposition; each task researches itself later)', question);
|
|
86
97
|
let text = await deps.runChild('clarify-triage', 'read', basePrompt);
|
|
87
98
|
if (!autoAnswerHasTag(text)) {
|
|
88
99
|
// No ANSWER/UNKNOWN/ALT tag — the model wrote prose. Reprompt once for
|
|
@@ -261,6 +272,15 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
261
272
|
if (planPhantoms.length > 0) {
|
|
262
273
|
logPlanDebug(cwd, `phantom specifiers rewritten in plan spec: ${planPhantoms.map(x => x.spec).join(', ')}`);
|
|
263
274
|
}
|
|
275
|
+
// Existing manifest/config on disk (REFINE_PRESERVE_DIRECTIVE + tier 0–1
|
|
276
|
+
// content), computed ONCE and fed to every triage call so a "scaffold X"
|
|
277
|
+
// question resolves to an in-place update instead of "greenfield". '' for a
|
|
278
|
+
// greenfield/non-git repo or orientation off → triage stays spec-only.
|
|
279
|
+
const existingFilesBlock = await refineExistingFilesBlock({
|
|
280
|
+
cwd,
|
|
281
|
+
taskId: '',
|
|
282
|
+
signal: new AbortController().signal
|
|
283
|
+
}).catch(() => '');
|
|
264
284
|
const answers = [];
|
|
265
285
|
// Plain text of every question already shown, for the duplicate backstop.
|
|
266
286
|
const askedQuestions = [];
|
|
@@ -299,7 +319,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
299
319
|
// this question, auto-resolve it and never show it. The resolved value is
|
|
300
320
|
// recorded so decompose sees the decision and the next gen call's priorQA
|
|
301
321
|
// won't re-ask it.
|
|
302
|
-
const autoResolved = await triageClarifyQuestion(deps, cwd, featureForModel, plainQ);
|
|
322
|
+
const autoResolved = await triageClarifyQuestion(deps, cwd, featureForModel, existingFilesBlock, plainQ);
|
|
303
323
|
if (autoResolved !== null) {
|
|
304
324
|
answers.push(`Q${answers.length + 1}: ${plainQ}\n`
|
|
305
325
|
+ `A${answers.length + 1}: ${autoResolved} (auto-resolved — already settled by the spec)`);
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
* orchestrator can fan out docs/URL lookups before the research phase.
|
|
4
4
|
*/
|
|
5
5
|
export declare function extractEnrichTargets(text: string): {
|
|
6
|
+
/** Packages that get a (heavy) docs fetch — capped at ENRICH_CAP. */
|
|
6
7
|
packages: string[];
|
|
8
|
+
/**
|
|
9
|
+
* Every named package, up to ENRICH_VERSION_CAP, for a cheap live npm version
|
|
10
|
+
* lookup. A superset of `packages`; the extras get a version block only (no
|
|
11
|
+
* docs body). Order-preserving and deduped, same as `packages`.
|
|
12
|
+
*/
|
|
13
|
+
versionPackages: string[];
|
|
7
14
|
urls: string[];
|
|
8
15
|
services: Array<{
|
|
9
16
|
name: string;
|
package/dist/task/enrichment.js
CHANGED
|
@@ -21,6 +21,13 @@ const ENRICH_DENYLIST = new Set([
|
|
|
21
21
|
'rm'
|
|
22
22
|
]);
|
|
23
23
|
const ENRICH_CAP = 3;
|
|
24
|
+
// Heavy docs/url fetches stay capped at ENRICH_CAP (each is a worker spawn +
|
|
25
|
+
// page fetch). A live npm VERSION lookup is just one cheap registry GET, so it
|
|
26
|
+
// can cover far more packages — every dependency a task explicitly names should
|
|
27
|
+
// get a grounded latest-version block, not just the first ENRICH_CAP of them. A
|
|
28
|
+
// task that named 6 runtime deps used to leave the 4th–6th (e.g. tailwindcss)
|
|
29
|
+
// with NO live version, so a version question fell back to stale training data.
|
|
30
|
+
const ENRICH_VERSION_CAP = 12;
|
|
24
31
|
const ENRICH_SERVICE_HEADER = 'EXTERNAL-DEPENDENCIES';
|
|
25
32
|
const ENRICH_HEADER_LINE_RE = /^[A-Z][A-Z0-9 -]+$/;
|
|
26
33
|
const ENRICH_SERVICE_BULLET_RE = /^\s*-\s+(.+?)\s{2,}(.+?)\s*$/;
|
|
@@ -61,13 +68,15 @@ function parseServices(text) {
|
|
|
61
68
|
return out;
|
|
62
69
|
}
|
|
63
70
|
export function extractEnrichTargets(text) {
|
|
64
|
-
const pkgs =
|
|
71
|
+
const pkgs = [];
|
|
72
|
+
const seen = new Set();
|
|
65
73
|
for (const m of text.matchAll(ENRICH_PKG_RE)) {
|
|
66
74
|
const t = m[1];
|
|
67
|
-
if (ENRICH_DENYLIST.has(t))
|
|
75
|
+
if (ENRICH_DENYLIST.has(t) || seen.has(t))
|
|
68
76
|
continue;
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
seen.add(t);
|
|
78
|
+
pkgs.push(t);
|
|
79
|
+
if (pkgs.length >= ENRICH_VERSION_CAP)
|
|
71
80
|
break;
|
|
72
81
|
}
|
|
73
82
|
const urls = new Set();
|
|
@@ -78,5 +87,5 @@ export function extractEnrichTargets(text) {
|
|
|
78
87
|
break;
|
|
79
88
|
}
|
|
80
89
|
const services = parseServices(text);
|
|
81
|
-
return { packages:
|
|
90
|
+
return { packages: pkgs.slice(0, ENRICH_CAP), versionPackages: pkgs, urls: [...urls], services };
|
|
82
91
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { docsRaw } from '../workers/docs-core.js';
|
|
12
12
|
import { fetchRaw } from '../workers/fetch-core.js';
|
|
13
|
+
import { npmVersionLookup } from '../workers/npm-version.js';
|
|
13
14
|
import type { SearchCoreInput, SearchCoreResult } from '../workers/search-core.js';
|
|
14
15
|
import type { PhaseDeps } from './child-runner.js';
|
|
15
16
|
/** Injectable workers so enrichment is testable without spawning real lookups. */
|
|
@@ -17,6 +18,7 @@ export interface ExternalContextDeps {
|
|
|
17
18
|
docsRaw?: typeof docsRaw;
|
|
18
19
|
fetchRaw?: typeof fetchRaw;
|
|
19
20
|
searchFn?: (input: SearchCoreInput) => Promise<SearchCoreResult>;
|
|
21
|
+
npmVersionLookup?: typeof npmVersionLookup;
|
|
20
22
|
}
|
|
21
23
|
type GatherDeps = Pick<PhaseDeps, 'cwd' | 'signal' | 'recordSubStep'>;
|
|
22
24
|
/**
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { docsRaw } from '../workers/docs-core.js';
|
|
12
12
|
import { fetchRaw } from '../workers/fetch-core.js';
|
|
13
|
-
import { formatNpmVersionSection } from '../workers/npm-version.js';
|
|
13
|
+
import { formatNpmVersionSection, npmVersionLookup } from '../workers/npm-version.js';
|
|
14
14
|
import { search as defaultSearch } from '../workers/search-core.js';
|
|
15
15
|
import { extractEnrichTargets } from './enrichment.js';
|
|
16
16
|
import { formatServiceBlock, formatFreshnessSkippedBlock } from './service-blocks.js';
|
|
@@ -22,7 +22,15 @@ export async function gatherExternalContext(refined, deps, researchDeps = {}) {
|
|
|
22
22
|
const docsRawFn = researchDeps.docsRaw ?? docsRaw;
|
|
23
23
|
const fetchRawFn = researchDeps.fetchRaw ?? fetchRaw;
|
|
24
24
|
const searchFn = researchDeps.searchFn ?? defaultSearch;
|
|
25
|
+
const npmVersionFn = researchDeps.npmVersionLookup ?? npmVersionLookup;
|
|
25
26
|
const enrichTargets = extractEnrichTargets(refined);
|
|
27
|
+
// Every named dep past the heavy-docs cap still gets a cheap live version
|
|
28
|
+
// lookup, so a version block exists for ALL of them (the docs-fetched ones
|
|
29
|
+
// emit their version below from the bundled lookup). Without this, deps 4..N
|
|
30
|
+
// had no live version and a "which version?" question fell back to the model's
|
|
31
|
+
// stale training data — how tailwindcss got pinned to an old major.
|
|
32
|
+
const docsPkgs = new Set(enrichTargets.packages);
|
|
33
|
+
const extraVersionPkgs = enrichTargets.versionPackages.filter(p => !docsPkgs.has(p));
|
|
26
34
|
if (enrichTargets.packages.length === 0
|
|
27
35
|
&& enrichTargets.urls.length === 0
|
|
28
36
|
&& enrichTargets.services.length === 0) {
|
|
@@ -30,7 +38,7 @@ export async function gatherExternalContext(refined, deps, researchDeps = {}) {
|
|
|
30
38
|
}
|
|
31
39
|
const enrichSections = [];
|
|
32
40
|
const tEnrichStart = Date.now();
|
|
33
|
-
const [docsResults, fetchResults, serviceResults] = await Promise.all([
|
|
41
|
+
const [docsResults, fetchResults, serviceResults, extraVersionResults] = await Promise.all([
|
|
34
42
|
Promise.all(enrichTargets.packages.map(pkg => docsRawFn({
|
|
35
43
|
pkg,
|
|
36
44
|
query: refined.split('\n').find(l => l.trim()) ?? refined,
|
|
@@ -42,16 +50,22 @@ export async function gatherExternalContext(refined, deps, researchDeps = {}) {
|
|
|
42
50
|
query: `${s.name} ${s.query}`,
|
|
43
51
|
count: 3,
|
|
44
52
|
signal: deps.signal
|
|
45
|
-
}).catch(() => null)))
|
|
53
|
+
}).catch(() => null))),
|
|
54
|
+
Promise.all(extraVersionPkgs.map(pkg => npmVersionFn(pkg, { signal: deps.signal }).catch(() => null)))
|
|
46
55
|
]);
|
|
47
|
-
// npm version blocks
|
|
48
|
-
//
|
|
49
|
-
// the
|
|
56
|
+
// npm version blocks lead the section so the model anchors on live version
|
|
57
|
+
// data before reading any docs body. The docs-fetched packages carry their
|
|
58
|
+
// version in docsRaw's bundled lookup; the remaining named deps get theirs
|
|
59
|
+
// from the cheap standalone lookup above. Together they cover EVERY named dep.
|
|
50
60
|
for (let i = 0; i < enrichTargets.packages.length; i++) {
|
|
51
61
|
const v = docsResults[i]?.npmVersion;
|
|
52
62
|
if (v)
|
|
53
63
|
enrichSections.push(formatNpmVersionSection(v));
|
|
54
64
|
}
|
|
65
|
+
for (const v of extraVersionResults) {
|
|
66
|
+
if (v)
|
|
67
|
+
enrichSections.push(formatNpmVersionSection(v));
|
|
68
|
+
}
|
|
55
69
|
for (let i = 0; i < enrichTargets.packages.length; i++) {
|
|
56
70
|
const r = docsResults[i];
|
|
57
71
|
if (r?.kind === 'ok' && r.chunks.length > 0) {
|
package/dist/task/prompts.js
CHANGED
|
@@ -221,6 +221,7 @@ LIVE-DATA RULE:
|
|
|
221
221
|
- "### service: <name>" blocks are LIVE web data; authoritative over training data for that service.
|
|
222
222
|
- "### freshness-check skipped" → tag UNKNOWN and say current state needs verification.
|
|
223
223
|
- No npm block + question is about latest/current version → tag UNKNOWN (training data goes stale).
|
|
224
|
+
- VERSION-PIN questions ("pin to X.y vs latest", "which major version") are costly-to-reverse build-shaping choices: unless the spec or an "### npm:" block already settles it (then ANSWER that value), tag UNKNOWN and surface it. NEVER auto-answer a downgrade to an OLDER major "to avoid breaking changes" from memory — that reasoning is exactly the stale-training-data trap. If an "### npm:" block shows a newer major than your instinct, that block is the live latest; do not silently pin an older major the live data and spec never asked for.
|
|
224
225
|
|
|
225
226
|
TRIAGE — run these checks IN ORDER first. The REVERSIBILITY TEST below applies ONLY to a question that survives all checks as a genuine preference.
|
|
226
227
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.7",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|