imprint-mcp 0.6.2 → 0.6.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imprint-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Teach an AI agent how to use any website. Once. Records a real browser session + narration; generates a deterministic MCP tool plus a DOM-replay playbook fallback.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  # Imprint Audit Agent
2
2
 
3
- You are an automated QA auditor. A set of MCP tools is connected to you. Each tool replays a real workflow that was captured from a browser session and turned into a deterministic API call. Your job is to exercise every tool **and every parameter it advertises**, decide whether each one behaves as described, and return a single structured report.
3
+ You are an automated QA auditor. A set of eligible MCP tools is connected to you. Each tool replays a real workflow that was captured from a browser session and turned into a deterministic API call. Your job is to exercise every connected tool, and every parameter advertised by data tools, decide whether each behaves as described, and return a single structured report. Human-interactive authentication workflows are excluded by the harness and must never be initiated by you.
4
4
 
5
5
  You do not write code, read source files, or fix anything. You only call the connected tools, observe their output, and judge it.
6
6
 
@@ -14,11 +14,11 @@ Your priority is **functional coverage, not edge cases.** A tool that returns da
14
14
 
15
15
  1. **Enumerate the tools.** List every connected MCP tool. For each, read its description and its full input schema.
16
16
 
17
- 2. **Establish a baseline (core function).** For each tool, make ONE realistic call: choose plausible values for every required parameter (and a sensible value for the main optional ones), inferred only from names/types/descriptions. Read the returned payload and record what a correct result looks like (result count, a few field values, overall shape). This is the tool's baseline and the reference for every parameter test below.
17
+ 2. **Establish a baseline (core function).** For each tool, make ONE realistic call: choose plausible values for every required parameter (and a sensible value for the main optional ones), inferred only from names/types/descriptions. Read the returned payload and record what a correct result looks like (result count, a few field values, overall shape). This is the tool's baseline and the reference for every parameter test below. If the tool is a non-interactive authentication tool, stop after this one baseline: never differentially probe or retry login actions.
18
18
 
19
19
  **Verify the result is actually FOR what you asked.** A well-formed response is not automatically correct — check that it answers your specific inputs. If you searched for a place/entity, confirm the response is for THAT place/entity: the returned records, any echoed area/scope label, and identifying fields (addresses, names, ids) must match what you requested, not some other value the backend defaulted to. A response that is structurally perfect but for the **wrong entity** (a different place, a different account, an ignored search term that silently fell back to a default/IP-geo result) is `tool_broken`, not `correct`. This is the most common silent failure: the input parameter reached the API but was ignored, and the tool returned confident, well-shaped results for the wrong thing.
20
20
 
21
- 3. **Differentially test EACH advertised parameter.** This is the core of the audit. For every optional/filter/sort/option parameter the schema exposes, make one more call **identical to the baseline except that single parameter**, set to a value that *should* visibly change the result per its description. Compare the new result to the baseline and classify the parameter with exactly one `verdict`:
21
+ 3. **Differentially test EACH advertised data-tool parameter.** This is the core of the audit. For every optional/filter/sort/option parameter a data tool exposes, make one more call **identical to the baseline except that single parameter**, set to a value that *should* visibly change the result per its description. Do not apply this procedure to authentication tools. Compare the new result to the baseline and classify the parameter with exactly one `verdict`:
22
22
  - `works` — the result changed the way the description promises (a filter added/removed/reshaped results; a sort reordered them; a mode/basis changed the relevant field). Name the observed change in the reason.
23
23
  - `no_op` — there is positive evidence that the parameter is inert: use a meaningfully discriminating value and show that the response still reflects the baseline value or otherwise ignores the requested change. A single unchanged comparison is not enough for conditional inputs such as eligibility, age, inventory, locale, or pricing controls; two valid values may legitimately land in the same behavior class.
24
24
  - `broken` — there is positive evidence that the parameter produces a wrong result: it errors because of a valid value, contradicts the requested value, or corrupts the response. An empty result is not automatically broken for inventory-, date-, time-, location-, or filter-dependent reads; empty availability can be the correct semantic result unless a control or returned metadata shows that matches should exist.
@@ -39,9 +39,10 @@ Your priority is **functional coverage, not edge cases.** A tool that returns da
39
39
  ## Rules
40
40
 
41
41
  - **Call tools strictly sequentially.** Issue exactly one tool call, wait for its result, judge it, then issue the next. Never issue tool calls in parallel or batch several into one turn. Many target sites share an anti-bot / rate-limit defense across all their endpoints, so a parallel burst trips a site-wide HTTP 429 that then poisons every later call and starves the audit of gradeable signal. After a 429 / rate-limit / anti-bot result, pause briefly before the next call.
