@mjasnikovs/pi-task 0.37.7 → 0.38.0
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-output.d.ts +19 -3
- package/dist/shared/child-output.js +21 -5
- package/dist/shared/git-runner.d.ts +39 -0
- package/dist/shared/git-runner.js +38 -0
- package/dist/task/accept-debt.d.ts +27 -58
- package/dist/task/accept-debt.js +60 -130
- package/dist/task/auto-orchestrator.d.ts +7 -57
- package/dist/task/auto-orchestrator.js +25 -499
- package/dist/task/child-runner.d.ts +2 -0
- package/dist/task/child-runner.js +74 -70
- package/dist/task/enforce-guidelines.d.ts +1 -1
- package/dist/task/enforce-guidelines.js +2 -2
- package/dist/task/external-context.d.ts +85 -7
- package/dist/task/external-context.js +100 -63
- package/dist/task/file-inventory.js +22 -41
- package/dist/task/final-gate.d.ts +80 -0
- package/dist/task/final-gate.js +102 -49
- package/dist/task/gate-deps.js +6 -23
- package/dist/task/git-state-guard.d.ts +1 -1
- package/dist/task/git-state-guard.js +1 -7
- package/dist/task/phases.js +40 -83
- package/dist/task/run-final-gate.d.ts +127 -0
- package/dist/task/run-final-gate.js +492 -0
- package/dist/task/task-gates.d.ts +20 -57
- package/dist/task/task-gates.js +11 -11
- package/dist/task/verify-work.d.ts +40 -32
- package/dist/task/verify-work.js +301 -241
- package/dist/workers/docs-core.d.ts +14 -0
- package/dist/workers/docs-core.js +28 -16
- package/dist/workers/fetch-core.d.ts +6 -1
- package/dist/workers/fetch-core.js +26 -33
- package/dist/workers/focused-extractor.d.ts +73 -0
- package/dist/workers/focused-extractor.js +72 -0
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +48 -42
- package/dist/workers/pi-worker-fetch.js +6 -8
- package/dist/workers/typeonly-log.d.ts +13 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ 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
|
+
import { type ExcerptVerification } from '../shared/child-output.js';
|
|
7
8
|
/**
|
|
8
9
|
* Provenance of an auto-installed package's version, so the answer can state
|
|
9
10
|
* what the version is grounded in instead of leaving it buried in tool details.
|
|
@@ -74,9 +75,22 @@ export interface DocsRawInput {
|
|
|
74
75
|
signal?: AbortSignal;
|
|
75
76
|
}
|
|
76
77
|
export interface DocsFocusedResult {
|
|
78
|
+
/**
|
|
79
|
+
* The child's answer — EMPTY when `failure` is set. A failed child's stdout is never
|
|
80
|
+
* parsed as an answer (it used to be: `parseChildOutput` returns the whole trimmed
|
|
81
|
+
* stdout when there is no `<answer>` tag, so a crashed child's error dump was handed
|
|
82
|
+
* to phaseAutoAnswer as if it were package documentation).
|
|
83
|
+
*/
|
|
77
84
|
answer: string;
|
|
78
85
|
excerpt?: string;
|
|
79
86
|
excerptVerified?: boolean;
|
|
87
|
+
/** Retained evidence for a false `excerptVerified`, diagnosable without re-running. */
|
|
88
|
+
excerptCheck?: ExcerptVerification;
|
|
89
|
+
/**
|
|
90
|
+
* Set exactly when the child failed (aborted, or non-zero exit): the standard
|
|
91
|
+
* child-failure message. Callers must treat `answer` as absent when this is present.
|
|
92
|
+
*/
|
|
93
|
+
failure?: string;
|
|
80
94
|
pkg: ResolvedPackage;
|
|
81
95
|
version: string;
|
|
82
96
|
exitCode: number;
|
|
@@ -7,17 +7,15 @@ import { ensureIndexed as defaultEnsureIndexed } from './docs-index.js';
|
|
|
7
7
|
import { resolvePackage as defaultResolvePackage, ResolveError, detectTypesRedirect, typesPackageName, hasTypeFiles, isDtsFile, splitRuntimeNamespace } from './docs-resolve.js';
|
|
8
8
|
import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
|
|
9
9
|
import { npmVersionLookup as defaultNpmVersionLookup } from './npm-version.js';
|
|
10
|
-
import { getPiInvocation } from '../shared/pi-invocation.js';
|
|
11
10
|
import { runChild } from '../shared/child-process.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { runFocusedExtraction } from './focused-extractor.js';
|
|
12
|
+
import { formatResultText as formatResultTextShared } from '../shared/child-output.js';
|
|
14
13
|
const DEFAULT_LIMIT = 8;
|
|
15
14
|
const DEFAULT_BUDGET = 24_000;
|
|
16
15
|
const NO_CACHE_HEAD = 25_000;
|
|
17
16
|
const NO_CACHE_TAIL = 5_000;
|
|
18
17
|
const NO_CACHE_TOTAL = NO_CACHE_HEAD + NO_CACHE_TAIL;
|
|
19
18
|
const NO_CACHE_MARKER = '\n\n[...content continues, truncated...]\n\n';
|
|
20
|
-
const childArgs = () => [...childBaseArgs(), '--no-tools'];
|
|
21
19
|
export function extractParentPackage(moduleName) {
|
|
22
20
|
if (moduleName.startsWith('@')) {
|
|
23
21
|
const parts = moduleName.split('/');
|
|
@@ -516,26 +514,40 @@ export async function docsFocused(input) {
|
|
|
516
514
|
}
|
|
517
515
|
const { pkg, chunks, hitCache, indexingMs } = rawResult;
|
|
518
516
|
const concatenated = chunks.map(c => c.content).join('\n\n');
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
517
|
+
const extraction = await runFocusedExtraction({
|
|
518
|
+
prompt: buildPrompt(pkg, input.query, concatenated),
|
|
519
|
+
// Exactly what went into the prompt — this path prompts with the whole concatenation,
|
|
520
|
+
// so the verify target and the prompt content are the same text.
|
|
521
|
+
verifyAgainst: concatenated,
|
|
522
|
+
cwd: input.cwd,
|
|
523
|
+
signal: input.signal,
|
|
524
|
+
spawn,
|
|
525
|
+
abortedMessage: 'Docs lookup aborted.'
|
|
526
|
+
});
|
|
527
|
+
const base = {
|
|
528
528
|
pkg,
|
|
529
529
|
version: pkg.version,
|
|
530
|
-
exitCode:
|
|
531
|
-
aborted:
|
|
532
|
-
stderr:
|
|
530
|
+
exitCode: extraction.exitCode,
|
|
531
|
+
aborted: extraction.aborted,
|
|
532
|
+
stderr: extraction.stderr,
|
|
533
533
|
hitCache,
|
|
534
534
|
indexingMs,
|
|
535
535
|
chunksRetrieved: chunks.length,
|
|
536
536
|
autoInstalled: rawResult.autoInstalled,
|
|
537
537
|
npmVersion: rawResult.npmVersion
|
|
538
538
|
};
|
|
539
|
+
// A failed child yields NO answer. The caller (phaseAutoAnswer) gates on `answer` being
|
|
540
|
+
// non-empty, so an empty one keeps a dead child's output out of the spec entirely;
|
|
541
|
+
// `failure` carries the reason for anyone who wants to report it.
|
|
542
|
+
if (!extraction.ok)
|
|
543
|
+
return { answer: '', failure: extraction.failure, ...base };
|
|
544
|
+
return {
|
|
545
|
+
answer: extraction.answer,
|
|
546
|
+
excerpt: extraction.excerpt,
|
|
547
|
+
excerptVerified: extraction.excerptVerified,
|
|
548
|
+
excerptCheck: extraction.excerptCheck,
|
|
549
|
+
...base
|
|
550
|
+
};
|
|
539
551
|
}
|
|
540
552
|
export function buildPrompt(pkg, query, content) {
|
|
541
553
|
return (`You answer one question about an npm package, using only the provided content.\n`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchAndClean as defaultFetchAndClean } from './html-clean.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { SpawnFn } from '../shared/child-process.js';
|
|
3
3
|
import { type ExcerptVerification } from '../shared/child-output.js';
|
|
4
4
|
/** The exact non-answers the child is instructed to emit, matched at the tool layer. */
|
|
5
5
|
export declare const UNCLEAR_ANSWER = "unclear from this page";
|
|
@@ -67,6 +67,11 @@ export interface FetchFocusedResult {
|
|
|
67
67
|
excerptCheck?: ExcerptVerification;
|
|
68
68
|
/** The prompt actually handed to the child — the A/B asserts surgery against this per rep. */
|
|
69
69
|
assembledPrompt: string;
|
|
70
|
+
/**
|
|
71
|
+
* Set exactly when the child failed (aborted, or non-zero exit): the standard
|
|
72
|
+
* child-failure message, already formatted. Present ⇒ `answer` is empty and means nothing.
|
|
73
|
+
*/
|
|
74
|
+
failure?: string;
|
|
70
75
|
childExitCode: number;
|
|
71
76
|
aborted: boolean;
|
|
72
77
|
stderr: string;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { spawn as defaultSpawn } from 'node:child_process';
|
|
2
1
|
import { fetchAndClean as defaultFetchAndClean } from './html-clean.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { childBaseArgs } from '../shared/child-extensions.js';
|
|
6
|
-
import { parseChildOutput, verifyExcerpt, formatResultText as formatResultTextShared } from '../shared/child-output.js';
|
|
2
|
+
import { runFocusedExtraction } from './focused-extractor.js';
|
|
3
|
+
import { formatResultText as formatResultTextShared } from '../shared/child-output.js';
|
|
7
4
|
const CONTENT_BUDGET = 30_000;
|
|
8
5
|
const HEAD_CHARS = 25_000;
|
|
9
6
|
const TAIL_CHARS = 5_000;
|
|
@@ -23,7 +20,6 @@ export const NOT_COVERED_ANSWER = 'not covered by this page';
|
|
|
23
20
|
* no recorded verdict.
|
|
24
21
|
*/
|
|
25
22
|
const NOT_COVERED_RE = /^not covered by this page[.\s]*$/i;
|
|
26
|
-
const childArgs = () => [...childBaseArgs(), '--no-tools'];
|
|
27
23
|
/**
|
|
28
24
|
* `github.com/{owner}/{repo}/blob/{ref}/{path}` renders the file through a client-side
|
|
29
25
|
* viewer, so the HTML we clean carries GitHub chrome ("Sign in", "Appearance settings")
|
|
@@ -59,7 +55,6 @@ export async function fetchRaw(input) {
|
|
|
59
55
|
}
|
|
60
56
|
export async function fetchFocused(input) {
|
|
61
57
|
const fetchAndCleanFn = input.fetchAndClean ?? defaultFetchAndClean;
|
|
62
|
-
const spawnFn = input.spawn ?? defaultSpawn;
|
|
63
58
|
const strategy = input.strategy ?? shippedStrategy;
|
|
64
59
|
const fetchedUrl = normaliseSourceUrl(input.url);
|
|
65
60
|
const cleaned = await fetchAndCleanFn(fetchedUrl, { signal: input.signal });
|
|
@@ -73,43 +68,41 @@ export async function fetchFocused(input) {
|
|
|
73
68
|
content: selected.content,
|
|
74
69
|
section: selected.section
|
|
75
70
|
});
|
|
76
|
-
const
|
|
77
|
-
|
|
71
|
+
const extraction = await runFocusedExtraction({
|
|
72
|
+
prompt,
|
|
73
|
+
// Verify against the FULL page, not the anchored slice we prompted with: the slice is
|
|
74
|
+
// a substring of it, so a genuine excerpt still verifies, and an excerpt the child
|
|
75
|
+
// pulled from memory still fails — the detector's discrimination is unchanged by
|
|
76
|
+
// fragment anchoring. This is the one call site that verifies against a SUPERSET of
|
|
77
|
+
// the prompt content, which is why the extractor takes the target by name.
|
|
78
|
+
verifyAgainst: cleaned.markdown,
|
|
79
|
+
cwd: input.cwd,
|
|
80
|
+
signal: input.signal,
|
|
81
|
+
spawn: input.spawn,
|
|
82
|
+
abortedMessage: 'Fetch aborted.'
|
|
83
|
+
});
|
|
78
84
|
const base = {
|
|
79
85
|
anchoredSection: selected.section,
|
|
80
86
|
assembledPrompt: prompt,
|
|
81
|
-
stderr:
|
|
82
|
-
stdout:
|
|
87
|
+
stderr: extraction.stderr,
|
|
88
|
+
stdout: extraction.stdout
|
|
83
89
|
};
|
|
84
|
-
if (
|
|
85
|
-
return {
|
|
86
|
-
answer: '',
|
|
87
|
-
coverageMiss: false,
|
|
88
|
-
childExitCode: childResult.exitCode,
|
|
89
|
-
aborted: true,
|
|
90
|
-
...base
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
if (childResult.exitCode !== 0) {
|
|
90
|
+
if (!extraction.ok) {
|
|
94
91
|
return {
|
|
95
92
|
answer: '',
|
|
96
93
|
coverageMiss: false,
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
failure: extraction.failure,
|
|
95
|
+
childExitCode: extraction.exitCode,
|
|
96
|
+
aborted: extraction.aborted,
|
|
99
97
|
...base
|
|
100
98
|
};
|
|
101
99
|
}
|
|
102
|
-
const
|
|
103
|
-
const coverageMiss = NOT_COVERED_RE.test(parsed.answer.trim());
|
|
104
|
-
// Verify against the FULL page, not the anchored slice: the slice is a substring of it,
|
|
105
|
-
// so a genuine excerpt still verifies, and an excerpt the child pulled from memory still
|
|
106
|
-
// fails — the detector's discrimination is unchanged by fragment anchoring.
|
|
107
|
-
const check = parsed.excerpt ? verifyExcerpt(parsed.excerpt, cleaned.markdown) : undefined;
|
|
100
|
+
const coverageMiss = NOT_COVERED_RE.test(extraction.answer.trim());
|
|
108
101
|
return {
|
|
109
|
-
answer:
|
|
110
|
-
excerpt:
|
|
111
|
-
excerptVerified:
|
|
112
|
-
excerptCheck:
|
|
102
|
+
answer: extraction.answer,
|
|
103
|
+
excerpt: extraction.excerpt,
|
|
104
|
+
excerptVerified: extraction.excerptVerified,
|
|
105
|
+
excerptCheck: extraction.excerptCheck,
|
|
113
106
|
coverageMiss,
|
|
114
107
|
nextStep: coverageMiss ? coverageMissNextStep(input.url, fetchedUrl) : undefined,
|
|
115
108
|
childExitCode: 0,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type SpawnFn } from '../shared/child-process.js';
|
|
2
|
+
import { type ExcerptVerification } from '../shared/child-output.js';
|
|
3
|
+
/**
|
|
4
|
+
* The argv every focused extraction child runs with: the shared child base (whitelisted
|
|
5
|
+
* extensions + `--print --no-session …`) plus `--no-tools`. This was three byte-identical
|
|
6
|
+
* one-liners in fetch-core, docs-core and pi-worker-docs.
|
|
7
|
+
*
|
|
8
|
+
* `--no-tools` is the contract, not a default: the child is given all the content it may use
|
|
9
|
+
* inside its prompt, so a tool call could only reach for something unsourced.
|
|
10
|
+
*/
|
|
11
|
+
export declare const focusedChildArgs: () => string[];
|
|
12
|
+
export interface FocusedRequest {
|
|
13
|
+
/** The fully assembled prompt, including the content block. Delivered on stdin. */
|
|
14
|
+
prompt: string;
|
|
15
|
+
/**
|
|
16
|
+
* The text the cited excerpt is checked against — a NAMED knob, because the four call
|
|
17
|
+
* sites deliberately disagree and the disagreement used to be invisible.
|
|
18
|
+
*
|
|
19
|
+
* `pi-worker-docs` and `docsFocused` pass exactly the concatenated chunks that went into
|
|
20
|
+
* the prompt. `fetchFocused` passes the FULL cleaned page while prompting with only the
|
|
21
|
+
* anchored `#fragment` section: the slice is a substring of the page, so a genuine excerpt
|
|
22
|
+
* still verifies, and an excerpt pulled from the model's memory still fails — fragment
|
|
23
|
+
* anchoring therefore cannot change the hallucination detector's discrimination.
|
|
24
|
+
*
|
|
25
|
+
* Passing the prompt's own content is the safe default; passing a SUPERSET (as fetch does)
|
|
26
|
+
* loosens the check deliberately. Passing anything narrower would manufacture false
|
|
27
|
+
* fabrication verdicts.
|
|
28
|
+
*/
|
|
29
|
+
verifyAgainst: string;
|
|
30
|
+
cwd: string;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
/** Defaults to node's spawn; injected by tests and by the worker `internals` hooks. */
|
|
33
|
+
spawn?: SpawnFn;
|
|
34
|
+
/** The message reported when the child was aborted, e.g. `'Docs lookup aborted.'`. */
|
|
35
|
+
abortedMessage: string;
|
|
36
|
+
}
|
|
37
|
+
/** What every outcome carries, success or failure — the raw child evidence. */
|
|
38
|
+
interface FocusedChildEvidence {
|
|
39
|
+
exitCode: number;
|
|
40
|
+
aborted: boolean;
|
|
41
|
+
stderr: string;
|
|
42
|
+
/** The child's raw stdout, retained for A/B harnesses and failure diagnosis. */
|
|
43
|
+
stdout: string;
|
|
44
|
+
}
|
|
45
|
+
/** The child aborted or exited non-zero. There is deliberately no `answer` here. */
|
|
46
|
+
export interface FocusedFailure extends FocusedChildEvidence {
|
|
47
|
+
ok: false;
|
|
48
|
+
/** `formatChildFailure`'s message — the abort message, or `Worker exited N` + stderr tail. */
|
|
49
|
+
failure: string;
|
|
50
|
+
}
|
|
51
|
+
/** The child exited 0; its output has been parsed and its citation checked. */
|
|
52
|
+
export interface FocusedAnswer extends FocusedChildEvidence {
|
|
53
|
+
ok: true;
|
|
54
|
+
aborted: false;
|
|
55
|
+
/** The `<answer>` body, or the whole trimmed stdout when the child emitted no tags. */
|
|
56
|
+
answer: string;
|
|
57
|
+
/** The `<excerpt>` body; absent when the child cited nothing (or cited only whitespace). */
|
|
58
|
+
excerpt?: string;
|
|
59
|
+
/** The full verification record — absent exactly when `excerpt` is. */
|
|
60
|
+
excerptCheck?: ExcerptVerification;
|
|
61
|
+
/** `excerptCheck?.verified`, for the many callers that only report the verdict. */
|
|
62
|
+
excerptVerified?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export type FocusedResult = FocusedFailure | FocusedAnswer;
|
|
65
|
+
/**
|
|
66
|
+
* Run one focused extraction: spawn the no-tools child on `prompt`, and on success parse its
|
|
67
|
+
* `<answer>`/`<excerpt>` and verify the excerpt against `verifyAgainst`.
|
|
68
|
+
*
|
|
69
|
+
* Never retries — a re-ask of a deterministic extraction over unchanged content is a second
|
|
70
|
+
* bill for the same answer, and all four call sites already had it that way.
|
|
71
|
+
*/
|
|
72
|
+
export declare function runFocusedExtraction(req: FocusedRequest): Promise<FocusedResult>;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **Focused extractor** — the one place a no-tools child pi is run to answer ONE question
|
|
3
|
+
* over content already in hand, cite a verbatim `<excerpt>`, and have that citation checked.
|
|
4
|
+
*
|
|
5
|
+
* It had four copies: `fetchFocused` (fetch-core), `docsFocused` (docs-core), and both paths
|
|
6
|
+
* of `pi-worker-docs` (project source, npm package) — the last two 17-of-20 lines identical
|
|
7
|
+
* and 150 lines apart inside one function. Everything the four did around the child was
|
|
8
|
+
* byte-identical: the `--no-tools` argv, `getPiInvocation`, `runChild`, `parseChildOutput`,
|
|
9
|
+
* and no retry. What genuinely differed was DATA, and the copies hid it:
|
|
10
|
+
*
|
|
11
|
+
* 1. the prompt body — so {@link FocusedRequest.prompt} is a string the caller assembles;
|
|
12
|
+
* 2. WHAT the excerpt is verified against — see {@link FocusedRequest.verifyAgainst};
|
|
13
|
+
* 3. what the answer MEANS (fetch's coverage-miss sentinel) — left to the caller, because
|
|
14
|
+
* classifying an answer is the caller's domain, not the child runner's.
|
|
15
|
+
*
|
|
16
|
+
* Two things the copies got wrong, fixed here once:
|
|
17
|
+
* - **Failure.** `docsFocused` never called `formatChildFailure`, so a child that exited
|
|
18
|
+
* non-zero had its raw stdout parsed as if it were an answer (`parseChildOutput` returns
|
|
19
|
+
* the whole trimmed stdout when there is no `<answer>` tag) and handed to the caller.
|
|
20
|
+
* The result type below makes that unrepresentable: there is no `answer` on a failure.
|
|
21
|
+
* - **Evidence.** Only fetch kept the rich {@link ExcerptVerification}; the other three
|
|
22
|
+
* reduced it to a bare boolean, which is exactly the diagnostic gap typeonly-log.ts
|
|
23
|
+
* names — an `excerptVerified === false` that cannot afterwards be attributed to
|
|
24
|
+
* fabrication vs a normaliser gap. Every caller now gets the struct.
|
|
25
|
+
*/
|
|
26
|
+
import { spawn as defaultSpawn } from 'node:child_process';
|
|
27
|
+
import { getPiInvocation } from '../shared/pi-invocation.js';
|
|
28
|
+
import { runChild } from '../shared/child-process.js';
|
|
29
|
+
import { childBaseArgs } from '../shared/child-extensions.js';
|
|
30
|
+
import { parseChildOutput, verifyExcerpt } from '../shared/child-output.js';
|
|
31
|
+
import { formatChildFailure } from './shared.js';
|
|
32
|
+
/**
|
|
33
|
+
* The argv every focused extraction child runs with: the shared child base (whitelisted
|
|
34
|
+
* extensions + `--print --no-session …`) plus `--no-tools`. This was three byte-identical
|
|
35
|
+
* one-liners in fetch-core, docs-core and pi-worker-docs.
|
|
36
|
+
*
|
|
37
|
+
* `--no-tools` is the contract, not a default: the child is given all the content it may use
|
|
38
|
+
* inside its prompt, so a tool call could only reach for something unsourced.
|
|
39
|
+
*/
|
|
40
|
+
export const focusedChildArgs = () => [...childBaseArgs(), '--no-tools'];
|
|
41
|
+
/**
|
|
42
|
+
* Run one focused extraction: spawn the no-tools child on `prompt`, and on success parse its
|
|
43
|
+
* `<answer>`/`<excerpt>` and verify the excerpt against `verifyAgainst`.
|
|
44
|
+
*
|
|
45
|
+
* Never retries — a re-ask of a deterministic extraction over unchanged content is a second
|
|
46
|
+
* bill for the same answer, and all four call sites already had it that way.
|
|
47
|
+
*/
|
|
48
|
+
export async function runFocusedExtraction(req) {
|
|
49
|
+
const spawn = req.spawn ?? defaultSpawn;
|
|
50
|
+
const invocation = getPiInvocation(focusedChildArgs(), req.prompt);
|
|
51
|
+
const child = await runChild(spawn, invocation, req.cwd, req.signal);
|
|
52
|
+
const evidence = {
|
|
53
|
+
exitCode: child.exitCode,
|
|
54
|
+
aborted: child.aborted,
|
|
55
|
+
stderr: child.stderr,
|
|
56
|
+
stdout: child.stdout
|
|
57
|
+
};
|
|
58
|
+
const failure = formatChildFailure(child, req.abortedMessage);
|
|
59
|
+
if (failure !== null)
|
|
60
|
+
return { ok: false, failure, ...evidence };
|
|
61
|
+
const parsed = parseChildOutput(child.stdout);
|
|
62
|
+
const excerptCheck = parsed.excerpt ? verifyExcerpt(parsed.excerpt, req.verifyAgainst) : undefined;
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
...evidence,
|
|
66
|
+
aborted: false,
|
|
67
|
+
answer: parsed.answer,
|
|
68
|
+
excerpt: parsed.excerpt,
|
|
69
|
+
excerptCheck,
|
|
70
|
+
excerptVerified: excerptCheck?.verified
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -4,7 +4,7 @@ import { ensureIndexed as defaultEnsureIndexed } from './docs-index.js';
|
|
|
4
4
|
import { resolvePackage as defaultResolvePackage } from './docs-resolve.js';
|
|
5
5
|
import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
|
|
6
6
|
import { npmVersionLookup as defaultNpmVersionLookup } from './npm-version.js';
|
|
7
|
-
import {
|
|
7
|
+
import type { SpawnFn } from '../shared/child-process.js';
|
|
8
8
|
/**
|
|
9
9
|
* Pull `@see {@link https://…}` pointers out of retrieved .d.ts/README text.
|
|
10
10
|
*
|
|
@@ -5,17 +5,13 @@ import { openCache as defaultOpenCache } from './docs-cache.js';
|
|
|
5
5
|
import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
|
|
6
6
|
import { docsRaw, formatResultText, buildPrompt, buildVersionBanner } from './docs-core.js';
|
|
7
7
|
import { formatNpmVersionSection } from './npm-version.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { parseChildOutput, isExcerptInContent } from '../shared/child-output.js';
|
|
11
|
-
import { getPiInvocation } from '../shared/pi-invocation.js';
|
|
12
|
-
import { formatChildFailure, makeWorkerTool } from './shared.js';
|
|
8
|
+
import { runFocusedExtraction } from './focused-extractor.js';
|
|
9
|
+
import { makeWorkerTool } from './shared.js';
|
|
13
10
|
import { isTypeOnlyAnswer } from '../task/type-only-answer.js';
|
|
14
11
|
import { logDocsAnswer } from './typeonly-log.js';
|
|
15
12
|
import { normalizeQuery } from './research-cache.js';
|
|
16
13
|
import { projectDocsRaw, buildProjectPrompt } from './docs-project.js';
|
|
17
14
|
import { projectDocsBudget, projectDocsBudgetExhausted } from '../task/research-fanout-budget.js';
|
|
18
|
-
const childArgs = () => [...childBaseArgs(), '--no-tools'];
|
|
19
15
|
const RENDER_QUERY_MAX = 100;
|
|
20
16
|
const Params = Type.Object({
|
|
21
17
|
module: Type.String({
|
|
@@ -57,6 +53,25 @@ export function packageRootOf(module) {
|
|
|
57
53
|
function pinDetails(pin) {
|
|
58
54
|
return pin ? { versionSource: pin.source, declaredRange: pin.range } : {};
|
|
59
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The tool result for a focused-extraction child that failed, shared by both docs paths —
|
|
58
|
+
* the last chunk they still held in common after the extractor seam took the child running.
|
|
59
|
+
* The paths differ only in `prefix`: the npm path leads every result, failures included, with
|
|
60
|
+
* its version banner and npm-version header; the project path has neither.
|
|
61
|
+
*
|
|
62
|
+
* `childExitCode` is recorded but NOT as 0, which is what keeps a failure out of the research
|
|
63
|
+
* cache (see `cacheable` below).
|
|
64
|
+
*/
|
|
65
|
+
function docsFailureResult(extraction, baseDetails, prefix) {
|
|
66
|
+
return {
|
|
67
|
+
text: prefix + extraction.failure,
|
|
68
|
+
details: {
|
|
69
|
+
...baseDetails,
|
|
70
|
+
...(extraction.aborted ? { aborted: true } : {}),
|
|
71
|
+
childExitCode: extraction.exitCode
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
60
75
|
export function registerPiWorkerDocs(pi, internals = {}) {
|
|
61
76
|
// CAP arm of nexttask 5B — OFF unless PI_TASK_PROJECT_DOCS_BUDGET is set, and
|
|
62
77
|
// then per-ATTEMPT by construction: the extension is loaded into a fresh pi
|
|
@@ -110,6 +125,18 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
110
125
|
// It was DEAD in production (pi runs under node) and BYPASSED under bun test
|
|
111
126
|
// (internals.spawn is always injected), i.e. untested, unreachable, and wrong.
|
|
112
127
|
const spawn = internals.spawn ?? defaultSpawn;
|
|
128
|
+
// Both paths below run the SAME extraction against this call's cwd/signal/spawn
|
|
129
|
+
// and verify the citation against exactly the content they prompted with; only
|
|
130
|
+
// the prompt body and the abort wording differ. (fetch is the site that verifies
|
|
131
|
+
// against a superset — see FocusedRequest.verifyAgainst.)
|
|
132
|
+
const extract = (prompt, content, abortedMessage) => runFocusedExtraction({
|
|
133
|
+
prompt,
|
|
134
|
+
verifyAgainst: content,
|
|
135
|
+
cwd: ctx.cwd,
|
|
136
|
+
signal,
|
|
137
|
+
spawn,
|
|
138
|
+
abortedMessage
|
|
139
|
+
});
|
|
113
140
|
// ── Project source lookup ───────────────────────────────────────
|
|
114
141
|
if (params.module === '.') {
|
|
115
142
|
const budget = projectDocsBudget();
|
|
@@ -158,29 +185,17 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
158
185
|
indexingMs
|
|
159
186
|
};
|
|
160
187
|
const concatenated = chunks.map(c => c.content).join('\n\n');
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
if (failure !== null) {
|
|
166
|
-
return {
|
|
167
|
-
text: failure,
|
|
168
|
-
details: {
|
|
169
|
-
...baseDetails,
|
|
170
|
-
...(child.aborted ? { aborted: true } : {}),
|
|
171
|
-
childExitCode: child.exitCode
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
const parsed = parseChildOutput(child.stdout);
|
|
176
|
-
const verified = parsed.excerpt ? isExcerptInContent(parsed.excerpt, concatenated) : undefined;
|
|
188
|
+
const extraction = await extract(buildProjectPrompt(projectName, params.query, concatenated), concatenated, 'Project docs lookup aborted.');
|
|
189
|
+
if (!extraction.ok)
|
|
190
|
+
return docsFailureResult(extraction, baseDetails, '');
|
|
191
|
+
const verified = extraction.excerptVerified;
|
|
177
192
|
const text = formatResultText({
|
|
178
193
|
name: projectName,
|
|
179
194
|
version: 'local',
|
|
180
195
|
root: ctx.cwd,
|
|
181
196
|
entryDts: null,
|
|
182
197
|
readme: null
|
|
183
|
-
},
|
|
198
|
+
}, extraction, verified);
|
|
184
199
|
// SAME instrumentation channel as the package path below, extended to the
|
|
185
200
|
// project-source branch because that branch is the MAJORITY of what
|
|
186
201
|
// worker:apis asks — 13 of 17 docs calls in run 15's fatal task, 7 of 12 in
|
|
@@ -196,10 +211,11 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
196
211
|
logDocsAnswer({
|
|
197
212
|
module: params.module,
|
|
198
213
|
query: params.query,
|
|
199
|
-
answer:
|
|
214
|
+
answer: extraction.answer,
|
|
200
215
|
typeOnly: false,
|
|
201
216
|
reason: 'project-source lookup — the type-only detector is not applied here',
|
|
202
217
|
excerptVerified: verified,
|
|
218
|
+
excerptCheck: extraction.excerptCheck,
|
|
203
219
|
toolText: text
|
|
204
220
|
});
|
|
205
221
|
return {
|
|
@@ -281,23 +297,12 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
281
297
|
...npmDetails
|
|
282
298
|
};
|
|
283
299
|
const concatenated = chunks.map(c => c.content).join('\n\n');
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const failure = formatChildFailure(child, 'Docs lookup aborted.');
|
|
288
|
-
if (failure !== null) {
|
|
289
|
-
return {
|
|
290
|
-
text: versionBanner + npmHeader + failure,
|
|
291
|
-
details: {
|
|
292
|
-
...baseDetails,
|
|
293
|
-
...(child.aborted ? { aborted: true } : {}),
|
|
294
|
-
childExitCode: child.exitCode
|
|
295
|
-
}
|
|
296
|
-
};
|
|
300
|
+
const extraction = await extract(buildPrompt(pkg, params.query, concatenated), concatenated, 'Docs lookup aborted.');
|
|
301
|
+
if (!extraction.ok) {
|
|
302
|
+
return docsFailureResult(extraction, baseDetails, versionBanner + npmHeader);
|
|
297
303
|
}
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
const body = formatResultText(pkg, parsed, verified);
|
|
304
|
+
const verified = extraction.excerptVerified;
|
|
305
|
+
const body = formatResultText(pkg, extraction, verified);
|
|
301
306
|
// F-2: a TYPE-ONLY answer is the dangerous failure. "unclear from this package"
|
|
302
307
|
// is honest and already escalates; a signature is a well-formed, confident,
|
|
303
308
|
// on-topic answer that names the very parameter asked about, so the worker
|
|
@@ -310,7 +315,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
310
315
|
// hono.dev present in run-15 cache values ONLY inside these JSDoc links, never
|
|
311
316
|
// fetched. Prompting the escalation beats performing it here: this tool runs in
|
|
312
317
|
// parallel execution mode and cannot cleanly spawn a fetch of its own.
|
|
313
|
-
const typeOnly = isTypeOnlyAnswer(
|
|
318
|
+
const typeOnly = isTypeOnlyAnswer(extraction.answer, params.query);
|
|
314
319
|
let text = versionBanner + npmHeader + body;
|
|
315
320
|
if (typeOnly.typeOnly) {
|
|
316
321
|
const seeUrls = extractSeeUrls(concatenated);
|
|
@@ -344,10 +349,11 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
344
349
|
logDocsAnswer({
|
|
345
350
|
module: params.module,
|
|
346
351
|
query: params.query,
|
|
347
|
-
answer:
|
|
352
|
+
answer: extraction.answer,
|
|
348
353
|
typeOnly: typeOnly.typeOnly,
|
|
349
354
|
reason: typeOnly.reason,
|
|
350
355
|
excerptVerified: verified,
|
|
356
|
+
excerptCheck: extraction.excerptCheck,
|
|
351
357
|
toolText: text
|
|
352
358
|
});
|
|
353
359
|
return {
|
|
@@ -2,7 +2,7 @@ import { Type } from '@sinclair/typebox';
|
|
|
2
2
|
import { Text } from '@earendil-works/pi-tui';
|
|
3
3
|
import { FetchAndCleanError } from './html-clean.js';
|
|
4
4
|
import { fetchFocused, formatResultText } from './fetch-core.js';
|
|
5
|
-
import {
|
|
5
|
+
import { makeWorkerTool } from './shared.js';
|
|
6
6
|
import { normalizeQuery } from './research-cache.js';
|
|
7
7
|
const RENDER_QUERY_MAX = 100;
|
|
8
8
|
const Params = Type.Object({
|
|
@@ -44,13 +44,11 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
44
44
|
fetchAndClean: internals.fetchAndClean,
|
|
45
45
|
spawn: internals.spawn
|
|
46
46
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (failure !== null) {
|
|
53
|
-
return { text: failure, details: { childExitCode: result.childExitCode } };
|
|
47
|
+
// Child failure is decided and formatted once, inside the focused extractor
|
|
48
|
+
// (workers/focused-extractor.ts) — this used to re-map the result back into a
|
|
49
|
+
// ChildOutcome just to ask formatChildFailure the same question.
|
|
50
|
+
if (result.failure !== undefined) {
|
|
51
|
+
return { text: result.failure, details: { childExitCode: result.childExitCode } };
|
|
54
52
|
}
|
|
55
53
|
const body = formatResultText({ answer: result.answer, excerpt: result.excerpt }, result.excerptVerified) || '(no output)';
|
|
56
54
|
// The coverage miss is the one outcome that carries an instruction. It goes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExcerptVerification } from '../shared/child-output.js';
|
|
1
2
|
/** Env var naming the JSONL sink. Unset (or empty) ⇒ instrumentation is entirely off. */
|
|
2
3
|
export declare const TYPEONLY_LOG_ENV = "PI_TASK_TYPEONLY_LOG";
|
|
3
4
|
/** One pi-worker-docs answer, as observed at the tool layer. */
|
|
@@ -18,6 +19,18 @@ export interface TypeOnlyLogRecord {
|
|
|
18
19
|
unclear: boolean;
|
|
19
20
|
/** child-output's excerpt check; undefined when the child cited no excerpt. */
|
|
20
21
|
excerptVerified?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The FULL verification record behind `excerptVerified` — the normalised excerpt that was
|
|
24
|
+
* searched for, plus a sha256 + length of the normalised content it was searched in.
|
|
25
|
+
*
|
|
26
|
+
* This is the other half of the F-3(f) hole described above: retaining the answer text
|
|
27
|
+
* says WHAT was claimed, and this says what it was checked against, so a false verdict can
|
|
28
|
+
* be attributed to fabrication (the excerpt is nowhere near the content) rather than a
|
|
29
|
+
* normaliser gap (a markdown-escape variant of text that IS present) without re-running
|
|
30
|
+
* the lookup. Only fetch's extractor used to keep it; all four focused-extractor call
|
|
31
|
+
* sites now can (workers/focused-extractor.ts). Optional — records predating it parse.
|
|
32
|
+
*/
|
|
33
|
+
excerptCheck?: ExcerptVerification;
|
|
21
34
|
/**
|
|
22
35
|
* The tool's ENTIRE return text — version banner, npm header, the answer prose, the cited
|
|
23
36
|
* excerpt, and (when it fires) the type-only banner. Optional so logs written before this
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|