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.
@@ -410,19 +410,31 @@ interface MultiToolResumeSelection {
410
410
  candidate: WorkflowState['candidate'];
411
411
  sharedContext: WorkflowState['sharedContext'];
412
412
  }[];
413
- skipped: { workflowKey: string; reason: 'different-recording' | 'not-resumable' }[];
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
 
@@ -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 = 3;
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 needs ≥2 tools, and
1222
- // a multi-tool generate resumes all of them) — not just the most-recent
1223
- // workflow. selectMultiToolResumePlans scopes this to the same recording as the
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
- const selection = selectMultiToolResumePlans(state, workflowKey, startFrom);
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
- : `Skipping tool "${skippedKey}" for --from-step ${startFrom}: its prior run didn't reach "${startFrom}" — resume an earlier step or re-run it.`,
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 = plan.candidate
2207
- ? await planToolCompile({
2208
- site,
2209
- toolName,
2210
- candidate: plan.candidate,
2211
- sharedContext: plan.sharedContext,
2212
- sessionPath: opts.sessionPath,
2213
- buildPlanPath: opts.buildPlanPath,
2214
- sharedModules: opts.sharedModules,
2215
- providerName: opts.providerName,
2216
- model: opts.compileModel,
2217
- })
2218
- : undefined;
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 {
@@ -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. `skip: true` skips this request, for
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