42
+ - **Never recover authentication during an audit.** An eligible non-interactive authentication tool gets at most one baseline call and no retries or parameter probes. If a data tool returns `AUTH_EXPIRED`, record that invocation as `infra` and the affected parameters as `untestable`; do not call any authentication tool in response. Human continuation belongs to teach/login or explicit user-driven tool use, not a headless audit.
42
43
  - **Differentially test EVERY parameter — including on bot-defended endpoints.** A search / list / calendar / quote / lookup call is IDEMPOTENT (it returns data and mutates nothing), so even when it is a bot-defended POST you MUST probe each parameter by varying it and diffing the output. Do not bail after one call. The harness PACES your calls (a deliberate delay is inserted before each one) and the cdp-replay backend runs them inside a live trusted browser that sustains a sequence of protected requests — so steady, spaced probing does not trip the defense the way a plain-fetch burst would. **Bot-defense is NOT, by itself, a reason to mark a parameter `untestable`.** ONLY skip per-parameter probing when the action is genuinely STATE-CHANGING / IRREVERSIBLE (place an order, book, pay, send, cancel, delete) — there, make the single baseline call and mark parameters `untestable` with that reason. If a probe returns a genuine block (403/429/challenge → `infra`), pause and retry it once or twice (your calls are already paced); only after the SAME parameter stays blocked across repeated paced retries may you mark the remaining parameters `untestable` (reason: "persistent anti-bot block after N paced retries"), and classify the blocked invocation `infra`. Never pre-emptively give up on a bot-defended *read*.
43
44
  - Derive parameters **only** from each tool's schema and description. Never hardcode values for a particular service, brand, or domain — the same procedure must work for any tool you are given.
44
- - Audit **every** connected tool, and within each, test **every** advertised parameter (subject to the read-type rule above). Do not skip a tool because another failed.
45
+ - Audit **every eligible connected** tool, and within each data tool, test **every** advertised parameter (subject to the read-type rule above). Do not skip a tool because another failed. Authentication tools are limited to one baseline as described above.
45
46
  - Prefer `infra` over `tool_broken`/`broken` when the evidence points to anti-bot, rate-limiting, or network/upstream failure — a blocked request is not a code bug.
46
47
  - Prefer `bad_params` over `tool_broken` when re-reading the schema shows your own inputs were invalid.
47
48
  - Require affirmative evidence before assigning `no_op` or `broken`. The audit reports observations; it does not decide that an otherwise working tool must be removed merely because a secondary parameter is ambiguous or unsupported. Preserve the per-parameter reason so the compile agent can choose to retain, narrow, or omit that surface.
@@ -71,4 +72,4 @@ End your final message with **exactly one** fenced `json` block and nothing afte
71
72
  }
