imprint-mcp 0.6.0 → 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.
Files changed (43) hide show
  1. package/README.md +2 -0
  2. package/package.json +4 -2
  3. package/prompts/audit-agent.md +4 -2
  4. package/prompts/auth-compile-agent.md +149 -158
  5. package/prompts/build-planning.md +14 -9
  6. package/prompts/compile-agent.md +46 -23
  7. package/prompts/live-verifier-agent.md +101 -0
  8. package/prompts/playbook-compilation.md +8 -1
  9. package/prompts/tool-candidate-detection.md +9 -29
  10. package/src/cli.ts +83 -1
  11. package/src/imprint/audit.ts +219 -20
  12. package/src/imprint/auth-compile-agent.ts +215 -155
  13. package/src/imprint/auth-compile-tools.ts +300 -118
  14. package/src/imprint/auth-verifier.ts +167 -180
  15. package/src/imprint/backend-cache.ts +18 -11
  16. package/src/imprint/backend-ladder.ts +225 -177
  17. package/src/imprint/build-plan.ts +3 -8
  18. package/src/imprint/cdp-browser-fetch.ts +930 -83
  19. package/src/imprint/claude-cli-compile.ts +46 -20
  20. package/src/imprint/codex-cli-compile.ts +436 -139
  21. package/src/imprint/compile-agent-types.ts +15 -10
  22. package/src/imprint/compile-agent.ts +88 -28
  23. package/src/imprint/compile-tools.ts +228 -39
  24. package/src/imprint/compile-verification.ts +187 -0
  25. package/src/imprint/compile.ts +71 -2
  26. package/src/imprint/credential-store.ts +57 -44
  27. package/src/imprint/emit.ts +3 -1
  28. package/src/imprint/freeform-redact.ts +27 -6
  29. package/src/imprint/live-verifier.ts +1665 -0
  30. package/src/imprint/llm.ts +22 -3
  31. package/src/imprint/login.ts +107 -25
  32. package/src/imprint/mcp-compile-server.ts +447 -114
  33. package/src/imprint/mcp-live-verifier-server.ts +359 -0
  34. package/src/imprint/mcp-server.ts +116 -35
  35. package/src/imprint/probe-backends.ts +88 -7
  36. package/src/imprint/recording-request.ts +36 -0
  37. package/src/imprint/request-capture.ts +40 -17
  38. package/src/imprint/runtime.ts +490 -337
  39. package/src/imprint/teach-state.ts +36 -2
  40. package/src/imprint/teach.ts +302 -146
  41. package/src/imprint/tool-candidates.ts +8 -10
  42. package/src/imprint/tool-loader.ts +12 -4
  43. package/src/imprint/types.ts +137 -68
@@ -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,8 +87,9 @@ 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),
92
+ buildSearchRequestsTool(session),
85
93
  buildRevealRequestTool(sessionPath),
86
94
  buildDiffRequestForEventTool(session, context),
87
95
  buildReadResponseBodyTool(session),
@@ -114,6 +122,10 @@ export interface CompileToolContext {
114
122
  buildPlanPath?: string;
115
123
  /** Shared-module build manifest (verified flags) for this site. */
116
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;
117
129
  }
118
130
 
119
131
  // ─── Tool: read_build_plan ───────────────────────────────────────────────────
