imprint-mcp 0.6.5 → 0.6.6

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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/prompts/audit-agent.md +3 -3
  3. package/prompts/auth-compile-agent.md +46 -4
  4. package/prompts/compile-agent.md +36 -12
  5. package/prompts/live-verifier-agent.md +15 -5
  6. package/prompts/request-triage.md +51 -9
  7. package/prompts/tool-candidate-detection.md +7 -1
  8. package/prompts/tool-planning.md +1 -1
  9. package/src/cli.ts +10 -4
  10. package/src/imprint/audit.ts +23 -5
  11. package/src/imprint/auth-compile-agent.ts +11 -7
  12. package/src/imprint/auth-compile-tools.ts +127 -18
  13. package/src/imprint/auth-verifier.ts +2 -1
  14. package/src/imprint/backend-ladder.ts +20 -0
  15. package/src/imprint/build-plan.ts +21 -0
  16. package/src/imprint/claude-cli-compile.ts +7 -1
  17. package/src/imprint/codex-cli-compile.ts +6 -0
  18. package/src/imprint/compile-agent.ts +50 -9
  19. package/src/imprint/compile-tools.ts +308 -25
  20. package/src/imprint/compile-verification.ts +11 -2
  21. package/src/imprint/compile.ts +298 -69
  22. package/src/imprint/effects.ts +9 -0
  23. package/src/imprint/live-verifier.ts +455 -34
  24. package/src/imprint/login.ts +30 -12
  25. package/src/imprint/mcp-compile-server.ts +56 -5
  26. package/src/imprint/mcp-live-verifier-server.ts +43 -10
  27. package/src/imprint/probe-backends.ts +6 -0
  28. package/src/imprint/recording-request.ts +7 -2
  29. package/src/imprint/replay-capture.ts +48 -29
  30. package/src/imprint/request-capture.ts +14 -0
  31. package/src/imprint/runtime.ts +102 -22
  32. package/src/imprint/teach-state.ts +6 -7
  33. package/src/imprint/teach.ts +342 -120
  34. package/src/imprint/tool-candidates.ts +17 -0
  35. package/src/imprint/tool-plan.ts +2 -0
  36. package/src/imprint/triage-selection.ts +44 -0
  37. package/src/imprint/types.ts +39 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imprint-mcp",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
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 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.
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 and irreversible 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
 
