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.
@@ -1238,6 +1238,22 @@ export function preferredAgentModel(provider: ProviderName): string {
1238
1238
  }
1239
1239
  }
1240
1240
 
1241
+ /** Model policy for the independent live semantic verifier. Keep this separate
1242
+ * from the compile-agent preference so the reviewer is intentionally a fresh,
1243
+ * differently configured agent. No fallback is allowed: an unavailable model
1244
+ * makes verification inconclusive and the compile fails closed. */
1245
+ export function preferredVerificationModel(provider: ProviderName): string {
1246
+ if (provider === 'codex-cli') return 'gpt-5.6-terra';
1247
+ if (provider === 'claude-cli' || provider === 'anthropic-api') {
1248
+ const latestSonnet = availableModelsForProvider(provider).find((option) =>
1249
+ option.model.startsWith('claude-sonnet-'),
1250
+ );
1251
+ if (latestSonnet) return latestSonnet.model;
1252
+ throw new Error(`No Sonnet model is available for verification on ${provider}`);
1253
+ }
1254
+ throw new Error(`Provider ${provider} does not support live semantic verification`);
1255
+ }
1256
+
1241
1257
  interface ModelOption {
1242
1258
  model: string;
1243
1259
  isDefault: boolean;
@@ -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';
@@ -40,8 +41,15 @@ import {
40
41
  buildCompileTools,
41
42
  externalVerification,
42
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';
43
50
  import { loadJsonFile } from './load-json.ts';
44
51
  import { createLog } from './log.ts';
52
+ import { rebindExistingBackendsCacheToWorkflow } from './probe-backends.ts';
45
53
  import { redactSession } from './redact.ts';
46
54
  import { loadCredentialStore } from './runtime.ts';
47
55
  import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
@@ -69,19 +77,155 @@ interface RunCompileMcpServerOptions {
69
77
  /** Site slug (required in auth mode) — used to load credentials for the live
70
78
  * auth-test tools. */
71
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;
72
85
  }
73
86
 
74
87
  const DONE_SENTINEL = '.compile-done.json';
75
88
  const GIVE_UP_SENTINEL = '.compile-give-up.json';
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;
76
94
  /** Auth mode only: a mid-loop checkpoint the agent reaches. The tool records
77
95
  * the request here and the
78
96
  * agent STOPS; the orchestrator (teach) performs the action and resumes the
79
97
  * agent (`claude --resume`) with the result. One pending checkpoint per segment. */
80
98
  const CHECKPOINT_SENTINEL = '.compile-checkpoint.json';
81
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
+
82
225
  export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Promise<void> {
83
226
  const isAuthMode = !!opts.authToolPlan;
84
227
  const maxVerificationCycles = opts.maxVerificationCycles ?? 5;
228
+ let pendingInconclusive: PendingInconclusiveDecision | undefined;
85
229
 
86
230
  // Load + auto-redact the session, exactly as compile-agent.ts does.
87
231
  let session: Session = loadJsonFile(
@@ -125,6 +269,7 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
125
269
  sharedContext: opts.sharedContext,
126
270
  buildPlanPath: opts.buildPlanPath,
127
271
  sharedModules: opts.sharedModules,
272
+ revisionMode: opts.revisionMode,
128
273
  });
129
274
 
130
275
  // Resolve the shared modules + producer→consumer token contracts + the general
@@ -148,6 +293,16 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
148
293
  type: 'object',
149
294
  properties: {
150
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
+ },
151
306
  },
152
307
  required: ['summary'],
153
308
  },
@@ -294,7 +449,12 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
294
449
 
295
450
  // Custom done — runs verification inline.
296
451
  if (name === 'done') {
297
- 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';
298
458
  log(`done() called: ${summary}`);
299
459
 
300
460
  // Auth mode: lightweight structural verification (the agent already proved
@@ -345,96 +505,234 @@ Fix the issues in workflow.json, re-test with run_verification, and call done ag
345
505
  };
346
506
  }
347
507
 
