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 +1 -1
- package/prompts/audit-agent.md +6 -5
- package/prompts/build-planning.md +2 -2
- package/prompts/compile-agent.md +6 -4
- package/prompts/live-verifier-agent.md +24 -11
- package/src/cli.ts +38 -3
- package/src/imprint/agent.ts +7 -2
- package/src/imprint/audit.ts +212 -20
- package/src/imprint/auth-compile-tools.ts +64 -13
- package/src/imprint/compile-agent-types.ts +25 -2
- package/src/imprint/compile-agent.ts +197 -40
- package/src/imprint/compile-tools.ts +164 -22
- package/src/imprint/live-verifier.ts +167 -6
- package/src/imprint/llm.ts +4 -0
- package/src/imprint/mcp-compile-server.ts +188 -83
- package/src/imprint/mcp-server.ts +16 -1
- package/src/imprint/probe-backends.ts +60 -25
- package/src/imprint/teach-state.ts +52 -2
- package/src/imprint/teach.ts +94 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imprint-mcp",
|
|
3
|
-
"version": "0.6.
|
|
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": {
|
package/prompts/audit-agent.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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.
|
package/prompts/compile-agent.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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`):
|
|
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
|
|
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.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
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,
|
package/src/imprint/agent.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|