@@ -39,8 +39,8 @@ 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.
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*.
42
+ - **Recover only unattended authentication during an audit, at most once total.** Establish each eligible non-interactive authentication tool once as its baseline and never differentially probe it. Across the entire audit, you may perform at most one recovery authentication call: if a data tool returns `AUTH_EXPIRED` before that allowance is used, rerun one already-proven non-interactive authentication tool and retry the interrupted data call once. After that single recovery call, classify any later `AUTH_EXPIRED` result as `infra` without authenticating again. Never initiate or retry authentication that can pause for human input; human continuation belongs to teach/login or explicit user-driven tool use.
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`.** Irreversible workflows are never connected to an audit; if one appears because of a harness defect, do not invoke it and report the exclusion failure in `notes`. For a connected state-changing but reversible workflow, 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*.
44
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.
45
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.
46
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.
@@ -12,8 +12,9 @@ The runtime only executes the program you write.
12
12
  1. Call `read_session_summary`.
13
13
  2. Inspect relevant requests and responses with `search_requests`,
14
14
  `read_request`, and `read_response_body`.
15
- 3. Write `workflow.json` and any recording-grounded
16
- `request-transform.ts` needed by the requests.
15
+ 3. Write `workflow.json`, an offline `request.test.ts`, and any
16
+ recording-grounded `request-transform.ts` needed by the requests. Call
17
+ `run_tests` and fix request construction before live verification.
17
18
  4. Call `run_verification` with one declared action and its parameters, then
18
19
  stop. The orchestrator runs it in a verification browser and resumes you with
19
20
  the observed status, body preview, continuation, and next action.
@@ -110,8 +111,11 @@ semantics.
110
111
  and the caller-facing message. Carry every prior capture referenced as
111
112
  `${state.X}` by the next action unless that action produces it before use.
112
113
  - A `success` outcome is the artifact's authentication completion criterion.
113
- - `persist` names captured non-cookie values that data tools consume later as
114
- `${credential.NAME}`. Cookies persist automatically.
114
+ - `persist` names durable credential interfaces that data tools consume later as
115
+ `${credential.NAME}`. Cookies persist automatically. These interface names do
116
+ not constrain your internal capture names. When they differ, set
117
+ `persistBindings` to `{ "credential_interface": "compiled_capture" }`; the
118
+ runtime stores the compiled capture under the downstream interface name.
115
119
  - Set `crossOriginCookieReinjection` only when the recording proves a
116
120
  cross-origin response cookie must be projected into the browser jar.
117
121
 
@@ -125,6 +129,21 @@ the request. It is required on requests whose captures appear in
125
129
  `authConfig.persist`, so `imprint login` extracts each value only from its
126
130
  producing recorded response.
127
131
 
132
+ For a response field that contains another JSON document as a string, use a
133
+ JSON capture with `path` for the outer field and `decodeJsonPath` for the value
134
+ inside the decoded document. Do not capture structured nested JSON credentials
135
+ with a regular expression; response escaping can change or truncate the value.
136
+
137
+ For every durable interface in `authConfig.persist`, add a
138
+ `request.test.ts` test whose title contains
139
+ `persisted-capture:<credential-interface-name>`. Load the redacted recording
140
+ through `process.env.IMPRINT_SESSION_PATH`, locate the declared
141
+ `recordingRequestSeq`, independently decode the intended response field, and
142
+ assert that evaluating the compiled capture produces that exact value. A
143
+ nonempty assertion or comparison of the locator to itself is insufficient:
144
+ the test must catch a broad regex or wrong nested path that resolves to a
145
+ different field. Keep captured values in memory; never inline or print them.
146
+
128
147
  `mode: "navigate"` performs a real top-level browser navigation for GET and
129
148
  `application/x-www-form-urlencoded` POST requests. Use it when the recording
130
149
  shows a document form submission whose redirects or browser-owned state must be
@@ -137,6 +156,29 @@ Use runtime templates exactly as supported:
137
156
  `${credential.X}`, `${param.X}`, `${state.X}`,
138
157
  `${response[N].path}`, and `${generated.uuid|nonce|epoch_s|epoch_ms|iso8601}`.
139
158
 
159
+ Every request body containing one of those runtime templates MUST declare
160
+ `bodyPlaceholderEncoding`. Choose it from the recorded wire format, not merely
161
+ the Content-Type:
162
+
163
+ - `"json-string"` when placeholders sit inside JSON string literals.
164
+ - `"form-urlencoded"` when each placeholder is a form field value.
165
+ - `"raw"` for multipart, opaque, or transform-owned payload bytes.
166
+
167
+ If the body mixes encoding layers, build it in `request-transform.ts` and keep
168
+ the workflow declaration `"raw"`; do not ask the runtime to infer a site-specific
169
+ codec.
170
+
171
+ Write `request.test.ts` to render the actual workflow offline with adversarial
172
+ synthetic credentials, decode the body according to its wire format, and assert
173
+ exact round-trip equality. Choose the cases and test structure from the recorded
174
+ request. Never place real credentials in the test or contact the site.
175
+
176
+ Independently classify every auth request's outward effect. If the session
177
+ summary marks a request `irreversible: true`, preserve `effect: "irreversible"`
178
+ and its exact `recordingRequestSeq`; do not request live verification for that
179
+ workflow. The host blocks live auth when triage provenance is missing or when
180
+ any generated request remains irreversible.
181
+
140
182
  Ground every request, capture, predicate, retry bound, navigation criterion, and
141
183
  transform in the recording. Preserve functional headers and request encoding.
142
184
  Do not store recorded secrets or one-time values as literals. When browser code
@@ -6,7 +6,7 @@ You are the imprint compile agent. Your job is to turn a recorded browser sessio
6
6
 
7
7
  ## The Goal
8
8
 
9
- You will produce three artifacts in the generated tool directory (`~/.imprint/<site>/<toolName>/` by default):
9
+ You will produce the following artifacts in the generated tool directory (`~/.imprint/<site>/<toolName>/` by default):
10
10
 
11
11
  1. **workflow.json** — a request template matching the `WorkflowSchema` defined below. This is a JSON object with:
12
12
  - `toolName`: snake_case verb phrase (e.g., `search_products`, `book_museum_pass`)
@@ -29,6 +29,8 @@ You will produce three artifacts in the generated tool directory (`~/.imprint/<s
29
29
 
30
30
  3. **parser.test.ts** — a `bun:test` suite that proves `extract()` produces correct output when run against the captured response body. Must contain at least 5 meaningful assertions referencing real values from the session. **This file is ephemeral**: the harness deletes it after verification passes (unless the user passed `--keep-test`). Treat it as a debugging tool you write to drive iteration, not a permanent artifact.
31
31
 
32
+ 4. **request.test.ts** — required whenever a request body contains runtime placeholders. Write an offline `bun:test` suite that exercises the generated request construction with synthetic adversarial values and proves the chosen body encoding round-trips. It must not contact the site.
33
+
32
34
  ## The Loop
33
35
 
34
36
  Follow these steps to compile the session:
@@ -36,7 +38,7 @@ Follow these steps to compile the session:
36
38
  1. **Orient yourself.** Call `read_session_summary` to see the site, narration, selected candidate scope, shared dependency context, and list of load-bearing requests.
37
39
  - If the summary contains `revisionContext`, this is a bounded revision of an existing tool. Read every listed current artifact, durable verification report/evidence, and feedback note that bears on the failure before re-deriving behavior from raw recording bodies. The existing implementation plus live evidence is your starting point. Preserve proven branches; make the smallest evidence-backed repair. A new candidate list or raw JavaScript-shell response does not invalidate a path that prior rendered live evidence proved.
38
40
 
39
- If the summary includes `selectedCandidate`, compile only that candidate. Other actions in the same recording are out of scope unless they are listed as shared dependencies.
41
+ If the summary includes `selectedCandidate`, compile only that candidate. Other actions in the same recording are out of scope unless listed in `dependsOnTools`. Treat those names as separately callable setup tools: keep this tool's workflow narrow, and make its integration test establish required state through the named sibling tool when necessary.
40
42
 
