@mjasnikovs/pi-task 0.18.49 → 0.18.51
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/remote/register.js +4 -2
- package/dist/remote/server.d.ts +14 -0
- package/dist/remote/server.js +61 -18
- package/dist/shared/child-output.d.ts +21 -7
- package/dist/shared/child-output.js +11 -0
- package/dist/task/apis-contract.d.ts +67 -0
- package/dist/task/apis-contract.js +77 -0
- package/dist/task/context-attribution.d.ts +123 -0
- package/dist/task/context-attribution.js +285 -0
- package/dist/task/context-silence.d.ts +53 -0
- package/dist/task/context-silence.js +93 -0
- package/dist/task/phases.js +187 -5
- package/dist/task/prompts.js +13 -0
- package/dist/task/spec-urls.d.ts +117 -0
- package/dist/task/spec-urls.js +214 -0
- package/dist/task/type-only-answer.d.ts +95 -0
- package/dist/task/type-only-answer.js +249 -0
- package/dist/workers/fetch-core.d.ts +50 -0
- package/dist/workers/fetch-core.js +118 -16
- package/dist/workers/pi-worker-core.d.ts +11 -0
- package/dist/workers/pi-worker-core.js +30 -0
- package/dist/workers/pi-worker-docs.d.ts +9 -0
- package/dist/workers/pi-worker-docs.js +119 -7
- package/dist/workers/pi-worker-fetch.js +3 -1
- package/dist/workers/typeonly-log.d.ts +45 -0
- package/dist/workers/typeonly-log.js +88 -0
- package/package.json +1 -1
|
@@ -15,6 +15,29 @@ import { streamStallHint } from '../shared/stream-watchdog.js';
|
|
|
15
15
|
// model starts producing — making waitMs the real queue/cold-start cost and
|
|
16
16
|
// workMs the real generation+tool-call cost.
|
|
17
17
|
const DEFAULT_TOOLS = 'read,grep,find,ls';
|
|
18
|
+
/**
|
|
19
|
+
* Tool calls that can GROUND an APIS claim — i.e. return content a signature or
|
|
20
|
+
* command could be cited from. `pi-worker-docs` (the primary), `read` and `grep`
|
|
21
|
+
* (project source), and the web escalations `pi-worker-search`/`pi-worker-fetch`.
|
|
22
|
+
*
|
|
23
|
+
* `ls` and `find` are deliberately EXCLUDED: they return file/directory NAMES,
|
|
24
|
+
* and APIS owns symbols by name only, never paths (RESEARCH_APIS_PROMPT). Bare
|
|
25
|
+
* enumeration cannot verify a signature, so a worker that fabricates its section
|
|
26
|
+
* from memory does not launder itself grounded by calling `ls` once. That
|
|
27
|
+
* exclusion is the anti-gaming property of any gate built on this count: "one
|
|
28
|
+
* trivial `ls` then fabricate the rest" leaves groundingRetrievalCount at 0.
|
|
29
|
+
*/
|
|
30
|
+
const GROUNDING_RETRIEVAL_TOOLS = new Set([
|
|
31
|
+
'pi-worker-docs',
|
|
32
|
+
'read',
|
|
33
|
+
'grep',
|
|
34
|
+
'pi-worker-search',
|
|
35
|
+
'pi-worker-fetch'
|
|
36
|
+
]);
|
|
37
|
+
/** True when a tool call retrieves content an APIS entry could be grounded in. */
|
|
38
|
+
export function isGroundingRetrieval(toolName) {
|
|
39
|
+
return GROUNDING_RETRIEVAL_TOOLS.has(toolName);
|
|
40
|
+
}
|
|
18
41
|
/**
|
|
19
42
|
* Hard wall-clock bound on a single research worker run (one spawn). The
|
|
20
43
|
* exact-match LoopDetector only catches *identical* repeated tool calls; a model
|
|
@@ -190,6 +213,10 @@ export async function runWorker(input) {
|
|
|
190
213
|
// SIGTERM that kill produces would surface as a bare non-zero exit the
|
|
191
214
|
// caller couldn't distinguish from a crash.
|
|
192
215
|
let loopHit;
|
|
216
|
+
// Reset EACH attempt: on a restart the previous attempt's calls are
|
|
217
|
+
// discarded with its text, so the count must describe only the attempt
|
|
218
|
+
// whose text this call returns.
|
|
219
|
+
let groundingRetrievalCount = 0;
|
|
193
220
|
const timeout = workerTimeout(input.signal, timeoutMs);
|
|
194
221
|
// Per-tool-call watchdog for this attempt (null when off). Its abort is
|
|
195
222
|
// OR'd with the worker timeout / external cancel into the child's signal.
|
|
@@ -214,6 +241,8 @@ export async function runWorker(input) {
|
|
|
214
241
|
onFirstByte: () => (tFirstByte = Date.now()),
|
|
215
242
|
onToolCall: call => {
|
|
216
243
|
cmdWatch?.onStart(call);
|
|
244
|
+
if (isGroundingRetrieval(call.name))
|
|
245
|
+
groundingRetrievalCount++;
|
|
217
246
|
if (!loopDetector)
|
|
218
247
|
return null;
|
|
219
248
|
const hit = loopDetector.record(call);
|
|
@@ -302,6 +331,7 @@ export async function runWorker(input) {
|
|
|
302
331
|
aborted: result.aborted,
|
|
303
332
|
waitMs,
|
|
304
333
|
workMs,
|
|
334
|
+
groundingRetrievalCount,
|
|
305
335
|
...(leaked ? { leakedToolCall: leaked } : {}),
|
|
306
336
|
...(loopHit ? { loopHit } : {}),
|
|
307
337
|
...(timedOut ? { timedOut: true } : {}),
|
|
@@ -5,6 +5,15 @@ 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
7
|
import { type SpawnFn } from '../shared/child-process.js';
|
|
8
|
+
/**
|
|
9
|
+
* Pull `@see {@link https://…}` pointers out of retrieved .d.ts/README text.
|
|
10
|
+
*
|
|
11
|
+
* F-2(d): the answer to a type-only lookup usually is not in the package at all — it lives
|
|
12
|
+
* at the `@see` URL that the very excerpt being returned already carries. In run 15,
|
|
13
|
+
* hono.dev appeared in cache values ONLY inside these JSDoc links, and was never fetched.
|
|
14
|
+
* Surfacing the link is therefore free: the pointer is already in hand.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractSeeUrls(content: string): string[];
|
|
8
17
|
/**
|
|
9
18
|
* The package NAME a module specifier belongs to — `hono/client` → `hono`,
|
|
10
19
|
* `@scope/name/sub` → `@scope/name`. The cache stores this (not the raw specifier) as an
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from 'node:child_process';
|
|
1
2
|
import { Type } from '@sinclair/typebox';
|
|
2
3
|
import { Text } from '@earendil-works/pi-tui';
|
|
3
4
|
import { openCache as defaultOpenCache } from './docs-cache.js';
|
|
@@ -9,6 +10,8 @@ import { childBaseArgs } from '../shared/child-extensions.js';
|
|
|
9
10
|
import { parseChildOutput, isExcerptInContent } from '../shared/child-output.js';
|
|
10
11
|
import { getPiInvocation } from '../shared/pi-invocation.js';
|
|
11
12
|
import { formatChildFailure, makeWorkerTool } from './shared.js';
|
|
13
|
+
import { isTypeOnlyAnswer } from '../task/type-only-answer.js';
|
|
14
|
+
import { logDocsAnswer } from './typeonly-log.js';
|
|
12
15
|
import { normalizeQuery } from './research-cache.js';
|
|
13
16
|
import { projectDocsRaw, buildProjectPrompt } from './docs-project.js';
|
|
14
17
|
const childArgs = () => [...childBaseArgs(), '--no-tools'];
|
|
@@ -21,6 +24,25 @@ const Params = Type.Object({
|
|
|
21
24
|
description: 'What to extract from the docs. The child pi reads ranked chunks and returns ONLY content answering this.'
|
|
22
25
|
})
|
|
23
26
|
});
|
|
27
|
+
/**
|
|
28
|
+
* Pull `@see {@link https://…}` pointers out of retrieved .d.ts/README text.
|
|
29
|
+
*
|
|
30
|
+
* F-2(d): the answer to a type-only lookup usually is not in the package at all — it lives
|
|
31
|
+
* at the `@see` URL that the very excerpt being returned already carries. In run 15,
|
|
32
|
+
* hono.dev appeared in cache values ONLY inside these JSDoc links, and was never fetched.
|
|
33
|
+
* Surfacing the link is therefore free: the pointer is already in hand.
|
|
34
|
+
*/
|
|
35
|
+
export function extractSeeUrls(content) {
|
|
36
|
+
const out = [];
|
|
37
|
+
const re = /@see\s*\{?\s*@?link\s+(https?:\/\/[^}\s)]+)/gi;
|
|
38
|
+
let m;
|
|
39
|
+
while ((m = re.exec(content)) !== null) {
|
|
40
|
+
const url = m[1].replace(/[.,;]+$/, '');
|
|
41
|
+
if (!out.includes(url))
|
|
42
|
+
out.push(url);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
24
46
|
/**
|
|
25
47
|
* The package NAME a module specifier belongs to — `hono/client` → `hono`,
|
|
26
48
|
* `@scope/name/sub` → `@scope/name`. The cache stores this (not the raw specifier) as an
|
|
@@ -72,10 +94,13 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
72
94
|
+ '- You need docs for a specific newer version than what is installed — use pi-worker-fetch on the upstream docs site',
|
|
73
95
|
parameters: Params,
|
|
74
96
|
async run(params, signal, ctx) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
// Always node:child_process spawn (matching fetch-core and every other
|
|
98
|
+
// worker). The former globalThis.Bun branch called Bun.spawn — whose signature
|
|
99
|
+
// is Bun.spawn([cmd, ...args], opts), NOT the node (cmd, args, opts) that
|
|
100
|
+
// runChild/SpawnFn require — so it threw "cmd must be an array" whenever it ran.
|
|
101
|
+
// It was DEAD in production (pi runs under node) and BYPASSED under bun test
|
|
102
|
+
// (internals.spawn is always injected), i.e. untested, unreachable, and wrong.
|
|
103
|
+
const spawn = internals.spawn ?? defaultSpawn;
|
|
79
104
|
// ── Project source lookup ───────────────────────────────────────
|
|
80
105
|
if (params.module === '.') {
|
|
81
106
|
const openCache = internals.openCache ?? defaultOpenCache;
|
|
@@ -138,6 +163,27 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
138
163
|
entryDts: null,
|
|
139
164
|
readme: null
|
|
140
165
|
}, parsed, verified);
|
|
166
|
+
// SAME instrumentation channel as the package path below, extended to the
|
|
167
|
+
// project-source branch because that branch is the MAJORITY of what
|
|
168
|
+
// worker:apis asks — 13 of 17 docs calls in run 15's fatal task, 7 of 12 in
|
|
169
|
+
// the first live diagnostic rep. With only the package path recorded, "the
|
|
170
|
+
// last docs answer before the worker stopped" was unanswerable: the sink's
|
|
171
|
+
// last row was routinely not the worker's last answer.
|
|
172
|
+
//
|
|
173
|
+
// `typeOnly` is recorded FALSE with an explicit reason rather than by running
|
|
174
|
+
// the detector: this path never applies it, and the record must say what the
|
|
175
|
+
// shipped tool decided, not what it would have decided. Inventing a verdict
|
|
176
|
+
// here would let a firing-rate computed off this sink count answers the lever
|
|
177
|
+
// does not reach.
|
|
178
|
+
logDocsAnswer({
|
|
179
|
+
module: params.module,
|
|
180
|
+
query: params.query,
|
|
181
|
+
answer: parsed.answer,
|
|
182
|
+
typeOnly: false,
|
|
183
|
+
reason: 'project-source lookup — the type-only detector is not applied here',
|
|
184
|
+
excerptVerified: verified,
|
|
185
|
+
toolText: text
|
|
186
|
+
});
|
|
141
187
|
return {
|
|
142
188
|
text,
|
|
143
189
|
details: {
|
|
@@ -233,13 +279,66 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
233
279
|
}
|
|
234
280
|
const parsed = parseChildOutput(child.stdout);
|
|
235
281
|
const verified = parsed.excerpt ? isExcerptInContent(parsed.excerpt, concatenated) : undefined;
|
|
236
|
-
const
|
|
282
|
+
const body = formatResultText(pkg, parsed, verified);
|
|
283
|
+
// F-2: a TYPE-ONLY answer is the dangerous failure. "unclear from this package"
|
|
284
|
+
// is honest and already escalates; a signature is a well-formed, confident,
|
|
285
|
+
// on-topic answer that names the very parameter asked about, so the worker
|
|
286
|
+
// stops — and worker:context then fills the semantic gap from memory (F-1).
|
|
287
|
+
// Measured: 14 of 17 live reps terminated on exactly this shape.
|
|
288
|
+
//
|
|
289
|
+
// The retrieved type is KEPT (it is real and useful) and an UNANSWERED banner
|
|
290
|
+
// is prepended, naming the gap and — when the excerpt carries one — the `@see`
|
|
291
|
+
// URL that actually documents the semantics. That pointer is free: F-2(d) found
|
|
292
|
+
// hono.dev present in run-15 cache values ONLY inside these JSDoc links, never
|
|
293
|
+
// fetched. Prompting the escalation beats performing it here: this tool runs in
|
|
294
|
+
// parallel execution mode and cannot cleanly spawn a fetch of its own.
|
|
295
|
+
const typeOnly = isTypeOnlyAnswer(parsed.answer, params.query);
|
|
296
|
+
let text = versionBanner + npmHeader + body;
|
|
297
|
+
if (typeOnly.typeOnly) {
|
|
298
|
+
const seeUrls = extractSeeUrls(concatenated);
|
|
299
|
+
text =
|
|
300
|
+
versionBanner
|
|
301
|
+
+ npmHeader
|
|
302
|
+
+ 'UNANSWERED — TYPE-ONLY: the package gave a declaration, not the '
|
|
303
|
+
+ 'usage semantics this question needs. A signature says what the '
|
|
304
|
+
+ 'parameter IS, not what it MEANS. Do NOT answer from memory and do '
|
|
305
|
+
+ 'NOT treat the type below as the answer.\n'
|
|
306
|
+
+ (seeUrls.length > 0 ?
|
|
307
|
+
`NEXT STEP: the retrieved excerpt itself cites documentation — `
|
|
308
|
+
+ `fetch ${seeUrls[0]} (pi-worker-fetch) and re-ask this same `
|
|
309
|
+
+ `question.\n`
|
|
310
|
+
: 'NEXT STEP: use pi-worker-search / pi-worker-fetch for the official '
|
|
311
|
+
+ 'documentation of this API, then re-ask this same question.\n')
|
|
312
|
+
+ '\nThe declaration that WAS retrieved (context only, not the answer):\n'
|
|
313
|
+
+ body;
|
|
314
|
+
}
|
|
315
|
+
// STAGE 1 INSTRUMENTATION — off unless PI_TASK_TYPEONLY_LOG names a sink, and
|
|
316
|
+
// side-effect only: nothing below reads it, and every failure inside is
|
|
317
|
+
// swallowed. It records EVERY answer, flagged or not, because the open question
|
|
318
|
+
// is a RATE — how often this fires — and a log of firings alone has no
|
|
319
|
+
// denominator. See typeonly-log.ts.
|
|
320
|
+
//
|
|
321
|
+
// It sits AFTER `text` is final (it used to sit above `text`'s first assignment)
|
|
322
|
+
// so the record carries what the worker was actually handed, banner and cited
|
|
323
|
+
// excerpt included, not just the child's prose. Purely a move: logDocsAnswer
|
|
324
|
+
// returns nothing and nothing between the two positions reads it, so the tool's
|
|
325
|
+
// behaviour and its return value are unchanged.
|
|
326
|
+
logDocsAnswer({
|
|
327
|
+
module: params.module,
|
|
328
|
+
query: params.query,
|
|
329
|
+
answer: parsed.answer,
|
|
330
|
+
typeOnly: typeOnly.typeOnly,
|
|
331
|
+
reason: typeOnly.reason,
|
|
332
|
+
excerptVerified: verified,
|
|
333
|
+
toolText: text
|
|
334
|
+
});
|
|
237
335
|
return {
|
|
238
336
|
text,
|
|
239
337
|
details: {
|
|
240
338
|
...baseDetails,
|
|
241
339
|
childExitCode: 0,
|
|
242
|
-
excerptVerified: verified
|
|
340
|
+
excerptVerified: verified,
|
|
341
|
+
...(typeOnly.typeOnly ? { typeOnly: true } : {})
|
|
243
342
|
}
|
|
244
343
|
};
|
|
245
344
|
},
|
|
@@ -268,6 +367,19 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
268
367
|
// Only a completed lookup (child exited 0) is a real answer; not-installed,
|
|
269
368
|
// no-chunks, resolve/cache errors, and aborts omit childExitCode:0 and fall
|
|
270
369
|
// through to a live retry next time.
|
|
271
|
-
|
|
370
|
+
//
|
|
371
|
+
// F-2(e): process health is NOT answer quality. A child that ran fine and answered
|
|
372
|
+
// "unclear from this package" exits 0, so the NON-ANSWER was memoised and re-served
|
|
373
|
+
// as a cache hit to every later sibling task — 52 of run 15's cached entries were
|
|
374
|
+
// "unclear" with hitCache true. One dead end, paid for many times, and escalation
|
|
375
|
+
// could never re-fire because the miss never recurred. So a non-answer is now never
|
|
376
|
+
// stored: the next task that asks pays for a real lookup and can escalate.
|
|
377
|
+
//
|
|
378
|
+
// `text` is supplied by makeWorkerTool (shared.ts) alongside details, so the
|
|
379
|
+
// content check needs no new plumbing.
|
|
380
|
+
cacheable: (d, text) => d.childExitCode === 0
|
|
381
|
+
&& d.typeOnly !== true
|
|
382
|
+
&& d.excerptVerified !== false
|
|
383
|
+
&& !/unclear from this package/i.test(text)
|
|
272
384
|
});
|
|
273
385
|
}
|
|
@@ -59,7 +59,9 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
59
59
|
childExitCode: 0,
|
|
60
60
|
answer: result.answer,
|
|
61
61
|
excerpt: result.excerpt,
|
|
62
|
-
excerptVerified: result.excerptVerified
|
|
62
|
+
excerptVerified: result.excerptVerified,
|
|
63
|
+
coverageMiss: result.coverageMiss,
|
|
64
|
+
anchoredSection: result.anchoredSection
|
|
63
65
|
}
|
|
64
66
|
};
|
|
65
67
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Env var naming the JSONL sink. Unset (or empty) ⇒ instrumentation is entirely off. */
|
|
2
|
+
export declare const TYPEONLY_LOG_ENV = "PI_TASK_TYPEONLY_LOG";
|
|
3
|
+
/** One pi-worker-docs answer, as observed at the tool layer. */
|
|
4
|
+
export interface TypeOnlyLogRecord {
|
|
5
|
+
/** ISO timestamp — lets records be attributed to a rep/phase window after the fact. */
|
|
6
|
+
at: string;
|
|
7
|
+
/** The `module` param: a package specifier, or "." for project source. */
|
|
8
|
+
module: string;
|
|
9
|
+
/** The query verbatim as the worker asked it (NOT the lowercased cache key). */
|
|
10
|
+
query: string;
|
|
11
|
+
/** The child's `<answer>` prose — retained so every counter can be re-scored offline. */
|
|
12
|
+
answer: string;
|
|
13
|
+
/** The shipped detector's verdict for (answer, query). */
|
|
14
|
+
typeOnly: boolean;
|
|
15
|
+
/** The detector's reason string — names which gate decided, for auditing precision. */
|
|
16
|
+
reason: string;
|
|
17
|
+
/** True when the answer is the explicit "unclear from this package" non-answer. */
|
|
18
|
+
unclear: boolean;
|
|
19
|
+
/** child-output's excerpt check; undefined when the child cited no excerpt. */
|
|
20
|
+
excerptVerified?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The tool's ENTIRE return text — version banner, npm header, the answer prose, the cited
|
|
23
|
+
* excerpt, and (when it fires) the type-only banner. Optional so logs written before this
|
|
24
|
+
* field existed still parse.
|
|
25
|
+
*
|
|
26
|
+
* WHY IT IS NOT REDUNDANT WITH `answer`. `answer` is the child's prose only. The worker
|
|
27
|
+
* receives strictly more than that, and the extra part is where the SYMBOL NAMES live: the
|
|
28
|
+
* cited `.d.ts` excerpt. Asking "did the worker write an APIS entry it had actually looked
|
|
29
|
+
* up, or one it produced from memory" is a substring question against what the worker was
|
|
30
|
+
* GIVEN, and answering it off `answer` alone would score a symbol that appeared verbatim in
|
|
31
|
+
* the retrieved declaration as ungrounded. That would inflate the very counter it is meant
|
|
32
|
+
* to measure. Recording the full text makes the grounding test conservative in the
|
|
33
|
+
* direction that cannot manufacture a finding.
|
|
34
|
+
*/
|
|
35
|
+
toolText?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Append one record to the JSONL sink named by `PI_TASK_TYPEONLY_LOG`, if set.
|
|
39
|
+
*
|
|
40
|
+
* @param rec everything but `at` and `unclear`, which are derived here so every call site
|
|
41
|
+
* stamps them identically.
|
|
42
|
+
*/
|
|
43
|
+
export declare function logDocsAnswer(rec: Omit<TypeOnlyLogRecord, 'at' | 'unclear'>, getEnv?: (k: string) => string | undefined): void;
|
|
44
|
+
/** Parse a sink written by {@link logDocsAnswer}; malformed lines are skipped, not thrown. */
|
|
45
|
+
export declare function readTypeOnlyLog(text: string): TypeOnlyLogRecord[];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STAGE 1 INSTRUMENTATION for F-2 / PROMPT 2 — firing-rate observability, no behaviour.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. PROMPT 2's live A/B measured 82% baseline vs 91% treatment, p = 0.88,
|
|
5
|
+
* and was written off as "the lever does not work". It was a broken EXPERIMENT, and it was
|
|
6
|
+
* broken for a reason this module fixes: the metric counted ALL research terminations while
|
|
7
|
+
* the lever touches only TYPE-ONLY answers, and nothing anywhere recorded how often a
|
|
8
|
+
* type-only answer actually occurs. The causal claim "workers stop BECAUSE of type-only
|
|
9
|
+
* answers" was asserted from one static corpus (1 flag in 150 recorded answers, 0.7%) and
|
|
10
|
+
* never measured live. You cannot size an arm, pick a metric, or decide whether the lever is
|
|
11
|
+
* a population fix or a single-case guard without that firing rate.
|
|
12
|
+
*
|
|
13
|
+
* WHAT IT DOES. When `PI_TASK_TYPEONLY_LOG` names a file, every pi-worker-docs answer — not
|
|
14
|
+
* only the flagged ones — appends one JSON line there. Denominator and numerator both, from
|
|
15
|
+
* the same channel, so a rate is computable rather than inferable. With the variable unset
|
|
16
|
+
* this is a no-op and nothing is written.
|
|
17
|
+
*
|
|
18
|
+
* MECHANICAL, NOT SELF-REPORT. The record is written at the TOOL layer from the verdict the
|
|
19
|
+
* shipped detector just returned, next to the same `details` the caller receives. No model is
|
|
20
|
+
* asked whether it thought the answer was a type signature.
|
|
21
|
+
*
|
|
22
|
+
* BEHAVIOUR-NEUTRALITY IS THE WHOLE POINT — this lands in a stage that forbids src/ behaviour
|
|
23
|
+
* change, so:
|
|
24
|
+
* - it is called for its side effect only; nothing reads its return value;
|
|
25
|
+
* - every failure is swallowed (a full disk or an unwritable path must not turn a working
|
|
26
|
+
* docs lookup into an error — instrumentation that can break the thing it measures is
|
|
27
|
+
* worse than no instrumentation);
|
|
28
|
+
* - it appends synchronously, because the process that writes it (a pi child running the
|
|
29
|
+
* docs extension) can exit immediately after the tool returns and a queued async write
|
|
30
|
+
* would be lost.
|
|
31
|
+
*
|
|
32
|
+
* THE FULL ANSWER TEXT IS RETAINED, deliberately. Run 15's audit could not decide F-3(f) —
|
|
33
|
+
* whether an `excerptVerified === false` was fabrication or a normaliser gap — because the
|
|
34
|
+
* text it judged was kept nowhere. The same hole would make every stability question here
|
|
35
|
+
* unanswerable: whether a question is type-only in EVERY rep or churns between identical
|
|
36
|
+
* reps can only be settled by re-scoring the recorded answers. Cheap to keep, impossible to
|
|
37
|
+
* reconstruct later.
|
|
38
|
+
*/
|
|
39
|
+
import * as fs from 'node:fs';
|
|
40
|
+
/** Env var naming the JSONL sink. Unset (or empty) ⇒ instrumentation is entirely off. */
|
|
41
|
+
export const TYPEONLY_LOG_ENV = 'PI_TASK_TYPEONLY_LOG';
|
|
42
|
+
/**
|
|
43
|
+
* The honest non-answer, in BOTH wordings the tool can emit. A package lookup is told to
|
|
44
|
+
* write "unclear from this package" (docs-core.ts:622); a project-source lookup is told
|
|
45
|
+
* "unclear from this project" (docs-project.ts:310). Matching only the first silently scored
|
|
46
|
+
* every project-source abstention as a valid answer — and project-source is the MAJORITY of
|
|
47
|
+
* what worker:apis asks (13 of 17 calls in run 15's fatal task), so that one missing word
|
|
48
|
+
* would have put the wrong denominator under the whole termination diagnostic.
|
|
49
|
+
*/
|
|
50
|
+
const UNCLEAR = /unclear from this (package|project)/i;
|
|
51
|
+
/**
|
|
52
|
+
* Append one record to the JSONL sink named by `PI_TASK_TYPEONLY_LOG`, if set.
|
|
53
|
+
*
|
|
54
|
+
* @param rec everything but `at` and `unclear`, which are derived here so every call site
|
|
55
|
+
* stamps them identically.
|
|
56
|
+
*/
|
|
57
|
+
export function logDocsAnswer(rec, getEnv = k => process.env[k]) {
|
|
58
|
+
const sink = getEnv(TYPEONLY_LOG_ENV);
|
|
59
|
+
if (!sink || sink.trim().length === 0)
|
|
60
|
+
return;
|
|
61
|
+
const full = {
|
|
62
|
+
at: new Date().toISOString(),
|
|
63
|
+
...rec,
|
|
64
|
+
unclear: UNCLEAR.test(rec.answer)
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
fs.appendFileSync(sink, `${JSON.stringify(full)}\n`, 'utf8');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Deliberately silent. This is a measurement side-channel; a docs lookup that
|
|
71
|
+
// succeeded must not be reported as failed because the sink was unwritable.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Parse a sink written by {@link logDocsAnswer}; malformed lines are skipped, not thrown. */
|
|
75
|
+
export function readTypeOnlyLog(text) {
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const line of text.split('\n')) {
|
|
78
|
+
if (line.trim().length === 0)
|
|
79
|
+
continue;
|
|
80
|
+
try {
|
|
81
|
+
out.push(JSON.parse(line));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// A truncated final line is normal when a process is killed mid-append.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.51",
|
|
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",
|