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.
@@ -28,6 +28,11 @@ import {
28
28
  externalVerification,
29
29
  } from './compile-tools.ts';
30
30
  import { type Replacement, extractCredentials } from './credential-extract.ts';
31
+ import {
32
+ mergeSemanticParamVerification,
33
+ runLiveSemanticVerification,
34
+ semanticVerificationFailures,
35
+ } from './live-verifier.ts';
31
36
  import {
32
37
  type LLMOptions,
33
38
  type ProviderName,
@@ -39,6 +44,7 @@ import {
39
44
  import { loadJsonFile } from './load-json.ts';
40
45
  import { createLog } from './log.ts';
41
46
  import { localSiteDir } from './paths.ts';
47
+ import { rebindExistingBackendsCacheToWorkflow } from './probe-backends.ts';
42
48
  import { detectPageMintedHeaders, redactSession } from './redact.ts';
43
49
  import type { ClassifiedValue } from './session-diff.ts';
44
50
  import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
@@ -114,6 +120,19 @@ interface CompileAgentOptions {
114
120
  * response parsing, shared-module imports). Injected into the agent's initial
115
121
  * message so the compile follows it. Generic — not tied to any site. */
116
122
  toolPlan?: string;
123
+ /** Revise an existing generated artifact, using durable verification feedback
124
+ * as the starting point instead of rebuilding it from raw capture. */
125
+ revisionMode?: boolean;
126
+ }
127
+
128
+ function formatRevisionMode(enabled: boolean | undefined): string {
129
+ return enabled
130
+ ? [
131
+ 'REVISION MODE: this is a bounded resume of an existing generated tool, not a from-scratch compile.',
132
+ 'Use read_session_summary.revisionContext, then read the listed current artifacts and durable verification feedback before inspecting raw response bodies.',
133
+ 'Preserve proven behavior and make the smallest evidence-backed repair or contract reduction.',
134
+ ].join(' ')
135
+ : '';
117
136
  }
118
137
 
119
138
  export async function compileAgent(opts: CompileAgentOptions): Promise<CompileAgentResult> {
@@ -216,6 +235,7 @@ export async function compileAgent(opts: CompileAgentOptions): Promise<CompileAg
216
235
  teachCredentials: opts.teachCredentials,
217
236
  buildPlanPath: opts.buildPlanPath,
218
237
  sharedModules: opts.sharedModules,
238
+ revisionMode: opts.revisionMode,
219
239
  }),
220
240
  doneTool(),
221
241
  giveUpTool(),
@@ -229,11 +249,12 @@ Tool directory: ${absoluteToolDir}
229
249
  You will write artifacts into the tool directory.
230
250
  ${formatCandidateContext(opts.candidate, opts.sharedContext, assignedSharedModules)}
231
251
  ${formatToolPlan(opts.toolPlan)}
252
+ ${formatRevisionMode(opts.revisionMode)}
232
253
 
