@mjasnikovs/pi-task 0.16.2 → 0.16.4
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/task/phases.js +16 -1
- package/dist/task/unknown-routing.d.ts +28 -0
- package/dist/task/unknown-routing.js +38 -0
- package/dist/task/verify-work.js +41 -16
- package/dist/workers/docs-core.d.ts +36 -1
- package/dist/workers/docs-core.js +85 -4
- package/dist/workers/pi-worker-docs.js +12 -4
- package/dist/workers/pi-worker-fetch.js +9 -2
- package/package.json +1 -1
package/dist/task/phases.js
CHANGED
|
@@ -9,6 +9,7 @@ import { runWorker } from '../workers/pi-worker-core.js';
|
|
|
9
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
|
+
import { isIntegrationUnknown } from './unknown-routing.js';
|
|
12
13
|
import { getFileInventory } from './file-inventory.js';
|
|
13
14
|
import { buildOrientation } from './orientation.js';
|
|
14
15
|
import { getConfig } from '../config/config.js';
|
|
@@ -459,7 +460,21 @@ export async function phaseAutoAnswer(deps, refined, research, question, autoDep
|
|
|
459
460
|
// otherwise a preamble line leaks out as the recommended answer.
|
|
460
461
|
text = await runPhaseChild(deps, 'grill-auto', 'read', prependHint(GRILL_AUTO_FORMAT_HINT, basePrompt));
|
|
461
462
|
}
|
|
462
|
-
|
|
463
|
+
const parsed = parseAutoAnswer(text);
|
|
464
|
+
// Surviving-unknown routing: an integration / build-wiring unknown whose
|
|
465
|
+
// wrong guess is a structural landmine must NOT be silently auto-answered.
|
|
466
|
+
// We first try to ground it from fetched docs (the enrichment fan-out
|
|
467
|
+
// above); only when NO doc/fetch worker produced a grounding section do we
|
|
468
|
+
// refuse the guess and surface it to the user — carrying the model's
|
|
469
|
+
// best-effort answer as the pre-filled recommendation so the user accepts
|
|
470
|
+
// it with one keystroke or overrides it. Benign unknowns are untouched.
|
|
471
|
+
const docResolved = docSections.filter(Boolean).length > 0;
|
|
472
|
+
if (parsed.kind === 'answered' && !docResolved && isIntegrationUnknown(question)) {
|
|
473
|
+
deps.logDebug?.(`grill-auto: integration unknown unresolved by fetch — surfacing to user `
|
|
474
|
+
+ `instead of auto-answering: ${question.replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
475
|
+
return { kind: 'unknown', suggested: parsed.text, raw: parsed.raw };
|
|
476
|
+
}
|
|
477
|
+
return parsed;
|
|
463
478
|
}
|
|
464
479
|
catch (err) {
|
|
465
480
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surviving-unknown routing.
|
|
3
|
+
*
|
|
4
|
+
* The grill phase generates one clarifying question per KNOWN-UNKNOWN and lets a
|
|
5
|
+
* `grill-auto` child either auto-answer it (ANSWER → the user never sees it) or
|
|
6
|
+
* surface it (UNKNOWN → asked). The dangerous class is an integration / build-
|
|
7
|
+
* wiring unknown — "how is this plugin wired into the build", "should the import
|
|
8
|
+
* structure in the HTML entry change" — which `grill-auto` happily auto-answers
|
|
9
|
+
* with a GUESS that then ships as a silent landmine (a real run shipped an
|
|
10
|
+
* unstyled build this way).
|
|
11
|
+
*
|
|
12
|
+
* This classifier identifies that class so the orchestrator can route it: try to
|
|
13
|
+
* resolve it from fetched docs first, and if nothing grounded the answer, surface
|
|
14
|
+
* it to the user instead of letting the guess stand. Pure + deterministic so it
|
|
15
|
+
* is unit-tested independently of the model.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately GENERIC: it keys on build/tooling CATEGORIES (plugin, bundler,
|
|
18
|
+
* loader, build pipeline, entry point, …) and wiring INTENT (wire, integrate,
|
|
19
|
+
* configure, set up, how/where to …), never on any specific tool or project.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* True when `question` is an integration / build-wiring unknown — one whose
|
|
23
|
+
* wrong guess is a structural landmine, so it must be resolved by a worker or
|
|
24
|
+
* surfaced to the user rather than auto-answered. Requires BOTH a tooling-surface
|
|
25
|
+
* mention and wiring intent, so benign questions ("which log format?", "what
|
|
26
|
+
* default page size?", "which file holds the auth handler?") do not trip it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isIntegrationUnknown(question: string): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surviving-unknown routing.
|
|
3
|
+
*
|
|
4
|
+
* The grill phase generates one clarifying question per KNOWN-UNKNOWN and lets a
|
|
5
|
+
* `grill-auto` child either auto-answer it (ANSWER → the user never sees it) or
|
|
6
|
+
* surface it (UNKNOWN → asked). The dangerous class is an integration / build-
|
|
7
|
+
* wiring unknown — "how is this plugin wired into the build", "should the import
|
|
8
|
+
* structure in the HTML entry change" — which `grill-auto` happily auto-answers
|
|
9
|
+
* with a GUESS that then ships as a silent landmine (a real run shipped an
|
|
10
|
+
* unstyled build this way).
|
|
11
|
+
*
|
|
12
|
+
* This classifier identifies that class so the orchestrator can route it: try to
|
|
13
|
+
* resolve it from fetched docs first, and if nothing grounded the answer, surface
|
|
14
|
+
* it to the user instead of letting the guess stand. Pure + deterministic so it
|
|
15
|
+
* is unit-tested independently of the model.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately GENERIC: it keys on build/tooling CATEGORIES (plugin, bundler,
|
|
18
|
+
* loader, build pipeline, entry point, …) and wiring INTENT (wire, integrate,
|
|
19
|
+
* configure, set up, how/where to …), never on any specific tool or project.
|
|
20
|
+
*/
|
|
21
|
+
// Factor A — the question concerns a build/tooling integration surface, not a
|
|
22
|
+
// value, a name, an output format, or a file location.
|
|
23
|
+
const TOOLING_CONCERN_RE = /\b(?:plugins?|bundlers?|loaders?|middlewares?|adapters?|toolchains?|transpilers?|compilers?|build\s+(?:pipeline|step|process|chain|config|tool|system)|import\s+structure|entry[\s-]*points?|dev[\s-]*servers?|module\s+resolution|build\s+wiring|wiring)\b/i;
|
|
24
|
+
// Factor B — the question is about HOW to connect/configure that surface (or
|
|
25
|
+
// expresses uncertainty about where something hooks in), not which value to pick.
|
|
26
|
+
const WIRING_INTENT_RE = /\b(?:wir(?:e|ed|ing)|integrat(?:e|ed|es|ing|ion)|hook(?:ed|s|ing)?\s+(?:it\s+|them\s+)?(?:up|in|into)|plug(?:ged|s|ging)?\s+(?:it\s+|them\s+)?(?:in|into)|configur(?:e|ed|es|ing|ation)|set[\s-]*up|register(?:ed|s|ing)?|mount(?:ed|s|ing)?|how\s+(?:to|do|does|should|is|are)|where\s+(?:to|should|does|is))\b/i;
|
|
27
|
+
/**
|
|
28
|
+
* True when `question` is an integration / build-wiring unknown — one whose
|
|
29
|
+
* wrong guess is a structural landmine, so it must be resolved by a worker or
|
|
30
|
+
* surfaced to the user rather than auto-answered. Requires BOTH a tooling-surface
|
|
31
|
+
* mention and wiring intent, so benign questions ("which log format?", "what
|
|
32
|
+
* default page size?", "which file holds the auth handler?") do not trip it.
|
|
33
|
+
*/
|
|
34
|
+
export function isIntegrationUnknown(question) {
|
|
35
|
+
if (!question)
|
|
36
|
+
return false;
|
|
37
|
+
return TOOLING_CONCERN_RE.test(question) && WIRING_INTENT_RE.test(question);
|
|
38
|
+
}
|
package/dist/task/verify-work.js
CHANGED
|
@@ -17,6 +17,18 @@
|
|
|
17
17
|
* PASS / FAIL verdict. If the spec's VERIFY is legitimately a no-op (config-only
|
|
18
18
|
* change, re-read of a file), the model says so and that is a PASS.
|
|
19
19
|
*
|
|
20
|
+
* It must verify the REAL deliverable, not a stand-in. The spec's VERIFY block is
|
|
21
|
+
* authored by the same weak model that did the work, so it frequently grades a
|
|
22
|
+
* SIMULATION it controls: it reconstructs the build in a scratch dir, compiles with
|
|
23
|
+
* options the project does not ship, or only greps the SOURCE for a string — all of
|
|
24
|
+
* which pass while the project's own build ships broken output. The prompt therefore
|
|
25
|
+
* anchors the child to the project's OWN build/run command and the artifact it SHIPS,
|
|
26
|
+
* treats source-text presence as insufficient (a build artifact that still contains an
|
|
27
|
+
* un-processed source directive means the build never ran), and treats "I had to add a
|
|
28
|
+
* dependency/flag/config the project lacks to make it pass" as the defect itself.
|
|
29
|
+
* A/B-proven on the live local model: the old prompt false-passed 3/3 on a
|
|
30
|
+
* broken-pipeline fixture; the anchored prompt caught it 3/3, naming the real defect.
|
|
31
|
+
*
|
|
20
32
|
* It runs as a GATE right after the implementation turn, BEFORE the task is
|
|
21
33
|
* checked off or committed. A FAIL stops the /task-auto run exactly like an
|
|
22
34
|
* implementation failure: the task is left unchecked and uncommitted so
|
|
@@ -94,24 +106,37 @@ export function buildVerifyPrompt(spec) {
|
|
|
94
106
|
'THE TASK SPEC (its ACCEPTANCE criteria and VERIFY block are the contract):',
|
|
95
107
|
spec.trim(),
|
|
96
108
|
'',
|
|
97
|
-
'How to verify:',
|
|
98
|
-
"1. Run the
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
' that
|
|
110
|
-
'
|
|
109
|
+
'How to verify — verify the REAL deliverable, not a stand-in:',
|
|
110
|
+
"1. Run the project's OWN build/run/serve command — the one it actually ships",
|
|
111
|
+
' (the scripts in package.json, the command the spec names). Run it exactly as',
|
|
112
|
+
' the project defines it, then inspect the artifact it PRODUCES. Also run the',
|
|
113
|
+
" spec's VERIFY block — but the project's real build/output is the bar.",
|
|
114
|
+
'2. NEVER substitute your own build, compile step, plugin, flag, scratch transform,',
|
|
115
|
+
' or config to make a check pass. If the VERIFY block reconstructs the output in',
|
|
116
|
+
' a temp/scratch dir, builds with options the project does not use, or only greps',
|
|
117
|
+
' the SOURCE for a string, that is INSUFFICIENT — it proves the code *could* work,',
|
|
118
|
+
" not that the shipping pipeline *does*. You must still run the project's real",
|
|
119
|
+
' build and judge its real output.',
|
|
120
|
+
'3. If, to make any check pass, you find you must add an import, plugin, flag, or',
|
|
121
|
+
' config the project itself does not have, STOP: that missing piece IS the defect.',
|
|
122
|
+
' Report FAIL and name exactly what the project is missing.',
|
|
123
|
+
'4. Presence of a token/directive/string in a SOURCE file is NOT verification. Judge',
|
|
124
|
+
' the produced/shipped artifact and the actual runtime behavior. (A build directive',
|
|
125
|
+
' belongs in the source; if that same raw directive SURVIVES into the built output,',
|
|
126
|
+
' the build did not run — that is a FAIL, not a PASS.)',
|
|
127
|
+
'5. Treat the ACCEPTANCE criteria as the bar. If a VERIFY command fails, or its',
|
|
128
|
+
' output (or the real build output) contradicts an ACCEPTANCE criterion, the work',
|
|
129
|
+
' has NOT verified.',
|
|
130
|
+
'6. If a check depends on something this environment genuinely lacks (a service, a',
|
|
131
|
+
' network resource) and that is clearly an environment gap rather than a defect in',
|
|
132
|
+
' the code, note it and judge the rest. Do not fail for a missing external service.',
|
|
133
|
+
'7. If the spec legitimately has no runnable verification (a pure docs/config change',
|
|
134
|
+
' with nothing to build or run), validating it cleanly is a PASS.',
|
|
135
|
+
'8. Do NOT edit anything to make a check pass. Report what you actually saw.',
|
|
111
136
|
'',
|
|
112
137
|
'When you are done, output EXACTLY ONE of these as the final line:',
|
|
113
|
-
' WORK-VERIFIED: PASS (
|
|
114
|
-
' WORK-VERIFIED: FAIL <text> (
|
|
138
|
+
' WORK-VERIFIED: PASS (the real build/run produced an artifact that meets the spec)',
|
|
139
|
+
' WORK-VERIFIED: FAIL <text> (the real build/output failed or does not meet the spec; say what failed)',
|
|
115
140
|
'Output the verdict line verbatim — it is parsed mechanically.'
|
|
116
141
|
].join('\n');
|
|
117
142
|
}
|
|
@@ -4,6 +4,19 @@ import { resolvePackage as defaultResolvePackage, type ResolvedPackage } from '.
|
|
|
4
4
|
import { retrieveChunks as defaultRetrieveChunks, type RetrievedChunk } from './docs-retrieve.js';
|
|
5
5
|
import { npmVersionLookup as defaultNpmVersionLookup, type NpmVersionInfo } from './npm-version.js';
|
|
6
6
|
import { type SpawnFn } from '../shared/child-process.js';
|
|
7
|
+
/**
|
|
8
|
+
* Provenance of an auto-installed package's version, so the answer can state
|
|
9
|
+
* what the version is grounded in instead of leaving it buried in tool details.
|
|
10
|
+
* - 'declared-range': install was pinned to the project's own package.json range
|
|
11
|
+
* (the resolved version therefore matches project intent).
|
|
12
|
+
* - 'npm-latest': nothing in the project declared the dep, so the install fell
|
|
13
|
+
* back to whatever npm tags `latest` — which may be a newer MAJOR than the
|
|
14
|
+
* project targets (this is what a scaffolding task hits, and needs a banner).
|
|
15
|
+
*/
|
|
16
|
+
export interface AutoInstallPin {
|
|
17
|
+
source: 'declared-range' | 'npm-latest';
|
|
18
|
+
range?: string;
|
|
19
|
+
}
|
|
7
20
|
export type DocsRawResult = {
|
|
8
21
|
kind: 'ok';
|
|
9
22
|
pkg: ResolvedPackage;
|
|
@@ -13,6 +26,7 @@ export type DocsRawResult = {
|
|
|
13
26
|
indexedFiles?: number;
|
|
14
27
|
cacheError?: string;
|
|
15
28
|
autoInstalled?: boolean;
|
|
29
|
+
autoInstallPin?: AutoInstallPin;
|
|
16
30
|
npmVersion?: NpmVersionInfo | null;
|
|
17
31
|
} | {
|
|
18
32
|
kind: 'not_installed';
|
|
@@ -25,6 +39,7 @@ export type DocsRawResult = {
|
|
|
25
39
|
indexedFiles?: number;
|
|
26
40
|
cacheError?: string;
|
|
27
41
|
autoInstalled?: boolean;
|
|
42
|
+
autoInstallPin?: AutoInstallPin;
|
|
28
43
|
npmVersion?: NpmVersionInfo | null;
|
|
29
44
|
} | {
|
|
30
45
|
kind: 'error';
|
|
@@ -35,6 +50,7 @@ export type DocsRawResult = {
|
|
|
35
50
|
hitCache?: boolean;
|
|
36
51
|
cacheError?: string;
|
|
37
52
|
autoInstalled?: boolean;
|
|
53
|
+
autoInstallPin?: AutoInstallPin;
|
|
38
54
|
npmVersion?: NpmVersionInfo | null;
|
|
39
55
|
};
|
|
40
56
|
export interface DocsRawInput {
|
|
@@ -69,9 +85,28 @@ export interface DocsFocusedResult {
|
|
|
69
85
|
}
|
|
70
86
|
export type DocsFocusedInput = DocsRawInput;
|
|
71
87
|
export declare function extractParentPackage(moduleName: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* The version range a project DECLARES for `parentPkg` in its package.json under
|
|
90
|
+
* `cwd`. Lets a not-yet-installed scaffolding dependency be documented against
|
|
91
|
+
* the major the project intends, instead of whatever npm currently tags
|
|
92
|
+
* `latest`. Scans the four standard dependency maps in priority order. Returns
|
|
93
|
+
* null — caller falls back to latest — when the dep is undeclared, the
|
|
94
|
+
* package.json is missing/unreadable, or the declared value is not a usable
|
|
95
|
+
* range. Best-effort; never throws.
|
|
96
|
+
*/
|
|
97
|
+
export declare function findDeclaredRange(parentPkg: string, cwd: string): string | null;
|
|
98
|
+
/**
|
|
99
|
+
* One-line version-provenance banner that LEADS a docs answer for a package the
|
|
100
|
+
* worker had to auto-install (not yet present in node_modules — every
|
|
101
|
+
* scaffolding task). Surfaces the version the answer is grounded in directly in
|
|
102
|
+
* the prose the impl model reads, rather than burying it in tool `details`.
|
|
103
|
+
* Empty string when there was no auto-install (already-installed packages need
|
|
104
|
+
* no banner — their version is the project's own).
|
|
105
|
+
*/
|
|
106
|
+
export declare function buildVersionBanner(pin: AutoInstallPin | undefined, pkgName: string, version: string): string;
|
|
72
107
|
export declare function getDocsModulesDir(): string;
|
|
73
108
|
export declare function ensureDocsModulesDir(dir: string): void;
|
|
74
|
-
export declare function runAutoInstall(spawn: SpawnFn, packageName: string, signal: AbortSignal | undefined): Promise<{
|
|
109
|
+
export declare function runAutoInstall(spawn: SpawnFn, packageName: string, signal: AbortSignal | undefined, versionRange?: string): Promise<{
|
|
75
110
|
success: boolean;
|
|
76
111
|
installDir: string;
|
|
77
112
|
stderr: string;
|
|
@@ -24,6 +24,70 @@ export function extractParentPackage(moduleName) {
|
|
|
24
24
|
}
|
|
25
25
|
return moduleName.split('/')[0];
|
|
26
26
|
}
|
|
27
|
+
const DEP_FIELDS = [
|
|
28
|
+
'dependencies',
|
|
29
|
+
'devDependencies',
|
|
30
|
+
'peerDependencies',
|
|
31
|
+
'optionalDependencies'
|
|
32
|
+
];
|
|
33
|
+
/** A declared value that is a real, installable npm semver range — not a
|
|
34
|
+
* wildcard (which pins nothing) and not a non-registry protocol (which is not
|
|
35
|
+
* an `install <pkg>@<range>` target). */
|
|
36
|
+
function isUsableRange(range) {
|
|
37
|
+
const r = range.trim();
|
|
38
|
+
if (r.length === 0 || r === '*' || r === 'x' || r === 'latest')
|
|
39
|
+
return false;
|
|
40
|
+
if (/^(?:workspace|link|file|git|github|http|https|portal|patch|npm):/i.test(r))
|
|
41
|
+
return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The version range a project DECLARES for `parentPkg` in its package.json under
|
|
46
|
+
* `cwd`. Lets a not-yet-installed scaffolding dependency be documented against
|
|
47
|
+
* the major the project intends, instead of whatever npm currently tags
|
|
48
|
+
* `latest`. Scans the four standard dependency maps in priority order. Returns
|
|
49
|
+
* null — caller falls back to latest — when the dep is undeclared, the
|
|
50
|
+
* package.json is missing/unreadable, or the declared value is not a usable
|
|
51
|
+
* range. Best-effort; never throws.
|
|
52
|
+
*/
|
|
53
|
+
export function findDeclaredRange(parentPkg, cwd) {
|
|
54
|
+
let json;
|
|
55
|
+
try {
|
|
56
|
+
json = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
for (const field of DEP_FIELDS) {
|
|
62
|
+
const map = json[field];
|
|
63
|
+
if (!map || typeof map !== 'object')
|
|
64
|
+
continue;
|
|
65
|
+
const range = map[parentPkg];
|
|
66
|
+
if (typeof range === 'string' && isUsableRange(range))
|
|
67
|
+
return range.trim();
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One-line version-provenance banner that LEADS a docs answer for a package the
|
|
73
|
+
* worker had to auto-install (not yet present in node_modules — every
|
|
74
|
+
* scaffolding task). Surfaces the version the answer is grounded in directly in
|
|
75
|
+
* the prose the impl model reads, rather than burying it in tool `details`.
|
|
76
|
+
* Empty string when there was no auto-install (already-installed packages need
|
|
77
|
+
* no banner — their version is the project's own).
|
|
78
|
+
*/
|
|
79
|
+
export function buildVersionBanner(pin, pkgName, version) {
|
|
80
|
+
if (!pin)
|
|
81
|
+
return '';
|
|
82
|
+
if (pin.source === 'declared-range') {
|
|
83
|
+
return (`[VERSION] "${pkgName}" resolved to this project's declared range `
|
|
84
|
+
+ `${pin.range} (installed v${version}); the answer below is pinned to that version.\n\n`);
|
|
85
|
+
}
|
|
86
|
+
return (`[VERSION — verify] "${pkgName}" is not declared in this project's package.json, `
|
|
87
|
+
+ `so this answer is based on npm latest (v${version}). Your project may target a `
|
|
88
|
+
+ `different MAJOR — confirm the version you intend to install and treat any API that `
|
|
89
|
+
+ `differs across majors as unverified until you check it against that version.\n\n`);
|
|
90
|
+
}
|
|
27
91
|
export function getDocsModulesDir() {
|
|
28
92
|
const base = process.env.XDG_CACHE_HOME?.trim() || path.join(os.homedir(), '.cache');
|
|
29
93
|
return path.join(base, 'pi-worker', 'docs-modules');
|
|
@@ -35,12 +99,15 @@ export function ensureDocsModulesDir(dir) {
|
|
|
35
99
|
fs.writeFileSync(pkgPath, '{"name":"pi-worker-docs-modules","private":true}\n', 'utf8');
|
|
36
100
|
}
|
|
37
101
|
}
|
|
38
|
-
export async function runAutoInstall(spawn, packageName, signal) {
|
|
102
|
+
export async function runAutoInstall(spawn, packageName, signal, versionRange) {
|
|
39
103
|
const installDir = getDocsModulesDir();
|
|
40
104
|
ensureDocsModulesDir(installDir);
|
|
105
|
+
// `shell: false` in runChild, so a `^`/`~`/space in the range stays a single
|
|
106
|
+
// literal arg — no glob/expansion risk from `<pkg>@<range>`.
|
|
107
|
+
const target = versionRange ? `${packageName}@${versionRange}` : packageName;
|
|
41
108
|
const result = await runChild(spawn, {
|
|
42
109
|
command: 'npm',
|
|
43
|
-
args: ['install', '--no-audit', '--no-fund', '--loglevel=error',
|
|
110
|
+
args: ['install', '--no-audit', '--no-fund', '--loglevel=error', target]
|
|
44
111
|
}, installDir, signal, { mode: 'text', discardStdout: true });
|
|
45
112
|
return { success: result.exitCode === 0 && !result.aborted, installDir, stderr: result.stderr };
|
|
46
113
|
}
|
|
@@ -111,20 +178,30 @@ export async function docsRaw(input) {
|
|
|
111
178
|
// Step 1: resolve package
|
|
112
179
|
let pkg;
|
|
113
180
|
let autoInstalled = false;
|
|
181
|
+
let autoInstallPin;
|
|
114
182
|
try {
|
|
115
183
|
pkg = resolvePackage(requested, input.cwd);
|
|
116
184
|
}
|
|
117
185
|
catch (firstErr) {
|
|
118
186
|
if (firstErr instanceof ResolveError && firstErr.kind === 'not_installed') {
|
|
119
|
-
// auto-install
|
|
187
|
+
// auto-install — but FIRST honour the version the project intends.
|
|
188
|
+
// If the dep is declared in the project's package.json, install that
|
|
189
|
+
// range so a scaffolding answer is grounded in the project's major,
|
|
190
|
+
// not whatever npm currently tags `latest`.
|
|
120
191
|
const parentPkg = extractParentPackage(requested);
|
|
121
|
-
const
|
|
192
|
+
const declaredRange = findDeclaredRange(parentPkg, input.cwd);
|
|
193
|
+
autoInstallPin =
|
|
194
|
+
declaredRange ?
|
|
195
|
+
{ source: 'declared-range', range: declaredRange }
|
|
196
|
+
: { source: 'npm-latest' };
|
|
197
|
+
const installResult = await runAutoInstall(spawn, parentPkg, undefined, declaredRange ?? undefined);
|
|
122
198
|
if (!installResult.success) {
|
|
123
199
|
return {
|
|
124
200
|
kind: 'error',
|
|
125
201
|
message: `Package "${parentPkg}" is not installed and auto-install failed.\n${installResult.stderr}`,
|
|
126
202
|
resolveError: 'not_installed',
|
|
127
203
|
installError: installResult.stderr,
|
|
204
|
+
autoInstallPin,
|
|
128
205
|
npmVersion: await npmVersionPromise
|
|
129
206
|
};
|
|
130
207
|
}
|
|
@@ -139,6 +216,7 @@ export async function docsRaw(input) {
|
|
|
139
216
|
message: retryErr.message,
|
|
140
217
|
resolveError: retryErr.kind,
|
|
141
218
|
autoInstalled,
|
|
219
|
+
autoInstallPin,
|
|
142
220
|
npmVersion: await npmVersionPromise
|
|
143
221
|
};
|
|
144
222
|
}
|
|
@@ -146,6 +224,7 @@ export async function docsRaw(input) {
|
|
|
146
224
|
kind: 'error',
|
|
147
225
|
message: `Could not resolve "${input.pkg}" after install: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`,
|
|
148
226
|
autoInstalled,
|
|
227
|
+
autoInstallPin,
|
|
149
228
|
npmVersion: await npmVersionPromise
|
|
150
229
|
};
|
|
151
230
|
}
|
|
@@ -186,6 +265,8 @@ export async function docsRaw(input) {
|
|
|
186
265
|
docsRawCached(cache, pkg, input.query, ensureIndexed, retrieveChunks, autoInstalled)
|
|
187
266
|
: docsRawUncached(pkg, cacheError ?? 'unknown cache error', autoInstalled);
|
|
188
267
|
result.npmVersion = await npmVersionPromise;
|
|
268
|
+
if (autoInstallPin && result.kind !== 'not_installed')
|
|
269
|
+
result.autoInstallPin = autoInstallPin;
|
|
189
270
|
return result;
|
|
190
271
|
}
|
|
191
272
|
function docsRawCached(cache, pkg, query, ensureIndexed, retrieveChunks, autoInstalled) {
|
|
@@ -2,7 +2,7 @@ import { Type } from '@sinclair/typebox';
|
|
|
2
2
|
import { Text } from '@earendil-works/pi-tui';
|
|
3
3
|
import { openCache as defaultOpenCache } from './docs-cache.js';
|
|
4
4
|
import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
|
|
5
|
-
import { docsRaw, formatResultText, buildPrompt } from './docs-core.js';
|
|
5
|
+
import { docsRaw, formatResultText, buildPrompt, buildVersionBanner } from './docs-core.js';
|
|
6
6
|
import { formatNpmVersionSection } from './npm-version.js';
|
|
7
7
|
import { runChild, CHILD_BASE_ARGS } from '../shared/child-process.js';
|
|
8
8
|
import { parseChildOutput, isExcerptInContent } from '../shared/child-output.js';
|
|
@@ -19,6 +19,9 @@ const Params = Type.Object({
|
|
|
19
19
|
description: 'What to extract from the docs. The child pi reads ranked chunks and returns ONLY content answering this.'
|
|
20
20
|
})
|
|
21
21
|
});
|
|
22
|
+
function pinDetails(pin) {
|
|
23
|
+
return pin ? { versionSource: pin.source, declaredRange: pin.range } : {};
|
|
24
|
+
}
|
|
22
25
|
export function registerPiWorkerDocs(pi, internals = {}) {
|
|
23
26
|
makeWorkerTool(pi, {
|
|
24
27
|
name: 'pi-worker-docs',
|
|
@@ -172,8 +175,10 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
172
175
|
};
|
|
173
176
|
}
|
|
174
177
|
if (rawResult.kind === 'no_chunks') {
|
|
178
|
+
const banner = buildVersionBanner(rawResult.autoInstallPin, rawResult.pkg.name, rawResult.pkg.version);
|
|
175
179
|
return {
|
|
176
|
-
text:
|
|
180
|
+
text: banner
|
|
181
|
+
+ npmHeader
|
|
177
182
|
+ `Package ${rawResult.pkg.name}@${rawResult.pkg.version} has no .d.ts files or README. Use pi-worker to read source directly.`,
|
|
178
183
|
details: {
|
|
179
184
|
version: rawResult.pkg.version,
|
|
@@ -181,12 +186,14 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
181
186
|
indexedFiles: rawResult.indexedFiles ?? 0,
|
|
182
187
|
cacheError: rawResult.cacheError,
|
|
183
188
|
autoInstalled: rawResult.autoInstalled,
|
|
189
|
+
...pinDetails(rawResult.autoInstallPin),
|
|
184
190
|
...npmDetails
|
|
185
191
|
}
|
|
186
192
|
};
|
|
187
193
|
}
|
|
188
194
|
// kind === 'ok'
|
|
189
195
|
const { pkg, chunks, hitCache, indexingMs, cacheError, autoInstalled } = rawResult;
|
|
196
|
+
const versionBanner = buildVersionBanner(rawResult.autoInstallPin, pkg.name, pkg.version);
|
|
190
197
|
const baseDetails = {
|
|
191
198
|
version: pkg.version,
|
|
192
199
|
hitCache,
|
|
@@ -194,6 +201,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
194
201
|
indexingMs,
|
|
195
202
|
cacheError,
|
|
196
203
|
autoInstalled,
|
|
204
|
+
...pinDetails(rawResult.autoInstallPin),
|
|
197
205
|
...npmDetails
|
|
198
206
|
};
|
|
199
207
|
const concatenated = chunks.map(c => c.content).join('\n\n');
|
|
@@ -203,7 +211,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
203
211
|
const failure = formatChildFailure(child, 'Docs lookup aborted.');
|
|
204
212
|
if (failure !== null) {
|
|
205
213
|
return {
|
|
206
|
-
text: npmHeader + failure,
|
|
214
|
+
text: versionBanner + npmHeader + failure,
|
|
207
215
|
details: {
|
|
208
216
|
...baseDetails,
|
|
209
217
|
...(child.aborted ? { aborted: true } : {}),
|
|
@@ -213,7 +221,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
213
221
|
}
|
|
214
222
|
const parsed = parseChildOutput(child.stdout);
|
|
215
223
|
const verified = parsed.excerpt ? isExcerptInContent(parsed.excerpt, concatenated) : undefined;
|
|
216
|
-
const text = npmHeader + formatResultText(pkg, parsed, verified);
|
|
224
|
+
const text = versionBanner + npmHeader + formatResultText(pkg, parsed, verified);
|
|
217
225
|
return {
|
|
218
226
|
text,
|
|
219
227
|
details: {
|
|
@@ -17,8 +17,15 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
17
17
|
description: 'Fetch a web page or text resource (HTML, markdown, plain text, JSON, '
|
|
18
18
|
+ 'XML/feeds), clean HTML to markdown, and hand it to an isolated child '
|
|
19
19
|
+ 'Pi session that extracts ONLY content answering `query`. Returns the '
|
|
20
|
-
+ 'focused answer
|
|
21
|
-
+ '
|
|
20
|
+
+ 'focused answer.\n'
|
|
21
|
+
+ 'REACH FOR THIS when you need to know how an external library, tool, '
|
|
22
|
+
+ 'plugin, framework, or service is CONFIGURED, WIRED, or INTEGRATED and '
|
|
23
|
+
+ 'the installed-package docs (pi-worker-docs) do not cover it: fetch its '
|
|
24
|
+
+ 'README or official documentation page and extract the setup/wiring you '
|
|
25
|
+
+ 'need, instead of guessing. Do NOT guess integration or configuration '
|
|
26
|
+
+ 'details from memory — fetch the authoritative page. Also use it to read '
|
|
27
|
+
+ 'any known URL, or after `pi-worker-search` to read a result, without '
|
|
28
|
+
+ 'stuffing raw content into the main context.',
|
|
22
29
|
parameters: Params,
|
|
23
30
|
async run(params, signal, ctx) {
|
|
24
31
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.4",
|
|
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",
|