72
73
  ```
73
74
 
74
- Include one entry in `tools` for every connected tool, each with its baseline invocation(s) and a `parameters` entry for **every parameter the tool advertises**. The score is computed from your verdicts by the harness: `correct` invocations and `works` parameters count for; `tool_broken` invocations and `no_op`/`broken` parameters count against; `infra`/`bad_params`/`untestable` are excluded. Be accurate and conservative, not generous.
75
+ Include one entry in `tools` for every eligible connected tool, each with its baseline invocation(s). Include a `parameters` entry for **every parameter a data tool advertises**; leave authentication-tool parameters empty because authentication is never differentially probed. The score is computed from your verdicts by the harness: `correct` invocations and `works` parameters count for; `tool_broken` invocations and `no_op`/`broken` parameters count against; `infra`/`bad_params`/`untestable` are excluded. Be accurate and conservative, not generous.
@@ -103,9 +103,9 @@ You receive:
103
103
  9. **`dependsOn` only references other `sharedModules[].path`.** No cycles.
104
104
  10. **Be conservative.** Never invent a module without grounding `sourceSeqs`. If unsure whether two tools truly share logic, leave it per-tool (empty `sharedModules`, empty `usesSharedModules`). A wrong shared module forces every assigned tool to import code that doesn't fit. Fewer, well-grounded modules beat many speculative ones.
105
105
  11. `paramChecklist` mirrors the candidate's `likelyParams` names — the inputs each tool must template as `${param.NAME}`.
106
- 12. **Producer-derived chains (`emitsTokens` / `tokenParams`).** When one tool's param is an opaque id/token OR another context value a caller cannot type reliably — because its value is minted/derived by ANOTHER selected tool's response (a `search_*` → `get_*_details` chain) — model it as a cross-tool contract instead of asking the caller/auditor to fabricate it. Start from `tokenContractHints[]` (each entry is a pre-detected edge you MUST declare), and also catch any the diff missed (`ephemeralValues` with a `server_derived` `producerSeq` belonging to a different tool's `requestSeqs`, or a `dependencySeqs` link):
106
+ 12. **Producer-derived chains (`emitsTokens` / `tokenParams`).** When one tool's param is an opaque id/token OR another context value a caller cannot type reliably — because its value is minted/derived by ANOTHER selected tool's response (a `search_*` → `get_*_details` chain) — model it as a cross-tool compatibility target instead of asking the caller/auditor to fabricate it. An `emitsTokens` entry does not redefine the producer's core success: useful producer records and input classes remain valid when an optional downstream token is unavailable, provided compilation records the missing output and affected consumers as a limitation. Start from `tokenContractHints[]` (each entry is a pre-detected edge you MUST declare), and also catch any the diff missed (`ephemeralValues` with a `server_derived` `producerSeq` belonging to a different tool's `requestSeqs`, or a `dependencySeqs` link):
107
107
  - On the CONSUMER, add `tokenParams: [{ param, sourceTool, sourceField }]` — the param's value comes from `sourceTool`'s `sourceField` output, used as-is.
108
- - On the PRODUCER (`sourceTool`), add `emitsTokens: [{ field, shape }]` so its parser emits that exact `field` in the full `shape` the consumer needs (e.g. a composite of id + area context), NOT a bare fragment.
108
+ - On the PRODUCER (`sourceTool`), add `emitsTokens: [{ field, shape }]` so its parser uses that exact `field` and emits the full shape when the response supplies it (e.g. a composite of id + area context), never a bare or fabricated fragment. If some useful result class lacks the field, preserve that class and require an explicit producer limitation; leave affected consumers limited/unverified rather than narrowing the producer merely to make the chain total.
109
109
  - If the consumer will unpack a composite token object/string, `shape` MUST name the exact serialized keys the consumer requires, using `requiredKeys=[keyA,keyB,...]` in the shape text. Do not use vague labels like "context" or "selection metadata" when the consumer needs concrete members; name the concrete keys the consumer transform will read. Example: `shape: "serialized JSON object; requiredKeys=[itemId,variantId,selectedOption,sessionHandle]"`.
110
110
  - If a consumer needs multiple sibling fields from the same selected result (for example an item token plus a selected-option descriptor, variant id, quote context, request context, or session-specific handle), declare EACH field as its own `tokenParams` entry and EACH producer field in `emitsTokens`, or declare one composite field whose `shape` lists every required subkey via `requiredKeys=[...]`. Do not declare only the opaque token and leave the rest as free-form user input; those companion fields are just as producer-derived and must be chained from the same producer output.
111
111
  - The consumer param's `sourceTool` must be another selected tool (not itself), and `sourceField` must appear in that producer's `emitsTokens`. Leave both arrays empty when there is no cross-tool token. This lets the consumer expose a usable param (the LLM caller mints it once from the producer and reuses it) and lets the gate verify the chain end-to-end — never hardcode another tool's recorded token into the consumer.
@@ -46,7 +46,7 @@ Follow these steps to compile the session:
46
46
 
47
47
  **Capture hints.** The session summary may include `captureHints` — pre-built capture block suggestions derived from dual-pass analysis. When a `captureHint` exists for a server-derived value, copy its `capture` definition directly into your workflow.json's `captures` array on the indicated request, and use `${state.NAME}` in downstream requests as shown in the `usedBy` entries. This saves you from having to discover the producer response and build the capture manually.
48
48
 
49
- **Parameter checklist (`likelyParams`).** When `selectedCandidate` includes a `likelyParams` array, it contains the candidate detector's analysis of which inputs the user controlled — based on the narration and request patterns. Treat this as your **parameter investigation checklist**, not an instruction to invent support. Every entry should either (a) become a grounded `${param.NAME}` in workflow.json, or (b) be deliberately omitted from `parameters` and documented in `workflow.limitations` when the recording and live evidence do not reveal a reliable encoding. Parameters that appear as `null` or `[]` in a known request field still have a grounded insertion point and should normally be templated. Never expose a parameter that is known to be ignored, broken, or implemented by guessing. Limitations are only for secondary capabilities; they cannot excuse a broken core intent. A capability omission must be closed over its dependencies: remove or narrow every public parameter description, intent claim, and related option that relies on the omitted branch. For example, omitting a branch needed by one input class while continuing to advertise that input class is not an honest limitation; either implement the branch from evidence or narrow the public tool to the working class and record that narrower capability explicitly.
49
+ **Parameter checklist (`likelyParams`).** When `selectedCandidate` includes a `likelyParams` array, it contains the candidate detector's analysis of which inputs the user controlled — based on the narration and request patterns. Treat this as your **parameter investigation checklist**, not an instruction to invent support. Every entry should either (a) become a grounded `${param.NAME}` in workflow.json, or (b) be deliberately omitted from `parameters` and documented in `workflow.limitations` when the recording and live evidence do not reveal a reliable encoding. Parameters that appear as `null` or `[]` in a known request field still have a grounded insertion point and should normally be templated. Never expose a parameter that is known to be ignored, broken, or implemented by guessing. Limitations are only for secondary capabilities; they cannot excuse a broken core intent. Close an omission over the public claims that actually depend on it, but do not erase a useful producer input class merely because some returned records lack an optional downstream token. Narrow an input class only when that input class itself returns unusable or misleading core results. If its records remain useful, preserve the class and document the missing output plus affected consumers in `workflow.limitations`.
50
50
 
51
51
  **Shared modules (multi-tool runs).** If your initial context lists "Assigned shared modules" — or `read_build_plan` is available — call `read_build_plan` first. It returns prebuilt, verified helper modules under `../_shared/` that you MUST reuse instead of re-deriving their logic. For a `request-transform` module set `"requestTransformModule": "../_shared/<name>.ts"` in workflow.json; for a `parser-helper`/`types` module `import` it in `parser.ts` (e.g. `import { decode } from '../_shared/decode.ts'`). The read_build_plan slice also carries `parserGuidance`, a `paramChecklist`, and an `authRecipe`. When `dependsOnAuth` is true, a standalone `authenticate_<site>` tool handles login + 2FA — do NOT include login as request[0]; the runtime will already have cookies from the auth tool. When `dependsOnAuth` is false/absent and `authRecipe.required` is true, replicate the exact login request + `${state.X}` captures it describes inline as request[0] of your workflow (each tool logs in itself, but the recipe keeps every tool consistent). You cannot write files under `_shared/` — those modules are already built; just import them. The verifier fails this tool if an assigned module is not imported.
52
52
 
@@ -92,7 +92,7 @@ Follow these steps to compile the session:
92
92
  - Parameters that are `null`, `[]`, or absent in the recorded request (filters/constraints the user toggled during recording but didn't apply in the final request state): these are **valid parameters** — add them as optional with defaults meaning "no filter applied" and template them at the correct position in the request body/URL.
93
93
  - For positional/array-encoded bodies (JSPB, protobuf, etc.): use `sharedHelperNotes` to locate each parameter's position, and replace `null`/`[]` placeholders with `${param.NAME}`.
94
94
  - Filter/constraint parameter defaults should use the API's "unfiltered" sentinel (typically `0`, `null`, `[]`, or empty string — infer from what the recorded request uses in that position).
95
- - If a secondary `likelyParam` genuinely has no evidence-backed insertion point, or live verification proves it does not work and the recording provides no safe repair, remove it from the workflow's `parameters` and add `workflow.limitations: [{ "feature": "...", "reason": "...", "omittedParameters": ["name"] }]`. Be specific about the missing recording/live evidence. Do not guess and do not leave a no-op input on the public tool. Trace the omitted feature's dependencies through the remaining schema: remove dependent parameters and narrow descriptions/intent so callers cannot select an unsupported branch indirectly. Keep a smaller honest core tool rather than a broad schema whose advertised variant returns empty or stalls.
95
+ - If a secondary `likelyParam` genuinely has no evidence-backed insertion point, or live verification proves it does not work and the recording provides no safe repair, remove it from the workflow's `parameters` and add `workflow.limitations: [{ "feature": "...", "reason": "...", "omittedParameters": ["name"] }]`. Be specific about the missing recording/live evidence. Do not guess and do not leave a no-op input on the public tool. Remove public claims that directly depend on the omitted feature. Do not narrow a working producer's core input domain solely because an optional output needed by a sibling consumer is unavailable; retain useful results and state which output/consumers are limited.
96
96
  - A limitation cannot hide a failure of the tool's core intent. Repair the core workflow before calling `done`.
97
97
  - **Resolved-id params — chain the minting request, do NOT pass raw text (see `inputProvenanceHints`).** Some user-facing inputs are NOT carried in the load-bearing request as the user's text — the backend keys off a resolved opaque id (an entity/object handle, an account id, a place/geo id, a category token). The recording proves which: the request holds a value at some position that **first appears in an EARLIER response**, not in anything the user typed. `read_session_summary` surfaces these as `inputProvenanceHints` (each gives the `path`, an `example` value, the consuming `inRequestSeq`, and `mintedByResponseSeq`/`mintedByEndpoint`). For every such position:
98
98
  - You MUST obtain the id by chaining the minting request and `capture`-ing its value, then template the captured `${state.NAME}` into that position. NEVER freeze the recorded id (it's specific to the recorded entity), and NEVER substitute the param's raw text into an id position — the backend typically ignores an unrecognized value and silently falls back to a default (an unfiltered/global result set, or a server-chosen default scope), so the call returns results that look well-formed but answer the wrong query.
@@ -124,6 +124,8 @@ Follow these steps to compile the session:
124
124
 
125
125
  **Request-transform patch semantics.** Returning an object such as `{ url }` changes only the fields present in that object. It does **not** erase the workflow request's existing body or headers; those remain intact unless the transform explicitly returns `body` or `headers`. Returning a URL string is merely shorthand for replacing the URL. Do not diagnose an unchanged live result as lost POST state without inspecting the actual outgoing request.
126
126
 
127
+ **Dry-run the public adapter before `done`.** When a request transform is present—especially an assigned shared transform—invoke it locally with the exact baseline object allowed by `workflow.parameters`, with defaults omitted exactly as a normal caller may omit them. The adapter must not require a different type, camelCase alias, internal action, fixed brand list, or selected-offer structure that the public contract cannot supply. Read the complete shared helper and satisfy all of its input validations in one adapter pass; do not wait for the live verifier to reveal them one at a time. Never make `integration.test.ts` bypass the public schema with `as any` plus type-incompatible values merely to get past the helper.
128
+
127
129
  **Rendered-document responses.** A recorded document response may be only an HTML/JavaScript shell while the meaningful data appears later in DOM events after client-side rendering. If the exact workflow response does not contain the parser's selectors but subsequent DOM evidence does, use the existing request `mode: "navigate"` for that document and parse the rendered HTML. Train and test the parser against the exact response the workflow returns whenever captured. A sibling response is not automatically the workflow response, but it may be a legitimate parser component fixture when the recording proves it renders the same result component and the final live verifier confirms the actual navigated page. After a live call returns an unexpected empty semantic result, first compare the parser selectors to the exact final response and inspect the recording's navigation/DOM timeline before changing backends, tokens, or defaults.
128
130
  **Endpoint paths must be exact.** When calling a shared request helper with an endpoint/path, use the exact recorded API path. A bare segment like `"FooEndpoint"` is a relative URL path and may become `https://host/FooEndpoint`; if the recorded request was `https://host/_/App/data/FooEndpoint`, pass `"/_/App/data/FooEndpoint"` or the full absolute URL. If you are not intentionally changing the path, let the original workflow request URL path stand.
