@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
- const basePrompt = GRILL_AUTO_ANSWER_PROMPT(featureForModel, '(none clarify runs before decomposition; each task researches itself later)', question);
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;
@@ -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 = new Set();
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
- pkgs.add(t);
70
- if (pkgs.size >= ENRICH_CAP)
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: [...pkgs], urls: [...urls], services };
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 come from docsRaw's bundled lookup and lead the
48
- // section so the model anchors on live version data before reading
49
- // the docs body.
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) {
@@ -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.5",
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",