@mjasnikovs/pi-task 0.18.48 → 0.18.50

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/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![npm](https://img.shields.io/npm/v/@mjasnikovs/pi-task?color=cb3837&logo=npm)](https://www.npmjs.com/package/@mjasnikovs/pi-task)
10
10
  [![license](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](./LICENSE)
11
11
  [![pi extension](https://img.shields.io/badge/pi-extension-7c3aed)](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
12
- [![tests](https://img.shields.io/badge/tests-1936%20passing-3fb950)](#development)
12
+ [![tests](https://img.shields.io/badge/tests-2077%20passing-3fb950)](#development)
13
13
  [![types](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](./tsconfig.json)
14
14
 
15
15
  </div>
@@ -65,7 +65,7 @@ A whole plan — `/task-auto` splits it into an ordered task list and runs each
65
65
  | `/task-auto <feature>` | Plan a feature into a task list and run each title through `/task` in order (resumable). |
66
66
  | `/task-auto-resume [--unattended]` | Resume the active `/task-auto` run at the next unfinished task. `--unattended` is the boot-hook form: in-flight runs only. |
67
67
  | `/task-auto-cancel` | Stop the `/task-auto` loop after the current task (still resumable). |
68
- | `/task-config` | Toggle pi-task settings in an editor dialog: remote server, compress reasoning, auto-commit, orientation, verify work, enforce guidelines, command timeout, stream watchdog, and the extension whitelist for child sessions. |
68
+ | `/task-config` | Toggle pi-task settings in an editor dialog: remote control, compress thinking, auto-commit, verify work, enforce guidelines, project tour, command timeout, stuck reply retry, and one `ext:` toggle per installed host extension. |
69
69
  | `/remote` | Show the QR code & URLs for the web view (`/remote stop` to stop). Answer grill questions, start tasks, and watch progress from your phone. |
70
70
 
71
71
  ## The pipeline
@@ -171,25 +171,25 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
171
171
 
172
172
  | Setting | Default | What it does |
173
173
  | --- | --- | --- |
174
- | **remote** | on | The remote UI server (QR code, phone access). Turn off to never start it. |
175
- | **compress reasoning** | on | After each message, compresses the model's `<think>` blocks down to the decisions/constraints/facts that matter later — keeping long local-model runs from drowning their own context in self-talk. |
174
+ | **remote control** | on | The remote UI server (QR code, phone access). Turn off to never start it. |
175
+ | **compress thinking** | on | After each message, compresses the model's `<think>` blocks down to the decisions/constraints/facts that matter later — keeping long local-model runs from drowning their own context in self-talk. |
176
176
  | **auto-commit** | on | Snapshots the working tree into one git commit per `/task-auto` sub-task (see above). |
177
- | **orientation** | on | Pre-reads the project's core files (manifest, config, domain types, schema, entrypoints, API surface) once and hands the contents to the read-heavy research workers, so they skip re-discovering the same files cold. Bounded by a hard byte budget; applied only where it helps (FILES/APIS workers). |
178
177
  | **verify work** | on | After each `/task` (and `/task-auto` task) implements — but **before** it's checked off or committed — actually **runs** the spec's own `VERIFY` block in the real workspace. pi-task otherwise only _authors_ a VERIFY block and never executes it, so a task that doesn't build is indistinguishable from one that works. A fresh `read` + `bash` child of the same local model runs the declared check, observes the real output, and reports **PASS/FAIL** (a legitimately no-op VERIFY is a PASS). On FAIL the run doesn't dead-stop: you get a boxed picker — **Autofix** (re-run the implementation turn against the failure, then re-verify; no attempt cap) or **Accept** (override a misjudged artifact) — and dismissing it pauses the run, resumable. A genuine clean pass is also the behavioral signal that lets **enforce guidelines** fix in place (see below). |
179
178
  | **enforce guidelines** | on | After each `/task` (and `/task-auto` task) is committed, re-checks that commit's work against the project's `AGENTS.md` / `CLAUDE.md` (in the working directory). A bare fix-in-place pass trashes working code (A/B-proven), so enforcement is gated on the **verify work** signal. **With** a genuine verify pass: a fresh `read` + `edit` child of the same local model reads the **last commit's** diff and fixes violations in place; its fixes are committed **separately** as an `ENFORCE GUIDELINES` commit, then the verify signal is re-run against the enforced tree — a regression **reverts** the enforce commit and keeps the verified work. **Without** that signal (verify off, no spec, or an accept-override): the pass runs read-only and only **reports** violations, never rewrites logic. Either way a violation it can't clear (or a pass that can't run) only **warns** — the task commit already landed, so the run continues. Skipped when nothing was committed for the task. |
179
+ | **project tour** | on | Pre-reads the project's core files (manifest, config, domain types, schema, entrypoints, API surface) once and hands the contents to the read-heavy research workers, so they skip re-discovering the same files cold. Bounded by a hard byte budget; applied only where it helps (FILES/APIS workers). |
180
180
  | **parallel research** | off | Run the four research workers concurrently instead of one at a time. Leave off on a single-GPU local backend (concurrent streams split the GPU and slow each other down); turn on only for a parallel-capable model server. |
181
181
  | **research cache** | on | Cache docs/search/fetch worker results for the duration of one `/task-auto` run so sibling tasks re-asking the same package/URL + query reuse the first pipeline's digest instead of re-fetching. Per-run isolated, external-only (project-source `.` lookups excluded), success-only. |
182
- | **search provider** | Exa | Engine behind `pi-worker-search` and freshness/enrichment checks. **Exa** (default) and **DuckDuckGo** need no API key; **Brave** requires `BRAVE_SEARCH_API_KEY`. |
182
+ | **search engine** | Exa | Engine behind `pi-worker-search` and freshness/enrichment checks. **Exa** (default) and **DuckDuckGo** need no API key; **Brave** requires `BRAVE_SEARCH_API_KEY`. |
183
183
  | **command timeout** | 15 min | Wall-clock ceiling on a **single** tool execution. Local models routinely run a command that never returns (a hung build, a dev server, a check with no timeout) and the run wedges until you abort by hand — pi's bash tool has an optional timeout with no default, so this is the missing one. One knob, two surfaces: in the main session the overrun call is cancelled (killing the tool's whole process tree) plus a reminder turn; in the verify/fix gate children the child is killed and re-spawned with a hint, halving the ceiling on repeat hangs. Choices: 5/10/15/30 min or **off** — off unguards both surfaces, gates included. |
184
- | **stream watchdog** | 10 min | Inactivity ceiling on the **model stream**. A hung or silently-dropped stream throws nothing at all, so neither the connection-error retry (it needs a reported error) nor the **command timeout** (tool calls only) nor the dead-backend stall guard (a reachable endpoint reads as proof of life) can see it — an mx5 run lost ~2.9h to three of them while the model server stayed healthy. Measured as time since the **last stream event of any kind**, so a slow model emitting one token every 30s is never touched, and it pauses while a tool runs. On expiry the main session aborts the turn (through the same channel the command watchdog uses) and posts a resume reminder; a child is killed and routed into the existing connection-error retry. Choices: 5/10/20/30 min or **off**. Keep it generous on local backends — prompt processing on a large context legitimately emits nothing for minutes. |
184
+ | **stuck reply retry** | 10 min | Inactivity ceiling on the **model stream**. A hung or silently-dropped stream throws nothing at all, so neither the connection-error retry (it needs a reported error) nor the **command timeout** (tool calls only) nor the dead-backend stall guard (a reachable endpoint reads as proof of life) can see it — an mx5 run lost ~2.9h to three of them while the model server stayed healthy. Measured as time since the **last stream event of any kind**, so a slow model emitting one token every 30s is never touched, and it pauses while a tool runs. On expiry the main session aborts the turn (through the same channel the command watchdog uses) and posts a resume reminder; a child is killed and routed into the existing connection-error retry. Choices: 5/10/20/30 min or **off**. Keep it generous on local backends — prompt processing on a large context legitimately emits nothing for minutes. |
185
185
  | **yolo mode** | off | **Unattended runs.** Wherever pi-task would stop and ask, it takes the option already marked RECOMMENDED, stamps the artifact `(YOLO)` so an audit can tell a machine decided, and shows no prompt at all — clarify/grill answers, the verify-FAIL picker (auto-**Accept**, recorded as a yolo debt), and the final-gate picker (autofix while the budget lasts, then leave the run FAILED). A question with no recommendation is **skipped**, never invented. For throwaway/test projects nobody is watching; a real run should decide these itself. |
186
- | **extension whitelist** | empty | Host `pi` extensions to load into every child session by explicit path. Children otherwise run with extensions off, so a provider registered by an extension (e.g. `pi-lmstudio`) doesn't exist in them and they can't resolve the default model. `/task-config` enumerates the currently installed extensions as individual `ext: …` toggles; the list is strictly additive (discovery stays off), and an entry whose file is gone is skipped at spawn time, never fatal. |
186
+ | **ext: …** | all off | One toggle per installed host `pi` extension, loading it into every child session by explicit path. Children otherwise run with extensions off, so a provider registered by an extension (e.g. `pi-lmstudio`) doesn't exist in them and they can't resolve the default model. Children also inherit the extension's tools and hooks, so only enable ones you trust. The list is strictly additive (discovery stays off), and an entry whose file is gone is skipped at spawn time, never fatal. |
187
187
 
188
188
  ## Configuration
189
189
 
190
190
  | Variable | Used by | Notes |
191
191
  | --- | --- | --- |
192
- | `BRAVE_SEARCH_API_KEY` / `BRAVE_API_KEY` | `pi-worker-search`, research enrichment | Required only when the **Brave** search provider is selected in `/task-config`. |
192
+ | `BRAVE_SEARCH_API_KEY` / `BRAVE_API_KEY` | `pi-worker-search`, research enrichment | Required only when the **Brave** search engine is selected in `/task-config`. |
193
193
  | `XDG_CACHE_HOME` | `pi-worker-docs` | Overrides the docs cache location (defaults to `~/.cache`). |
194
194
  | `XDG_DATA_HOME` | remote push | Where the VAPID keypair is stored (defaults to `~/.local/share`). |
195
195
  | `PI_REMOTE_PUSH_SUBJECT` | remote push | VAPID JWT `sub` contact. Defaults to the project URL; set your own `mailto:you@domain.com` or `https://…`. |
@@ -202,7 +202,7 @@ Tasks are persisted to `<cwd>/.pi-tasks/TASK_NNNN.md`. Add `.pi-tasks/` to your
202
202
 
203
203
  ```sh
204
204
  bun install
205
- bun run test # 1939 tests across 121 files
205
+ bun run test # 2078 tests across 129 files
206
206
  bun run lint # prettier + eslint + tsc --noEmit
207
207
  bun run build # tsc → dist/
208
208
  ```
@@ -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;