233
254
  Begin by calling read_session_summary to orient yourself, then proceed per the system prompt.`;
234
255
 
235
256
  // 7. Compute deadline
236
- const deadlineMs = Date.now() + (opts.maxDurationMs ?? 20 * 60 * 1000);
257
+ let deadlineMs = Date.now() + (opts.maxDurationMs ?? 20 * 60 * 1000);
237
258
 
238
259
  // 8. Instantiate provider (or use injected one for testing).
239
260
  // CLI providers take a different path: they don't implement Anthropic
@@ -260,6 +281,7 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
260
281
  buildPlanPath: opts.buildPlanPath,
261
282
  sharedModules: opts.sharedModules,
262
283
  toolPlan: opts.toolPlan,
284
+ revisionMode: opts.revisionMode,
263
285
  model: opts.llmConfig?.model,
264
286
  });
265
287
  }
@@ -278,6 +300,7 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
278
300
  buildPlanPath: opts.buildPlanPath,
279
301
  sharedModules: opts.sharedModules,
280
302
  toolPlan: opts.toolPlan,
303
+ revisionMode: opts.revisionMode,
281
304
  model: opts.llmConfig?.model,
282
305
  });
283
306
  }
@@ -351,11 +374,9 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
351
374
  }
352
375
 
353
376
  // Perform external verification
354
- const { failures, warnings, paramVerification, liveVerification } = await externalVerification(
355
- absoluteToolDir,
356
- session,
357
- sessionPathAbs,
358
- {
377
+ const deterministicVerificationStartedAt = Date.now();
378
+ const { failures, warnings, paramVerification, integrationEvidence } =
379
+ await externalVerification(absoluteToolDir, session, sessionPathAbs, {
359
380
  expectedToolName: opts.candidate?.toolName,
360
381
  likelyParams: opts.candidate?.likelyParams,
361
382
  candidateRequestSeqs: opts.candidate?.requestSeqs,
@@ -370,35 +391,58 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
370
391
  requiredInputs,
371
392
  credentialValues: opts.teachCredentials?.values,
372
393
  credentialNames: opts.sharedContext?.credentialNames,
373
- },
374
- );
394
+ deferLiveIntegrationToSemanticAgent: true,
395
+ });
396
+ deadlineMs += Date.now() - deterministicVerificationStartedAt;
375
397
 
376
398
  if (warnings.length > 0) {
377
399
  log(`verification warnings (non-blocking):\n${warnings.join('\n')}`);
378
400
  }
379
401
 
380
402
  if (failures.length === 0) {
381
- // Success (possibly with warnings). Persist per-parameter verified flags
382
- // and the live-verification stamp into workflow.json so downstream
383
- // (audit, teach summary) can see which tools shipped without a passing
384
- // live call.
385
- applyLiveVerification(absoluteToolDir, liveVerification);
386
- const paramWarnings = applyParamVerification(absoluteToolDir, paramVerification);
387
- const allWarnings = [...warnings, ...paramWarnings];
388
- if (paramWarnings.length > 0) {
389
- log(`parameter verification:\n${paramWarnings.join('\n')}`);
390
- }
391
- message = result.doneSummary ?? 'Task completed';
392
- if (allWarnings.length > 0) {
393
- message += `\n\nWarnings:\n${allWarnings.join('\n')}`;
403
+ const semanticVerificationStartedAt = Date.now();
404
+ const semantic = await runLiveSemanticVerification({
405
+ provider: provider.name,
406
+ toolDir: absoluteToolDir,
407
+ evidence: integrationEvidence,
408
+ // Live semantic verification is a distinct phase. Do not inherit a
409
+ // nearly exhausted compile-agent deadline.
410
+ });
411
+ deadlineMs += Date.now() - semanticVerificationStartedAt;
412
+ failures.push(...semanticVerificationFailures(semantic.report));
413
+ if (semantic.report.status === 'approved_with_gaps') {
414
+ warnings.push(
415
+ `independent semantic verification approved with gaps: ${semantic.report.gaps.join('; ')}`,
416
+ );
394
417
  }
395
- if (!opts.keepTest) {
396
- for (const f of ['parser.test.ts', 'integration.test.ts']) {
397
- const testPath = pathJoin(absoluteToolDir, f);
398
- if (existsSync(testPath)) unlinkSync(testPath);
418
+ if (failures.length === 0) {
419
+ // Success (possibly with explicit semantic gaps). Persist per-parameter
420
+ // flags and mint liveVerified=true only after independent approval.
421
+ applyLiveVerification(absoluteToolDir, undefined);
422
+ const paramWarnings = applyParamVerification(
423
+ absoluteToolDir,
424
+ mergeSemanticParamVerification(paramVerification, semantic.report),
425
+ );
426
+ // liveVerified/parameter annotations change the hash-strict cache key,
427
+ // but not transport capabilities. Rebind the backend already exercised
428
+ // by the verifier instead of triggering another probe.
429
+ rebindExistingBackendsCacheToWorkflow(absoluteToolDir);
430
+ const allWarnings = [...warnings, ...paramWarnings];
431
+ if (paramWarnings.length > 0) {
432
+ log(`parameter verification:\n${paramWarnings.join('\n')}`);
433
+ }
434
+ message = result.doneSummary ?? 'Task completed';
435
+ if (allWarnings.length > 0) {
436
+ message += `\n\nWarnings:\n${allWarnings.join('\n')}`;
399
437
  }
438
+ if (!opts.keepTest) {
439
+ for (const f of ['parser.test.ts', 'integration.test.ts']) {
440
+ const testPath = pathJoin(absoluteToolDir, f);
441
+ if (existsSync(testPath)) unlinkSync(testPath);
442
+ }
443
+ }
444
+ break;
400
445
  }
401
- break;
402
446
  }
403
447
 
404
448
  // Verification failed — re-enter the loop with a continuation message
@@ -414,6 +458,7 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
414
458
  ${failures.map((f) => `- ${f}`).join('\n')}
415
459
 
416
460
  Resume your work. Read the files you wrote (workflow.json, parser.ts, parser.test.ts), fix the issues, re-run tests, and call done again when fixed.`;
