@mjasnikovs/pi-task 0.14.8 → 0.14.10

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 { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasks
17
17
  import { gitCommitAll } from './auto-commit.js';
18
18
  import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-guidelines.js';
19
19
  import { runWorker } from '../workers/pi-worker-core.js';
20
+ import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
20
21
  import { runPhaseChild, prependHint, formatLoopHint, USER_CANCELLED } from './child-runner.js';
21
22
  import { SessionUI, registerBridgeCommand } from '../remote/bridge.js';
22
23
  import { pushNotify } from '../remote/push.js';
@@ -42,6 +43,19 @@ const MENTION_TRAILING_PUNCT = /[.,;:!?)\]}>"']+$/;
42
43
  function mentionPath(token) {
43
44
  return token.replace(MENTION_TRAILING_PUNCT, '');
44
45
  }
46
+ /**
47
+ * Fire-and-forget debug line for the PLAN phase (clarify/decompose), which runs
48
+ * before any task file — hence any per-task `TASK_XXXX-debug.log` — exists. Writes
49
+ * to `.pi-tasks/plan-debug.log`; the `*-debug.log` suffix keeps it grep-compatible
50
+ * with the per-task logs. Never throws (mkdir + append are best-effort).
51
+ */
52
+ function logPlanDebug(cwd, msg) {
53
+ const line = `${new Date().toISOString()} ${msg}\n`;
54
+ const dir = tasksDir(cwd);
55
+ fsp.mkdir(dir, { recursive: true })
56
+ .then(() => fsp.appendFile(path.join(dir, 'plan-debug.log'), line))
57
+ .catch(() => { });
58
+ }
45
59
  /**
46
60
  * Expand any @file references in the feature text by appending each referenced
47
61
  * file's contents, so the planning children (clarify, decompose) always see the
@@ -180,7 +194,22 @@ export async function planAuto(ctx, cwd, feature, deps) {
180
194
  const ui = new SessionUI(ctx);
181
195
  // Inline any @file spec the user referenced so clarify/decompose reason over
182
196
  // the real content, not a one-line "Implement @file" that reads as trivial.
183
- const featureForModel = await expandFeatureMentions(cwd, feature);
197
+ const rawFeatureForModel = await expandFeatureMentions(cwd, feature);
198
+ // Strike phantom runtime specifiers (`bun:sql`) out of the inlined spec BEFORE
199
+ // clarify/decompose ever see it. Layer A only rewrites the per-task `refined`
200
+ // text — which is DOWNSTREAM of here: clarify is the first phase and runs on
201
+ // this raw inline, so the doc's affirmative `bun:sql` is parroted straight into
202
+ // the very first clarifying question ("instantly bun:sql is back"). Apply the
203
+ // same deterministic, no-LLM strike at the single point that feeds both planning
204
+ // children. Silent + no-op when nothing is flagged or the runtime's types aren't
205
+ // installed.
206
+ const planPhantoms = findPhantomImports(rawFeatureForModel, cwd);
207
+ const featureForModel = planPhantoms.length === 0 ?
208
+ rawFeatureForModel
209
+ : rewritePhantomSpecifiers(rawFeatureForModel, planPhantoms);
210
+ if (planPhantoms.length > 0) {
211
+ logPlanDebug(cwd, `phantom specifiers rewritten in plan spec: ${planPhantoms.map(x => x.spec).join(', ')}`);
212
+ }
184
213
  const answers = [];
185
214
  // Plain text of every question already shown, for the duplicate backstop.
186
215
  const askedQuestions = [];
@@ -6,7 +6,7 @@ import { docsFocused } from '../workers/docs-core.js';
6
6
  import { fetchFocused } from '../workers/fetch-core.js';
7
7
  import { formatNpmVersionSection } from '../workers/npm-version.js';
8
8
  import { runWorker } from '../workers/pi-worker-core.js';
9
- import { findPhantomImports, formatApiCorrections } from '../workers/phantom-imports.js';
9
+ import { findPhantomImports, formatApiCorrections, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
10
10
  import { search as defaultSearch } from '../workers/search-core.js';
11
11
  import { extractEnrichTargets } from './enrichment.js';
12
12
  import { getFileInventory } from './file-inventory.js';
@@ -651,7 +651,21 @@ export const PHASES = [
651
651
  name: 'refine',
652
652
  section: 'refined prompt',
653
653
  field: 'refined',
654
- run: (d, p) => phaseRefine(d, p.rawPrompt, p.planContext)
654
+ run: async (d, p) => {
655
+ const refined = await phaseRefine(d, p.rawPrompt, p.planContext);
656
+ // Subtractively strike any phantom runtime specifier (`bun:sql`) the
657
+ // refine carried up verbatim from the spec doc, BEFORE it flows to
658
+ // research/grill/compose. An appended correction alone loses: the
659
+ // affirmative survives into the composed GOAL and on to the implementer
660
+ // (proven: compose re-leaks it 4/4). Rewriting the source so compose has
661
+ // nothing to contradict is the fix. Silent + no-op when nothing is wrong
662
+ // or the runtime's types aren't installed.
663
+ const phantoms = findPhantomImports(refined, d.cwd);
664
+ if (phantoms.length === 0)
665
+ return refined;
666
+ d.logDebug?.(`phantom specifiers rewritten in refined: ${phantoms.map(x => x.spec).join(', ')}`);
667
+ return rewritePhantomSpecifiers(refined, phantoms);
668
+ }
655
669
  },
656
670
  {
657
671
  name: 'research',
@@ -5,6 +5,9 @@ export interface PhantomImport {
5
5
  realModules: string[];
6
6
  /** Corrective guidance to inject into the spec. */
