imprint-mcp 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +3 -2
- package/prompts/audit-agent.md +4 -2
- package/prompts/compile-agent.md +46 -23
- package/prompts/live-verifier-agent.md +101 -0
- package/prompts/playbook-compilation.md +8 -1
- package/src/cli.ts +83 -1
- package/src/imprint/audit.ts +219 -20
- package/src/imprint/backend-cache.ts +18 -11
- package/src/imprint/backend-ladder.ts +48 -34
- package/src/imprint/cdp-browser-fetch.ts +135 -42
- package/src/imprint/claude-cli-compile.ts +38 -16
- package/src/imprint/codex-cli-compile.ts +120 -21
- package/src/imprint/compile-agent.ts +71 -26
- package/src/imprint/compile-tools.ts +169 -38
- package/src/imprint/compile-verification.ts +187 -0
- package/src/imprint/compile.ts +71 -2
- package/src/imprint/freeform-redact.ts +27 -6
- package/src/imprint/live-verifier.ts +1665 -0
- package/src/imprint/llm.ts +16 -0
- package/src/imprint/mcp-compile-server.ts +380 -81
- package/src/imprint/mcp-live-verifier-server.ts +359 -0
- package/src/imprint/mcp-server.ts +12 -2
- package/src/imprint/probe-backends.ts +88 -7
- package/src/imprint/runtime.ts +61 -11
- package/src/imprint/teach-state.ts +18 -1
- package/src/imprint/teach.ts +77 -19
- package/src/imprint/types.ts +39 -2
|
@@ -410,19 +410,31 @@ interface MultiToolResumeSelection {
|
|
|
410
410
|
candidate: WorkflowState['candidate'];
|
|
411
411
|
sharedContext: WorkflowState['sharedContext'];
|
|
412
412
|
}[];
|
|
413
|
-
skipped: {
|
|
413
|
+
skipped: {
|
|
414
|
+
workflowKey: string;
|
|
415
|
+
reason: 'different-recording' | 'not-resumable' | 'not-in-build-plan';
|
|
416
|
+
}[];
|
|
414
417
|
}
|
|
415
418
|
|
|
416
419
|
export function selectMultiToolResumePlans(
|
|
417
420
|
state: TeachState,
|
|
418
421
|
targetWorkflowKey: string,
|
|
419
422
|
fromStep: TeachStep,
|
|
423
|
+
opts?: {
|
|
424
|
+
noInteractive?: boolean;
|
|
425
|
+
allTools?: boolean;
|
|
426
|
+
plannedToolNames?: ReadonlySet<string>;
|
|
427
|
+
},
|
|
420
428
|
): MultiToolResumeSelection {
|
|
421
429
|
const target = state.workflows[targetWorkflowKey];
|
|
422
430
|
const plans: MultiToolResumeSelection['plans'] = [];
|
|
423
431
|
const skipped: MultiToolResumeSelection['skipped'] = [];
|
|
424
432
|
for (const [key, ws] of Object.entries(state.workflows)) {
|
|
425
433
|
if (key.startsWith('_pending_') || !ws.candidate) continue;
|
|
434
|
+
if (opts?.plannedToolNames && !opts.plannedToolNames.has(ws.candidate.toolName)) {
|
|
435
|
+
skipped.push({ workflowKey: key, reason: 'not-in-build-plan' });
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
426
438
|
if (target && ws.sessionPath !== target.sessionPath) {
|
|
427
439
|
skipped.push({ workflowKey: key, reason: 'different-recording' });
|
|
428
440
|
continue;
|
|
@@ -433,6 +445,11 @@ export function selectMultiToolResumePlans(
|
|
|
433
445
|
}
|
|
434
446
|
plans.push({ workflowKey: key, candidate: ws.candidate, sharedContext: ws.sharedContext });
|
|
435
447
|
}
|
|
448
|
+
if (opts?.noInteractive && !opts.allTools && plans.length > 1) {
|
|
449
|
+
const primary = plans.find((plan) => plan.candidate?.primary);
|
|
450
|
+
const selected = primary ?? plans.find((plan) => plan.workflowKey === targetWorkflowKey);
|
|
451
|
+
return { plans: selected ? [selected] : [], skipped };
|
|
452
|
+
}
|
|
436
453
|
return { plans, skipped };
|
|
437
454
|
}
|
|
438
455
|
|
package/src/imprint/teach.ts
CHANGED
|
@@ -69,6 +69,7 @@ import { loadJsonFile } from './load-json.ts';
|
|
|
69
69
|
import { createLog, muteLog, unmuteLog } from './log.ts';
|
|
70
70
|
import { MultiProgress } from './multi-progress.ts';
|
|
71
71
|
import { localSiteDir, localToolDir } from './paths.ts';
|
|
72
|
+
import { rebindExistingBackendsCacheToWorkflow } from './probe-backends.ts';
|
|
72
73
|
import { describeAgentActivity, formatElapsed } from './progress.ts';
|
|
73
74
|
import { record } from './record.ts';
|
|
74
75
|
import { detectPageMintedHeaders, redactSession } from './redact.ts';
|
|
@@ -161,9 +162,11 @@ export {
|
|
|
161
162
|
|
|
162
163
|
/**
|
|
163
164
|
* How many compile agents run in parallel when more than one tool is selected.
|
|
165
|
+
* Each compiler can now invoke a separate live semantic verifier, so keep this
|
|
166
|
+
* at 2 to avoid provider rate-limit bursts and same-site anti-bot pressure.
|
|
164
167
|
* Single-tool runs still use concurrency 1.
|
|
165
168
|
*/
|
|
166
|
-
const COMPILE_CONCURRENCY =
|
|
169
|
+
const COMPILE_CONCURRENCY = 2;
|
|
167
170
|
|
|
168
171
|
/** Module logger — suppressed during teach's spinner phases via muteLog(). */
|
|
169
172
|
const log = createLog('teach');
|
|
@@ -1218,21 +1221,36 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1218
1221
|
});
|
|
1219
1222
|
}
|
|
1220
1223
|
// A `--from-step` resume into plan-prereqs/generate reconstructs the prior
|
|
1221
|
-
// run's tools from persisted state (shared-module planning
|
|
1222
|
-
//
|
|
1223
|
-
//
|
|
1224
|
+
// run's tools from persisted state (shared-module planning may need ≥2 tools).
|
|
1225
|
+
// An explicit --all-tools or interactive resume keeps the full set; documented
|
|
1226
|
+
// non-interactive behavior selects only the persisted primary candidate.
|
|
1227
|
+
// selectMultiToolResumePlans scopes reconstruction to the same recording as the
|
|
1224
1228
|
// resume target and to tools that actually reached `startFrom`'s prerequisites,
|
|
1225
1229
|
// so a sibling from a different run can't be compiled against the wrong session
|
|
1226
1230
|
// and one that failed earlier can't crash loading a missing artifact. Confined
|
|
1227
1231
|
// to --from-step so interactive resume keeps its single-tool behavior.
|
|
1228
1232
|
const allCandidatePlans: CandidateCompilePlan[] = [];
|
|
1229
1233
|
if (opts.fromStep) {
|
|
1230
|
-
|
|
1234
|
+
let plannedToolNames: Set<string> | undefined;
|
|
1235
|
+
const persistedBuildPlanPath = resolveTeachStatePath(site, ws?.buildPlanPath);
|
|
1236
|
+
if (persistedBuildPlanPath && existsSync(persistedBuildPlanPath)) {
|
|
1237
|
+
const persistedBuildPlan = readBuildPlanFile(persistedBuildPlanPath);
|
|
1238
|
+
if (persistedBuildPlan) {
|
|
1239
|
+
plannedToolNames = new Set(persistedBuildPlan.perTool.map((tool) => tool.toolName));
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const selection = selectMultiToolResumePlans(state, workflowKey, startFrom, {
|
|
1243
|
+
noInteractive: opts.noInteractive,
|
|
1244
|
+
allTools: opts.allTools,
|
|
1245
|
+
plannedToolNames,
|
|
1246
|
+
});
|
|
1231
1247
|
for (const { workflowKey: skippedKey, reason } of selection.skipped) {
|
|
1232
1248
|
p.log.warn(
|
|
1233
1249
|
reason === 'different-recording'
|
|
1234
1250
|
? `Skipping tool "${skippedKey}" for --from-step ${startFrom}: it belongs to a different recording than the resume target.`
|
|
1235
|
-
:
|
|
1251
|
+
: reason === 'not-in-build-plan'
|
|
1252
|
+
? `Skipping tool "${skippedKey}" for --from-step ${startFrom}: it is not in the current build plan.`
|
|
1253
|
+
: `Skipping tool "${skippedKey}" for --from-step ${startFrom}: its prior run didn't reach "${startFrom}" — resume an earlier step or re-run it.`,
|
|
1236
1254
|
);
|
|
1237
1255
|
}
|
|
1238
1256
|
for (const sel of selection.plans) {
|
|
@@ -1681,6 +1699,7 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1681
1699
|
allTools: opts.allTools,
|
|
1682
1700
|
buildPlanPath: buildPlanPath || undefined,
|
|
1683
1701
|
sharedModules: sharedModulesManifest.length > 0 ? sharedModulesManifest : undefined,
|
|
1702
|
+
resumeExistingArtifacts: Boolean(opts.fromStep),
|
|
1684
1703
|
});
|
|
1685
1704
|
} finally {
|
|
1686
1705
|
unmuteLog();
|
|
@@ -1774,6 +1793,21 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1774
1793
|
}
|
|
1775
1794
|
}
|
|
1776
1795
|
|
|
1796
|
+
// Capability reductions are agent decisions, not silent compile losses.
|
|
1797
|
+
// Keep the reasoning in workflow.json and repeat it in the final teach
|
|
1798
|
+
// result so operators can distinguish an intentionally smaller honest tool
|
|
1799
|
+
// from a compiler that simply forgot an input.
|
|
1800
|
+
for (const result of results) {
|
|
1801
|
+
for (const limitation of result.workflow.limitations ?? []) {
|
|
1802
|
+
const omitted = limitation.omittedParameters?.length
|
|
1803
|
+
? `; omitted inputs: ${limitation.omittedParameters.join(', ')}`
|
|
1804
|
+
: '';
|
|
1805
|
+
p.log.warn(
|
|
1806
|
+
`tool "${result.workflow.toolName}" capability limitation — ${limitation.feature}: ${limitation.reason}${omitted}`,
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1777
1811
|
// `--to-step emit/generate/compile-playbook` runs the per-tool compile but stops
|
|
1778
1812
|
// before register (platform integration) — inWindow('register') already skipped
|
|
1779
1813
|
// it above. Finish with the phase-window summary instead of the normal "Done!"
|
|
@@ -1918,6 +1952,9 @@ async function compileCandidatePlans(opts: {
|
|
|
1918
1952
|
buildPlanPath?: string;
|
|
1919
1953
|
/** Shared-module build manifest (verified flags) for this site. */
|
|
1920
1954
|
sharedModules?: SharedModuleManifestEntry[];
|
|
1955
|
+
/** The user explicitly resumed a prior teach phase; generated artifacts that
|
|
1956
|
+
* already exist are revision inputs, not disposable scratch output. */
|
|
1957
|
+
resumeExistingArtifacts?: boolean;
|
|
1921
1958
|
}): Promise<TeachToolResult[]> {
|
|
1922
1959
|
const concurrency = opts.plans.length === 1 ? 1 : COMPILE_CONCURRENCY;
|
|
1923
1960
|
const mp = opts.plans.length > 1 ? new MultiProgress() : null;
|
|
@@ -2179,12 +2216,15 @@ async function compileSelectedCandidate(opts: {
|
|
|
2179
2216
|
teachCredentials?: { site: string; values: Record<string, string> };
|
|
2180
2217
|
buildPlanPath?: string;
|
|
2181
2218
|
sharedModules?: SharedModuleManifestEntry[];
|
|
2219
|
+
resumeExistingArtifacts?: boolean;
|
|
2182
2220
|
}): Promise<TeachToolResult> {
|
|
2183
2221
|
const { plan, site, state } = opts;
|
|
2184
2222
|
const startIdx = STEPS.indexOf(plan.startFrom);
|
|
2185
2223
|
const toolName = plan.candidate?.toolName ?? plan.workflowKey;
|
|
2186
2224
|
const workflowDir = localToolDir(site, toolName);
|
|
2187
2225
|
mkdirSync(workflowDir, { recursive: true });
|
|
2226
|
+
const revisionMode =
|
|
2227
|
+
Boolean(opts.resumeExistingArtifacts) && existsSync(pathJoin(workflowDir, 'workflow.json'));
|
|
2188
2228
|
|
|
2189
2229
|
// The per-tool compile (generate → compile-playbook → emit) is ATOMIC by design:
|
|
2190
2230
|
// each phase gates on startIdx ONLY (not the window's stopIdx), so once started it
|
|
@@ -2203,19 +2243,20 @@ async function compileSelectedCandidate(opts: {
|
|
|
2203
2243
|
// mapping, request construction, response parsing, shared-module imports),
|
|
2204
2244
|
// then run a single compile that follows it. Best-effort — a timeout or
|
|
2205
2245
|
// error yields no plan and the compile proceeds exactly as before.
|
|
2206
|
-
const toolPlan =
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2246
|
+
const toolPlan =
|
|
2247
|
+
plan.candidate && !revisionMode
|
|
2248
|
+
? await planToolCompile({
|
|
2249
|
+
site,
|
|
2250
|
+
toolName,
|
|
2251
|
+
candidate: plan.candidate,
|
|
2252
|
+
sharedContext: plan.sharedContext,
|
|
2253
|
+
sessionPath: opts.sessionPath,
|
|
2254
|
+
buildPlanPath: opts.buildPlanPath,
|
|
2255
|
+
sharedModules: opts.sharedModules,
|
|
2256
|
+
providerName: opts.providerName,
|
|
2257
|
+
model: opts.compileModel,
|
|
2258
|
+
})
|
|
2259
|
+
: undefined;
|
|
2219
2260
|
|
|
2220
2261
|
const result = await generate({
|
|
2221
2262
|
sessionPath: opts.sessionPath,
|
|
@@ -2232,6 +2273,7 @@ async function compileSelectedCandidate(opts: {
|
|
|
2232
2273
|
buildPlanPath: opts.buildPlanPath,
|
|
2233
2274
|
sharedModules: opts.sharedModules,
|
|
2234
2275
|
toolPlan,
|
|
2276
|
+
revisionMode,
|
|
2235
2277
|
});
|
|
2236
2278
|
|
|
2237
2279
|
assertCandidateToolName('Compiled workflow', result.workflow.toolName, plan.candidate);
|
|
@@ -2253,6 +2295,16 @@ async function compileSelectedCandidate(opts: {
|
|
|
2253
2295
|
if (!genResult) {
|
|
2254
2296
|
throw new Error(`generate step did not produce a workflow for "${toolName}".`);
|
|
2255
2297
|
}
|
|
2298
|
+
// Semantic verification may revise the workflow after generate() returns
|
|
2299
|
+
// (for example, omitting an unsupported secondary parameter and recording a
|
|
2300
|
+
// limitation). Reload that final artifact before compiling any fallback so
|
|
2301
|
+
// later agents cannot work from the pre-verification object.
|
|
2302
|
+
genResult.workflow = loadJsonFile(
|
|
2303
|
+
genResult.workflowPath,
|
|
2304
|
+
WorkflowSchema,
|
|
2305
|
+
{ notFound: `workflow.json not found at ${genResult.workflowPath}` },
|
|
2306
|
+
'workflow.json',
|
|
2307
|
+
);
|
|
2256
2308
|
|
|
2257
2309
|
// ── Step 2: compile-playbook (after generate — runtime artifact, not needed for dual-pass) ──
|
|
2258
2310
|
let pbResult: { playbook: Playbook; playbookPath: string };
|
|
@@ -2262,6 +2314,7 @@ async function compileSelectedCandidate(opts: {
|
|
|
2262
2314
|
outPath: pathJoin(workflowDir, 'playbook.yaml'),
|
|
2263
2315
|
llmConfig: { provider: opts.providerName },
|
|
2264
2316
|
candidate: plan.candidate,
|
|
2317
|
+
workflow: genResult.workflow,
|
|
2265
2318
|
sharedContext: plan.sharedContext,
|
|
2266
2319
|
preTriagedSession: opts.sharedTriageResult,
|
|
2267
2320
|
});
|
|
@@ -2292,6 +2345,11 @@ async function compileSelectedCandidate(opts: {
|
|
|
2292
2345
|
|
|
2293
2346
|
exportSiteManifest(site, workflowDir, genResult.workflow, pbResult.playbook);
|
|
2294
2347
|
|
|
2348
|
+
// Emission and post-verification annotations can change workflow.json's
|
|
2349
|
+
// metadata hash without changing the already-proven transport behavior.
|
|
2350
|
+
// Reuse the existing no-network rebind so a bounded resume does not trigger
|
|
2351
|
+
// an unnecessary comprehensive probe on its first real call.
|
|
2352
|
+
rebindExistingBackendsCacheToWorkflow(workflowDir);
|
|
2295
2353
|
await writeQuickBackendsCache(workflowDir, genResult.workflow);
|
|
2296
2354
|
|
|
2297
2355
|
return {
|
package/src/imprint/types.ts
CHANGED
|
@@ -257,6 +257,28 @@ const WorkflowRequestSchema = z.object({
|
|
|
257
257
|
timeoutMs: z.number().int().positive().optional(),
|
|
258
258
|
pollIntervalMs: z.number().int().positive().optional(),
|
|
259
259
|
urlIncludes: z.string().min(1).optional(),
|
|
260
|
+
/** Wait until the rendered document contains this CSS selector before
|
|
261
|
+
* snapshotting outerHTML. Useful for pages whose meaningful results
|
|
262
|
+
* hydrate after the load event. */
|
|
263
|
+
selector: z.string().min(1).optional(),
|
|
264
|
+
/** Optional browser interactions performed after the initial navigation
|
|
265
|
+
* predicate is satisfied. This is a generic escape hatch for workflows
|
|
266
|
+
* whose next request is assembled from private in-page state rather than
|
|
267
|
+
* values serialized into HTML. */
|
|
268
|
+
actions: z
|
|
269
|
+
.array(
|
|
270
|
+
z
|
|
271
|
+
.object({
|
|
272
|
+
action: z.literal('click'),
|
|
273
|
+
selector: z.string().min(1),
|
|
274
|
+
})
|
|
275
|
+
.strict(),
|
|
276
|
+
)
|
|
277
|
+
.min(1)
|
|
278
|
+
.optional(),
|
|
279
|
+
/** After navigation actions run, wait for this rendered selector before
|
|
280
|
+
* snapshotting the final document. */
|
|
281
|
+
resultSelector: z.string().min(1).optional(),
|
|
260
282
|
cookie: z
|
|
261
283
|
.object({
|
|
262
284
|
name: z.string().min(1),
|
|
@@ -371,15 +393,30 @@ export const WorkflowSchema = z.object({
|
|
|
371
393
|
*
|
|
372
394
|
* Return value:
|
|
373
395
|
* - `string` — the transformed URL (backward-compatible).
|
|
374
|
-
* - `{ url?: string; body?: string; headers?: Record<string, string>; skip?: boolean }` —
|
|
396
|
+
* - `{ url?: string; body?: string; headers?: Record<string, string>; navigation?: object; skip?: boolean }` —
|
|
375
397
|
* URL plus optional body and header overrides for complex body formats
|
|
376
398
|
* (JSPB, nested JSON-in-form) where placeholder substitution alone
|
|
377
|
-
* cannot handle the encoding. `
|
|
399
|
+
* cannot handle the encoding. `navigation` may supply dynamic browser
|
|
400
|
+
* navigation actions derived from resolved parameters. `skip: true` skips this request, for
|
|
378
401
|
* conditional follow-up requests such as pagination/detail calls.
|
|
379
402
|
*
|
|
380
403
|
* The optional 4th arg `params` carries the resolved workflow parameters
|
|
381
404
|
* so the transform can construct request bodies programmatically. */
|
|
382
405
|
requestTransformModule: z.string().optional(),
|
|
406
|
+
/** Agent-authored capability limitations retained with the generated tool.
|
|
407
|
+
* A compiler may deliberately omit a secondary feature or parameter when the
|
|
408
|
+
* recording and live evidence cannot support it without guessing. Runtime
|
|
409
|
+
* execution does not interpret these notes; MCP descriptions and teach
|
|
410
|
+
* summaries surface them to callers and operators. */
|
|
411
|
+
limitations: z
|
|
412
|
+
.array(
|
|
413
|
+
z.object({
|
|
414
|
+
feature: z.string().min(1),
|
|
415
|
+
reason: z.string().min(1),
|
|
416
|
+
omittedParameters: z.array(z.string().min(1)).optional(),
|
|
417
|
+
}),
|
|
418
|
+
)
|
|
419
|
+
.optional(),
|
|
383
420
|
/** Did this tool's integration test produce live data at compile time?
|
|
384
421
|
*
|
|
385
422
|
* - `liveVerified: true` (default when present) — the integration test
|