461
+ currentInitialMessage += `\n\nUse the verifier's semantic evidence, not a requirement for 100% of the original candidate inputs. If the core intent works but a secondary parameter cannot be supported from the recording and live evidence without guessing, remove that parameter and add workflow.limitations with omittedParameters and a specific reason. Do not use a limitation to hide a broken core tool, and do not keep a known broken or ignored public input.`;
417
462
  }
418
463
 
419
464
  // 10. Final flush of the complete conversation log
@@ -11,7 +11,9 @@ import {
11
11
  existsSync,
12
12
  mkdirSync,
13
13
  readFileSync,
14
+ readdirSync,
14
15
  realpathSync,
16
+ statSync,
15
17
  unlinkSync,
16
18
  writeFileSync,
17
19
  } from 'node:fs';
@@ -27,6 +29,11 @@ import {
27
29
  readBuildPlanFile,
28
30
  resolveAssignedModules,
29
31
  } from './build-plan.ts';
32
+ import {
33
+ LIVE_EVIDENCE_PATH_ENV,
34
+ type LiveIntegrationEvidence,
35
+ readLiveIntegrationEvidence,
36
+ } from './compile-verification.ts';
30
37
  import { splitSetCookieHeader } from './cookie-jar.ts';
31
38
  import { isSameRegistrableDomain, registrableDomain } from './etld.ts';