7
7
  suggestion: string;
8
+ /** Base-module symbol the submodule leaf maps to (`bun:sql` → `SQL`), or null
9
+ * when the types declare no matching symbol. Drives the canonical-import rewrite. */
10
+ baseSymbol: string | null;
8
11
  }
9
12
  /** Every distinct runtime-namespace specifier mentioned in the text, in first-seen order. */
10
13
  export declare function extractRuntimeSpecifiers(text: string): string[];
@@ -33,3 +36,19 @@ export declare function loadRuntimeTypeText(runtime: string, cwd: string): strin
33
36
  export declare function findPhantomImports(text: string, cwd: string, loadText?: (runtime: string, cwd: string) => string | null): PhantomImport[];
34
37
  /** Render flagged phantoms as an authoritative research section, or '' if none. */
35
38
  export declare function formatApiCorrections(phantoms: PhantomImport[]): string;
39
+ /**
40
+ * Subtractively rewrite every flagged phantom specifier in `text` to the canonical
41
+ * import the installed types prove, so NO affirmative occurrence of the non-existent
42
+ * specifier survives downstream. This is the half an appended correction can't do:
43
+ * REFINE preserves identifiers verbatim, so `bun:sql` otherwise rides into the
44
+ * composed GOAL (proven: compose re-leaks it 4/4) and on to the implementer. Strike
45
+ * it at the source and compose has nothing to contradict.
46
+ *
47
+ * Deterministic, no LLM. Idempotent on healthy input: once a specifier is replaced
48
+ * the literal is gone, so a second pass is a no-op (the only residue is a `declare
49
+ * module` comment that quotes the spec inside a slash-star, which the bare-word rule
50
+ * skips via its lookbehind). Handles the four forms a phantom takes: an import/require
51
+ * `from "<spec>"`, a fabricated `declare module "<spec>"`, a backticked prose mention
52
+ * `` `<spec>` ``, and a bare word `<spec>`.
53
+ */
54
+ export declare function rewritePhantomSpecifiers(text: string, phantoms: PhantomImport[]): string;
@@ -173,7 +173,8 @@ export function findPhantomImports(text, cwd, loadText = loadRuntimeTypeText) {
173
173
  out.push({
174
174
  spec,
175
175
  realModules: verdict.realModules,
176
- suggestion: suggestionFor(verdict, ns.runtime)
176
+ suggestion: suggestionFor(verdict, ns.runtime),
177
+ baseSymbol: verdict.baseSymbol
177
178
  });
178
179
  }
179
180
  return out;
@@ -185,3 +186,43 @@ export function formatApiCorrections(phantoms) {
185
186
  const lines = phantoms.map(p => ` - ${p.suggestion}`);
186
187
  return `API CORRECTIONS\n${lines.join('\n')}`;
187
188
  }
189
+ /**
190
+ * Subtractively rewrite every flagged phantom specifier in `text` to the canonical
191
+ * import the installed types prove, so NO affirmative occurrence of the non-existent
192
+ * specifier survives downstream. This is the half an appended correction can't do:
193
+ * REFINE preserves identifiers verbatim, so `bun:sql` otherwise rides into the
194
+ * composed GOAL (proven: compose re-leaks it 4/4) and on to the implementer. Strike
195
+ * it at the source and compose has nothing to contradict.
196
+ *
197
+ * Deterministic, no LLM. Idempotent on healthy input: once a specifier is replaced
198
+ * the literal is gone, so a second pass is a no-op (the only residue is a `declare
199
+ * module` comment that quotes the spec inside a slash-star, which the bare-word rule
200
+ * skips via its lookbehind). Handles the four forms a phantom takes: an import/require
201
+ * `from "<spec>"`, a fabricated `declare module "<spec>"`, a backticked prose mention
202
+ * `` `<spec>` ``, and a bare word `<spec>`.
203
+ */
204
+ export function rewritePhantomSpecifiers(text, phantoms) {
205
+ let out = text;
206
+ for (const p of phantoms) {
207
+ const ns = splitRuntimeNamespace(p.spec);
208
+ if (!ns)
209
+ continue;
210
+ const runtime = ns.runtime;
211
+ const canonical = p.baseSymbol ?
212
+ `import { ${p.baseSymbol} } from "${runtime}"`
213
+ : `the "${runtime}" module`;
214
+ const spec = escapeRe(p.spec);
215
+ out = out
216
+ // `from "bun:sql"` / `require("bun:sql")` → point at the base runtime module
217
+ .replace(new RegExp(`((?:from\\s+|require\\(\\s*)["'])${spec}(["'])`, 'g'), `$1${runtime}$2`)
218
+ // a fabricated `declare module "bun:sql"` → a comment naming the real import.
219
+ // Deliberately does NOT echo the spec literal, so the bare-word pass below
220
+ // can't re-corrupt this comment (keeps the rewrite idempotent).
221
+ .replace(new RegExp(`declare\\s+module\\s+["']${spec}["']`, 'g'), `/* not a module — use ${canonical} */`)
222
+ // backticked prose mention → the canonical import
223
+ .replace(new RegExp('`' + spec + '`', 'g'), '`' + canonical + '`')
224
+ // bare word, not already inside quotes/backticks/a path or our own comment
225
+ .replace(new RegExp(`(?<![\`"'/])\\b${spec}\\b(?![\`"'])`, 'g'), canonical);
226
+ }
227
+ return out;
228
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.14.8",
3
+ "version": "0.14.10",
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",