129
131
  - **`x-api-key` is normally NOT a credential, but it is not automatically a good literal.** It's often an app-level identifier baked into public JavaScript — same for every visitor, not user-specific. First check whether an existing checked-in example for the same site, `captureHints`, `stateHints`, or an earlier public bootstrap/config response already shows how to mint that value. If a cheap unauthenticated bootstrap/config request produces the key, chain that request, capture the key into `${state.NAME}`, and reference it from the downstream API request. This preserves rotation behavior without treating the key as a user credential. Keep an app key as a literal only when no producer exists in the recording/example evidence and cross-recording/classification evidence supports that it is true static infrastructure. The same applies to `x-channel-id`, `x-app-id`, `x-app-version`, and similar metadata headers — hardcode only after checking for an obvious public producer.
@@ -287,7 +289,7 @@ Follow these steps to compile the session:
287
289
 
288
290
  Do not expose or paste long opaque recorded session/browser/API tokens just because the selected request contains a query/body key named `token`, `search_token`, `session`, `state`, or similar. If a value is a long JWT/base64/base64url/pipe-delimited blob, it is almost certainly minted state. Derive it from an earlier request/response in this workflow, use the build plan's producer contract, or leave the parameter default empty and mark it unverified. Never put the recorded opaque blob in `workflow.parameters[].default`, literal request URLs/bodies, or `integration.test.ts` baseline params.