32
39
  import {
@@ -80,7 +87,7 @@ export function buildCompileTools(
80
87
  ? { IMPRINT_TEACH_CREDENTIALS: JSON.stringify(context.teachCredentials) }
81
88
  : undefined;
82
89
  const tools = [
83
- buildReadSessionSummaryTool(session, context),
90
+ buildReadSessionSummaryTool(session, context, toolDir),
84
91
  buildReadRequestTool(session),
85
92
  buildSearchRequestsTool(session),
86
93
  buildRevealRequestTool(sessionPath),
@@ -115,6 +122,10 @@ export interface CompileToolContext {
115
122
  buildPlanPath?: string;
116
123
  /** Shared-module build manifest (verified flags) for this site. */
117
124
  sharedModules?: SharedModuleManifestEntry[];
125
+ /** A bounded teach resume is revising an already-generated tool. Surface the
126
+ * existing artifact and durable verifier/audit feedback to the agent so it
127
+ * preserves proven behavior instead of re-deriving the tool from raw capture. */
128
+ revisionMode?: boolean;
118
129
  }
119
130
 
120
131
  // ─── Tool: read_build_plan ───────────────────────────────────────────────────
@@ -164,7 +175,7 @@ function buildReadBuildPlanTool(
164
175
  ? ` The producer declares shape: ${producerShape}. If that shape contains requiredKeys=[...], request-transform.ts must decode/unpack the fresh value and reference each required key in the outgoing request.`
165
176
  : '';
166
177
  tokenNotes.push(
167
- `CONSUMER CONTRACT: param \`${tp.param}\` is an opaque token minted by the \`${tp.sourceTool}\` tool's \`${tp.sourceField}\` output.${shapeNote} Write a CHAINED \`param:${tp.param}\` integration test that calls \`runWorkflowWithLadder\` on \`../${tp.sourceTool}/workflow.json\` directly inside the \`param:${tp.param}\` test block, reads \`${tp.sourceField}\` from the producer's ACTUAL output shape (top-level field, or an item in any returned collection such as flights/items/results), and passes THAT fresh value (not the recorded constant) into this tool — then asserts the response is non-empty. When several params come from the same producer, mint once and choose one producer item containing all sibling fields so the values stay from the same result. On producer bot/infra error, rethrow so the suite waives; if the producer ran but the field is absent, fix the producer parser/output contract instead of waiving. Do not fabricate a fallback value or mutate producer.result.data to make the test pass.`,
178
+ `CONSUMER CONTRACT: param \`${tp.param}\` is an opaque token minted by the \`${tp.sourceTool}\` tool's \`${tp.sourceField}\` output.${shapeNote} Write a CHAINED \`param:${tp.param}\` integration test that calls \`runCapturedIntegrationCase\` on \`../${tp.sourceTool}/workflow.json\` directly inside the \`param:${tp.param}\` test block, reads \`${tp.sourceField}\` from the producer's ACTUAL output shape (top-level field, or an item in any returned collection such as flights/items/results), and passes THAT fresh value (not the recorded constant) into this tool — then asserts the response is non-empty. When several params come from the same producer, mint once and choose one producer item containing all sibling fields so the values stay from the same result. On producer bot/infra error, rethrow so the suite waives; if the producer ran but the field is absent, fix the producer parser/output contract instead of waiving. Do not fabricate a fallback value or mutate producer.result.data to make the test pass.`,
168
179
  );
169
180
  }
170
181
  // Per-source wiring guidance for the general dependency contract. These are
@@ -232,6 +243,7 @@ function buildReadBuildPlanTool(
232
243
  export function buildReadSessionSummaryTool(
233
244
  session: Session,
234
245
  context: CompileToolContext,
246
+ toolDir?: string,
235
247
  ): AgentTool {
236
248
  return {
237
249
  name: 'read_session_summary',
@@ -343,6 +355,10 @@ export function buildReadSessionSummaryTool(
343
355
  captureHints: captureHints.length > 0 ? captureHints : undefined,
344
356
  paramGroundingHints: paramGroundingHints.length > 0 ? paramGroundingHints : undefined,
345
357
  inputProvenanceHints: inputProvenanceHints.length > 0 ? inputProvenanceHints : undefined,
358
+ revisionContext:
359
+ context.revisionMode && toolDir
360
+ ? buildExistingArtifactRevisionContext(toolDir)
361
+ : undefined,
346
362
  loadBearingRequests,
347
363
  };
348
364
 
@@ -361,6 +377,65 @@ export function buildReadSessionSummaryTool(
361
377
  };
362
378
  }
363
379
 
380
+ const REVISION_ARTIFACT_NAMES = [
381
+ 'workflow.json',
382
+ 'parser.ts',
383
+ 'request-transform.ts',
384
+ 'integration.test.ts',
385
+ 'playbook.yaml',
386
+ ] as const;
387
+
388
+ const REVISION_DIAGNOSTIC_NAMES = [
389
+ '.live-verification.json',
390
+ '.live-verification-evidence.json',
391
+ '.live-verifier-log.jsonl',
392
+ ] as const;
393
+
394
+ /** Describe, but do not interpret or gate on, prior compile evidence. The
395
+ * compile agent owns the judgment: it can read any listed file through the
396
+ * existing read_file tool and decide what to preserve, repair, narrow, or
397
+ * document. This is intentionally agent context rather than another runtime
398
+ * validator. */
399
+ function buildExistingArtifactRevisionContext(toolDir: string): Record<string, unknown> {
400
+ const describe = (name: string): { path: string; bytes: number } | null => {
401
+ const absolute = pathJoin(toolDir, name);
402
+ if (!existsSync(absolute)) return null;
403
+ try {
404
+ return { path: name, bytes: statSync(absolute).size };
405
+ } catch {
406
+ return null;
407
+ }
408
+ };
409
+
410
+ const artifacts = REVISION_ARTIFACT_NAMES.map(describe).filter(
411
+ (entry): entry is { path: string; bytes: number } => entry !== null,
412
+ );
413
+ const diagnostics = REVISION_DIAGNOSTIC_NAMES.map(describe).filter(
414
+ (entry): entry is { path: string; bytes: number } => entry !== null,
415
+ );
416
+ const notesDir = pathJoin(toolDir, 'notes');
417
+ const feedbackNotes = existsSync(notesDir)
418
+ ? readdirSync(notesDir)
419
+ .filter((name) => name.endsWith('.md'))
420
+ .sort()
421
+ .map((name) => describe(pathJoin('notes', name)))
422
+ .filter((entry): entry is { path: string; bytes: number } => entry !== null)
423
+ : [];
424
+
425
+ return {
426
+ mode: 'revise_existing_artifact',
427
+ instruction: [
428
+ 'This bounded resume is a revision, not a from-scratch compile.',
429
+ 'Read the existing workflow, parser, request transform, integration test, durable live-verification diagnostics, and feedback notes before re-reading raw recording bodies.',
430
+ 'Preserve behavior that live evidence proves works. Repair or narrow only the behavior contradicted by evidence.',
431
+ 'If a secondary branch cannot be grounded, remove its dependent parameters and claims together and record the reasoning in workflow limitations; do not guess.',
432
+ ].join(' '),
433
+ existingArtifacts: artifacts,
434
+ durableDiagnostics: diagnostics,
435
+ feedbackNotes,
436
+ };
437
+ }
438
+
364
439
  // ─── Inline request/response data for candidate-scoped requests ─────────────
365
440
 
366
441
  // claude-cli truncates tool results > ~40K chars. Keep the total summary
@@ -1345,6 +1420,7 @@ export async function runCommand(
1345
1420
  cwd: string,
1346
1421
  timeoutMs: number,
1347
1422
  extraEnv?: Record<string, string>,
1423
+ onOutput?: (stream: 'stdout' | 'stderr', chunk: string) => void,
1348
1424
  ): Promise<{ result: string; isError?: boolean }> {
1349
1425
  return new Promise((resolve) => {
1350
1426
  // `detached: true` makes the child its own process-group leader so a timeout
@@ -1362,11 +1438,15 @@ export async function runCommand(
1362
1438
  const TRUNCATE_LIMIT = 16 * 1024; // 16KB
1363
1439
 
1364
1440
  proc.stdout.on('data', (chunk) => {
1365
- stdout += chunk.toString();
1441
+ const text = chunk.toString();
1442
+ stdout += text;
1443
+ onOutput?.('stdout', text);
1366
1444
  });
1367
1445
 
1368
1446
  proc.stderr.on('data', (chunk) => {
1369
- stderr += chunk.toString();
1447
+ const text = chunk.toString();
1448
+ stderr += text;
1449
+ onOutput?.('stderr', text);
1370
1450
  });
1371
1451
 
1372
1452
  const timeout = setTimeout(() => {
@@ -1716,7 +1796,7 @@ interface BunTestRun {
1716
1796
 
1717
1797
  /** Per-exposed-parameter verification outcome. `verified` is true only when a
1718
1798
  * `param:<name>` integration test actually ran green against live data. */
1719
- interface ParamVerification {
1799
+ export interface ParamVerification {
1720
1800
  name: string;
1721
1801
  verified: boolean;
1722
1802
  /** Why an exposed param is unverified. Undefined when `verified` is true.
@@ -1727,7 +1807,7 @@ interface ParamVerification {
1727
1807
  * - `waived-chain`: the param is a producer-sourced token but the producer
1728
1808
  * tool could not be run at compile time (anti-bot / not compiled), so the
1729
1809
  * chain could not be verified. */
1730
- reason?: 'waived-bot' | 'waived-infra' | 'annotated' | 'waived-chain';
1810
+ reason?: 'waived-bot' | 'waived-infra' | 'annotated' | 'waived-chain' | 'semantic-gap';
1731
1811
  /** For a producer-sourced token param, the sibling tool + output field its
1732
1812
  * value comes from. Stamped into workflow.json (`param.sourcedFrom`) so the
1733
1813
  * MCP description tells the orchestrating LLM where to mint it and the audit
@@ -1751,11 +1831,15 @@ interface TokenSource {
1751
1831
  * bot-defense / infra detection and error surfacing) and the per-test pass/fail
1752
1832
  * names via a JUnit report written to a transient file in the tool dir.
1753
1833
  */
1754
- async function runBunTestWithResults(
1834
+ export async function runBunTestWithResults(
1755
1835
  testPath: string,
1756
1836
  toolDir: string,
1757
1837
  timeoutMs: number,
1758
1838
  env: Record<string, string> = {},
1839
+ opts: {
1840
+ bail?: boolean;
1841
+ onOutput?: (stream: 'stdout' | 'stderr', chunk: string) => void;
1842
+ } = {},
1759
1843
  ): Promise<BunTestRun> {
1760
1844
  const junitPath = pathJoin(toolDir, `.imprint-junit-${basename(testPath)}.xml`);
1761
1845
  try {
@@ -1764,10 +1848,11 @@ async function runBunTestWithResults(
1764
1848
  // best-effort
1765
1849
  }
1766
1850
  const result = await runCommand(
1767
- `bun test ${testPath} --reporter=junit --reporter-outfile=${junitPath}`,
1851
+ `bun test ${testPath} --reporter=junit --reporter-outfile=${junitPath}${opts.bail ? ' --bail=1' : ''}`,
1768
1852
  toolDir,
1769
1853
  timeoutMs,
1770
1854
  env,
1855
+ opts.onOutput,
1771
1856
  );
1772
1857
  const output = JSON.parse(result.result) as {
1773
1858
  stdout: string;
@@ -1802,6 +1887,10 @@ interface TestBlock {
1802
1887
  body: string;
1803
1888
  }
1804
1889
 
1890
+ function hasCapturedLiveWorkflowCall(source: string): boolean {
1891
+ return /\b(?:runWorkflowWithLadder|runCapturedIntegrationCase)\s*\(/.test(source);
1892
+ }
1893
+
1805
1894
  /** Split a test file into `test(...)` / `it(...)` blocks (title + source from
1806
1895
  * that test's start to the next test's start). Good enough to check whether a
1807
1896
  * named per-parameter test's body actually calls the workflow. */
@@ -2241,7 +2330,7 @@ export function classifyParamCoverage(opts: {
2241
2330
  if (passedLive) {
2242
2331
  const chained =
2243
2332
  !!block &&
2244
- /runWorkflowWithLadder\s*\(/.test(block.body) &&
2333
+ hasCapturedLiveWorkflowCall(block.body) &&
2245
2334
  SIBLING_WORKFLOW_RE.test(block.body);
2246
2335
  if (chained) {
2247
2336
  paramVerification.push({ name: lp.name, verified: true, sourcedFrom });
@@ -2269,7 +2358,7 @@ export function classifyParamCoverage(opts: {
2269
2358
  if (passedLive) {
2270
2359
  // Anti-tautology: a passing per-param test must actually exercise the live
2271
2360
  // workflow, not assert a constant.
2272
- if (block && !/runWorkflowWithLadder\s*\(/.test(block.body)) {
2361
+ if (block && !hasCapturedLiveWorkflowCall(block.body)) {
2273
2362
  tautological.push(lp.name);
2274
2363
  } else {
2275
2364
  paramVerification.push({ name: lp.name, verified: true });
@@ -2302,7 +2391,7 @@ export function hasLiveBaselineWorkflowTest(integrationSrc: string): boolean {
2302
2391
  return extractTestBlocks(integrationSrc).some(
2303
2392
  (block) =>
2304
2393
  !block.title.includes('param:') &&
2305
- /\brunWorkflowWithLadder\s*\(/.test(block.body) &&
2394
+ hasCapturedLiveWorkflowCall(block.body) &&
2306
2395
  !returnsOnFailedResult.test(block.body) &&
2307
2396
  (requiresOk.test(block.body) || throwsOnFailure.test(block.body)),
2308
2397
  );
@@ -3427,6 +3516,10 @@ export async function externalVerification(
3427
3516
  * verifier blocks invented ${credential.X} placeholders that are not in this
3428
3517
  * set or in requiredInputs. */
3429
3518
  credentialNames?: string[];
3519
+ /** Agentic compile path only: validate the integration suite's source here,
3520
+ * but let the independent semantic verifier invoke the final live suite as
3521
+ * a tool. Parser/type/schema checks remain deterministic. */
3522
+ deferLiveIntegrationToSemanticAgent?: boolean;
3430
3523
  } = {},
3431
3524
  ): Promise<{
3432
3525
  failures: string[];
@@ -3440,9 +3533,13 @@ export async function externalVerification(
3440
3533
  firstError: string;
3441
3534
  exhaustedBackends: string[];
3442
3535
  };
3536
+ /** Actual tool-level inputs and parsed results captured by the single live
3537
+ * integration run. The caller hands these to the independent semantic agent. */
3538
+ integrationEvidence: LiveIntegrationEvidence[];
3443
3539
  }> {
3444
3540
  const failures: string[] = [];
3445
3541
  const warnings: string[] = [];
3542
+ let integrationEvidence: LiveIntegrationEvidence[] = [];
3446
3543
  const paramVerification: ParamVerification[] = [];
3447
3544
  let liveVerification:
3448
3545
  | { kind: 'waived-bot' | 'waived-infra'; firstError: string; exhaustedBackends: string[] }
@@ -3647,6 +3744,7 @@ export async function externalVerification(
3647
3744
 
3648
3745
  const notTemplated: string[] = [];
3649
3746
  const inventedOnly: string[] = [];
3747
+ const intentionallyOmitted: Array<{ name: string; feature: string; reason: string }> = [];
3650
3748
 
3651
3749
  for (const lp of opts.likelyParams) {
3652
3750
  const placeholder = `\${param.${lp.name}}`;
@@ -3684,7 +3782,19 @@ export async function externalVerification(
3684
3782
  }
3685
3783
 
3686
3784
  if (!inBody && !inHeader && !inOriginalQuery && !inInventedQuery) {
3687
- notTemplated.push(lp.name);
3785
+ const limitation = workflow.limitations?.find((entry) =>
3786
+ entry.omittedParameters?.includes(lp.name),
3787
+ );
3788
+ const stillDeclared = workflow.parameters.some((param) => param.name === lp.name);
3789
+ if (limitation && !stillDeclared) {
3790
+ intentionallyOmitted.push({
3791
+ name: lp.name,
3792
+ feature: limitation.feature,
3793
+ reason: limitation.reason,
3794
+ });
3795
+ } else {
3796
+ notTemplated.push(lp.name);
3797
+ }
3688
3798
  } else if (!inBody && !inHeader && !inOriginalQuery && inInventedQuery) {
3689
3799
  inventedOnly.push(lp.name);
3690
3800
  }
@@ -3692,7 +3802,7 @@ export async function externalVerification(
3692
3802
 
3693
3803
  if (notTemplated.length > 0) {
3694
3804
  failures.push(
3695
- `${notTemplated.length} likelyParam(s) are not templated in any request: ${notTemplated.join(', ')}. Each must appear as \${param.NAME} in a request URL, body, or header. For parameters recorded as null or [] (filters the user toggled but didn\'t apply), find the correct position in the request body and replace the placeholder value with \${param.NAME}.`,
3805
+ `${notTemplated.length} likelyParam(s) have no valid disposition: ${notTemplated.join(', ')}. Each must either appear as \${param.NAME} in a request URL, body, or header, or be removed from the public parameters list and named in workflow.limitations[].omittedParameters with an evidence-based reason. For parameters recorded as null or [] (filters the user toggled but didn\'t apply), find the correct position in the request body and replace the placeholder value with \${param.NAME}.`,
3696
3806
  );
3697
3807
  }
3698
3808
  if (inventedOnly.length > 0) {
@@ -3700,6 +3810,13 @@ export async function externalVerification(
3700
3810
  `${inventedOnly.length} likelyParam(s) are templated only in URL query params that do not exist in any recorded request URL: ${inventedOnly.join(', ')}. The API server likely ignores these invented params — wire them into the request body or an existing query param instead. For complex body formats, use a requestTransformModule to construct the body programmatically.`,
3701
3811
  );
3702
3812
  }
3813
+ if (intentionallyOmitted.length > 0) {
3814
+ warnings.push(
3815
+ `${intentionallyOmitted.length} detector-suggested parameter(s) were intentionally omitted by the compile agent: ${intentionallyOmitted
3816
+ .map(({ name, feature, reason }) => `${name} (${feature}: ${reason})`)
3817
+ .join('; ')}. The limitations are retained in workflow.json and surfaced to callers.`,
3818
+ );
3819
+ }
3703
3820
  }
3704
3821
  } catch (err) {
3705
3822
  failures.push(
@@ -3816,8 +3933,13 @@ export async function externalVerification(
3816
3933
  if (!hasLiveBaselineWorkflowTest(integrationSrc)) {
3817
3934
  integrationOutcome = 'failed';
3818
3935
  failures.push(
3819
- 'integration.test.ts has no strict baseline live workflow test: add a non-`param:` test that calls `runWorkflowWithLadder(...)` on this tool workflow, fails when `result.ok` is false, and asserts the successful response contains real data. Static workflow-shape assertions or green returns on bot/infra errors are not enough to verify a generated tool.',
3936
+ 'integration.test.ts has no strict baseline live workflow test: add a non-`param:` test that calls `runCapturedIntegrationCase(...)` on this tool workflow, fails when `result.ok` is false, and asserts the successful response contains real data. Static workflow-shape assertions or green returns on bot/infra errors are not enough to verify a generated tool.',
3820
3937
  );
3938
+ } else if (opts.deferLiveIntegrationToSemanticAgent) {
3939
+ // The independent verifier owns the final live execution. Keeping the
3940
+ // suite out of this deterministic gate avoids two disconnected verdicts
3941
+ // and lets the reviewer inspect the exact outputs it requested.
3942
+ integrationOutcome = 'absent';
3821
3943
  } else {
3822
3944
  // Scale the verifier's live-test timeout to the suite size: the baseline plus
3823
3945
  // one live `runWorkflowWithLadder` per param, each gated by the ~25s compile
@@ -3827,26 +3949,30 @@ export async function externalVerification(
3827
3949
  const paramCount = opts.likelyParams?.length ?? 0;
3828
3950
  const pacingMs = Number(process.env.IMPRINT_COMPILE_ACT_SPACING_MS ?? 25_000) || 0;
3829
3951
  const verifierTimeoutMs = Math.min(120_000 + paramCount * (pacingMs + 20_000), 10 * 60_000);
3830
- let run: BunTestRun = {
3831
- stdout: '',
3832
- stderr: '',
3833
- exitCode: 1,
3834
- timedOut: false,
3835
- passed: new Set(),
3836
- failed: new Set(),
3837
- };
3838
- for (let attempt = 0; attempt < 3; attempt++) {
3839
- run = await runBunTestWithResults(integrationTestPath, toolDir, verifierTimeoutMs);
3840
- if (run.exitCode === 0) break;
3841
- // A timeout, bot-defense, or ladder-exhaustion failure will NOT clear on a
3842
- // retry — re-running only fires more state-changing calls and deepens the
3843
- // per-IP rate flag. One attempt is enough to classify it; stop early.
3844
- if (run.timedOut) break;
3845
- const out = `${run.stdout}\n${run.stderr}`;
3846
- const ladderExhausted =
3847
- /\bRATE_LIMITED\b|\bFORBIDDEN\b|\bNETWORK\b/.test(out) &&
3848
- /non-escalatable|giving up|ladder exhausted|all backends failed/.test(out);
3849
- if (isBotDefenseFailure(out) || ladderExhausted) break;
3952
+ const evidencePath = pathJoin(
3953
+ toolDir,
3954
+ `.imprint-live-evidence-${process.pid}-${Date.now()}.jsonl`,
3955
+ );
3956
+ try {
3957
+ if (existsSync(evidencePath)) unlinkSync(evidencePath);
3958
+ } catch {
3959
+ // best-effort cleanup before the run
3960
+ }
3961
+ const run = await runBunTestWithResults(integrationTestPath, toolDir, verifierTimeoutMs, {
3962
+ [LIVE_EVIDENCE_PATH_ENV]: evidencePath,
3963
+ });
3964
+ try {
3965
+ integrationEvidence = readLiveIntegrationEvidence(evidencePath);
3966
+ } catch (error) {
3967
+ failures.push(
3968
+ `could not read captured live integration evidence: ${error instanceof Error ? error.message : String(error)}`,
3969
+ );
3970
+ } finally {
3971
+ try {
3972
+ if (existsSync(evidencePath)) unlinkSync(evidencePath);
3973
+ } catch {
3974
+ // Evidence is ephemeral and intentionally never kept with artifacts.
3975
+ }
3850
3976
  }
3851
3977
  integrationPassedTests = run.passed;
3852
3978
 
@@ -3862,7 +3988,11 @@ export async function externalVerification(
3862
3988
  integrationOutcome = verdict.outcome;
3863
3989
 
3864
3990
  if (verdict.outcome === 'passed') {
3865
- // exitCode 0 — nothing to surface.
3991
+ if (integrationEvidence.length === 0) {
3992
+ failures.push(
3993
+ 'integration.test.ts passed but captured no live evidence. Import `runCapturedIntegrationCase` from `imprint/compile-verification` and use it for every live workflow call so the independent verifier can inspect the actual tool inputs and parsed outputs.',
3994
+ );
3995
+ }
3866
3996
  } else if (verdict.captureFailName !== null) {
3867
3997
  const capName = verdict.captureFailName;
3868
3998
  // If the failing capture is a `response_header` on a REPLAYED workflow
@@ -3913,7 +4043,7 @@ export async function externalVerification(
3913
4043
  );
3914
4044
  } else {
3915
4045
  failures.push(
3916
- `bun test integration.test.ts exited ${run.exitCode} — the workflow failed to produce live data (tried 3 times).\nstdout:\n${run.stdout}\nstderr:\n${run.stderr}`,
4046
+ `bun test integration.test.ts exited ${run.exitCode} — the workflow failed to produce live data. The baseline suite runs exactly once per verification cycle.\nstdout:\n${run.stdout}\nstderr:\n${run.stderr}`,
3917
4047
  );
3918
4048
  }
3919
4049
  }
@@ -3930,7 +4060,8 @@ export async function externalVerification(
3930
4060
  !referencedStateBroken &&
3931
4061
  existsSync(integrationTestPath) &&
3932
4062
  opts.likelyParams &&
3933
- opts.likelyParams.length > 0
4063
+ opts.likelyParams.length > 0 &&
4064
+ !opts.deferLiveIntegrationToSemanticAgent
3934
4065
  ) {
3935
4066
  const integrationSrc = readFileSync(integrationTestPath, 'utf8');
3936
4067
 
@@ -4146,5 +4277,5 @@ export async function externalVerification(
4146
4277
  }
4147
4278
  }
4148
4279
 
4149
- return { failures, warnings, paramVerification, liveVerification };
4280
+ return { failures, warnings, paramVerification, liveVerification, integrationEvidence };
4150
4281
  }