@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
package/dist/remote/register.js
CHANGED
|
@@ -67,7 +67,9 @@ export function registerRemote(pi) {
|
|
|
67
67
|
bridge.currentCtx = makeShimmedCtx(ctx);
|
|
68
68
|
}
|
|
69
69
|
if (getConfig().remote) {
|
|
70
|
-
|
|
70
|
+
// Optional feature: a bind failure must never take pi down. Degrade
|
|
71
|
+
// to a one-line warning and keep the agent running without remote.
|
|
72
|
+
void ensureServer().catch(err => ctx.ui.notify(`Remote UI unavailable: ${err.message}`, 'warning'));
|
|
71
73
|
}
|
|
72
74
|
});
|
|
73
75
|
pi.on('session_shutdown', (event, _ctx) => {
|
|
@@ -158,7 +160,7 @@ export function registerRemote(pi) {
|
|
|
158
160
|
ctx.ui.notify(`Remote running at ${primaryUrl}`, 'info');
|
|
159
161
|
}
|
|
160
162
|
catch (err) {
|
|
161
|
-
ctx.ui.notify(`
|
|
163
|
+
ctx.ui.notify(`Remote UI unavailable: ${err.message}`, 'error');
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
});
|
package/dist/remote/server.d.ts
CHANGED
|
@@ -28,5 +28,19 @@ export declare function getLocalIPs(nets?: NodeJS.Dict<import("node:os").Network
|
|
|
28
28
|
* Tailscale line uses the MagicDNS host when known (resolves to the same node,
|
|
29
29
|
* but is what SSH and webpush certs need), falling back to the raw IP. */
|
|
30
30
|
export declare function formatAddresses(ips: LocalIPs, port: number, tsHost?: string): AddressLine[];
|
|
31
|
+
/** Bind the REAL `server` to the first free port at or above `start`, trying up
|
|
32
|
+
* to `max` consecutive ports. On EADDRINUSE we bump the port and re-listen; any
|
|
33
|
+
* other error (e.g. EACCES), or exhausting the range, REJECTS the promise —
|
|
34
|
+
* never throws uncaught.
|
|
35
|
+
*
|
|
36
|
+
* Binding the real server directly (rather than probing a throwaway socket with
|
|
37
|
+
* createServer()/listen()/close() first, then binding the real one) removes a
|
|
38
|
+
* TOCTOU race: between "probe says port free" and "real listen", the port can be
|
|
39
|
+
* taken by someone else, and on Windows/Bun the PROBE socket's own port isn't
|
|
40
|
+
* fully released before the real listen runs — so the real bind hits EADDRINUSE
|
|
41
|
+
* on the very port that just tested free, and (with no 'error' listener on the
|
|
42
|
+
* real server) escapes as an uncaughtException that crashes pi (issue #7).
|
|
43
|
+
* Retrying the real bind has no probe and no window. */
|
|
44
|
+
export declare function listenWithRetry(server: import('node:http').Server, start: number, max: number): Promise<number>;
|
|
31
45
|
export declare function startServer(onMessage: MessageCallback, getHtml: (wsUrl: string) => string, onInterrupt?: () => void): Promise<ServerHandle>;
|
|
32
46
|
export {};
|
package/dist/remote/server.js
CHANGED
|
@@ -48,27 +48,60 @@ export function formatAddresses(ips, port, tsHost) {
|
|
|
48
48
|
out.push({ label: '', url: `http://${ips.primary}:${port}` });
|
|
49
49
|
return out;
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
/** Bind the REAL `server` to the first free port at or above `start`, trying up
|
|
52
|
+
* to `max` consecutive ports. On EADDRINUSE we bump the port and re-listen; any
|
|
53
|
+
* other error (e.g. EACCES), or exhausting the range, REJECTS the promise —
|
|
54
|
+
* never throws uncaught.
|
|
55
|
+
*
|
|
56
|
+
* Binding the real server directly (rather than probing a throwaway socket with
|
|
57
|
+
* createServer()/listen()/close() first, then binding the real one) removes a
|
|
58
|
+
* TOCTOU race: between "probe says port free" and "real listen", the port can be
|
|
59
|
+
* taken by someone else, and on Windows/Bun the PROBE socket's own port isn't
|
|
60
|
+
* fully released before the real listen runs — so the real bind hits EADDRINUSE
|
|
61
|
+
* on the very port that just tested free, and (with no 'error' listener on the
|
|
62
|
+
* real server) escapes as an uncaughtException that crashes pi (issue #7).
|
|
63
|
+
* Retrying the real bind has no probe and no window. */
|
|
64
|
+
export function listenWithRetry(server, start, max) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
let port = start;
|
|
67
|
+
// Persistent 'listening'/'error' listeners (not one-shot listen(cb)):
|
|
68
|
+
// under Bun, a listen(port, host, cb) callback from a FAILED first bind
|
|
69
|
+
// is NOT carried over to a later listen() retry, so it never fires — the
|
|
70
|
+
// retry silently hangs. Registering both via .on() and re-calling
|
|
71
|
+
// listen(port) with no callback routes each attempt's outcome correctly
|
|
72
|
+
// on both Bun and Node.
|
|
73
|
+
const cleanup = () => {
|
|
74
|
+
server.removeListener('error', onError);
|
|
75
|
+
server.removeListener('listening', onListening);
|
|
76
|
+
};
|
|
77
|
+
const onListening = () => {
|
|
78
|
+
cleanup();
|
|
79
|
+
resolve(port);
|
|
80
|
+
};
|
|
81
|
+
const onError = (err) => {
|
|
82
|
+
if (err.code === 'EADDRINUSE' && port < start + max - 1) {
|
|
83
|
+
port++;
|
|
84
|
+
server.listen(port, '0.0.0.0');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
cleanup();
|
|
88
|
+
reject(err.code === 'EADDRINUSE' ?
|
|
89
|
+
new Error(`No free port found in range ${start}–${start + max - 1}`)
|
|
90
|
+
: err);
|
|
91
|
+
};
|
|
92
|
+
server.on('error', onError);
|
|
93
|
+
server.on('listening', onListening);
|
|
94
|
+
server.listen(port, '0.0.0.0');
|
|
58
95
|
});
|
|
59
96
|
}
|
|
60
|
-
async function findPort(start, max) {
|
|
61
|
-
for (let p = start; p < start + max; p++) {
|
|
62
|
-
if (await tryBind(p))
|
|
63
|
-
return p;
|
|
64
|
-
}
|
|
65
|
-
throw new Error(`No free port found in range ${start}–${start + max - 1}`);
|
|
66
|
-
}
|
|
67
97
|
export async function startServer(onMessage, getHtml, onInterrupt) {
|
|
68
|
-
const port = await findPort(8800, 100);
|
|
69
98
|
const ips = getLocalIPs();
|
|
70
99
|
const ip = ips.primary;
|
|
71
|
-
|
|
100
|
+
// The bound port isn't known until listenWithRetry succeeds, and wsUrl
|
|
101
|
+
// depends on it. The request handler only ever runs once the server is
|
|
102
|
+
// listening (real client I/O, long after we set wsUrl below), so reading it
|
|
103
|
+
// lazily from this closure variable is safe.
|
|
104
|
+
let wsUrl = '';
|
|
72
105
|
const httpServer = createServer((req, res) => {
|
|
73
106
|
if (req.method === 'GET' && (req.url === '/' || req.url === '')) {
|
|
74
107
|
const body = getHtml(wsUrl);
|
|
@@ -108,7 +141,6 @@ export async function startServer(onMessage, getHtml, onInterrupt) {
|
|
|
108
141
|
res.end('Not found');
|
|
109
142
|
}
|
|
110
143
|
});
|
|
111
|
-
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
|
|
112
144
|
// Track every accepted TCP socket so stop() can forcibly destroy lingering
|
|
113
145
|
// keep-alive / WebSocket connections. Without this, httpServer.close() only
|
|
114
146
|
// stops accepting new connections and waits for existing ones to drain — an
|
|
@@ -124,6 +156,18 @@ export async function startServer(onMessage, getHtml, onInterrupt) {
|
|
|
124
156
|
sockets.add(s);
|
|
125
157
|
s.on('close', () => sockets.delete(s));
|
|
126
158
|
});
|
|
159
|
+
// Bind the real server now, retrying past any in-use ports. A bind failure
|
|
160
|
+
// REJECTS (see listenWithRetry) — register.ts's callers catch it and let pi
|
|
161
|
+
// continue without the remote UI; the remote server is optional.
|
|
162
|
+
const port = await listenWithRetry(httpServer, 8800, 100);
|
|
163
|
+
wsUrl = `ws://${ip}:${port}/ws`;
|
|
164
|
+
// Attach the WebSocket server only AFTER the http server is bound. ws adds an
|
|
165
|
+
// 'error' listener to the http server that re-emits on the WebSocketServer
|
|
166
|
+
// (which has no error listener) — so if it were attached during the bind, an
|
|
167
|
+
// EADDRINUSE on the first port would be forwarded to wss and thrown as an
|
|
168
|
+
// uncaughtException, crashing pi even though listenWithRetry handled it. ws
|
|
169
|
+
// works fine on an already-listening server.
|
|
170
|
+
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
|
|
127
171
|
const handle = {
|
|
128
172
|
port,
|
|
129
173
|
ip,
|
|
@@ -177,6 +221,5 @@ export async function startServer(onMessage, getHtml, onInterrupt) {
|
|
|
177
221
|
removeClient(ws);
|
|
178
222
|
});
|
|
179
223
|
});
|
|
180
|
-
await new Promise(resolve => httpServer.listen(port, '0.0.0.0', resolve));
|
|
181
224
|
return handle;
|
|
182
225
|
}
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for parsing and formatting child pi output.
|
|
3
|
-
*
|
|
4
|
-
* Used by both fetch-core (web page extraction) and docs-core (npm package
|
|
5
|
-
* docs extraction). The child pi outputs <answer> and <excerpt> XML tags;
|
|
6
|
-
* these functions parse, verify, and format the result.
|
|
7
|
-
*/
|
|
8
1
|
export declare function parseChildOutput(stdout: string): {
|
|
9
2
|
answer: string;
|
|
10
3
|
excerpt?: string;
|
|
@@ -13,6 +6,27 @@ export declare function normaliseWhitespace(s: string): string;
|
|
|
13
6
|
/** Check whether an excerpt appears verbatim in the source content
|
|
14
7
|
* (whitespace-normalised). Returns false for empty excerpts. */
|
|
15
8
|
export declare function isExcerptInContent(excerpt: string, content: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The same verdict as {@link isExcerptInContent}, PLUS a retained record of what was
|
|
11
|
+
* actually checked — the whitespace-normalised excerpt and a hash+length of the normalised
|
|
12
|
+
* content it was searched in. This is PROMPT-3 item 4: make an `excerptVerified === false`
|
|
13
|
+
* DIAGNOSABLE after the fact, so it can be attributed to fabrication (the excerpt is nowhere
|
|
14
|
+
* near the content) versus a normaliser gap (it is a markdown-escape or entity variant of
|
|
15
|
+
* text that IS present) WITHOUT re-fetching. It deliberately does NOT loosen the verifier:
|
|
16
|
+
* `.verified` is identical to `isExcerptInContent`. F-3(f) — whether the normaliser needs
|
|
17
|
+
* markdown-escape handling — is left unproven on purpose; you decide that from the retained
|
|
18
|
+
* evidence, not by weakening the one working hallucination detector first.
|
|
19
|
+
*/
|
|
20
|
+
export interface ExcerptVerification {
|
|
21
|
+
verified: boolean;
|
|
22
|
+
/** sha256 of the whitespace-normalised content the excerpt was checked against. */
|
|
23
|
+
contentSha256: string;
|
|
24
|
+
/** Length of that normalised content, so a short/empty page is visible at a glance. */
|
|
25
|
+
contentLength: number;
|
|
26
|
+
/** The whitespace-normalised excerpt that was searched for. */
|
|
27
|
+
normalisedExcerpt: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function verifyExcerpt(excerpt: string, content: string): ExcerptVerification;
|
|
16
30
|
/** Format the child's parsed output with a header and optional excerpt block.
|
|
17
31
|
* When `verified === false` a warning is prepended. */
|
|
18
32
|
export declare function formatResultText(header: string, parsed: {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* docs extraction). The child pi outputs <answer> and <excerpt> XML tags;
|
|
6
6
|
* these functions parse, verify, and format the result.
|
|
7
7
|
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
8
9
|
export function parseChildOutput(stdout) {
|
|
9
10
|
const trimmed = stdout.trim();
|
|
10
11
|
const answerMatch = /<answer>([\s\S]*?)<\/answer>/i.exec(trimmed);
|
|
@@ -26,6 +27,16 @@ export function isExcerptInContent(excerpt, content) {
|
|
|
26
27
|
return false;
|
|
27
28
|
return normaliseWhitespace(content).includes(normaliseWhitespace(excerpt));
|
|
28
29
|
}
|
|
30
|
+
export function verifyExcerpt(excerpt, content) {
|
|
31
|
+
const nc = normaliseWhitespace(content);
|
|
32
|
+
const ne = normaliseWhitespace(excerpt);
|
|
33
|
+
return {
|
|
34
|
+
verified: ne.length > 0 && nc.includes(ne),
|
|
35
|
+
contentSha256: createHash('sha256').update(nc).digest('hex'),
|
|
36
|
+
contentLength: nc.length,
|
|
37
|
+
normalisedExcerpt: ne
|
|
38
|
+
};
|
|
39
|
+
}
|
|
29
40
|
/** Format the child's parsed output with a header and optional excerpt block.
|
|
30
41
|
* When `verified === false` a warning is prepended. */
|
|
31
42
|
export function formatResultText(header, parsed, verified) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STAGE 2 LEVER — the APIS OUTPUT CONTRACT.
|
|
3
|
+
*
|
|
4
|
+
* ── WHAT STAGE 1 MEASURED, AND WHY THIS IS THE ONLY VARIABLE LEFT ─────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* worker:apis terminates when every entry on its output list has a SIGNATURE. That is not an
|
|
7
|
+
* inference; it is what 16 live reps say (commit 807ffad, raw data ~/tmp/apis-stopping-point):
|
|
8
|
+
* - it does not stop at a budget — docs calls/rep mean 8.5, sd 3.8, cv 0.45, range 3-14;
|
|
9
|
+
* - it does not stop because it is circling — near-repeats 2/45 = 4.4% over the last three
|
|
10
|
+
* calls, LOWER than the 10.2% whole-trajectory rate;
|
|
11
|
+
* - it does not stop because nothing new is arriving — the FINAL answer of a rep still
|
|
12
|
+
* returns 39 symbols no earlier answer in that rep carried;
|
|
13
|
+
* - it does not stop with holes in its own output — ungrounded symbols 2/588 = 0.3%,
|
|
14
|
+
* strict open-gap entries 2/277 = 0.7%.
|
|
15
|
+
* What it does instead: 95.3% of its lookups reach the emitted section and 72.2% of emitted
|
|
16
|
+
* entries were themselves asked about. The trajectory and the output are the SAME LIST. And
|
|
17
|
+
* the list's format is `<name> <one-line signature or use>` — so of 61 package queries across
|
|
18
|
+
* 15 reps, 52 (85.2%) are signature questions and THREE are behaviour questions.
|
|
19
|
+
*
|
|
20
|
+
* The worker stops because the artifact it was asked for is complete by the standard its
|
|
21
|
+
* format sets, and a signature satisfies that standard. "What does this parameter MEAN" is not
|
|
22
|
+
* a field of the thing it is building, so nothing in its output is ever left unfilled by not
|
|
23
|
+
* asking it. That is why it never escalates: escalation answers a question it has no reason to
|
|
24
|
+
* ask.
|
|
25
|
+
*
|
|
26
|
+
* ── WHY THIS BLOCK AND NOT A THIRD INSTRUCTION ────────────────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* Two levers have already failed against this seam, and they failed for the same reason:
|
|
29
|
+
* PROMPT 2 conditioned on RECOGNISING an answer as inadequate (type-only). Reach 9/1680 =
|
|
30
|
+
* 0.54% of answers. But a type signature is not an inadequate answer to the
|
|
31
|
+
* question actually being asked — it is exactly the requested field.
|
|
32
|
+
* PROMPT 4 conditioned on nothing at all, and pointed the worker at the exact page that
|
|
33
|
+
* would have prevented the fatal bug. 2/20 vs 3/20, Fisher p = 0.50, with delivery
|
|
34
|
+
* of the block into the assembled prompt PROVEN separately. A pointer only helps a
|
|
35
|
+
* worker that has an unmet slot to fill.
|
|
36
|
+
* Both acted on the ANSWER side or the TARGET side. This one adds a FIELD A SIGNATURE CANNOT
|
|
37
|
+
* FILL, which is the variable that is not flat.
|
|
38
|
+
*
|
|
39
|
+
* ── STEP 3 OF THE FALLBACK IS LOAD-BEARING. DO NOT "CLOSE" IT ─────────────────────────────
|
|
40
|
+
*
|
|
41
|
+
* The `UNVERIFIED:` escape is mandatory and is not a loophole. Forbidding abstention is
|
|
42
|
+
* precisely how F-1 manufactured the confident wrong claim that killed run 15: worker:context,
|
|
43
|
+
* holding one true citable fact (the pinned hono version), fused it with an uncheckable one
|
|
44
|
+
* (what `hc`'s base URL means) under a single attribution — `hc<AppType>('/api')` — and every
|
|
45
|
+
* request in the shipped product went to /api/api/… and 404'd. A lever that buys behaviour
|
|
46
|
+
* questions with fabrication is a FAIL, not a win. The A/B asserts it: excerptVerified===false
|
|
47
|
+
* and the ungrounded-symbol rate must not rise.
|
|
48
|
+
*
|
|
49
|
+
* Exported unwired first, wired into RESEARCH_APIS_PROMPT in the same series; the STEP A
|
|
50
|
+
* feasibility probe splices this exact text into a patched dist so the probe and the shipped
|
|
51
|
+
* lever can never drift apart.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* The extra output-contract clause for RESEARCH_APIS_PROMPT.
|
|
55
|
+
*
|
|
56
|
+
* Wording notes, because each of these is answering something measured:
|
|
57
|
+
* - "NOT DONE WHEN THEY HAVE A SIGNATURE" is the whole lever. Stage 1's mechanism is
|
|
58
|
+
* completion-by-format, so the change has to move the completion bar, not add advice.
|
|
59
|
+
* - the worked example is the run-15 fatal case verbatim (`hc(baseUrl: Prefix, …)`), because
|
|
60
|
+
* a rule without an instance of what does NOT satisfy it reads as satisfied by anything.
|
|
61
|
+
* - step 2 says escalation is EXPECTED rather than permitted: bundled .d.ts files genuinely
|
|
62
|
+
* do not carry semantics, and a worker that reads "you may escalate" has been told nothing
|
|
63
|
+
* it did not already have (PROMPT 4 measured what permission alone achieves: nothing).
|
|
64
|
+
* - step 3 is stated as CORRECT and REQUIRED, in those words, so the field cannot be closed
|
|
65
|
+
* by guessing. See the header.
|
|
66
|
+
*/
|
|
67
|
+
export declare const APIS_SEMANTICS_CONTRACT = "THIRD-PARTY PACKAGE ENTRIES ARE NOT DONE WHEN THEY HAVE A SIGNATURE. For every entry whose symbol comes from a third-party npm package \u2014 not this project's own source, not a runtime builtin \u2014 the line carries a SECOND field saying what the thing MEANS in use: what one of its arguments stands for, what it defaults to, what a path/URL/prefix it is handed is relative to, or what its return value actually is. Format:\n <name> <one-line signature or use> \u2014 SEMANTICS: <what it means in use>\n\nA TYPE SIGNATURE IS NOT A SEMANTICS CLAUSE, and restating one in prose does not make it one. `hc(baseUrl: Prefix, options?: ClientRequestOptions)` names the argument and says nothing about whether that argument is an origin, or a mount prefix, or how it is joined to each route path \u2014 which is the fact the implementing agent actually needs, and the one it will otherwise guess wrong. An entry whose SEMANTICS field is missing is UNFINISHED, and your section is not ready to emit while any package entry is unfinished.\n\nHOW TO FILL THAT FIELD \u2014 in this order. Do not skip a step, and do not stop after step 1 because you already hold the declaration:\n 1. ASK `pi-worker-docs` A BEHAVIOUR QUESTION about that package. NOT \"what is X's signature\", NOT \"what types does X export\" \u2014 those return the declaration you already have. Ask what an argument MEANS, what it DEFAULTS to, what it is RELATIVE to, what HAPPENS when it is given a particular value. For example: `pi-worker-docs(\"hono/client\", \"what does the baseUrl argument to hc MEAN \u2014 an origin or a mount prefix \u2014 and how is it joined to each route path?\")`.\n 2. IF THE PACKAGE TEXT DOES NOT ANSWER IT, ESCALATE. Expect this: bundled `.d.ts` declarations frequently carry no semantics at all, because the semantics live in the package's documentation. Call `pi-worker-search` with the question, or `pi-worker-fetch` on a documentation URL \u2014 including any `@see {@link https://\u2026}` link that appeared in the text `pi-worker-docs` just returned to you.\n 3. ONLY IF BOTH FAIL, WRITE THE OPEN QUESTION DOWN, in this exact form:\n <name> <signature> \u2014 SEMANTICS: UNVERIFIED: <the exact question you could not answer>\n THIS IS A CORRECT AND REQUIRED OUTCOME, not a failure. A named open question is worth far more to the implementing agent than a confident guess, and it is the only acceptable way to finish an entry you could not verify. NEVER fill this field from memory, from what the symbol is named, or from what the API \"obviously\" does: a plausible wrong semantics clause is the single most damaging thing this section can carry.";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STAGE 2 LEVER — the APIS OUTPUT CONTRACT.
|
|
3
|
+
*
|
|
4
|
+
* ── WHAT STAGE 1 MEASURED, AND WHY THIS IS THE ONLY VARIABLE LEFT ─────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* worker:apis terminates when every entry on its output list has a SIGNATURE. That is not an
|
|
7
|
+
* inference; it is what 16 live reps say (commit 807ffad, raw data ~/tmp/apis-stopping-point):
|
|
8
|
+
* - it does not stop at a budget — docs calls/rep mean 8.5, sd 3.8, cv 0.45, range 3-14;
|
|
9
|
+
* - it does not stop because it is circling — near-repeats 2/45 = 4.4% over the last three
|
|
10
|
+
* calls, LOWER than the 10.2% whole-trajectory rate;
|
|
11
|
+
* - it does not stop because nothing new is arriving — the FINAL answer of a rep still
|
|
12
|
+
* returns 39 symbols no earlier answer in that rep carried;
|
|
13
|
+
* - it does not stop with holes in its own output — ungrounded symbols 2/588 = 0.3%,
|
|
14
|
+
* strict open-gap entries 2/277 = 0.7%.
|
|
15
|
+
* What it does instead: 95.3% of its lookups reach the emitted section and 72.2% of emitted
|
|
16
|
+
* entries were themselves asked about. The trajectory and the output are the SAME LIST. And
|
|
17
|
+
* the list's format is `<name> <one-line signature or use>` — so of 61 package queries across
|
|
18
|
+
* 15 reps, 52 (85.2%) are signature questions and THREE are behaviour questions.
|
|
19
|
+
*
|
|
20
|
+
* The worker stops because the artifact it was asked for is complete by the standard its
|
|
21
|
+
* format sets, and a signature satisfies that standard. "What does this parameter MEAN" is not
|
|
22
|
+
* a field of the thing it is building, so nothing in its output is ever left unfilled by not
|
|
23
|
+
* asking it. That is why it never escalates: escalation answers a question it has no reason to
|
|
24
|
+
* ask.
|
|
25
|
+
*
|
|
26
|
+
* ── WHY THIS BLOCK AND NOT A THIRD INSTRUCTION ────────────────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* Two levers have already failed against this seam, and they failed for the same reason:
|
|
29
|
+
* PROMPT 2 conditioned on RECOGNISING an answer as inadequate (type-only). Reach 9/1680 =
|
|
30
|
+
* 0.54% of answers. But a type signature is not an inadequate answer to the
|
|
31
|
+
* question actually being asked — it is exactly the requested field.
|
|
32
|
+
* PROMPT 4 conditioned on nothing at all, and pointed the worker at the exact page that
|
|
33
|
+
* would have prevented the fatal bug. 2/20 vs 3/20, Fisher p = 0.50, with delivery
|
|
34
|
+
* of the block into the assembled prompt PROVEN separately. A pointer only helps a
|
|
35
|
+
* worker that has an unmet slot to fill.
|
|
36
|
+
* Both acted on the ANSWER side or the TARGET side. This one adds a FIELD A SIGNATURE CANNOT
|
|
37
|
+
* FILL, which is the variable that is not flat.
|
|
38
|
+
*
|
|
39
|
+
* ── STEP 3 OF THE FALLBACK IS LOAD-BEARING. DO NOT "CLOSE" IT ─────────────────────────────
|
|
40
|
+
*
|
|
41
|
+
* The `UNVERIFIED:` escape is mandatory and is not a loophole. Forbidding abstention is
|
|
42
|
+
* precisely how F-1 manufactured the confident wrong claim that killed run 15: worker:context,
|
|
43
|
+
* holding one true citable fact (the pinned hono version), fused it with an uncheckable one
|
|
44
|
+
* (what `hc`'s base URL means) under a single attribution — `hc<AppType>('/api')` — and every
|
|
45
|
+
* request in the shipped product went to /api/api/… and 404'd. A lever that buys behaviour
|
|
46
|
+
* questions with fabrication is a FAIL, not a win. The A/B asserts it: excerptVerified===false
|
|
47
|
+
* and the ungrounded-symbol rate must not rise.
|
|
48
|
+
*
|
|
49
|
+
* Exported unwired first, wired into RESEARCH_APIS_PROMPT in the same series; the STEP A
|
|
50
|
+
* feasibility probe splices this exact text into a patched dist so the probe and the shipped
|
|
51
|
+
* lever can never drift apart.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* The extra output-contract clause for RESEARCH_APIS_PROMPT.
|
|
55
|
+
*
|
|
56
|
+
* Wording notes, because each of these is answering something measured:
|
|
57
|
+
* - "NOT DONE WHEN THEY HAVE A SIGNATURE" is the whole lever. Stage 1's mechanism is
|
|
58
|
+
* completion-by-format, so the change has to move the completion bar, not add advice.
|
|
59
|
+
* - the worked example is the run-15 fatal case verbatim (`hc(baseUrl: Prefix, …)`), because
|
|
60
|
+
* a rule without an instance of what does NOT satisfy it reads as satisfied by anything.
|
|
61
|
+
* - step 2 says escalation is EXPECTED rather than permitted: bundled .d.ts files genuinely
|
|
62
|
+
* do not carry semantics, and a worker that reads "you may escalate" has been told nothing
|
|
63
|
+
* it did not already have (PROMPT 4 measured what permission alone achieves: nothing).
|
|
64
|
+
* - step 3 is stated as CORRECT and REQUIRED, in those words, so the field cannot be closed
|
|
65
|
+
* by guessing. See the header.
|
|
66
|
+
*/
|
|
67
|
+
export const APIS_SEMANTICS_CONTRACT = `THIRD-PARTY PACKAGE ENTRIES ARE NOT DONE WHEN THEY HAVE A SIGNATURE. For every entry whose symbol comes from a third-party npm package — not this project's own source, not a runtime builtin — the line carries a SECOND field saying what the thing MEANS in use: what one of its arguments stands for, what it defaults to, what a path/URL/prefix it is handed is relative to, or what its return value actually is. Format:
|
|
68
|
+
<name> <one-line signature or use> — SEMANTICS: <what it means in use>
|
|
69
|
+
|
|
70
|
+
A TYPE SIGNATURE IS NOT A SEMANTICS CLAUSE, and restating one in prose does not make it one. \`hc(baseUrl: Prefix, options?: ClientRequestOptions)\` names the argument and says nothing about whether that argument is an origin, or a mount prefix, or how it is joined to each route path — which is the fact the implementing agent actually needs, and the one it will otherwise guess wrong. An entry whose SEMANTICS field is missing is UNFINISHED, and your section is not ready to emit while any package entry is unfinished.
|
|
71
|
+
|
|
72
|
+
HOW TO FILL THAT FIELD — in this order. Do not skip a step, and do not stop after step 1 because you already hold the declaration:
|
|
73
|
+
1. ASK \`pi-worker-docs\` A BEHAVIOUR QUESTION about that package. NOT "what is X's signature", NOT "what types does X export" — those return the declaration you already have. Ask what an argument MEANS, what it DEFAULTS to, what it is RELATIVE to, what HAPPENS when it is given a particular value. For example: \`pi-worker-docs("hono/client", "what does the baseUrl argument to hc MEAN — an origin or a mount prefix — and how is it joined to each route path?")\`.
|
|
74
|
+
2. IF THE PACKAGE TEXT DOES NOT ANSWER IT, ESCALATE. Expect this: bundled \`.d.ts\` declarations frequently carry no semantics at all, because the semantics live in the package's documentation. Call \`pi-worker-search\` with the question, or \`pi-worker-fetch\` on a documentation URL — including any \`@see {@link https://…}\` link that appeared in the text \`pi-worker-docs\` just returned to you.
|
|
75
|
+
3. ONLY IF BOTH FAIL, WRITE THE OPEN QUESTION DOWN, in this exact form:
|
|
76
|
+
<name> <signature> — SEMANTICS: UNVERIFIED: <the exact question you could not answer>
|
|
77
|
+
THIS IS A CORRECT AND REQUIRED OUTCOME, not a failure. A named open question is worth far more to the implementing agent than a confident guess, and it is the only acceptable way to finish an entry you could not verify. NEVER fill this field from memory, from what the symbol is named, or from what the API "obviously" does: a plausible wrong semantics clause is the single most damaging thing this section can carry.`;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic detector for the F-1 laundering shape: a CONTEXT bullet that asserts
|
|
3
|
+
* external API USAGE SEMANTICS under an attribution cue that no EXTERNAL CONTEXT block
|
|
4
|
+
* can actually support.
|
|
5
|
+
*
|
|
6
|
+
* THE SHAPE, from mx5 run 15 (TASK_0027.md, verbatim):
|
|
7
|
+
*
|
|
8
|
+
* - The `hono` dependency is pinned at `^4.12.31` in package.json, and the external
|
|
9
|
+
* context confirms `hc<AppType>` pattern with base URL `/api` for same-origin
|
|
10
|
+
* relative paths works correctly (per Hono RPC docs LIVE data).
|
|
11
|
+
*
|
|
12
|
+
* worker:context has tools `read,grep` only (phases.ts:623) — it cannot consult any
|
|
13
|
+
* documentation — so the base-URL claim was necessarily from model memory. It shipped
|
|
14
|
+
* into TASK_0027's CONSTRAINTS and ACCEPTANCE, the implementation obeyed it exactly
|
|
15
|
+
* (`hc<AppType>('/api')` plus `api.api.auth.login.$post()`), and every request went to
|
|
16
|
+
* `/api/api/...` ⇒ 404 ⇒ the product's entire API surface was dead.
|
|
17
|
+
*
|
|
18
|
+
* WHY THE OBVIOUS TEST DOES NOT WORK. "Flag a bullet whose package has no EXTERNAL
|
|
19
|
+
* CONTEXT block" misses this case: EXTERNAL CONTEXT *did* carry a `### npm: hono` block.
|
|
20
|
+
* That block contains version numbers and nothing else, so it cannot support a claim
|
|
21
|
+
* about what a base URL MEANS — yet it lends the sentence an air of having been checked.
|
|
22
|
+
* That is the whole mechanism (F-1e): one citable fact, the pinned version, fused in a
|
|
23
|
+
* single sentence with an uncitable one under a shared attribution. The true half
|
|
24
|
+
* launders the false half.
|
|
25
|
+
*
|
|
26
|
+
* So the rule keys on what a block CAN support, mirroring the LIVE-DATA RULE's own
|
|
27
|
+
* taxonomy (RESEARCH_CONTEXT_PROMPT in prompts.ts):
|
|
28
|
+
* ### npm: version numbers only -> cannot source a semantics claim
|
|
29
|
+
* ### docs: retrieved package doc/.d.ts -> CAN source a semantics claim
|
|
30
|
+
* ### url: fetched page content -> CAN source a semantics claim
|
|
31
|
+
* ### service: 3 search-result SNIPPETS -> cannot source a semantics claim
|
|
32
|
+
*
|
|
33
|
+
* The `service` exclusion is not a judgement call — it is the LIVE-DATA RULE's own scope.
|
|
34
|
+
* That rule makes a service block authoritative for "current API surface, deprecation
|
|
35
|
+
* status, and replacement systems", i.e. versions/status/names. A service block is a
|
|
36
|
+
* title + URL + one-line description per result (service-blocks.ts:10); it cannot carry
|
|
37
|
+
* what a parameter MEANS. This matters concretely: TASK_0027's enrichment produced
|
|
38
|
+
* exactly one service block, `### service: Hono RPC client` (extractEnrichTargets on the
|
|
39
|
+
* verbatim refined task yields services=[Hono RPC client], urls=[], packages=[any,api,hc]).
|
|
40
|
+
* Were `service` treated as source-capable for semantics, the subject string "Hono RPC
|
|
41
|
+
* client" would match the package `hono` and the fatal bullet would pass unflagged — the
|
|
42
|
+
* detector would be unable to catch the very defect it exists for.
|
|
43
|
+
*
|
|
44
|
+
* A bullet is FLAGGED iff all three hold:
|
|
45
|
+
* 1. it carries an attribution cue ("per ... LIVE data", "the external context
|
|
46
|
+
* confirms", "docs confirm", "per the official docs", ...);
|
|
47
|
+
* 2. it asserts API usage semantics — how something is called, what a parameter means,
|
|
48
|
+
* what a default is, what behaviour results — as opposed to a version or a status;
|
|
49
|
+
* 3. no `### url:` or `### docs:` block exists for any package the bullet names.
|
|
50
|
+
*
|
|
51
|
+
* Pure and side-effect free; unit-tested in context-attribution.test.ts against the real
|
|
52
|
+
* run-15 bullets, including the three legitimate attributed bullets (TASK_0007, _0012,
|
|
53
|
+
* _0031) that must NOT be flagged.
|
|
54
|
+
*/
|
|
55
|
+
/** A block that actually appears in an EXTERNAL CONTEXT header. */
|
|
56
|
+
export interface ContextBlock {
|
|
57
|
+
kind: 'npm' | 'docs' | 'url' | 'service' | 'freshness-skipped';
|
|
58
|
+
/** The block's subject: a package name, a URL, or a service name. */
|
|
59
|
+
subject: string;
|
|
60
|
+
}
|
|
61
|
+
export interface AttributionFinding {
|
|
62
|
+
bullet: string;
|
|
63
|
+
/** The attribution cue that made this bullet a claim of provenance. */
|
|
64
|
+
cue: string;
|
|
65
|
+
/** The semantics marker that made it an API-behaviour claim rather than a version. */
|
|
66
|
+
semantics: string;
|
|
67
|
+
/** Packages the bullet names that have no source-capable block. */
|
|
68
|
+
unsourced: string[];
|
|
69
|
+
}
|
|
70
|
+
/** One bullet plus the line range it occupies, so a rewrite can be surgical. */
|
|
71
|
+
export interface BulletSpan {
|
|
72
|
+
/** The bullet's text, continuation lines folded in, marker stripped. */
|
|
73
|
+
text: string;
|
|
74
|
+
/** Index of the line carrying the `-`/`*` marker. */
|
|
75
|
+
startLine: number;
|
|
76
|
+
/** Index of the last line belonging to this bullet (inclusive). */
|
|
77
|
+
endLine: number;
|
|
78
|
+
/** Leading whitespace of the marker line, preserved on rewrite. */
|
|
79
|
+
indent: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Split a CONTEXT section into bullets WITH their line ranges. Continuation lines are
|
|
83
|
+
* folded into the bullet above so a hard-wrapped claim is judged as one sentence — which
|
|
84
|
+
* is exactly how the fatal run-15 bullet was written.
|
|
85
|
+
*/
|
|
86
|
+
export declare function splitBulletSpans(context: string): BulletSpan[];
|
|
87
|
+
/** Split a CONTEXT section into its bullets, joining hard-wrapped continuation lines. */
|
|
88
|
+
export declare function splitBullets(context: string): string[];
|
|
89
|
+
/** Parse the `### npm:` / `### docs:` / `### url:` / `### service:` blocks out of an EXTERNAL CONTEXT header. */
|
|
90
|
+
export declare function parseContextBlocks(externalContext: string): ContextBlock[];
|
|
91
|
+
/**
|
|
92
|
+
* Find bullets that assert external API semantics under an attribution no available
|
|
93
|
+
* block can support.
|
|
94
|
+
*
|
|
95
|
+
* @param context the emitted CONTEXT section text
|
|
96
|
+
* @param externalContext the EXTERNAL CONTEXT header actually passed to that worker
|
|
97
|
+
* @param packages dependency names to look for in a bullet (from package.json)
|
|
98
|
+
*/
|
|
99
|
+
export declare function findUnsourcedAttributions(context: string, externalContext: string, packages: string[]): AttributionFinding[];
|
|
100
|
+
/** The result of demoting the flagged bullets out of a CONTEXT section. */
|
|
101
|
+
export interface DemotedContext {
|
|
102
|
+
/** The CONTEXT text with every flagged bullet rewritten as an open question. */
|
|
103
|
+
text: string;
|
|
104
|
+
/** What was demoted, in emission order. Empty means the section was untouched. */
|
|
105
|
+
demoted: AttributionFinding[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Rewrite every bullet that findUnsourcedAttributions flags into an OPEN QUESTION, in
|
|
109
|
+
* place, leaving every other byte of the section alone.
|
|
110
|
+
*
|
|
111
|
+
* DEMOTE, DO NOT DELETE. PROMPT 1 allows either, and its invariant is that neither the
|
|
112
|
+
* bullet count nor the count of legitimately-sourced bullets may collapse — "a worker
|
|
113
|
+
* silenced into saying nothing is a regression, not a fix". Demotion satisfies that
|
|
114
|
+
* mechanically: one flagged bullet becomes exactly one bullet, so the count is invariant,
|
|
115
|
+
* and the observation survives for the grill to ask about instead of reaching compose as
|
|
116
|
+
* fact. The attribution cue is removed, which is what makes the claim stop reading as
|
|
117
|
+
* sourced — and it also makes the rewrite idempotent, since the cue was condition (i).
|
|
118
|
+
*
|
|
119
|
+
* @param context the emitted CONTEXT section text
|
|
120
|
+
* @param externalContext the EXTERNAL CONTEXT header actually passed to that worker
|
|
121
|
+
* @param packages dependency names to look for in a bullet (from package.json)
|
|
122
|
+
*/
|
|
123
|
+
export declare function demoteUnsourcedAttributions(context: string, externalContext: string, packages: string[]): DemotedContext;
|