348
- const { failures, warnings, paramVerification, liveVerification } =
349
- await externalVerification(opts.toolDir, session, opts.sessionPath, {
350
- expectedToolName: opts.candidate?.toolName,
351
- likelyParams: opts.candidate?.likelyParams,
352
- candidateRequestSeqs: opts.candidate?.requestSeqs,
353
- // Widen Fix B's variation pool to dependency requests so a token that
354
- // varies only across them and is frozen as a literal in the tool's
355
- // request is caught (the cross-request session-token leak case).
356
- dependencyRequestSeqs: opts.candidate?.dependencySeqs,
357
- assignedSharedModules,
358
- tokenParams,
359
- tokenParamShapes,
360
- emittedTokens,
361
- requiredInputs,
362
- credentialValues,
363
- credentialNames: opts.sharedContext?.credentialNames,
364
- });
365
- if (warnings.length > 0) {
366
- log(`verification warnings (non-blocking):\n${warnings.join('\n')}`);
367
- }
368
- if (failures.length === 0) {
369
- // Persist per-parameter verified flags + the live-verification stamp
370
- // onto workflow.json. Audit and teach read the stamp.
371
- applyLiveVerification(opts.toolDir, liveVerification);
372
- const paramWarnings = applyParamVerification(opts.toolDir, paramVerification);
373
- if (paramWarnings.length > 0) {
374
- log(`parameter verification:\n${paramWarnings.join('\n')}`);
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})`);
686
+ if (verificationFailures >= maxVerificationCycles) {
687
+ const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
688
+ writeFileSync(
689
+ sentinel,
690
+ JSON.stringify(
691
+ {
692
+ summary,
693
+ verification: 'failed',
694
+ cycles: verificationFailures,
695
+ failures,
696
+ warnings,
697
+ timestamp: Date.now(),
698
+ },
699
+ null,
700
+ 2,
701
+ ),
702
+ 'utf8',
703
+ );
704
+ return {
705
+ isError: true,
706
+ content: [
707
+ {
708
+ type: 'text',
709
+ text: `Verification failed after ${maxVerificationCycles} cycles. Giving up. Final failures:\n${failures.map((f) => `- ${f}`).join('\n')}`,
710
+ },
711
+ ],
712
+ };
375
713
  }
376
- const allWarnings = [...warnings, ...paramWarnings];
377
- const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
378
- writeFileSync(
379
- sentinel,
380
- JSON.stringify(
381
- { summary, verification: 'passed', warnings: allWarnings, timestamp: Date.now() },
382
- null,
383
- 2,
384
- ),
385
- 'utf8',
386
- );
387
- log(`verification passed; wrote ${sentinel}`);
388
- return {
389
- content: [
390
- {
391
- type: 'text',
392
- text: 'DONE_VERIFIED — verification passed. The orchestrator will exit shortly. Do not call any more tools.',
393
- },
394
- ],
395
- };
396
- }
397
714
 
398
- verificationFailures++;
399
- log(`verification failed (cycle ${verificationFailures}/${maxVerificationCycles})`);
400
- if (verificationFailures >= maxVerificationCycles) {
401
- const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
402
- writeFileSync(
403
- sentinel,
404
- JSON.stringify(
405
- {
406
- summary,
407
- verification: 'failed',
408
- cycles: verificationFailures,
409
- failures,
410
- warnings,
411
- timestamp: Date.now(),
412
- },
413
- null,
414
- 2,
415
- ),
416
- 'utf8',
417
- );
715
+ const continuationMessage = `You called done but verification failed (cycle ${verificationFailures}/${maxVerificationCycles}):
716
+
717
+ ${failures.map((f) => `- ${f}`).join('\n')}
718
+
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
+ : '';
418
724
  return {
419
725
  isError: true,
420
726
  content: [
421
727
  {
422
728
  type: 'text',
423
- text: `Verification failed after ${maxVerificationCycles} cycles. Giving up. Final failures:\n${failures.map((f) => `- ${f}`).join('\n')}`,
729
+ text: continuationMessage + compilerDecisionGuidance + inconclusiveDecisionGuidance,
424
730
  },
425
731
  ],
426
732
  };
733
+ } finally {
734
+ finishVerification();
427
735
  }
428
-
429
- const continuationMessage = `You called done but verification failed (cycle ${verificationFailures}/${maxVerificationCycles}):
430
-
431
- ${failures.map((f) => `- ${f}`).join('\n')}
432
-
433
- 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.`;
434
- return {
435
- isError: true,
436
- content: [{ type: 'text', text: continuationMessage }],
437
- };
438
736
  }
439
737
 
440
738
  // Auth-mode checkpoint tools — record the request and end the segment. The
@@ -558,4 +856,5 @@ export const COMPILE_SENTINELS = {
558
856
  done: DONE_SENTINEL,
559
857
  giveUp: GIVE_UP_SENTINEL,
560
858
  checkpoint: CHECKPOINT_SENTINEL,
859
+ verificationState: VERIFICATION_STATE_SENTINEL,
561
860
  } as const;