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
@@ -14,7 +14,8 @@
14
14
  * loss.
15
15
  */
16
16
 
17
- import { writeFileSync } from 'node:fs';
17
+ import { createHash, randomUUID } from 'node:crypto';
18
+ import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
18
19
  import { join as pathJoin } from 'node:path';
19
20
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
20
21
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -24,7 +25,11 @@ import {
24
25
  ListToolsRequestSchema,
25
26
  type Tool,
26
27
  } from '@modelcontextprotocol/sdk/types.js';
27
- import { authExternalVerification, buildAuthCompileTools } from './auth-compile-tools.ts';
28
+ import {
29
+ authExternalVerification,
30
+ authWorkflowPreflightFailures,
31
+ buildAuthCompileTools,
32
+ } from './auth-compile-tools.ts';
28
33
  import {
29
34
  type AuthToolPlan,
30
35
  type SharedModuleManifestEntry,
@@ -36,8 +41,15 @@ import {
36
41
  buildCompileTools,
37
42
  externalVerification,
38
43
  } from './compile-tools.ts';
44
+ import {
45
+ mergeSemanticParamVerification,
46
+ runLiveSemanticVerification,
47
+ semanticVerificationFailures,
48
+ } from './live-verifier.ts';
49
+ import type { ProviderName } from './llm.ts';
39
50
  import { loadJsonFile } from './load-json.ts';
40
51
  import { createLog } from './log.ts';
52
+ import { rebindExistingBackendsCacheToWorkflow } from './probe-backends.ts';
41
53
  import { redactSession } from './redact.ts';
42
54
  import { loadCredentialStore } from './runtime.ts';
43
55
  import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
@@ -50,8 +62,7 @@ interface RunCompileMcpServerOptions {
50
62
  sessionPath: string;
51
63
  /** Absolute path to the generated tool directory where artifacts go. */
52
64
  toolDir: string;
53
- /** Hard cap on done() verification failures before we permanently fail.
54
- * Mirrors compile-agent.ts MAX_VERIFICATION_CYCLES. */
65
+ /** Data-tool cap on done() verification failures. Auth is deadline-bounded. */
55
66
  maxVerificationCycles?: number;
56
67
  candidate?: ToolCandidate;
57
68
  sharedContext?: SharedCompileContext;
@@ -59,26 +70,162 @@ interface RunCompileMcpServerOptions {
59
70
  buildPlanPath?: string;
60
71
  /** Shared-module build manifest for this site (verified flags). */
61
72
  sharedModules?: SharedModuleManifestEntry[];
62
- /** Present → run in AUTH mode: register the auth toolset (run_verification)
73
+ /** Present → run in AUTH mode: register the auth checkpoint toolset
63
74
  * and run authExternalVerification in done() instead of the data-tool
64
75
  * verification. */
65
76
  authToolPlan?: NonNullable<AuthToolPlan>;
66
77
  /** Site slug (required in auth mode) — used to load credentials for the live
67
78
  * auth-test tools. */
68
79
  site?: string;
80
+ /** Compile provider. The independent verifier uses the same provider family
81
+ * with its dedicated review model (Terra for Codex, latest Sonnet for Claude). */
82
+ provider?: ProviderName;
83
+ /** Bounded resume of an already-generated data tool. */
84
+ revisionMode?: boolean;
69
85
  }
70
86
 
71
87
  const DONE_SENTINEL = '.compile-done.json';
72
88
  const GIVE_UP_SENTINEL = '.compile-give-up.json';
73
- /** Auth mode only: a mid-loop checkpoint the agent reaches (run_verification /
74
- * prompt_user / wait_for_cooldown). The tool records the request here and the
89
+ const VERIFICATION_STATE_SENTINEL = '.compile-verification-state.json';
90
+ // Keep the compile watchdog from charging live verification against the model's
91
+ // reasoning budget, while retaining a hard bound if the MCP server dies during
92
+ // done(). This matches the longest supported MCP tool call.
93
+ const MAX_VERIFICATION_WALL_TIME_MS = 30 * 60_000;
94
+ /** Auth mode only: a mid-loop checkpoint the agent reaches. The tool records
95
+ * the request here and the
75
96
  * agent STOPS; the orchestrator (teach) performs the action and resumes the
76
97
  * agent (`claude --resume`) with the result. One pending checkpoint per segment. */
77
98
  const CHECKPOINT_SENTINEL = '.compile-checkpoint.json';
78
99
 
100
+ const COMPILE_ARTIFACT_FILES = [
101
+ 'workflow.json',
102
+ 'parser.ts',
103
+ 'parser.test.ts',
104
+ 'integration.test.ts',
105
+ 'request-transform.ts',
106
+ 'playbook.yaml',
107
+ ] as const;
108
+
109
+ interface PendingInconclusiveDecision {
110
+ artifactFingerprint: string;
111
+ report: Awaited<ReturnType<typeof runLiveSemanticVerification>>;
112
+ }
113
+
114
+ interface CompileVerificationState {
115
+ excludedMs: number;
116
+ activeSinceMs?: number;
117
+ activeUntilMs?: number;
118
+ }
119
+
120
+ function readCompileVerificationState(toolDir: string): CompileVerificationState {
121
+ try {
122
+ const parsed = JSON.parse(
123
+ readFileSync(pathJoin(toolDir, VERIFICATION_STATE_SENTINEL), 'utf8'),
124
+ ) as Partial<CompileVerificationState>;
125
+ return {
126
+ excludedMs:
127
+ typeof parsed.excludedMs === 'number' && Number.isFinite(parsed.excludedMs)
128
+ ? Math.max(0, parsed.excludedMs)
129
+ : 0,
130
+ activeSinceMs:
131
+ typeof parsed.activeSinceMs === 'number' && Number.isFinite(parsed.activeSinceMs)
132
+ ? parsed.activeSinceMs
133
+ : undefined,
134
+ activeUntilMs:
135
+ typeof parsed.activeUntilMs === 'number' && Number.isFinite(parsed.activeUntilMs)
136
+ ? parsed.activeUntilMs
137
+ : undefined,
138
+ };
139
+ } catch {
140
+ return { excludedMs: 0 };
141
+ }
142
+ }
143
+
144
+ function writeCompileVerificationState(toolDir: string, state: CompileVerificationState): void {
145
+ const destination = pathJoin(toolDir, VERIFICATION_STATE_SENTINEL);
146
+ const temporary = pathJoin(
147
+ toolDir,
148
+ `${VERIFICATION_STATE_SENTINEL}.${process.pid}.${randomUUID()}.tmp`,
149
+ );
150
+ try {
151
+ writeFileSync(temporary, `${JSON.stringify(state)}\n`, {
152
+ encoding: 'utf8',
153
+ flag: 'wx',
154
+ mode: 0o600,
155
+ });
156
+ renameSync(temporary, destination);
157
+ } finally {
158
+ // rename removes the source on success; clean up a partial temp file if a
159
+ // write or rename fails without disturbing the last complete state.
160
+ if (existsSync(temporary)) unlinkSync(temporary);
161
+ }
162
+ }
163
+
164
+ function beginCompileVerification(toolDir: string): () => void {
165
+ const previous = readCompileVerificationState(toolDir);
166
+ const startedAt = Date.now();
167
+ writeCompileVerificationState(toolDir, {
168
+ excludedMs: previous.excludedMs,
169
+ activeSinceMs: startedAt,
170
+ activeUntilMs: startedAt + MAX_VERIFICATION_WALL_TIME_MS,
171
+ });
172
+ return () => {
173
+ const finishedAt = Date.now();
174
+ writeCompileVerificationState(toolDir, {
175
+ excludedMs:
176
+ previous.excludedMs +
177
+ Math.min(MAX_VERIFICATION_WALL_TIME_MS, Math.max(0, finishedAt - startedAt)),
178
+ });
179
+ };
180
+ }
181
+
182
+ /** Effective compiler deadline after excluding bounded time spent inside done()
183
+ * verification. Exported for the CLI watchdogs and focused lifecycle tests. */
184
+ export function compileDeadlineAfterVerification(
185
+ toolDir: string,
186
+ compilerDeadlineMs: number,
187
+ now = Date.now(),
188
+ ): number {
189
+ const state = readCompileVerificationState(toolDir);
190
+ const activeElapsedMs =
191
+ state.activeSinceMs === undefined
192
+ ? 0
193
+ : Math.max(
194
+ 0,
195
+ Math.min(now, state.activeUntilMs ?? state.activeSinceMs) - state.activeSinceMs,
196
+ );
197
+ return compilerDeadlineMs + state.excludedMs + activeElapsedMs;
198
+ }
199
+
200
+ export function compileArtifactFingerprint(toolDir: string): string {
201
+ const hash = createHash('sha256');
202
+ for (const relativePath of COMPILE_ARTIFACT_FILES) {
203
+ const absolutePath = pathJoin(toolDir, relativePath);
204
+ hash.update(relativePath);
205
+ hash.update('\0');
206
+ if (existsSync(absolutePath)) hash.update(readFileSync(absolutePath));
207
+ hash.update('\0');
208
+ }
209
+ return hash.digest('hex');
210
+ }
211
+
212
+ export function canAcceptInconclusiveDecision(opts: {
213
+ pendingFingerprint?: string;
214
+ currentFingerprint: string;
215
+ acceptInconclusive?: boolean;
216
+ inconclusiveReason?: string;
217
+ }): boolean {
218
+ return (
219
+ opts.acceptInconclusive === true &&
220
+ Boolean(opts.inconclusiveReason?.trim()) &&
221
+ opts.pendingFingerprint === opts.currentFingerprint
222
+ );
223
+ }
224
+
79
225
  export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Promise<void> {
80
226
  const isAuthMode = !!opts.authToolPlan;
81
- const maxVerificationCycles = opts.maxVerificationCycles ?? (isAuthMode ? 3 : 5);
227
+ const maxVerificationCycles = opts.maxVerificationCycles ?? 5;
228
+ let pendingInconclusive: PendingInconclusiveDecision | undefined;
82
229
 
83
230
  // Load + auto-redact the session, exactly as compile-agent.ts does.
84
231
  let session: Session = loadJsonFile(
@@ -110,9 +257,8 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
110
257
 
111
258
  if (isAuthMode) {
112
259
  const site = opts.site ?? session.site;
113
- // Credentials power the live run_verification tool (and any login playbook
114
- // it drives). Loaded here (not passed on the command line) to keep secrets
115
- // out of argv.
260
+ // Credentials power the orchestrator-owned live verifier. Load them here
261
+ // rather than passing secrets on the command line.
116
262
  const creds = await loadCredentialStore(site);
117
263
  const teachCredentials = { site, values: creds?.values ?? {} };
118
264
  compileTools = buildAuthCompileTools(session, opts.toolDir, opts.sessionPath, teachCredentials);
@@ -123,6 +269,7 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
123
269
  sharedContext: opts.sharedContext,
124
270
  buildPlanPath: opts.buildPlanPath,
125
271
  sharedModules: opts.sharedModules,
272
+ revisionMode: opts.revisionMode,
126
273
  });
127
274
 
128
275
  // Resolve the shared modules + producer→consumer token contracts + the general
@@ -146,6 +293,16 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
146
293
  type: 'object',
147
294
  properties: {
148
295
  summary: { type: 'string', description: 'Brief summary of what was accomplished' },
296
+ accept_inconclusive: {
297
+ type: 'boolean',
298
+ description:
299
+ 'After an infrastructure-only inconclusive verifier result, explicitly ship the unchanged deterministic artifact without a liveVerified stamp.',
300
+ },
301
+ inconclusive_reason: {
302
+ type: 'string',
303
+ description:
304
+ 'Compiler reasoning for accepting the unchanged artifact after reviewing the inconclusive verifier evidence.',
305
+ },
149
306
  },
150
307
  required: ['summary'],
151
308
  },
@@ -176,20 +333,53 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
176
333
  {
177
334
  name: 'run_verification',
178
335
  description:
179
- 'Hand the current workflow.json to the verification stage to run LIVE (the only thing that fires a real login). phase="initiate" sends the OTP/push (reaches AWAITING_2FA); phase="submit_otp" submits an otp_code; phase="complete" polls a push. After calling this you MUST stop — the orchestrator runs it and resumes you with the result as a new message. Do NOT call any other tool in the same turn.',
336
+ 'Run one action from the current workflow.json live in the verification browser. Supply the action name and only its declared parameters. Set freshSession only when observed evidence means the prior browser and continuation state must be discarded. Set cleanSession only when evidence requires also withholding stored cookies and browser storage. After calling this, stop; the orchestrator resumes you with the observed result.',
180
337
  inputSchema: {
181
338
  type: 'object',
182
339
  properties: {
183
- phase: { type: 'string', enum: ['initiate', 'submit_otp', 'complete'] },
184
- otp_code: { type: 'string', description: 'For submit_otp only.' },
340
+ action: { type: 'string', description: 'An action declared in authConfig.actions.' },
341
+ parameters: {
342
+ type: 'object',
343
+ description:
344
+ 'Scalar values required by this action, keyed by workflow parameter name.',
345
+ },
346
+ freshSession: {
347
+ type: 'boolean',
348
+ description:
349
+ 'Close and discard the prior verification browser and continuation state before running this action. Defaults to false.',
350
+ },
351
+ cleanSession: {
352
+ type: 'boolean',
353
+ description:
354
+ 'Also withhold stored cookies and browser storage for this fresh run. Credential values remain available.',
355
+ },
356
+ },
357
+ required: ['action'],
358
+ },
359
+ },
360
+ {
361
+ name: 'inspect_verification_page',
362
+ description:
363
+ 'Inspect the currently rendered page in the existing verification browser without navigating, resetting state, or running another auth action. Returns rendered body text, final URL, title, and optional cookie metadata (never cookie values). Use when observed verification evidence warrants inspecting the page. After calling this, stop; the orchestrator resumes you with the snapshot.',
364
+ inputSchema: {
365
+ type: 'object',
366
+ properties: {
367
+ maxChars: {
368
+ type: 'number',
369
+ description: 'Maximum rendered body-text characters to return (256–20000).',
370
+ },
371
+ includeCookies: {
372
+ type: 'boolean',
373
+ description:
374
+ 'Include cookie names, domains, paths, expirations, and flags. Cookie values are never returned. Defaults to true.',
375
+ },
185
376
  },
186
- required: ['phase'],
187
377
  },
188
378
  },
189
379
  {
190
380
  name: 'prompt_user',
191
381
  description:
192
- 'Ask the human (in the teach TUI) to supply the live second factor — e.g. enter the code they received, click the emailed link then confirm, or approve the push then confirm. Provide a clear message and optional choice options. After calling this you MUST stop; the orchestrator collects the answer and resumes you with it.',
382
+ 'Ask the human for input or an external action required by the compiled auth program. After calling this, stop; the orchestrator collects the answer and resumes you with it.',
193
383
  inputSchema: {
194
384
  type: 'object',
195
385
  properties: {
@@ -206,7 +396,7 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
206
396
  {
207
397
  name: 'wait_for_cooldown',
208
398
  description:
209
- 'When a verification failed only because the site rate-flagged repeated logins (not a defect in your workflow), wait out a cool-off WITHOUT firing any login. After calling this you MUST stop; the orchestrator waits (informing the user) and resumes you so you can run_verification once more.',
399
+ 'Request a delay without running any live action. Use only when the observed site response justifies waiting. After calling this, stop.',
210
400
  inputSchema: {
211
401
  type: 'object',
212
402
  properties: {
@@ -233,7 +423,7 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
233
423
  {
234
424
  capabilities: { tools: {} },
235
425
  instructions: isAuthMode
236
- ? 'These tools let you turn the captured login + 2FA flow into an auth workflow.json. Read the recording, write workflow.json, test it live with run_verification (AWAITING_2FA = success), and call done() when login works. The done tool verifies the workflow structure and will tell you what to fix.'
426
+ ? 'Compile the recorded login into an auth action program in workflow.json. Define the actions, evidence, state carry, retries, and success criteria from the recording; run those actions live with run_verification, then call done after a declared success action passes.'
237
427
  : 'These tools let you reverse-engineer the captured session into workflow.json + parser.ts + parser.test.ts. Read the recording, write the artifacts, run tests, and call done() when verified. The done tool runs external verification and will tell you what to fix if anything is wrong.',
238
428
  },
239
429
  );
@@ -259,7 +449,12 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
259
449
 
260
450
  // Custom done — runs verification inline.
261
451
  if (name === 'done') {
262
- const summary = (args as { summary?: string }).summary ?? 'Task completed';
452
+ const doneArgs = args as {
453
+ summary?: string;
454
+ accept_inconclusive?: boolean;
455
+ inconclusive_reason?: string;
456
+ };
457
+ const summary = doneArgs.summary ?? 'Task completed';
263
458
  log(`done() called: ${summary}`);
264
459
 
265
460
  // Auth mode: lightweight structural verification (the agent already proved
@@ -268,6 +463,7 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
268
463
  const failures = authExternalVerification(
269
464
  opts.toolDir,
270
465
  (opts.authToolPlan?.captures ?? []).map((c) => ({ name: c.name, usedAs: c.usedAs })),
466
+ { requireLiveAttempt: true },
271
467
  );
272
468
  if (failures.length === 0) {
273
469
  const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
@@ -292,7 +488,201 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
292
488
  }
293
489
 
294
490
  verificationFailures++;
295
- log(`auth verification failed (cycle ${verificationFailures}/${maxVerificationCycles})`);
491
+ log(`auth verification failed (cycle ${verificationFailures})`);
492
+
493
+ return {
494
+ isError: true,
495
+ content: [
496
+ {
497
+ type: 'text',
498
+ text: `You called done but verification failed (cycle ${verificationFailures}):
499
+
500
+ ${failures.map((f) => `- ${f}`).join('\n')}
501
+
502
+ Fix the issues in workflow.json, re-test with run_verification, and call done again when fixed.`,
503
+ },
504
+ ],
505
+ };
506
+ }
507
+
508
+ const finishVerification = beginCompileVerification(opts.toolDir);
509
+ try {
510
+ const { failures, warnings, paramVerification, integrationEvidence } =
511
+ await externalVerification(opts.toolDir, session, opts.sessionPath, {
512
+ expectedToolName: opts.candidate?.toolName,
513
+ likelyParams: opts.candidate?.likelyParams,
514
+ candidateRequestSeqs: opts.candidate?.requestSeqs,
515
+ // Widen Fix B's variation pool to dependency requests so a token that
516
+ // varies only across them and is frozen as a literal in the tool's
517
+ // request is caught (the cross-request session-token leak case).
518
+ dependencyRequestSeqs: opts.candidate?.dependencySeqs,
519
+ assignedSharedModules,
520
+ tokenParams,
521
+ tokenParamShapes,
522
+ emittedTokens,
523
+ requiredInputs,
524
+ credentialValues,
525
+ credentialNames: opts.sharedContext?.credentialNames,
526
+ deferLiveIntegrationToSemanticAgent: true,
527
+ });
528
+ if (warnings.length > 0) {
529
+ log(`verification warnings (non-blocking):\n${warnings.join('\n')}`);
530
+ }
531
+ if (failures.length === 0) {
532
+ if (!opts.provider) {
533
+ failures.push('compile provider was not supplied to the independent semantic verifier');
534
+ } else {
535
+ const currentFingerprint = compileArtifactFingerprint(opts.toolDir);
536
+ if (
537
+ pendingInconclusive &&
538
+ canAcceptInconclusiveDecision({
539
+ pendingFingerprint: pendingInconclusive.artifactFingerprint,
540
+ currentFingerprint,
541
+ acceptInconclusive: doneArgs.accept_inconclusive,
542
+ inconclusiveReason: doneArgs.inconclusive_reason,
543
+ })
544
+ ) {
545
+ const semantic = pendingInconclusive.report;
546
+ const semanticParams = mergeSemanticParamVerification(
547
+ paramVerification,
548
+ semantic.report,
549
+ );
550
+ const inconclusiveReason = doneArgs.inconclusive_reason?.trim() ?? '';
551
+ applyLiveVerification(opts.toolDir, {
552
+ kind: 'waived-infra',
553
+ firstError: inconclusiveReason,
554
+ exhaustedBackends: [],
555
+ });
556
+ const paramWarnings = applyParamVerification(opts.toolDir, semanticParams);
557
+ // The unverified stamp and parameter annotations are metadata-only,
558
+ // so carry the already-proven preference to the final workflow hash.
559
+ rebindExistingBackendsCacheToWorkflow(opts.toolDir);
560
+ const allWarnings = [
561
+ ...warnings,
562
+ `independent semantic verification was inconclusive: ${semantic.report.summary}`,
563
+ ...paramWarnings,
564
+ ];
565
+ const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
566
+ writeFileSync(
567
+ sentinel,
568
+ JSON.stringify(
569
+ {
570
+ summary,
571
+ verification: 'passed',
572
+ liveVerified: false,
573
+ semanticVerification: {
574
+ status: semantic.report.status,
575
+ provider: semantic.provider,
576
+ model: semantic.model,
577
+ attempts: semantic.attempts,
578
+ evidenceArtifact: semantic.report.evidenceArtifact,
579
+ logArtifact: semantic.report.logArtifact,
580
+ },
581
+ compilerDecision: {
582
+ decision: 'ship_unverified',
583
+ reason: inconclusiveReason,
584
+ },
585
+ warnings: allWarnings,
586
+ timestamp: Date.now(),
587
+ },
588
+ null,
589
+ 2,
590
+ ),
591
+ 'utf8',
592
+ );
593
+ log(`compiler accepted unchanged inconclusive artifact; wrote ${sentinel}`);
594
+ return {
595
+ content: [
596
+ {
597
+ type: 'text',
598
+ text: `DONE_UNVERIFIED — deterministic verification passed and the compiler explicitly accepted the unchanged artifact after an infrastructure-only inconclusive live review. Evidence: ${semantic.report.evidenceArtifact ?? '.live-verification-evidence.json'}. Verifier log: ${semantic.report.logArtifact ?? '.live-verifier-log.jsonl'}. The orchestrator will exit shortly. Do not call any more tools.`,
599
+ },
600
+ ],
601
+ };
602
+ }
603
+
604
+ if (
605
+ pendingInconclusive &&
606
+ pendingInconclusive.artifactFingerprint !== currentFingerprint
607
+ ) {
608
+ log(
609
+ 'compile artifacts changed after inconclusive review; running verification again',
610
+ );
611
+ pendingInconclusive = undefined;
612
+ }
613
+ const semantic = await runLiveSemanticVerification({
614
+ provider: opts.provider,
615
+ toolDir: opts.toolDir,
616
+ evidence: integrationEvidence,
617
+ });
618
+ if (semantic.report.status === 'inconclusive') {
619
+ pendingInconclusive = {
620
+ artifactFingerprint: compileArtifactFingerprint(opts.toolDir),
621
+ report: semantic,
622
+ };
623
+ } else {
624
+ pendingInconclusive = undefined;
625
+ }
626
+ const semanticFailures = semanticVerificationFailures(semantic.report);
627
+ failures.push(...semanticFailures);
628
+ if (semantic.report.status === 'approved_with_gaps') {
629
+ warnings.push(
630
+ `independent semantic verification approved with gaps: ${semantic.report.gaps.join('; ')}`,
631
+ );
632
+ }
633
+ if (semanticFailures.length === 0) {
634
+ const semanticParams = mergeSemanticParamVerification(
635
+ paramVerification,
636
+ semantic.report,
637
+ );
638
+ // Only the independent semantic agent can mint liveVerified=true.
639
+ applyLiveVerification(opts.toolDir, undefined);
640
+ const paramWarnings = applyParamVerification(opts.toolDir, semanticParams);
641
+ // liveVerified/parameter annotations are metadata-only, but they
642
+ // change the hash-strict workflow cache key. Keep the verifier's
643
+ // already-proven backend preference current without probing again.
644
+ rebindExistingBackendsCacheToWorkflow(opts.toolDir);
645
+ if (paramWarnings.length > 0) {
646
+ log(`parameter verification:\n${paramWarnings.join('\n')}`);
647
+ }
648
+ const allWarnings = [...warnings, ...paramWarnings];
649
+ const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
650
+ writeFileSync(
651
+ sentinel,
652
+ JSON.stringify(
653
+ {
654
+ summary,
655
+ verification: 'passed',
656
+ semanticVerification: {
657
+ status: semantic.report.status,
658
+ provider: semantic.provider,
659
+ model: semantic.model,
660
+ attempts: semantic.attempts,
661
+ evidenceArtifact: semantic.report.evidenceArtifact,
662
+ logArtifact: semantic.report.logArtifact,
663
+ },
664
+ warnings: allWarnings,
665
+ timestamp: Date.now(),
666
+ },
667
+ null,
668
+ 2,
669
+ ),
670
+ 'utf8',
671
+ );
672
+ log(`verification passed; wrote ${sentinel}`);
673
+ return {
674
+ content: [
675
+ {
676
+ type: 'text',
677
+ text: `DONE_VERIFIED — deterministic checks and independent live semantic verification passed (${semantic.report.status}). Evidence: ${semantic.report.evidenceArtifact ?? '.live-verification-evidence.json'}. Verifier log: ${semantic.report.logArtifact ?? '.live-verifier-log.jsonl'}. The orchestrator will exit shortly. Do not call any more tools.`,
678
+ },
679
+ ],
680
+ };
681
+ }
682
+ }
683
+ }
684
+ verificationFailures++;
685
+ log(`verification failed (cycle ${verificationFailures}/${maxVerificationCycles})`);
296
686
  if (verificationFailures >= maxVerificationCycles) {
297
687
  const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
298
688
  writeFileSync(
@@ -303,6 +693,7 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
303
693
  verification: 'failed',
304
694
  cycles: verificationFailures,
305
695
  failures,
696
+ warnings,
306
697
  timestamp: Date.now(),
307
698
  },
308
699
  null,
@@ -321,116 +712,57 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
321
712
  };
322
713
  }
323
714
 
324
- return {
325
- isError: true,
326
- content: [
327
- {
328
- type: 'text',
329
- text: `You called done but verification failed (cycle ${verificationFailures}/${maxVerificationCycles}):
715
+ const continuationMessage = `You called done but verification failed (cycle ${verificationFailures}/${maxVerificationCycles}):
330
716
 
331
717
  ${failures.map((f) => `- ${f}`).join('\n')}
332
718
 
333
- Fix the issues in workflow.json, re-test with run_verification, and call done again when fixed.`,
334
- },
335
- ],
336
- };
337
- }
338
-
339
- const { failures, warnings, paramVerification, liveVerification } =
340
- await externalVerification(opts.toolDir, session, opts.sessionPath, {
341
- expectedToolName: opts.candidate?.toolName,
342
- likelyParams: opts.candidate?.likelyParams,
343
- candidateRequestSeqs: opts.candidate?.requestSeqs,
344
- // Widen Fix B's variation pool to dependency requests so a token that
345
- // varies only across them and is frozen as a literal in the tool's
346
- // request is caught (the cross-request session-token leak case).
347
- dependencyRequestSeqs: opts.candidate?.dependencySeqs,
348
- assignedSharedModules,
349
- tokenParams,
350
- tokenParamShapes,
351
- emittedTokens,
352
- requiredInputs,
353
- credentialValues,
354
- credentialNames: opts.sharedContext?.credentialNames,
355
- });
356
- if (warnings.length > 0) {
357
- log(`verification warnings (non-blocking):\n${warnings.join('\n')}`);
358
- }
359
- if (failures.length === 0) {
360
- // Persist per-parameter verified flags + the live-verification stamp
361
- // onto workflow.json. Audit and teach read the stamp.
362
- applyLiveVerification(opts.toolDir, liveVerification);
363
- const paramWarnings = applyParamVerification(opts.toolDir, paramVerification);
364
- if (paramWarnings.length > 0) {
365
- log(`parameter verification:\n${paramWarnings.join('\n')}`);
366
- }
367
- const allWarnings = [...warnings, ...paramWarnings];
368
- const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
369
- writeFileSync(
370
- sentinel,
371
- JSON.stringify(
372
- { summary, verification: 'passed', warnings: allWarnings, timestamp: Date.now() },
373
- null,
374
- 2,
375
- ),
376
- 'utf8',
377
- );
378
- log(`verification passed; wrote ${sentinel}`);
379
- return {
380
- content: [
381
- {
382
- type: 'text',
383
- text: 'DONE_VERIFIED — verification passed. The orchestrator will exit shortly. Do not call any more tools.',
384
- },
385
- ],
386
- };
387
- }
388
-
389
- verificationFailures++;
390
- log(`verification failed (cycle ${verificationFailures}/${maxVerificationCycles})`);
391
- if (verificationFailures >= maxVerificationCycles) {
392
- const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
393
- writeFileSync(
394
- sentinel,
395
- JSON.stringify(
396
- {
397
- summary,
398
- verification: 'failed',
399
- cycles: verificationFailures,
400
- failures,
401
- warnings,
402
- timestamp: Date.now(),
403
- },
404
- null,
405
- 2,
406
- ),
407
- 'utf8',
408
- );
719
+ 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.`;
720
+ const compilerDecisionGuidance = `\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. Close that omission over its dependencies: also remove dependent public inputs and narrow parameter/intent descriptions so callers cannot still enter the unsupported branch indirectly. Do not use a limitation to hide a broken core tool, and do not keep a known broken or ignored public input.`;
721
+ const inconclusiveDecisionGuidance = pendingInconclusive
722
+ ? '\n\nThe live verifier was infrastructure-only inconclusive. Review its evidence and log. If they expose a tool defect, revise the artifacts and call done normally so verification reruns. If they expose no tool-level defect and the deterministic artifact should ship explicitly unverified, call done again without changing compile artifacts, set accept_inconclusive=true, and provide your concrete reasoning in inconclusive_reason. This compiler decision is persisted and does not mint liveVerified.'
723
+ : '';
409
724
  return {
410
725
  isError: true,
411
726
  content: [
412
727
  {
413
728
  type: 'text',
414
- text: `Verification failed after ${maxVerificationCycles} cycles. Giving up. Final failures:\n${failures.map((f) => `- ${f}`).join('\n')}`,
729
+ text: continuationMessage + compilerDecisionGuidance + inconclusiveDecisionGuidance,
415
730
  },
416
731
  ],
417
732
  };
733
+ } finally {
734
+ finishVerification();
418
735
  }
419
-
420
- const continuationMessage = `You called done but verification failed (cycle ${verificationFailures}/${maxVerificationCycles}):
421
-
422
- ${failures.map((f) => `- ${f}`).join('\n')}
423
-
424
- 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.`;
425
- return {
426
- isError: true,
427
- content: [{ type: 'text', text: continuationMessage }],
428
- };
429
736
  }
430
737
 
431
738
  // Auth-mode checkpoint tools — record the request and end the segment. The
432
739
  // orchestrator performs the action live and resumes the agent with the result.
433
- if (name === 'run_verification' || name === 'prompt_user' || name === 'wait_for_cooldown') {
740
+ if (
741
+ name === 'run_verification' ||
742
+ name === 'inspect_verification_page' ||
743
+ name === 'prompt_user' ||
744
+ name === 'wait_for_cooldown'
745
+ ) {
746
+ if (name === 'run_verification') {
747
+ const failures = authWorkflowPreflightFailures(
748
+ opts.toolDir,
749
+ session,
750
+ opts.authToolPlan?.credentialNames,
751
+ );
752
+ if (failures.length > 0) {
753
+ return {
754
+ isError: true,
755
+ content: [
756
+ {
757
+ type: 'text',
758
+ text: `run_verification blocked by auth workflow preflight. Fix workflow.json before requesting live verification:\n${failures
759
+ .map((failure) => `- ${failure}`)
760
+ .join('\n')}`,
761
+ },
762
+ ],
763
+ };
764
+ }
765
+ }
434
766
  if (checkpointWritten) {
435
767
  return {
436
768
  isError: true,
@@ -524,4 +856,5 @@ export const COMPILE_SENTINELS = {
524
856
  done: DONE_SENTINEL,
525
857
  giveUp: GIVE_UP_SENTINEL,
526
858
  checkpoint: CHECKPOINT_SENTINEL,
859
+ verificationState: VERIFICATION_STATE_SENTINEL,
527
860
  } as const;