@mjasnikovs/pi-task 0.17.6 → 0.17.8
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/shared/child-process.d.ts +9 -1
- package/dist/shared/child-process.js +12 -1
- package/dist/shared/fs-text.d.ts +18 -0
- package/dist/shared/fs-text.js +23 -0
- package/dist/shared/pi-invocation.d.ts +8 -2
- package/dist/shared/pi-invocation.js +11 -6
- package/dist/task/auto-io.js +2 -1
- package/dist/task/auto-orchestrator.js +4 -1
- package/dist/task/child-runner.d.ts +1 -1
- package/dist/task/child-runner.js +7 -3
- package/dist/task/enrichment.d.ts +7 -0
- package/dist/task/enrichment.js +14 -5
- package/dist/task/external-context.d.ts +2 -0
- package/dist/task/external-context.js +20 -6
- package/dist/task/orchestrator.js +2 -1
- package/dist/task/prompts.js +1 -0
- package/dist/task/task-io.js +5 -1
- package/dist/workers/docs-core.js +1 -1
- package/dist/workers/fetch-core.js +1 -1
- package/dist/workers/pi-worker-core.js +1 -1
- package/dist/workers/pi-worker-docs.js +2 -2
- package/package.json +1 -1
|
@@ -3,7 +3,14 @@ import type { EventEmitter } from 'node:events';
|
|
|
3
3
|
export declare const KILL_GRACE_MS = 5000;
|
|
4
4
|
/** Base flags shared by all child pi invocations. */
|
|
5
5
|
export declare const CHILD_BASE_ARGS: readonly ["--print", "--no-skills", "--no-extensions", "--no-prompt-templates", "--no-context-files", "--no-session"];
|
|
6
|
+
/** The subset of a child's stdin we use: write the prompt, then close it. */
|
|
7
|
+
export interface WritableLike {
|
|
8
|
+
write(chunk: string): boolean;
|
|
9
|
+
end(): void;
|
|
10
|
+
}
|
|
6
11
|
export interface ProcLike extends EventEmitter {
|
|
12
|
+
/** Present only when the child was spawned with stdin 'pipe' (prompt delivery). */
|
|
13
|
+
stdin: WritableLike | null;
|
|
7
14
|
stdout: EventEmitter | null;
|
|
8
15
|
stderr: EventEmitter | null;
|
|
9
16
|
killed: boolean;
|
|
@@ -12,7 +19,7 @@ export interface ProcLike extends EventEmitter {
|
|
|
12
19
|
export type SpawnFn = (command: string, args: ReadonlyArray<string>, options: {
|
|
13
20
|
cwd: string;
|
|
14
21
|
shell: boolean;
|
|
15
|
-
stdio: ['ignore', 'pipe', 'pipe'];
|
|
22
|
+
stdio: ['ignore' | 'pipe', 'pipe', 'pipe'];
|
|
16
23
|
}) => ProcLike;
|
|
17
24
|
export interface ChildResult {
|
|
18
25
|
stdout: string;
|
|
@@ -105,6 +112,7 @@ export declare class JsonEventSink {
|
|
|
105
112
|
export declare function runChild(spawn: SpawnFn, invocation: {
|
|
106
113
|
command: string;
|
|
107
114
|
args: ReadonlyArray<string>;
|
|
115
|
+
stdin?: string;
|
|
108
116
|
}, cwd: string, signal: AbortSignal | undefined, opts?: RunChildOptions): Promise<ChildResult>;
|
|
109
117
|
export declare function runChildDefault(invocation: {
|
|
110
118
|
command: string;
|
|
@@ -169,11 +169,22 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
|
|
|
169
169
|
let stderr = '';
|
|
170
170
|
let aborted = false;
|
|
171
171
|
const discardStdout = opts?.mode === 'text' && opts.discardStdout === true;
|
|
172
|
+
// Deliver the prompt on stdin, not argv: a large prompt (e.g. an inlined
|
|
173
|
+
// design doc) blows past the OS command-line limit — Windows CreateProcessW
|
|
174
|
+
// caps it at 32767 chars and Node throws `spawn ENAMETOOLONG`. When a
|
|
175
|
+
// prompt is present we open stdin as a pipe; otherwise keep it 'ignore'
|
|
176
|
+
// (git and other arg-only spawns are unaffected). See GitHub issue #1.
|
|
177
|
+
const usesStdin = invocation.stdin !== undefined;
|
|
172
178
|
const proc = spawn(invocation.command, invocation.args, {
|
|
173
179
|
cwd,
|
|
174
180
|
shell: false,
|
|
175
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
181
|
+
stdio: [usesStdin ? 'pipe' : 'ignore', 'pipe', 'pipe']
|
|
176
182
|
});
|
|
183
|
+
if (usesStdin) {
|
|
184
|
+
// pi reads the prompt from stdin and waits for EOF, so write then end.
|
|
185
|
+
proc.stdin?.write(invocation.stdin);
|
|
186
|
+
proc.stdin?.end();
|
|
187
|
+
}
|
|
177
188
|
// One kill path, shared by user-abort and loop-kill: SIGTERM, then
|
|
178
189
|
// SIGKILL after a grace period if the child ignored the term.
|
|
179
190
|
const killProc = () => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-file reads with line endings normalized to LF.
|
|
3
|
+
*
|
|
4
|
+
* Every task-file parser in pi-task assumes `\n` line endings: the front-matter
|
|
5
|
+
* regex (`^---\n`), the body-delimiter strip, `sectionRegex` (`## h[ \t]*\n`),
|
|
6
|
+
* the checkbox/VERIFY-token line splits. On Windows a task file can carry CRLF
|
|
7
|
+
* (an editor save, or a `git` checkout with `core.autocrlf=true`), and classic
|
|
8
|
+
* Mac tooling can emit lone CR. Any of those makes `parseFrontMatter` return
|
|
9
|
+
* null ("malformed front matter") and every `extractSection` miss.
|
|
10
|
+
*
|
|
11
|
+
* Rather than teach each parser about `\r`, we normalize ONCE at the read
|
|
12
|
+
* boundary — the single place file bytes enter the task layer — so everything
|
|
13
|
+
* downstream sees LF. See GitHub issue #1.
|
|
14
|
+
*/
|
|
15
|
+
/** Collapse CRLF and lone CR to LF. `\r\n?` matches Windows CRLF and classic-Mac CR. */
|
|
16
|
+
export declare function normalizeNewlines(text: string): string;
|
|
17
|
+
/** Read a UTF-8 text file with line endings normalized to LF. */
|
|
18
|
+
export declare function readTextFile(filePath: string): Promise<string>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-file reads with line endings normalized to LF.
|
|
3
|
+
*
|
|
4
|
+
* Every task-file parser in pi-task assumes `\n` line endings: the front-matter
|
|
5
|
+
* regex (`^---\n`), the body-delimiter strip, `sectionRegex` (`## h[ \t]*\n`),
|
|
6
|
+
* the checkbox/VERIFY-token line splits. On Windows a task file can carry CRLF
|
|
7
|
+
* (an editor save, or a `git` checkout with `core.autocrlf=true`), and classic
|
|
8
|
+
* Mac tooling can emit lone CR. Any of those makes `parseFrontMatter` return
|
|
9
|
+
* null ("malformed front matter") and every `extractSection` miss.
|
|
10
|
+
*
|
|
11
|
+
* Rather than teach each parser about `\r`, we normalize ONCE at the read
|
|
12
|
+
* boundary — the single place file bytes enter the task layer — so everything
|
|
13
|
+
* downstream sees LF. See GitHub issue #1.
|
|
14
|
+
*/
|
|
15
|
+
import * as fsp from 'node:fs/promises';
|
|
16
|
+
/** Collapse CRLF and lone CR to LF. `\r\n?` matches Windows CRLF and classic-Mac CR. */
|
|
17
|
+
export function normalizeNewlines(text) {
|
|
18
|
+
return text.replace(/\r\n?/g, '\n');
|
|
19
|
+
}
|
|
20
|
+
/** Read a UTF-8 text file with line endings normalized to LF. */
|
|
21
|
+
export async function readTextFile(filePath) {
|
|
22
|
+
return normalizeNewlines(await fsp.readFile(filePath, 'utf8'));
|
|
23
|
+
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/** Pick the right way to re-invoke pi: prefer the current pi script under the
|
|
2
2
|
* current node/bun runtime; fall back to the `pi` shim on PATH. Mirrors the
|
|
3
|
-
* pattern from pi-coding-agent's official subagent example.
|
|
4
|
-
|
|
3
|
+
* pattern from pi-coding-agent's official subagent example.
|
|
4
|
+
*
|
|
5
|
+
* `stdin`, when given, is the prompt to feed the child over stdin instead of as
|
|
6
|
+
* an argv element — pi reads its prompt from stdin when no positional message
|
|
7
|
+
* is passed, which keeps a large prompt off the length-limited command line
|
|
8
|
+
* (see runChild / GitHub issue #1). It is threaded through unchanged. */
|
|
9
|
+
export declare function getPiInvocation(args: string[], stdin?: string): {
|
|
5
10
|
command: string;
|
|
6
11
|
args: string[];
|
|
12
|
+
stdin?: string;
|
|
7
13
|
};
|
|
@@ -2,23 +2,28 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
/** Pick the right way to re-invoke pi: prefer the current pi script under the
|
|
4
4
|
* current node/bun runtime; fall back to the `pi` shim on PATH. Mirrors the
|
|
5
|
-
* pattern from pi-coding-agent's official subagent example.
|
|
6
|
-
|
|
5
|
+
* pattern from pi-coding-agent's official subagent example.
|
|
6
|
+
*
|
|
7
|
+
* `stdin`, when given, is the prompt to feed the child over stdin instead of as
|
|
8
|
+
* an argv element — pi reads its prompt from stdin when no positional message
|
|
9
|
+
* is passed, which keeps a large prompt off the length-limited command line
|
|
10
|
+
* (see runChild / GitHub issue #1). It is threaded through unchanged. */
|
|
11
|
+
export function getPiInvocation(args, stdin) {
|
|
7
12
|
// Test/dev override: point at a specific pi binary directly. Bypasses the
|
|
8
13
|
// re-invoke-current-script heuristic, which goes wrong under `bun test`
|
|
9
14
|
// (currentScript is the .test.ts file, not a pi entrypoint).
|
|
10
15
|
if (process.env.PI_BIN) {
|
|
11
|
-
return { command: process.env.PI_BIN, args };
|
|
16
|
+
return { command: process.env.PI_BIN, args, stdin };
|
|
12
17
|
}
|
|
13
18
|
const currentScript = process.argv[1];
|
|
14
19
|
const isBunVirtualScript = currentScript?.startsWith('/$bunfs/root/');
|
|
15
20
|
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
16
|
-
return { command: process.execPath, args: [currentScript, ...args] };
|
|
21
|
+
return { command: process.execPath, args: [currentScript, ...args], stdin };
|
|
17
22
|
}
|
|
18
23
|
const execName = path.basename(process.execPath).toLowerCase();
|
|
19
24
|
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
20
25
|
if (!isGenericRuntime) {
|
|
21
|
-
return { command: process.execPath, args };
|
|
26
|
+
return { command: process.execPath, args, stdin };
|
|
22
27
|
}
|
|
23
|
-
return { command: 'pi', args };
|
|
28
|
+
return { command: 'pi', args, stdin };
|
|
24
29
|
}
|
package/dist/task/auto-io.js
CHANGED
|
@@ -9,6 +9,7 @@ import * as fsp from 'node:fs/promises';
|
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { tasksDir, ensureTasksDir, readTaskFile, setTaskSection } from './task-io.js';
|
|
11
11
|
import { extractSection, parseFrontMatter } from './task-parsers.js';
|
|
12
|
+
import { readTextFile } from '../shared/fs-text.js';
|
|
12
13
|
import { RESUMABLE_STATES } from './task-types.js';
|
|
13
14
|
const AUTO_FILE_RE = /^(TASK_AUTO_\d{4,})\.md$/;
|
|
14
15
|
const MAX_TASKS = 30;
|
|
@@ -117,7 +118,7 @@ export async function findResumableAuto(cwd) {
|
|
|
117
118
|
if (!m)
|
|
118
119
|
continue;
|
|
119
120
|
try {
|
|
120
|
-
const raw = await
|
|
121
|
+
const raw = await readTextFile(path.join(tasksDir(cwd), f));
|
|
121
122
|
const fm = parseFrontMatter(raw);
|
|
122
123
|
if (!fm)
|
|
123
124
|
continue;
|
|
@@ -15,6 +15,7 @@ import { GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT } from './prompts.js';
|
|
|
15
15
|
import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './question-dedup.js';
|
|
16
16
|
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
|
|
17
17
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
|
|
18
|
+
import { readTextFile } from '../shared/fs-text.js';
|
|
18
19
|
import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
19
20
|
import { runPhaseChild, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
20
21
|
import { refineExistingFilesBlock } from './phases.js';
|
|
@@ -134,7 +135,9 @@ export async function expandFeatureMentions(cwd, feature) {
|
|
|
134
135
|
continue;
|
|
135
136
|
seen.add(rel);
|
|
136
137
|
try {
|
|
137
|
-
|
|
138
|
+
// Normalize CRLF/CR so an @-mentioned design doc saved on Windows
|
|
139
|
+
// inlines with LF endings the downstream phase parsers expect.
|
|
140
|
+
const body = await readTextFile(path.resolve(cwd, rel));
|
|
138
141
|
if (body.trim().length > 0) {
|
|
139
142
|
blocks.push(`--- contents of ${rel} ---\n${body.trim()}`);
|
|
140
143
|
}
|
|
@@ -23,7 +23,7 @@ export interface PhaseRunResult {
|
|
|
23
23
|
/** Set when the child's final turn failed with stopReason "error" (model/provider failure). */
|
|
24
24
|
modelError?: string;
|
|
25
25
|
}
|
|
26
|
-
export declare function childArgs(tools: string
|
|
26
|
+
export declare function childArgs(tools: string): string[];
|
|
27
27
|
export declare const USER_CANCELLED = "__user_cancelled__";
|
|
28
28
|
/**
|
|
29
29
|
* Run a child pi process with JSON event-stream output, loop detection, and
|
|
@@ -44,7 +44,7 @@ export function connectionRetryBackoffMs(attempt) {
|
|
|
44
44
|
}
|
|
45
45
|
const defaultSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
46
46
|
// ─── Spawn helpers ───────────────────────────────────────────────────────────
|
|
47
|
-
export function childArgs(tools
|
|
47
|
+
export function childArgs(tools) {
|
|
48
48
|
// `--mode json` puts the child into the structured event stream the
|
|
49
49
|
// unified runner parses in `mode: 'json-events'`. Without it the child
|
|
50
50
|
// emits plain text, every line fails JSON.parse, finalText stays empty,
|
|
@@ -55,8 +55,12 @@ export function childArgs(tools, prompt) {
|
|
|
55
55
|
// instead of `--tools ''` (which pi would reject). Used by pure-judgment
|
|
56
56
|
// phases like critique-triage that should reason only over the text we
|
|
57
57
|
// hand them, never spend time reading the repo.
|
|
58
|
+
//
|
|
59
|
+
// The prompt is NOT an argv element: it goes to the child over stdin (see
|
|
60
|
+
// runChild below / getPiInvocation), so a large inlined-design prompt can't
|
|
61
|
+
// overflow the OS command-line limit (Windows `spawn ENAMETOOLONG`).
|
|
58
62
|
const toolFlags = tools === '' ? ['--no-tools'] : ['--tools', tools];
|
|
59
|
-
return [...CHILD_BASE_ARGS, '--mode', 'json', ...toolFlags
|
|
63
|
+
return [...CHILD_BASE_ARGS, '--mode', 'json', ...toolFlags];
|
|
60
64
|
}
|
|
61
65
|
// Sentinel error thrown when the user dismisses a grill-me dialog.
|
|
62
66
|
// Defined here (not in failure-classifier.ts) to avoid circular dependency.
|
|
@@ -68,7 +72,7 @@ export const USER_CANCELLED = '__user_cancelled__';
|
|
|
68
72
|
* phase-level code.
|
|
69
73
|
*/
|
|
70
74
|
export async function runChild(cwd, tools, prompt, signal, onLine, onContextUsage, onToolCall, spawnFn) {
|
|
71
|
-
const invocation = getPiInvocation(childArgs(tools, prompt)
|
|
75
|
+
const invocation = getPiInvocation(childArgs(tools), prompt);
|
|
72
76
|
let loopHit;
|
|
73
77
|
const result = await runChildUnified(spawnFn ?? spawn, invocation, cwd, signal, {
|
|
74
78
|
mode: 'json-events',
|
|
@@ -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) {
|
|
@@ -21,6 +21,7 @@ import { PHASES, postCommitPhase } from './phases.js';
|
|
|
21
21
|
import { handleFailure } from './failure-classifier.js';
|
|
22
22
|
import { PHASE_INDEX, PHASE_ORDER, RESUMABLE_STATES } from './task-types.js';
|
|
23
23
|
import { normaliseTaskId, parseFrontMatter, extractSection } from './task-parsers.js';
|
|
24
|
+
import { readTextFile } from '../shared/fs-text.js';
|
|
24
25
|
import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile } from './task-io.js';
|
|
25
26
|
import { startWidget } from './widget.js';
|
|
26
27
|
import { armImplWidget, disarmImplWidget, setupImplWidget } from './impl-widget.js';
|
|
@@ -705,7 +706,7 @@ async function handleTaskList(_args, ctx) {
|
|
|
705
706
|
const rows = [];
|
|
706
707
|
for (const f of taskFiles) {
|
|
707
708
|
try {
|
|
708
|
-
const raw = await
|
|
709
|
+
const raw = await readTextFile(path.join(tasksDir(cwd), f));
|
|
709
710
|
const fm = parseFrontMatter(raw);
|
|
710
711
|
if (!fm)
|
|
711
712
|
continue;
|
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/dist/task/task-io.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as fsp from 'node:fs/promises';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { TASKS_DIR_NAME } from './task-types.js';
|
|
10
10
|
import { emitFrontMatter, parseFrontMatter, sectionRegex } from './task-parsers.js';
|
|
11
|
+
import { readTextFile } from '../shared/fs-text.js';
|
|
11
12
|
// ─── Directory & path helpers ────────────────────────────────────────────────
|
|
12
13
|
export function tasksDir(cwd) {
|
|
13
14
|
return path.join(cwd, TASKS_DIR_NAME);
|
|
@@ -58,7 +59,10 @@ export async function allocateTaskId(cwd) {
|
|
|
58
59
|
}
|
|
59
60
|
// ─── File read/write ─────────────────────────────────────────────────────────
|
|
60
61
|
export async function readTaskFile(cwd, id) {
|
|
61
|
-
|
|
62
|
+
// Normalize CRLF/CR → LF at the read boundary: every parser below (front
|
|
63
|
+
// matter, body strip, sectionRegex) assumes '\n'. A Windows/autocrlf file
|
|
64
|
+
// would otherwise fail as "malformed front matter". See shared/fs-text.ts.
|
|
65
|
+
const raw = await readTextFile(taskFilePath(cwd, id));
|
|
62
66
|
const fm = parseFrontMatter(raw);
|
|
63
67
|
if (!fm)
|
|
64
68
|
throw new Error(`malformed front matter in ${id}.md`);
|
|
@@ -423,7 +423,7 @@ export async function docsFocused(input) {
|
|
|
423
423
|
const { pkg, chunks, hitCache, indexingMs } = rawResult;
|
|
424
424
|
const concatenated = chunks.map(c => c.content).join('\n\n');
|
|
425
425
|
const prompt = buildPrompt(pkg, input.query, concatenated);
|
|
426
|
-
const invocation = getPiInvocation([...CHILD_ARGS, prompt
|
|
426
|
+
const invocation = getPiInvocation([...CHILD_ARGS], prompt);
|
|
427
427
|
const child = await runChild(spawn, invocation, input.cwd, input.signal);
|
|
428
428
|
const parsed = parseChildOutput(child.stdout);
|
|
429
429
|
const excerptVerified = parsed.excerpt ? isExcerptInContent(parsed.excerpt, concatenated) : undefined;
|
|
@@ -24,7 +24,7 @@ export async function fetchFocused(input) {
|
|
|
24
24
|
title: cleaned.title,
|
|
25
25
|
content: truncated
|
|
26
26
|
});
|
|
27
|
-
const invocation = getPiInvocation([...CHILD_ARGS, prompt
|
|
27
|
+
const invocation = getPiInvocation([...CHILD_ARGS], prompt);
|
|
28
28
|
const childResult = await runChild(spawnFn, invocation, input.cwd, input.signal);
|
|
29
29
|
if (childResult.aborted) {
|
|
30
30
|
return {
|
|
@@ -72,7 +72,7 @@ export async function runWorker(input) {
|
|
|
72
72
|
let leakRetries = 0;
|
|
73
73
|
for (;;) {
|
|
74
74
|
const prompt = hint === null ? input.prompt : `${hint}\n\n${input.prompt}`;
|
|
75
|
-
const invocation = getPiInvocation([...baseArgs, prompt
|
|
75
|
+
const invocation = getPiInvocation([...baseArgs], prompt);
|
|
76
76
|
const tStart = Date.now();
|
|
77
77
|
let tFirstByte = null;
|
|
78
78
|
// loop === false turns the guard off entirely (detector is null and no
|
|
@@ -104,7 +104,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
104
104
|
};
|
|
105
105
|
const concatenated = chunks.map(c => c.content).join('\n\n');
|
|
106
106
|
const prompt = buildProjectPrompt(projectName, params.query, concatenated);
|
|
107
|
-
const invocation = getPiInvocation([...CHILD_ARGS, prompt
|
|
107
|
+
const invocation = getPiInvocation([...CHILD_ARGS], prompt);
|
|
108
108
|
const child = await runChild(spawn, invocation, ctx.cwd, signal);
|
|
109
109
|
const failure = formatChildFailure(child, 'Project docs lookup aborted.');
|
|
110
110
|
if (failure !== null) {
|
|
@@ -206,7 +206,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
206
206
|
};
|
|
207
207
|
const concatenated = chunks.map(c => c.content).join('\n\n');
|
|
208
208
|
const prompt = buildPrompt(pkg, params.query, concatenated);
|
|
209
|
-
const invocation = getPiInvocation([...CHILD_ARGS, prompt
|
|
209
|
+
const invocation = getPiInvocation([...CHILD_ARGS], prompt);
|
|
210
210
|
const child = await runChild(spawn, invocation, ctx.cwd, signal);
|
|
211
211
|
const failure = formatChildFailure(child, 'Docs lookup aborted.');
|
|
212
212
|
if (failure !== null) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.8",
|
|
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",
|