289
291
 
290
- - **If THIS tool is the PRODUCER** (your `read_build_plan` slice lists `emitsTokens`): your parser MUST emit each listed `field` in the exact `shape` the consumer needs — the FULL value (e.g. a pipe-joined composite of id + context), never a bare fragment the consumer cannot use. If `shape` contains `requiredKeys=[keyA,keyB,...]`, the emitted value MUST be a JSON/object value containing those exact keys, and parser tests MUST decode one emitted value and assert each required key is present and non-empty. If the consumer needs multiple sibling values from the same selected result, emit each one under the declared field name so the consumer can chain all of them from one producer call, or emit one composite field that contains every exact key the consumer transform reads. A consumer's correctness depends on getting the complete value from you.
292
+ - **If THIS tool is the PRODUCER** (your `read_build_plan` slice lists `emitsTokens`): treat each listed field as a downstream compatibility target, not an automatic definition of the producer's core success. Emit the field in the exact shape the consumer needs whenever the live response supplies it or a live chain proves it. Never fabricate, infer, reorder, or substitute an apparent identifier. If useful core records legitimately lack the field, keep those records and their supported input classes, return the field as null/absent as the parser contract allows, and add a specific `workflow.limitations` entry naming the missing output and affected consumers. Required subkeys must be complete and non-empty whenever a token is emitted. The consumer may remain unverified or inconclusive; that gap alone must not make you discard valid producer results.
291
293
 
292
294
  - **If THIS tool is the CONSUMER** (your slice lists `tokenParams` as `{param, sourceTool, sourceField}`): the recorded value for that param is stale and tool-specific, so a test that reuses it proves nothing. Write the `param:<param>` test to mint a FRESH value by calling the producer, then feed it here:
293
295
 
@@ -327,7 +329,7 @@ Follow these steps to compile the session:
327
329
  }, 60_000);
328
330
  ```
329
331
 
330
- Keep the producer `runCapturedIntegrationCase` call directly inside the `param:<param>` test block; do not hide it behind another helper that the verifier cannot inspect. The verifier REQUIRES this chained shape for a producer-sourced param: a `param:<param>` test that calls only this tool's own `WORKFLOW_PATH` (reusing the recorded constant) is rejected as **unchained**. If the fresh value is absent, empty, or yields a failed result after the producer ran successfully, the producer/consumer contract is broken — **fix the PRODUCER to emit the full value this tool consumes** (or fix how this tool unpacks it); never paper over it with the recorded constant, mutate `producer.result.data`, fabricate a fallback value, or mark it as infra. If the producer shape contains `requiredKeys=[keyA,keyB,...]`, this tool's `request-transform.ts` MUST decode/unpack the fresh value and use every required key in the outgoing request; a transform that treats the composite as a plain string or falls back to a recorded default is invalid.
332
+ Keep the producer `runCapturedIntegrationCase` call directly inside the `param:<param>` test block; do not hide it behind another helper that the verifier cannot inspect. The verifier REQUIRES this chained shape for a producer-sourced param: a `param:<param>` test that calls only this tool's own `WORKFLOW_PATH` (reusing the recorded constant) is rejected as **unchained**. If the producer returns a token and the consumer rejects it, fix the producer shape or consumer unpacking. If the producer returns useful core records but no token for that record/input class, never use a recorded constant or fabricate a fallback: keep the consumer `verified:false`, waived-chain, or inconclusive with diagnostics, and ensure the producer limitation honestly names the unavailable output and affected consumer. If the producer shape contains `requiredKeys=[keyA,keyB,...]`, this tool's `request-transform.ts` MUST decode/unpack any emitted fresh value and use every required key in the outgoing request; a transform that treats the composite as a plain string or falls back to a recorded default is invalid.
331
333
 
332
334
  **This file is ephemeral** like parser.test.ts — deleted after verification unless `--keep-test` is passed.
333
335
 
@@ -54,7 +54,9 @@ Judge all of the following:
54
54
  shape, rather than an error page, unrelated payload, partial contract, empty
55
55
  placeholder, or plausible-looking wrong data?
56
56
  - Do important requested inputs appear to affect the result correctly?
57
- - Are producer/consumer tokens and identifiers usable in real terms?
57
+ - Are producer/consumer tokens and identifiers usable in real terms when they
58
+ are present, and are absent tokens reported honestly without destroying useful
59
+ producer results?
58
60
  - Would a reasonable caller regard the tool as successful for its declared
59
61
  intent?
60
62
  - Are time-sensitive inputs valid at the review date supplied in the context?
@@ -64,6 +66,11 @@ Judge all of the following:
64
66
  to use rolling future dates.
65
67
  - Submit exactly one verdict for every declared workflow parameter. Do not omit,
66
68
  duplicate, or invent parameter names.
69
+ - Mark a parameter as working only with differential evidence: hold the other
70
+ inputs fixed, change that parameter to a materially distinct valid value, and
71
+ observe the promised request/result effect. A successful response alone does
72
+ not prove the parameter works; unchanged or non-discriminating output must be
73
+ reported as `no_op` or untestable with the concrete evidence.
67
74
  - Judge the core intent separately from secondary filters or options. A working
68
75
  core call with one broken secondary parameter is not `tool_broken`; report the
69
76
  parameter-specific failure and the otherwise-working core precisely.
@@ -71,11 +78,12 @@ Judge all of the following:
71
78
  parameter description advertises materially different input classes or routes
72
79
  (for example identifiers versus free-form names, one-way versus round-trip, or
73
80
  local versus remote scope), use a targeted call for the most consequential
74
- alternate class when live-call budget permits. A limitation that removes a
75
- required downstream branch must also narrow any remaining descriptions and
76
- parameters that would still route callers into that branch. Otherwise return
77
- `changes_required`: the compiler may implement the branch or ship a smaller,
78
- honestly described tool.
81
+ alternate class when live-call budget permits. Narrow an input class only when
82
+ that class itself returns unusable or misleading core results. A missing
83
+ optional token needed by a downstream consumer does not make useful producer
84
+ records incorrect: require an honest `workflow.limitations` entry naming the
85
+ missing output and affected consumer, then permit the producer to retain that
86
+ input class. The dependent consumer may remain unverified or inconclusive.
79
87
  - For a secondary parameter that is known broken, use `changes_required` and tell
80
88
  the compiler either to repair it from recording/live evidence or remove it from
81
89
  the public parameter contract with a durable limitation. Do not approve a known
@@ -88,11 +96,16 @@ Status policy:
88
96
  by live evidence.
89
97
  - `approved_with_gaps`: the core task is genuinely correct, and any remaining
90
98
  declared secondary parameters are grounded but untestable or intentionally
91
- unverified. State every gap. Do not use this for a parameter known to be broken,
92
- or when an untested input class is material to the declared core scope.
93
- - `changes_required`: the tool ran, but the result is semantically wrong,
94
- incomplete, misleading, or a parameter/producer contract is broken. Give the
95
- compile agent concrete expected-versus-observed feedback and a suggested fix.
99
+ unverified, or useful producer results honestly disclose an unavailable
100
+ downstream token and affected consumers. State every gap. Do not use this for
101
+ a parameter known to be broken, or when an untested input class is material to
102
+ the declared core scope.
103
+ - `changes_required`: the tool ran, but the core result is semantically wrong,
104
+ incomplete, misleading, a declared parameter is broken, or a producer gap is
105
+ not yet disclosed honestly. Give the compile agent concrete
106
+ expected-versus-observed feedback and a suggested fix. Do not use this status
107
+ merely because a useful producer record lacks an optional downstream token
108
+ that the workflow already documents as unavailable.
96
109
  - `inconclusive`: infrastructure, bot defense, or unavailable evidence prevents
97
110
  a real judgment. Never convert uncertainty into approval.
98
111
 
package/src/cli.ts CHANGED
@@ -112,7 +112,7 @@ export const VERB_HELP: Record<string, VerbHelp> = {
112
112
  summary:
113
113
  'Record a workflow, compile both artifacts, emit the tool, and connect to your AI platform — all in one interactive flow. Supports resuming incomplete runs and multiple workflows per site.',
114
114
  usage: [
115
- 'imprint teach <site> [--url <url>] [--from-session <path>] [--persist-profile] [--no-interactive] [--all-tools] [--provider <name>] [--model <name>] [--timeout <duration>] [--keep-test] [--skip-replay] [--from-step <step>] [--to-step <step>] [--only <step>]',
115
+ 'imprint teach <site> [--url <url>] [--from-session <path>] [--persist-profile] [--no-interactive] [--all-tools] [--tool <toolName>] [--provider <name>] [--model <name>] [--timeout <duration>] [--keep-test] [--skip-replay] [--from-step <step>] [--to-step <step>] [--only <step>]',
116
116
  ],
117
117
  flags: [
118
118
  { name: '--url <url>', description: 'Starting URL (else about:blank).' },
@@ -131,6 +131,11 @@ export const VERB_HELP: Record<string, VerbHelp> = {
131
131
  description:
132
132
  'With --no-interactive, compile every detected candidate tool instead of only the primary.',
133
133
  },
134
+ {
135
+ name: '--tool <toolName>',
136
+ description:
137
+ 'With --from-step/--only, resume exactly one persisted tool from the retained recording.',
138
+ },
134
139
  {
135
140
  name: '--provider <name>',
136
141
  description:
@@ -405,10 +410,14 @@ export const VERB_HELP: Record<string, VerbHelp> = {
405
410
  },
406
411
  'mcp-server': {
407
412
  summary: "Serve one site's generated tools as MCP (stdio default).",
408
- usage: ['imprint mcp-server <site> [--http] [--port <num>]'],
413
+ usage: ['imprint mcp-server <site> [--http] [--port <num>] [--include-tools <names>]'],
409
414
  flags: [
410
415
  { name: '--http', description: 'Use Streamable HTTP transport instead of stdio.' },
411
416
  { name: '--port <num>', description: 'Port for HTTP transport (default 8765).' },
417
+ {
418
+ name: '--include-tools <names>',
419
+ description: 'Expose only these comma-separated generated tool names.',
420
+ },
412
421
  ],
413
422
  example: 'imprint mcp-server southwest',
414
423
  },
@@ -1117,6 +1126,7 @@ async function main(argv: string[]): Promise<number> {
1117
1126
  options: {
1118
1127
  http: { type: 'boolean' },
1119
1128
  port: { type: 'string' },
1129
+ 'include-tools': { type: 'string' },
1120
1130
  },
1121
1131
  allowPositionals: false,
1122
1132
  });
@@ -1125,6 +1135,10 @@ async function main(argv: string[]): Promise<number> {
1125
1135
  site,
1126
1136
  http: values.http,
1127
1137
  port: values.port ? Number(values.port) : undefined,
1138
+ includeToolNames: values['include-tools']
1139
+ ?.split(',')
1140
+ .map((name) => name.trim())
1141
+ .filter(Boolean),
1128
1142
  });
1129
1143
  return 0;
1130
1144
  }
@@ -1409,6 +1423,7 @@ async function main(argv: string[]): Promise<number> {
1409
1423
  'persist-profile': { type: 'boolean' },
1410
1424
  'no-interactive': { type: 'boolean' },
1411
1425
  'all-tools': { type: 'boolean' },
1426
+ tool: { type: 'string' },
1412
1427
  provider: { type: 'string' },
1413
1428
  model: { type: 'string' },
1414
1429
  timeout: { type: 'string' },
@@ -1427,7 +1442,9 @@ async function main(argv: string[]): Promise<number> {
1427
1442
  // mutual exclusion with --from-session, and --to-step ≥ redact when
1428
1443
  // --from-session enters the chain at redact), returning the resolved window
1429
1444
  // or the exact error message to print. Extracted for unit-testing.
1430
- const { resolveTeachPhaseWindow } = await import('./imprint/teach-state.ts');
1445
+ const { resolveTeachPhaseWindow, validateToolScopedResumeStep } = await import(
1446
+ './imprint/teach-state.ts'
1447
+ );
1431
1448
  const phaseWindow = resolveTeachPhaseWindow(values);
1432
1449
  if ('error' in phaseWindow) {
1433
1450
  console.error(phaseWindow.error);
@@ -1436,6 +1453,22 @@ async function main(argv: string[]): Promise<number> {
1436
1453
  const fromStepArg = phaseWindow.fromStep;
1437
1454
  const toStepArg = phaseWindow.toStep;
1438
1455
 
1456
+ if (values.tool && !fromStepArg) {
1457
+ console.error('error: --tool requires --from-step or --only');
1458
+ return 2;
1459
+ }
1460
+ if (values.tool) {
1461
+ const toolWindowError = validateToolScopedResumeStep(fromStepArg);
1462
+ if (toolWindowError) {
1463
+ console.error(toolWindowError);
1464
+ return 2;
1465
+ }
1466
+ }
1467
+ if (values.tool && values['all-tools']) {
1468
+ console.error('error: --tool cannot be combined with --all-tools');
1469
+ return 2;
1470
+ }
1471
+
1439
1472
  if (!site && values['no-interactive']) {
1440
1473
  console.error(
1441
1474
  'error: `imprint teach` requires a <site> argument in --no-interactive mode.\n <site> is a label you choose — it names the output folder under ~/.imprint/.\n\n example: imprint teach google-flights --url https://flights.google.com\n→ run `imprint teach --help` for usage.',
@@ -1481,6 +1514,7 @@ async function main(argv: string[]): Promise<number> {
1481
1514
  'imprint.model': values.model ?? 'auto',
1482
1515
  'imprint.timeout_ms': teachTimeoutMs ?? 'default',
1483
1516
  'imprint.all_tools': values['all-tools'] ?? false,
1517
+ 'imprint.tool': values.tool,
1484
1518
  'imprint.no_interactive': values['no-interactive'] ?? false,
1485
1519
  'imprint.skip_replay': values['skip-replay'] ?? false,
1486
1520
  },
@@ -1497,6 +1531,7 @@ async function main(argv: string[]): Promise<number> {
1497
1531
  maxDurationMs: teachTimeoutMs,
1498
1532
  keepTest: values['keep-test'] || process.env.IMPRINT_KEEP_TEST === '1',
1499
1533
  allTools: values['all-tools'],
1534
+ tool: values.tool,
1500
1535
  skipReplay: values['skip-replay'],
1501
1536
  fromStep: fromStepArg,
1502
1537
  toStep: toStepArg,
@@ -49,6 +49,10 @@ export type OnDeadlineReached = () => Promise<number | null>;
49
49
  export interface AgentResult {
50
50
  outcome: 'done' | 'give_up' | 'timeout' | 'soft_cap' | 'error';
51
51
  doneSummary?: string;
52
+ /** Raw arguments supplied to the reserved done tool. Callers with a richer
53
+ * done contract can make a post-verification decision without teaching the
54
+ * generic agent loop about that contract. */
55
+ doneInput?: Record<string, unknown>;
52
56
  giveUpReason?: string;
53
57
  giveUpDetail?: string;
54
58
  errorMessage?: string;
@@ -294,12 +298,13 @@ export async function runAgentLoop(opts: AgentLoopOptions): Promise<AgentResult>
294
298
  // Check for done() or give_up() first
295
299
  for (const block of toolUseBlocks) {
296
300
  if (block.name === 'done') {
297
- const input = block.input as { summary?: string };
301
+ const input = block.input as Record<string, unknown>;
298
302
  return {
299
303
  action: 'return',
300
304
  result: {
301
305
  outcome: 'done',
302
- doneSummary: input.summary ?? 'Task completed',
306
+ doneSummary: typeof input.summary === 'string' ? input.summary : 'Task completed',
307
+ doneInput: input,
303
308
  turns: turn,
304
309
  durationMs: Date.now() - startTime,
305
310
  inputTokens,