41
43
  Read `stateHints` carefully. They are deterministic, redacted equality relationships discovered before the LLM step, such as “request B header equals cookie set by request A” or “request header equals a storage key.” Use these hints to emit named `captures` plus `${state.name}` references. Never copy `[REDACTED:...]` marker IDs into workflow.json.
42
44
 
@@ -120,7 +122,16 @@ Follow these steps to compile the session:
120
122
  }
121
123
  ```
122
124
  Returning a plain `string` (just the URL) still works for simple URL-signing. Use the object return when you need to build or modify the request body or headers. Do NOT invent URL query parameters as a workaround for body-encoding complexity — the server ignores unknown query params and the parameters will have no effect.
123
- **Do not erase runtime placeholders in transforms.** If the workflow URL/body/header contains `${state.X}`, `${response[N].X}`, `${credential.X}`, or `${generated.X}`, preserve those placeholder strings or pass them through as strings. Do NOT parse them with `new URL(...).searchParams.get(...)`, `Number(...)`, `JSON.parse(...)`, date parsers, or other runtime evaluation that can turn the placeholder into `null`, `NaN`, an empty string, or the literal word `undefined`. A request transform runs before runtime substitution has real values; its job is to construct a template. If a shared helper needs `sessionId`, `buildVersion`, `csrfToken`, `nonce`, or another state value, pass the corresponding `'\${state.<name>}'` placeholder directly from the workflow contract, not by extracting it back out of an already-templated URL.
125
+
126
+ **Choose body placeholder encoding; do not delegate the decision to runtime.** Every request whose `body` contains `${param.X}`, `${credential.X}`, `${state.X}`, `${response[N].X}`, or `${generated.X}` MUST declare `bodyPlaceholderEncoding`:
127
+ - `"json-string"` — the placeholder is inside a JSON string literal; runtime applies the complete JSON string escaping algorithm but the template owns the surrounding quotes.
128
+ - `"form-urlencoded"` — the placeholder is one application/x-www-form-urlencoded field value; runtime applies WHATWG form-component encoding.
129
+ - `"raw"` — bytes are spliced verbatim, as required for multipart fields, opaque protocols, or a transform-owned payload.
130
+
131
+ Content-Type is evidence, not the decision. Inspect the recorded body and choose the mode that reproduces its actual framing. If one body mixes encoding layers (for example JSON nested inside a form field), use `request-transform.ts`; a single body-wide declaration is intentionally not expressive enough to guess nested protocols.
132
+
133
+ For every placeholder-bearing body, write `request.test.ts` that renders the actual workflow entirely offline and substitutes adversarial synthetic values, including delimiters, quotes, whitespace, escapes, and Unicode. Decode the rendered body according to its declared wire format and assert exact round-trip equality. Choose the test structure and names that best fit the request; never put real credentials in the test. `run_tests` executes this file alongside parser tests, so fix the workflow or transform until it passes.
134
+ **Do not erase runtime values in transforms.** The runtime substitutes the workflow request before calling `request-transform.ts`, but the transform API receives only the substituted `url`, raw prior `responses`, and resolved `params` shown in the signature above. It does **not** receive the workflow's current body or headers, and extra function arguments are ignored. Preserve available values as opaque strings unless the recorded protocol requires a documented conversion. Do NOT casually reparse them with `new URL(...).searchParams.get(...)`, `Number(...)`, `JSON.parse(...)`, date parsers, or similar coercions that can turn a valid value into `null`, `NaN`, an empty string, or the literal word `undefined`. If a transform returns `body` or `headers`, it must construct each replacement completely from those supported inputs; cover the resulting bytes with `request.test.ts` so substitution is encoded exactly once.
124
135
 
125
136
  **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
137
 
@@ -130,6 +141,7 @@ Follow these steps to compile the session:
130
141
  **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.
131
142
  - **`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.
132
143
  - **NEVER use `${env.NAME}` placeholders.** The `${env.X}` syntax exists in the runtime but is reserved for operator-level configuration, not for values you can see in the recording. If a value appears in the captured request, hardcode it. If multiple candidates in the same session use different API keys for different endpoints, hardcode each one — they are endpoint-specific app constants, not secrets. The only valid placeholder types for your workflow are `${param.NAME}`, `${credential.NAME}`, `${state.NAME}`, and `${response[N].NAME}`.