@@ -163,7 +175,7 @@ function buildReadBuildPlanTool(
163
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.`
164
176
  : '';
165
177
  tokenNotes.push(
166
- `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.`,
167
179
  );
168
180
  }
169
181
  // Per-source wiring guidance for the general dependency contract. These are
@@ -231,6 +243,7 @@ function buildReadBuildPlanTool(
231
243
  export function buildReadSessionSummaryTool(
232
244
  session: Session,
233
245
  context: CompileToolContext,
246
+ toolDir?: string,
234
247
  ): AgentTool {
235
248
  return {
236
249
  name: 'read_session_summary',
@@ -342,6 +355,10 @@ export function buildReadSessionSummaryTool(
342
355
  captureHints: captureHints.length > 0 ? captureHints : undefined,
343
356
  paramGroundingHints: paramGroundingHints.length > 0 ? paramGroundingHints : undefined,
344
357
  inputProvenanceHints: inputProvenanceHints.length > 0 ? inputProvenanceHints : undefined,
358
+ revisionContext:
359
+ context.revisionMode && toolDir
360
+ ? buildExistingArtifactRevisionContext(toolDir)
361
+ : undefined,
345
362
  loadBearingRequests,
346
363
  };
347
364
 
@@ -360,6 +377,65 @@ export function buildReadSessionSummaryTool(
360
377
  };
361
378
  }
362
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
+
363
439
  // ─── Inline request/response data for candidate-scoped requests ─────────────
364
440
 
365
441
  // claude-cli truncates tool results > ~40K chars. Keep the total summary
@@ -891,6 +967,63 @@ export function buildReadRequestTool(session: Session): AgentTool {
891
967
  };
892
968
  }
893
969
 
970
+ export function buildSearchRequestsTool(session: Session): AgentTool {
971
+ return {
972
+ name: 'search_requests',
973
+ description:
974
+ 'Search recorded requests by method, resource type, URL fragment, response status, and sequence range. Returns exact seq IDs for read_request/read_response_body.',
975
+ input_schema: {
976
+ type: 'object',
977
+ properties: {
978
+ method: { type: 'string' },
979
+ resourceType: { type: 'string' },
980
+ urlContains: { type: 'string' },
981
+ status: { type: 'number' },
982
+ afterSeq: { type: 'number' },
983
+ beforeSeq: { type: 'number' },
984
+ limit: { type: 'number', description: 'Default 50, maximum 200' },
985
+ },
986
+ required: [],
987
+ },
988
+ handler: async (input: unknown) => {
989
+ const filters = input as {
990
+ method?: string;
991
+ resourceType?: string;
992
+ urlContains?: string;
993
+ status?: number;
994
+ afterSeq?: number;
995
+ beforeSeq?: number;
996
+ limit?: number;
997
+ };
998
+ const method = filters.method?.toUpperCase();
999
+ const resourceType = filters.resourceType?.toLowerCase();
1000
+ const url = filters.urlContains?.toLowerCase();
1001
+ const requests = session.requests
1002
+ .filter((request) => {
1003
+ if (method && request.method.toUpperCase() !== method) return false;
1004
+ if (resourceType && request.resourceType?.toLowerCase() !== resourceType) return false;
1005
+ if (url && !request.url.toLowerCase().includes(url)) return false;
1006
+ if (filters.status !== undefined && request.response?.status !== filters.status)
1007
+ return false;
1008
+ if (filters.afterSeq !== undefined && request.seq <= filters.afterSeq) return false;
1009
+ if (filters.beforeSeq !== undefined && request.seq >= filters.beforeSeq) return false;
1010
+ return true;
1011
+ })
1012
+ .sort((a, b) => a.seq - b.seq)
1013
+ .slice(0, Math.max(1, Math.min(200, Math.trunc(filters.limit ?? 50))))
1014
+ .map((request) => ({
1015
+ seq: request.seq,
1016
+ resourceType: request.resourceType,
1017
+ method: request.method,
1018
+ url: request.url,
1019
+ status: request.response?.status,
1020
+ mimeType: request.response?.mimeType,
1021
+ }));
1022
+ return { result: JSON.stringify({ count: requests.length, requests }, null, 2) };
1023
+ },
1024
+ };
1025
+ }
1026
+
894
1027
  // ─── Tool: reveal_request ─────────────────────────────────────────────────────
895
1028
 
896
1029
  /** Read the raw recording from disk, bypassing the in-memory (possibly-redacted)
@@ -1287,6 +1420,7 @@ export async function runCommand(
1287
1420
  cwd: string,
1288
1421
  timeoutMs: number,
1289
1422
  extraEnv?: Record<string, string>,
1423
+ onOutput?: (stream: 'stdout' | 'stderr', chunk: string) => void,
1290
1424
  ): Promise<{ result: string; isError?: boolean }> {
1291
1425
  return new Promise((resolve) => {
1292
1426
  // `detached: true` makes the child its own process-group leader so a timeout
@@ -1304,11 +1438,15 @@ export async function runCommand(
1304
1438
  const TRUNCATE_LIMIT = 16 * 1024; // 16KB
1305
1439
 
1306
1440
  proc.stdout.on('data', (chunk) => {
1307
- stdout += chunk.toString();
1441
+ const text = chunk.toString();
1442
+ stdout += text;
1443
+ onOutput?.('stdout', text);
1308
1444
  });
1309
1445
 
1310
1446
  proc.stderr.on('data', (chunk) => {
1311
- stderr += chunk.toString();
1447
+ const text = chunk.toString();
1448
+ stderr += text;
1449
+ onOutput?.('stderr', text);
1312
1450
  });
1313
1451
 
1314
1452
  const timeout = setTimeout(() => {
@@ -1658,7 +1796,7 @@ interface BunTestRun {
1658
1796
 
1659
1797
  /** Per-exposed-parameter verification outcome. `verified` is true only when a
1660
1798
  * `param:<name>` integration test actually ran green against live data. */
1661
- interface ParamVerification {
1799
+ export interface ParamVerification {
1662
1800
  name: string;
1663
1801
  verified: boolean;
1664
1802
  /** Why an exposed param is unverified. Undefined when `verified` is true.
@@ -1669,7 +1807,7 @@ interface ParamVerification {
1669
1807
  * - `waived-chain`: the param is a producer-sourced token but the producer
1670
1808
  * tool could not be run at compile time (anti-bot / not compiled), so the
1671
1809
  * chain could not be verified. */
1672
- reason?: 'waived-bot' | 'waived-infra' | 'annotated' | 'waived-chain';
1810
+ reason?: 'waived-bot' | 'waived-infra' | 'annotated' | 'waived-chain' | 'semantic-gap';
1673
1811
  /** For a producer-sourced token param, the sibling tool + output field its
1674
1812
  * value comes from. Stamped into workflow.json (`param.sourcedFrom`) so the
1675
1813
  * MCP description tells the orchestrating LLM where to mint it and the audit
@@ -1693,11 +1831,15 @@ interface TokenSource {
1693
1831
  * bot-defense / infra detection and error surfacing) and the per-test pass/fail
1694
1832
  * names via a JUnit report written to a transient file in the tool dir.
1695
1833
  */
1696
- async function runBunTestWithResults(
1834
+ export async function runBunTestWithResults(
1697
1835
  testPath: string,
1698
1836
  toolDir: string,
1699
1837
  timeoutMs: number,
1700
1838
  env: Record<string, string> = {},
1839
+ opts: {
1840
+ bail?: boolean;
1841
+ onOutput?: (stream: 'stdout' | 'stderr', chunk: string) => void;
1842
+ } = {},
1701
1843
  ): Promise<BunTestRun> {
1702
1844
  const junitPath = pathJoin(toolDir, `.imprint-junit-${basename(testPath)}.xml`);
1703
1845
  try {
@@ -1706,10 +1848,11 @@ async function runBunTestWithResults(
1706
1848
  // best-effort
1707
1849
  }
1708
1850
  const result = await runCommand(
1709
- `bun test ${testPath} --reporter=junit --reporter-outfile=${junitPath}`,
1851
+ `bun test ${testPath} --reporter=junit --reporter-outfile=${junitPath}${opts.bail ? ' --bail=1' : ''}`,
1710
1852
  toolDir,
1711
1853
  timeoutMs,
1712
1854
  env,
1855
+ opts.onOutput,
1713
1856
  );
1714
1857
  const output = JSON.parse(result.result) as {
1715
1858
  stdout: string;
@@ -1744,6 +1887,10 @@ interface TestBlock {
1744
1887
  body: string;
1745
1888
  }
1746
1889
 
1890
+ function hasCapturedLiveWorkflowCall(source: string): boolean {
1891
+ return /\b(?:runWorkflowWithLadder|runCapturedIntegrationCase)\s*\(/.test(source);
1892
+ }
1893
+
1747
1894
  /** Split a test file into `test(...)` / `it(...)` blocks (title + source from
1748
1895
  * that test's start to the next test's start). Good enough to check whether a
1749
1896
  * named per-parameter test's body actually calls the workflow. */
@@ -2183,7 +2330,7 @@ export function classifyParamCoverage(opts: {
2183
2330
  if (passedLive) {
2184
2331
  const chained =
2185
2332
  !!block &&
2186
- /runWorkflowWithLadder\s*\(/.test(block.body) &&
2333
+ hasCapturedLiveWorkflowCall(block.body) &&
2187
2334
  SIBLING_WORKFLOW_RE.test(block.body);
2188
2335
  if (chained) {
2189
2336
  paramVerification.push({ name: lp.name, verified: true, sourcedFrom });
@@ -2211,7 +2358,7 @@ export function classifyParamCoverage(opts: {
2211
2358
  if (passedLive) {
2212
2359
  // Anti-tautology: a passing per-param test must actually exercise the live
2213
2360
  // workflow, not assert a constant.
2214
- if (block && !/runWorkflowWithLadder\s*\(/.test(block.body)) {
2361
+ if (block && !hasCapturedLiveWorkflowCall(block.body)) {
2215
2362
  tautological.push(lp.name);
2216
2363
  } else {
2217
2364
  paramVerification.push({ name: lp.name, verified: true });
@@ -2244,7 +2391,7 @@ export function hasLiveBaselineWorkflowTest(integrationSrc: string): boolean {
2244
2391
  return extractTestBlocks(integrationSrc).some(
2245
2392
  (block) =>
2246
2393
  !block.title.includes('param:') &&
2247
- /\brunWorkflowWithLadder\s*\(/.test(block.body) &&
2394
+ hasCapturedLiveWorkflowCall(block.body) &&
2248
2395
  !returnsOnFailedResult.test(block.body) &&
2249
2396
  (requiresOk.test(block.body) || throwsOnFailure.test(block.body)),
2250
2397
  );
@@ -3307,7 +3454,7 @@ export function contractedInputGate(
3307
3454
  unresolved++;
3308
3455
  const how =
3309
3456
  ri.source === 'auth'
3310
- ? `wire it as \`${token}\` (the authenticate tool persists it via sessionCapture)`
3457
+ ? `wire it as \`${token}\` (the authenticate tool names it in authConfig.persist)`
3311
3458
  : ri.source === 'browser_state'
3312
3459
  ? `wire it as \`${token}\` and add the capture/bootstrap that produces it`
3313
3460
  : ri.source === 'generated'
@@ -3369,6 +3516,10 @@ export async function externalVerification(
3369
3516
  * verifier blocks invented ${credential.X} placeholders that are not in this
3370
3517
  * set or in requiredInputs. */
3371
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;
3372
3523
  } = {},
3373
3524
  ): Promise<{
3374
3525
  failures: string[];
@@ -3382,9 +3533,13 @@ export async function externalVerification(
3382
3533
  firstError: string;
3383
3534
  exhaustedBackends: string[];
3384
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[];
3385
3539
  }> {
3386
3540
  const failures: string[] = [];
3387
3541
  const warnings: string[] = [];
3542
+ let integrationEvidence: LiveIntegrationEvidence[] = [];
3388
3543
  const paramVerification: ParamVerification[] = [];
3389
3544
  let liveVerification:
3390
3545
  | { kind: 'waived-bot' | 'waived-infra'; firstError: string; exhaustedBackends: string[] }
@@ -3589,6 +3744,7 @@ export async function externalVerification(
3589
3744
 
3590
3745
  const notTemplated: string[] = [];
3591
3746
  const inventedOnly: string[] = [];
3747
+ const intentionallyOmitted: Array<{ name: string; feature: string; reason: string }> = [];
3592
3748
 
3593
3749
  for (const lp of opts.likelyParams) {
3594
3750
  const placeholder = `\${param.${lp.name}}`;
@@ -3626,7 +3782,19 @@ export async function externalVerification(
3626
3782
  }
3627
3783
 
3628
3784
  if (!inBody && !inHeader && !inOriginalQuery && !inInventedQuery) {
3629
- 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
+ }
3630
3798
  } else if (!inBody && !inHeader && !inOriginalQuery && inInventedQuery) {
3631
3799
  inventedOnly.push(lp.name);
3632
3800
  }
@@ -3634,7 +3802,7 @@ export async function externalVerification(
3634
3802
 
3635
3803
  if (notTemplated.length > 0) {
3636
3804
  failures.push(
3637
- `${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}.`,
3638
3806
  );
3639
3807
  }
3640
3808
  if (inventedOnly.length > 0) {
@@ -3642,6 +3810,13 @@ export async function externalVerification(
3642
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.`,
3643
3811
  );
3644
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
+ }
3645
3820
  }
3646
3821
  } catch (err) {
3647
3822
  failures.push(
@@ -3758,8 +3933,13 @@ export async function externalVerification(
3758
3933
  if (!hasLiveBaselineWorkflowTest(integrationSrc)) {
3759
3934
  integrationOutcome = 'failed';
3760
3935
  failures.push(
3761
- '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.',
3762
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';
3763
3943
  } else {
3764
3944
  // Scale the verifier's live-test timeout to the suite size: the baseline plus
3765
3945
  // one live `runWorkflowWithLadder` per param, each gated by the ~25s compile
@@ -3769,26 +3949,30 @@ export async function externalVerification(
3769
3949
  const paramCount = opts.likelyParams?.length ?? 0;
3770
3950
  const pacingMs = Number(process.env.IMPRINT_COMPILE_ACT_SPACING_MS ?? 25_000) || 0;
3771
3951
  const verifierTimeoutMs = Math.min(120_000 + paramCount * (pacingMs + 20_000), 10 * 60_000);
3772
- let run: BunTestRun = {
3773
- stdout: '',
3774
- stderr: '',
3775
- exitCode: 1,
3776
- timedOut: false,
3777
- passed: new Set(),
3778
- failed: new Set(),
3779
- };
3780
- for (let attempt = 0; attempt < 3; attempt++) {
3781
- run = await runBunTestWithResults(integrationTestPath, toolDir, verifierTimeoutMs);
3782
- if (run.exitCode === 0) break;
3783
- // A timeout, bot-defense, or ladder-exhaustion failure will NOT clear on a
3784
- // retry — re-running only fires more state-changing calls and deepens the
3785
- // per-IP rate flag. One attempt is enough to classify it; stop early.
3786
- if (run.timedOut) break;
3787
- const out = `${run.stdout}\n${run.stderr}`;
3788
- const ladderExhausted =
3789
- /\bRATE_LIMITED\b|\bFORBIDDEN\b|\bNETWORK\b/.test(out) &&
3790
- /non-escalatable|giving up|ladder exhausted|all backends failed/.test(out);
3791
- 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
+ }
3792
3976
  }
3793
3977
  integrationPassedTests = run.passed;
3794
3978
 
@@ -3804,7 +3988,11 @@ export async function externalVerification(
3804
3988
  integrationOutcome = verdict.outcome;
3805
3989
 
3806
3990
  if (verdict.outcome === 'passed') {
3807
- // 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
+ }
3808
3996
  } else if (verdict.captureFailName !== null) {
3809
3997
  const capName = verdict.captureFailName;
3810
3998
  // If the failing capture is a `response_header` on a REPLAYED workflow
@@ -3855,7 +4043,7 @@ export async function externalVerification(
3855
4043
  );
3856
4044
  } else {
3857
4045
  failures.push(
3858
- `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}`,
3859
4047
  );
3860
4048
  }
3861
4049
  }
@@ -3872,7 +4060,8 @@ export async function externalVerification(
3872
4060
  !referencedStateBroken &&
3873
4061
  existsSync(integrationTestPath) &&
3874
4062
  opts.likelyParams &&
3875
- opts.likelyParams.length > 0
4063
+ opts.likelyParams.length > 0 &&
4064
+ !opts.deferLiveIntegrationToSemanticAgent
3876
4065
  ) {
3877
4066
  const integrationSrc = readFileSync(integrationTestPath, 'utf8');
3878
4067
 
@@ -4088,5 +4277,5 @@ export async function externalVerification(
4088
4277
  }
4089
4278
  }
4090
4279
 
4091
- return { failures, warnings, paramVerification, liveVerification };
4280
+ return { failures, warnings, paramVerification, liveVerification, integrationEvidence };
4092
4281
  }