144
+ - **Irreversible requests.** Independently review every candidate request, its narration, and nearby browser events for irreversible outward effects, even when triage did not flag it. For each irreversible operation, set the matching workflow request to `"effect": "irreversible"` and copy its captured seq into `recordingRequestSeq`. Treat a session-summary `irreversible: true` marker as a mandatory classification to preserve, not as the complete set. This is an agent judgment: POST does not imply irreversible, and GET does not imply safe. Never live-test an irreversible workflow. Verify request construction and parsing from recorded evidence only, and state clearly in `intent.description` that production invocation performs an irreversible action.
133
145
  - If the workflow chains multiple requests (request N+1 uses a value from request N's response), add an `extract` field to request N and reference it in request N+1 via `${response[N].name}`
134
146
  - **Chaining complementary endpoints.** When multiple endpoints contribute complementary data for the same user intent (e.g. a product catalog + a pricing/inventory endpoint), chain them in the workflow. The parser's `extract(rawResponse, context)` receives `context.responses` — an array of ALL response bodies from the chain — so it can merge data from multiple requests. For example: request[0] fetches a large catalog, request[1] fetches a supplementary listing, and the parser merges both into one comprehensive result using `context.responses[0]` and `context.responses[1]`. The parser also receives `context.params` for constructing values the API doesn't echo back (e.g. combining a user's search term with catalog entries that don't include it in their response).
135
147
  - **If you write a `parser.ts`, you MUST set `"parserModule": "./parser.ts"` in workflow.json.** Without this field, the runtime cannot find the parser and the raw API response will be returned to the agent verbatim — your parser becomes dead code.
@@ -238,7 +250,9 @@ Follow these steps to compile the session:
238
250
 
239
251
  If both rungs fail (400, 403 across both, expired tokens), this test fails and you must fix the workflow. Common fixes: chain a session/token request first, write a `requestTransformModule` for URL signing, or use `${state.X}` captures instead of hardcoded values. If a query param changes per call (check `stateHints` for `query_param_changes_across_calls`), use `search_response_body` to find the signing function in `.js` responses and replicate it in `request-transform.ts`.
240
252
 
241
- **Per-parameter coverage tests.** Beyond the baseline test above, you must write one integration test for **every parameter that has a non-default value in any captured request** (visible in `inlineData.requestBodyDecoded` or via `read_request`). Walk every recorded request, decode its body, and enumerate the set of `(paramName, nonDefaultValue)` tuples. Each tuple is a coverage unit — write a test that overrides that param and asserts a constraint on the response.
253
+ **Irreversible workflows are never live-tested.** Do not call `runWorkflowWithLadder`, direct fetch, curl, browser actions, or any generated MCP tool for a workflow containing `effect: "irreversible"`. Use recorded response fixtures for parser tests and offline request rendering for parameter fidelity. The deterministic harness will waive live verification for safety; it will not fabricate a successful response.
254
+
255
+ **Per-parameter coverage tests (non-irreversible workflows only).** Beyond the baseline test above, you must write one integration test for **every parameter that has a non-default value in any captured request** (visible in `inlineData.requestBodyDecoded` or via `read_request`). Walk every recorded request, decode its body, and enumerate the set of `(paramName, nonDefaultValue)` tuples. Each tuple is a coverage unit — write a test that overrides that param and asserts a constraint on the response. For irreversible workflows, omit the live integration suite and use offline request rendering plus recorded fixtures instead.
242
256
 
243
257
  **Title each per-parameter test `param:<name> …`** — begin the title with the literal token `param:` followed by the exact parameter name (e.g. `test('param:max_price=50 constrains all results', …)`). The verifier determines coverage by which `param:<name>` tests **actually ran green against live data**, not by scanning the source: a test that is merely present but did not pass — or a whole suite that was waived by anti-bot — does NOT count as coverage. Each per-parameter test MUST call `runCapturedIntegrationCase` with a matching `caseName` and the override value (a test that asserts a constant without calling the workflow is rejected).
244
258
 
@@ -333,10 +347,10 @@ Follow these steps to compile the session:
333
347
 
334
348
  **This file is ephemeral** like parser.test.ts — deleted after verification unless `--keep-test` is passed.
335
349
 
336
- 11. **Run deterministic tests.** Use `run_tests` for `parser.test.ts` and fix those failures. Do not run `integration.test.ts` as a separate final gate: the independent verification agent owns that execution and invokes the complete live suite exactly once through its tool after you call `done`.
350
+ 11. **Run deterministic tests.** Use `run_tests` for `parser.test.ts` and `request.test.ts` and fix those failures. For irreversible workflows this runner disables network access; `run_bash` and every live integration path remain unavailable. Do not run `integration.test.ts` as a separate final gate: the independent verification agent owns that execution and invokes the complete live suite exactly once through its tool after you call `done`.
337
351
 
338
352
  12. **Fix and iterate.** If tests fail:
339
- - **parser.test.ts failures**: re-read the response body, adjust the parser logic
353
+ - **parser.test.ts or request.test.ts failures**: re-read the recorded request/response and adjust the parser, encoding declaration, or transform
340
354
  - **integration.test.ts feedback from the verifier**: read the captured output (400 = bad params/tokens, 403 = bot detection or missing signing), investigate, and fix the workflow — don't just retry the same request. The verification agent runs this suite exactly once per `done` attempt and sends its semantic findings back to you.
341
355
  - Re-run tests
342
356
  - Repeat until all tests pass
@@ -527,10 +541,10 @@ The goal is a working tool, not a perfect tool. You can always refine later. Get
527
541
  | `reveal_request` | Full UNREDACTED request + response for one or more seqs, read straight from the recording — use to read the real value of an auth/session/gateway header (or body field) before deciding how to wire it. Never copy a raw secret into artifacts. |
528
542
  | `read_response_body` | Response body for a given seq (paginated for large bodies via offset/length) |
529
543
  | `search_response_body` | Find substrings in a response body and return matching offsets+context (essential for anchoring on known values inside opaque JSPB) |
530
- | `write_file` | Write workflow.json, parser.ts, parser.test.ts, or notes/*.md in the generated tool directory |
544
+ | `write_file` | Write workflow.json, parser.ts, parser.test.ts, request.test.ts, request-transform.ts, integration.test.ts, or notes/*.md in the generated tool directory |
531
545
  | `read_file` | Read a file by relative path (e.g. `parser.ts`, `workflow.json`) |
532
- | `run_bash` | Run a shell command (60s timeout, output truncated to 16KB). cwd is the tool directory |
533
- | `run_tests` | Convenience wrapper for `bun test parser.test.ts` |
546
+ | `run_bash` | For non-irreversible recordings only: run a shell command (60s timeout, output truncated to 16KB) in the tool directory |
547
+ | `run_tests` | For non-irreversible recordings only: run parser.test.ts and/or request.test.ts, then typecheck generated parser/transform modules |
534
548
  | `done` | Claim the task is complete; triggers external verification |
535
549
  | `give_up` | Give up with a documented reason (heavily discouraged, see constraints above) |
536
550
 
@@ -580,11 +594,16 @@ StateCapability = 'ordinary_http' | 'browser_bootstrap' | 'stealth_bootstrap' |
580
594
 
581
595
  // Request-level captures (extract values from responses for chaining)
582
596
  RequestCapture =
583
- | { source: 'json'; name: string; path: string; required?: boolean; capability?: StateCapability }
597
+ | { source: 'json'; name: string; path: string; decodeJsonPath?: string; required?: boolean; capability?: StateCapability }
584
598
  | { source: 'response_header'; name: string; header: string; mode?: 'first' | 'last' | 'all'; required?: boolean; capability?: StateCapability }
585
599
  | { source: 'text_regex'; name: string; pattern: string; group?: number; required?: boolean; capability?: StateCapability }
586
600
  | { source: 'cookie'; name: string; cookie: string; url?: string; domain?: string; path?: string; sameSite?: string; allowHttpOnlyProjection?: boolean; required?: boolean; capability?: StateCapability };
587
601
 
602
+ When a JSON field contains another JSON document as a string, select the outer
603
+ field with `path` and the value inside it with `decodeJsonPath`. Do not use a
604
+ regular expression for structured nested JSON; wire escaping can change or
605
+ truncate the captured value.
606
+
588
607
  // Bootstrap captures (from page load, for browser-minted state)
589
608
  BootstrapCapture =
590
609
  | { source: 'cookie'; name: string; cookie: string; url?: string; domain?: string; path?: string; sameSite?: string; allowHttpOnlyProjection?: boolean; required?: boolean; capability?: StateCapability }
@@ -600,9 +619,11 @@ WorkflowRequest = {
600
619
  url: string; // template: ${param.X}, ${response[N].path}, ${state.X}
601
620
  headers: Record<string, string>;
602
621
  body?: string;
622
+ bodyPlaceholderEncoding?: 'raw' | 'json-string' | 'form-urlencoded'; // required when body has runtime placeholders
603
623
  extract?: Record<string, string>; // name → jsonpath; later requests use ${response[N].name}
604
624
  captures?: RequestCapture[];
605
- effect?: 'safe' | 'idempotent' | 'unsafe';
625
+ effect?: "safe" | "idempotent" | "unsafe" | "irreversible";
626
+ recordingRequestSeq?: number; // required provenance for irreversible requests
606
627
  }
607
628
 
608
629
  // Top-level workflow
@@ -640,6 +661,7 @@ Workflow = {
640
661
  "url": "https://api.example.com/login",
641
662
  "headers": { "Content-Type": "application/json" },
642
663
  "body": "{\"username\":\"${credential.username}\",\"password\":\"${credential.password}\"}",
664
+ "bodyPlaceholderEncoding": "json-string",
643
665
  "captures": [
644
666
  { "source": "json", "name": "access_token", "path": "$.token" }
645
667
  ]
@@ -669,6 +691,7 @@ Workflow = {
669
691
  "url": "https://api.example.com/guest-login",
670
692
  "headers": { "Content-Type": "application/json" },
671
693
  "body": "{\"authcode\":\"${state.auth_code}\"}",
694
+ "bodyPlaceholderEncoding": "json-string",
672
695
  "captures": [
673
696
  { "source": "json", "name": "fingerprint", "path": "$.result.fingerprint" }
674
697
  ]
@@ -677,7 +700,8 @@ Workflow = {
677
700
  "method": "POST",
678
701
  "url": "https://api.example.com/query",
679
702
  "headers": { "Content-Type": "application/json" },
680
- "body": "{\"fingerprint\":\"${state.fingerprint}\",\"action\":\"${param.action}\"}"
703
+ "body": "{\"fingerprint\":\"${state.fingerprint}\",\"action\":\"${param.action}\"}",
704
+ "bodyPlaceholderEncoding": "json-string"
681
705
  }
682
706
  ]
683
707
  }
@@ -20,9 +20,19 @@ is `changes_required` even when infrastructure prevents the live suite from
20
20
  starting. Do not label a report infrastructure-only inconclusive when the
21
21
  supplied artifacts already explain the failure.
22
22
 
23
- You have four tools:
23
+ You have five tools:
24
24
 
25
- 1. `prepare_live_backend` checks whether the workflow already has a valid backend
25
+ 1. `refresh_auth_session` re-runs the site's generated authentication tool. Use
26
+ it only after actual live evidence says the stored session is expired,
27
+ invalid, or unauthenticated. Set `cleanSession: true` only when rejected state
28
+ must be withheld; this clears browser state and auth-produced durable values
29
+ while retaining provisioned login credentials. You have one stateful refresh
30
+ session. Omit `action` on its first call. The result may name `nextAction`;
31
+ inspect its message and call again with exactly that action and any parameters
32
+ declared by the compiled auth flow. You decide whether and how to continue;
33
+ the runtime does not auto-follow the action graph. Never invent unavailable
34
+ human input. After success, prepare the backend again and run the suite.
35
+ 2. `prepare_live_backend` checks whether the workflow already has a valid backend
26
36
  preference. Supply realistic complete parameters. A valid cache is reused
27
37
  without live probing, including across compiler revisions. A missing or invalid
28
38
  preference runs the existing backend probe. Call it with `forceReprobe: true`
@@ -32,16 +42,16 @@ You have four tools:
32
42
  Preparation can legitimately take several minutes. Wait for its completed
33
43
  tool result; never infer timeout from elapsed wall time or start a duplicate
34
44
  preparation while one is still running.
35
- 2. `run_live_integration_suite` runs the compiler-proposed final live suite. You
45
+ 3. `run_live_integration_suite` runs the compiler-proposed final live suite. You
36
46
  own this execution and must inspect both its test outcome and every captured
37
47
  parsed output. Run it after backend preparation. A justified rerun is allowed
38
48
  after failure, timeout, or successful reprobe; explain why in `reason`.
39
- 3. `run_live_integration_test` makes a targeted live call for a specific unresolved
49
+ 4. `run_live_integration_test` makes a targeted live call for a specific unresolved
40
50
  semantic question. Explain the question in `reason` and choose discriminating
41
51
  parameters. You may make as many useful calls as the shared deadline permits.
42
52
  Repeating parameters is valid when testing after a reprobe or confirming an
43
53
  unstable result, but say why.
44
- 4. `submit_verification_report` submits your structured final judgment. Call it
54
+ 5. `submit_verification_report` submits your structured final judgment. Call it
45
55
  exactly once when you have enough evidence.
46
56
 
47
57
  Probe time has a separate infrastructure budget. Do not treat a successful or
@@ -8,6 +8,8 @@ You receive a JSON object:
8
8
 
9
9
  ```json
10
10
  {
11
+ "mode": "relevance | effect",
12
+ "batch": { "index": 1, "total": 3 },
11
13
  "site": "string",
12
14
  "url": "string (starting URL)",
13
15
  "narration": [
@@ -22,27 +24,45 @@ You receive a JSON object:
22
24
  "timestamp": ms,
23
25
  "method": "GET|POST|...",
24
26
  "url": "string",
25
- "resourceType": "XHR|Fetch|Document",
27
+ "resourceType": "Document|XHR|Fetch|Script|Stylesheet|Image|Media|Font|Other",
26
28
  "status": int,
27
29
  "mimeType": "string",
28
30
  "headers": "truncated request headers",
29
31
  "body": "request payload (NOT the response body)",
30
32
  "bodyLength": int,
33
+ "responsePreview": "truncated response payload",
31
34
  "responseBodyLength": int,
35
+ "keepEligible": boolean,
32
36
  "repeatCount": int,
33
37
  "repeatedSeqs": [int],
34
38
  "lastTimestamp": ms
35
39
  }
40
+ ],
41
+ "outboundWebSockets": [
42
+ { "seq": int, "timestamp": ms, "type": "ws-sent", "detail": "truncated outbound frame" }
36
43
  ]
37
44
  }
38
45
  ```
39
46
 
40
- The narration is the user's own description of what they did. Use it to understand the workflow's intent, then select the requests that serve that intent.
47
+ In `relevance` mode, the request list contains relevance candidates after
48
+ lossless compaction. Use the narration and events to select the requests that
49
+ serve the user's intent. Return empty `irreversible` and `irreversibleEvents`
50
+ arrays; the separate effect passes own that decision.
51
+
52
+ In `effect` mode, each numbered batch contains a bounded portion of the complete
53
+ request and outbound-WebSocket inventory. Classify every presented item for
54
+ irreversible effects. Do not reference items from another batch. `keep` is
55
+ ignored in effect mode and should be empty.
56
+
57
+ The narration is the user's own description of what they did.
41
58
  The events are the browser actions captured during recording. Use input/change/submit
42
59
  event timestamps to disambiguate repeated endpoint calls when narration was spoken
43
60
  after the action.
44
61
 
45
- Request entries may include `repeatCount`, `repeatedSeqs`, and `lastTimestamp` when identical requests were compacted. Select the representative `seq` unless a specific repeated seq is needed for an intentional multi-step workflow.
62
+ Request entries may include `repeatCount`, `repeatedSeqs`, and `lastTimestamp` when identical requests were compacted. Select the representative `seq` unless a specific repeated seq is needed for an intentional multi-step workflow. An effect classification on any compacted row applies to every seq represented by that row.
63
+
64
+ `keepEligible` is a deterministic relevance hint. It does not decide effects.
65
+ Relevance and replay safety are separate decisions.
46
66
 
47
67
  ## What to include
48
68
 
@@ -71,14 +91,36 @@ Request entries may include `repeatCount`, `repeatedSeqs`, and `lastTimestamp` w
71
91
  3. **Use browser events for repeated calls.** If the same endpoint appears more
72
92
  than once with different user-controlled values, keep the request closest to
73
93
  the input/change/submit event, even if narration came later.
74
- 4. **Prefer POST/PUT/PATCH over GET** when both exist for the same endpoint -- the mutation is usually the load-bearing one.
75
- 5. **When in doubt, include it.** A false positive (including a noise request) is cheaper than a false negative (excluding the result-bearing XHR). The downstream compilation LLM can ignore noise, but it can't work with data it never sees.
76
- 6. **Aim for 5-50 requests** out of potentially hundreds. If you're selecting more than 50, you're probably not filtering aggressively enough. If fewer than 3, double-check you haven't dropped the key data-fetch.
94
+ 4. **When in doubt, include it.** A false positive (including a noise request) is cheaper than a false negative (excluding the result-bearing XHR). The downstream compilation LLM can ignore noise, but it can't work with data it never sees.
95
+ 5. **Aim for 5-50 requests** out of potentially hundreds. If you're selecting more than 50, you're probably not filtering aggressively enough. If fewer than 3, double-check you haven't dropped the key data-fetch.
96
+
97
+ ## Deciding what's irreversible
98
+
99
+ A request is **irreversible** when re-issuing it causes an irreversible, outward-facing side effect:
100
+ - **Placing an order** (food, merchandise, tickets)
101
+ - **Charging a payment** (submitting a credit-card transaction, confirming a purchase)
102
+ - **Sending a message** (email, SMS, chat, notification push)
103
+ - **Deleting a resource** (account, record, reservation cancellation that cannot be undone)
104
+ - **Confirming a reservation** (hotel booking, appointment, seat assignment)
105
+
106
+ Mutations such as cart edits, preferences, login/session refresh, search, browse,
107
+ filtering, and pagination are often reversible or effect-free, but these are
108
+ examples rather than exemptions. Classify the recorded consequence: a nominal
109
+ "cart" or "login" request can still submit an order, send a challenge, revoke a
110
+ session, or trigger another outward effect.
111
+
112
+ Judge every input row from narration, browser events, URL, request body, and response metadata—not the HTTP method or `keepEligible` alone. A POST search can be safe; a GET unsubscribe can be irreversible. When uncertain, classify it as irreversible so downstream agents can review it and replay/live verification are withheld.
77
113
 
78
114
  ## Output
79
115
 
80
- A JSON array of `seq` numbers, and ONLY that array (no prose before or after, no code fences):
116
+ A JSON object with three fields, and ONLY that object (no prose before or after, no code fences):
117
+
118
+ { "keep": [3, 17, 42, 98], "irreversible": [42], "irreversibleEvents": [123] }
81
119
 
82
- [3, 17, 42, 98]
120
+ - **`keep`**: seq numbers of all relevant requests (same selection criteria as above).
121
+ - **`irreversible`**: representative seq numbers whose re-issuance would cause an irreversible outward-facing side effect, across EVERY input row. This is independent of `keep`: include an irreversible request even when it is irrelevant to compilation or has `keepEligible: false`. If no requests are irreversible, use an empty array: `"irreversible": []`.
122
+ - **`irreversibleEvents`**: seq numbers of outbound WebSocket frames whose
123
+ re-issuance, or whose triggering browser action, can cause an irreversible
124
+ outward effect. If none are irreversible, use an empty array.
83
125
 
84
- The order does not matter. The downstream system will sort by seq.
126
+ The order within each array does not matter. The downstream system will sort by seq.
@@ -28,7 +28,8 @@ Schema:
28
28
  "likelyParams": [
29
29
  { "name": "snake_case_param", "type": "string", "description": "short description" }
30
30
  ],
31
- "dependencySeqs": [number]
31
+ "dependencySeqs": [number],
32
+ "dependsOnTools": ["snake_case_producer_tool"]
32
33
  }
33
34
  ]
34
35
  }
@@ -122,3 +123,8 @@ Rules:
122
123
  If every seq in requestSeqs is a distinct API call (different endpoints
123
124
  or fundamentally different operations), set representativeSeqs equal to
124
125
  requestSeqs or omit it.
126
+ 21. Put independently callable prerequisite candidates in `dependsOnTools`.
127
+ Dependencies are callable prerequisites, not workflows that merely contain
128
+ the consumer's requests. When several candidates establish the required
129
+ state, choose the smallest producer. Keep unexposed setup requests in
130
+ `dependencySeqs`.
@@ -4,7 +4,7 @@ You are the PLANNER for ONE MCP tool that a second agent will COMPILE from a bro
4
4
 
5
5
  You receive `{ site, url, tool, sharedContext?, planGuidance?, assignedModules, requests }`:
6
6
 
7
- - `tool` — `{ toolName, description, expectedOutput, likelyParams, requestSeqs, dependencySeqs }`. The compiled tool must expose these parameters and produce `expectedOutput`. `likelyParams` are the detector's best guess — confirm or correct each against the recorded requests.
7
+ - `tool` — `{ toolName, description, expectedOutput, likelyParams, requestSeqs, dependencySeqs, dependsOnTools }`. The compiled tool must expose these parameters and produce `expectedOutput`. `dependsOnTools` names independently callable setup tools the caller should run first; do not inline or replace them with a broader terminal workflow. `likelyParams` are the detector's best guess — confirm or correct each against the recorded requests.
8
8
  - `planGuidance?` — present when a global build plan ran first: `{ parserGuidance, paramChecklist, authRecipe, loadBearingSeqs }` for THIS tool. Treat it as prior guidance and reconcile it with the recorded data; if the recording contradicts it, prefer the recording and say so.
9
9
  - `assignedModules[]` — verified shared modules this tool MUST import instead of re-implementing: `{ path, kind, importPath, exportSignatures, purpose }`. A `request-transform` module reproduces the site's request signing/construction; a `parser-helper` extracts data from the response. Reference each by its exact `importPath`.
10
10
  - `requests[]` — the recorded requests in scope for this tool: `{ seq, method, url, headers, body, status, mimeType, responsePreview, ... }`. These are the ground truth — decode them, do not guess. `responsePreview` is truncated; note where the full body must be read.
package/src/cli.ts CHANGED
@@ -194,17 +194,17 @@ export const VERB_HELP: Record<string, VerbHelp> = {
194
194
  {
195
195
  name: '--keep-test',
196
196
  description:
197
- 'Retain the agent-generated parser.test.ts after compile (debug). Default deletes it; the test reads the gitignored redacted session via $IMPRINT_SESSION_PATH and is not portable. Also settable via IMPRINT_KEEP_TEST=1.',
197
+ 'Retain the agent-generated parser.test.ts and request.test.ts after compile (debug). Default deletes them; parser tests read the gitignored redacted session via $IMPRINT_SESSION_PATH and are not portable. Also settable via IMPRINT_KEEP_TEST=1.',
198
198
  },
199
199
  {
200
200
  name: '--skip-replay',
201
201
  description:
202
- "Skip the replay-and-diff stage. Faster, but the compile agent won't be able to distinguish browser-minted values from constants, which may reduce workflow accuracy.",
202
+ "Skip the replay-and-diff stage. Faster, but the compile agent won't be able to distinguish browser-minted values from constants, which may reduce workflow accuracy. By default, the preceding effect-aware triage suppresses replay entirely when the recording contains an irreversible request.",
203
203
  },
204
204
  {
205
205
  name: '--from-step <step>',
206
206
  description:
207
- 'Resume a prior run starting at <step> (record, redact, replay-and-diff, triage, detect-candidates, plan-prereqs, generate, compile-playbook, emit, register). Only allowed if a prior run reached/crossed that point — earlier phase outputs are reused. Not combinable with --from-session.',
207
+ 'Resume a prior run starting at <step> (record, redact, triage, replay-and-diff, detect-candidates, plan-prereqs, generate, compile-playbook, emit, register). Only allowed if a prior run reached/crossed that point — earlier phase outputs are reused. Not combinable with --from-session.',
208
208
  },
209
209
  {
210
210
  name: '--to-step <step>',
@@ -265,7 +265,7 @@ export const VERB_HELP: Record<string, VerbHelp> = {
265
265
  {
266
266
  name: '--keep-test',
267
267
  description:
268
- 'Retain the agent-generated parser.test.ts after compile (debug). Default deletes it; the test reads the gitignored redacted session via $IMPRINT_SESSION_PATH and is not portable. Also settable via IMPRINT_KEEP_TEST=1.',
268
+ 'Retain the agent-generated parser.test.ts and request.test.ts after compile (debug). Default deletes them; parser tests read the gitignored redacted session via $IMPRINT_SESSION_PATH and are not portable. Also settable via IMPRINT_KEEP_TEST=1.',
269
269
  },
270
270
  ],
271
271
  example: 'imprint generate ~/.imprint/acmecorp/sessions/<ts>.redacted.json',
@@ -1622,6 +1622,7 @@ async function main(argv: string[]): Promise<number> {
1622
1622
  'revision-mode': { type: 'boolean' },
1623
1623
  provider: { type: 'string' },
1624
1624
  site: { type: 'string' },
1625
+ 'shared-triage-json': { type: 'string' },
1625
1626
  },
1626
1627
  allowPositionals: false,
1627
1628
  });
@@ -1639,6 +1640,7 @@ async function main(argv: string[]): Promise<number> {
1639
1640
  const { SharedModuleManifestSchema, AuthToolPlanSchema } = await import(
1640
1641
  './imprint/build-plan.ts'
1641
1642
  );
1643
+ const { SharedTriageSelectionSchema } = await import('./imprint/triage-selection.ts');
1642
1644
  const candidate = values['candidate-json']
1643
1645
  ? ToolCandidateSchema.parse(JSON.parse(values['candidate-json']))
1644
1646
  : undefined;
@@ -1651,6 +1653,9 @@ async function main(argv: string[]): Promise<number> {
1651
1653
  const authToolPlan = values['auth-plan-json']
1652
1654
  ? AuthToolPlanSchema.parse(JSON.parse(values['auth-plan-json']))
1653
1655
  : undefined;
1656
+ const sharedTriageSelection = values['shared-triage-json']
1657
+ ? SharedTriageSelectionSchema.parse(JSON.parse(values['shared-triage-json']))
1658
+ : undefined;
1654
1659
  await runCompileMcpServer({
1655
1660
  sessionPath: values['session-path'],
1656
1661
  toolDir,
@@ -1667,6 +1672,7 @@ async function main(argv: string[]): Promise<number> {
1667
1672
  | 'cursor-cli'
1668
1673
  | undefined,
1669
1674
  site: values.site,
1675
+ sharedTriageSelection,
1670
1676
  });
1671
1677
  return 0;
1672
1678
  }