agentxchain 2.155.73 → 2.156.0

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 (38) hide show
  1. package/bin/agentxchain.js +22 -0
  2. package/dashboard/app.js +54 -0
  3. package/dashboard/components/org-audit-trail.js +161 -0
  4. package/dashboard/components/org-history.js +140 -0
  5. package/dashboard/components/org-overview.js +145 -0
  6. package/dashboard/components/org-runs.js +168 -0
  7. package/dashboard/index.html +4 -0
  8. package/package.json +2 -1
  9. package/src/commands/ci-report.js +80 -0
  10. package/src/commands/doctor.js +22 -1
  11. package/src/commands/intake-approve.js +1 -0
  12. package/src/commands/replay.js +1 -0
  13. package/src/commands/run.js +47 -0
  14. package/src/commands/serve.js +64 -0
  15. package/src/commands/step.js +16 -0
  16. package/src/commands/verify.js +1 -0
  17. package/src/lib/adapters/local-cli-adapter.js +147 -0
  18. package/src/lib/api/execution-worker.js +192 -0
  19. package/src/lib/api/hosted-runner.js +494 -0
  20. package/src/lib/api/job-queue.js +152 -0
  21. package/src/lib/api/org-state-aggregator.js +428 -0
  22. package/src/lib/api/project-registry.js +148 -0
  23. package/src/lib/api/protocol-bridge.js +476 -0
  24. package/src/lib/approval-policy.js +12 -0
  25. package/src/lib/ci-reporter.js +188 -0
  26. package/src/lib/claude-local-auth.js +75 -1
  27. package/src/lib/connector-probe.js +21 -0
  28. package/src/lib/continuous-run.js +9 -0
  29. package/src/lib/dashboard/bridge-server.js +10 -5
  30. package/src/lib/governed-state.js +1 -1
  31. package/src/lib/intake.js +32 -6
  32. package/src/lib/normalized-config.js +2 -0
  33. package/src/lib/scope-overlap.js +214 -0
  34. package/src/lib/turn-checkpoint.js +21 -0
  35. package/src/lib/turn-result-validator.js +5 -0
  36. package/src/lib/validation.js +11 -3
  37. package/src/lib/verification-replay.js +125 -4
  38. package/src/lib/vision-reader.js +7 -2
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Org Runs component — renders cross-project run list as a filterable data table.
3
+ *
4
+ * Pure render function: takes data, returns HTML string.
5
+ * Follows the same pattern as ledger.js filter bar and run-history.js table.
6
+ */
7
+
8
+ function esc(str) {
9
+ if (str == null) return '';
10
+ return String(str)
11
+ .replace(/&/g, '&')
12
+ .replace(/</g, '&lt;')
13
+ .replace(/>/g, '&gt;')
14
+ .replace(/"/g, '&quot;')
15
+ .replace(/'/g, '&#39;');
16
+ }
17
+
18
+ function badge(label, color = 'var(--text-dim)') {
19
+ return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
20
+ }
21
+
22
+ function statusBadge(status) {
23
+ switch (status) {
24
+ case 'active': return badge('active', 'var(--green)');
25
+ case 'blocked': return badge('blocked', 'var(--yellow)');
26
+ case 'completed': return badge('completed', 'var(--green)');
27
+ case 'failed': return badge('failed', 'var(--red)');
28
+ default: return badge(status || 'unknown', 'var(--text-dim)');
29
+ }
30
+ }
31
+
32
+ function phaseBadge(phase) {
33
+ switch (phase) {
34
+ case 'planning': return badge('planning', 'var(--accent)');
35
+ case 'implementation': return badge('implementation', '#38bdf8');
36
+ case 'qa': return badge('qa', 'var(--yellow)');
37
+ default: return badge(phase || 'unknown', 'var(--text-dim)');
38
+ }
39
+ }
40
+
41
+ function formatUsd(amount) {
42
+ return '$' + (amount || 0).toFixed(2);
43
+ }
44
+
45
+ function relativeTime(isoString) {
46
+ if (!isoString) return '—';
47
+ try {
48
+ const dt = new Date(isoString);
49
+ const now = Date.now();
50
+ const diffMs = now - dt.getTime();
51
+ if (diffMs < 0) return esc(isoString);
52
+ const seconds = Math.floor(diffMs / 1000);
53
+ if (seconds < 60) return `${seconds}s ago`;
54
+ const minutes = Math.floor(seconds / 60);
55
+ if (minutes < 60) return `${minutes}m ago`;
56
+ const hours = Math.floor(minutes / 60);
57
+ if (hours < 24) return `${hours}h ago`;
58
+ const days = Math.floor(hours / 24);
59
+ return `${days}d ago`;
60
+ } catch {
61
+ return esc(isoString);
62
+ }
63
+ }
64
+
65
+ function truncateRunId(runId) {
66
+ if (!runId) return '—';
67
+ return runId.length > 20 ? runId.slice(0, 20) + '...' : runId;
68
+ }
69
+
70
+ function renderFilterBar(runs, filter) {
71
+ const projectSet = new Set(runs.map(r => r.project_name));
72
+ const projectOptions = ['all', ...Array.from(projectSet).sort()];
73
+ const phaseOptions = ['all', 'planning', 'implementation', 'qa'];
74
+ const statusOptions = ['all', 'active', 'blocked', 'completed', 'failed'];
75
+
76
+ function options(list, selected) {
77
+ return list.map(v =>
78
+ `<option value="${esc(v)}"${v === selected ? ' selected' : ''}>${esc(v)}</option>`
79
+ ).join('');
80
+ }
81
+
82
+ return `<div class="filter-bar">
83
+ <label class="filter-control">
84
+ Project
85
+ <select data-view-control="org-runs-project">
86
+ ${options(projectOptions, filter?.project || 'all')}
87
+ </select>
88
+ </label>
89
+ <label class="filter-control">
90
+ Phase
91
+ <select data-view-control="org-runs-phase">
92
+ ${options(phaseOptions, filter?.phase || 'all')}
93
+ </select>
94
+ </label>
95
+ <label class="filter-control">
96
+ Status
97
+ <select data-view-control="org-runs-status">
98
+ ${options(statusOptions, filter?.status || 'all')}
99
+ </select>
100
+ </label>
101
+ </div>`;
102
+ }
103
+
104
+ function applyFilter(runs, filter) {
105
+ let filtered = runs;
106
+ if (filter?.project && filter.project !== 'all') {
107
+ filtered = filtered.filter(r => r.project_name === filter.project);
108
+ }
109
+ if (filter?.phase && filter.phase !== 'all') {
110
+ filtered = filtered.filter(r => r.phase === filter.phase);
111
+ }
112
+ if (filter?.status && filter.status !== 'all') {
113
+ filtered = filtered.filter(r => r.status === filter.status);
114
+ }
115
+ return filtered;
116
+ }
117
+
118
+ /**
119
+ * @param {{ orgRuns: object, liveMeta: object, filter?: object }} data
120
+ * @returns {string} HTML string
121
+ */
122
+ export function render(data) {
123
+ const runsData = data?.orgRuns?.data || data?.orgRuns || null;
124
+ const runs = Array.isArray(runsData) ? runsData : (runsData?.data || []);
125
+ const filter = data?.filter || {};
126
+
127
+ if (!runs.length && !filter.project && !filter.phase && !filter.status) {
128
+ return `<div class="placeholder">
129
+ <h2>Org Runs</h2>
130
+ <p>No cross-project runs available. Register projects and start governed runs.</p>
131
+ </div>`;
132
+ }
133
+
134
+ const filtered = applyFilter(runs, filter);
135
+
136
+ const tableRows = filtered.map(r => `<tr>
137
+ <td>${esc(r.project_name)}</td>
138
+ <td class="mono">${esc(truncateRunId(r.run_id))}</td>
139
+ <td>${phaseBadge(r.phase)}</td>
140
+ <td>${statusBadge(r.status)}</td>
141
+ <td>${r.turns_completed || 0}</td>
142
+ <td>${formatUsd(r.cost_usd)}</td>
143
+ <td>${relativeTime(r.started_at)}</td>
144
+ <td>${relativeTime(r.updated_at)}</td>
145
+ </tr>`).join('');
146
+
147
+ return `<div class="section">
148
+ <h3>Org Runs (${filtered.length})</h3>
149
+ ${renderFilterBar(runs, filter)}
150
+ <table class="data-table">
151
+ <thead>
152
+ <tr>
153
+ <th>Project</th>
154
+ <th>Run ID</th>
155
+ <th>Phase</th>
156
+ <th>Status</th>
157
+ <th>Turns</th>
158
+ <th>Cost</th>
159
+ <th>Started</th>
160
+ <th>Updated</th>
161
+ </tr>
162
+ </thead>
163
+ <tbody>
164
+ ${tableRows || '<tr><td colspan="8" style="text-align:center;color:var(--text-dim)">No runs match filters</td></tr>'}
165
+ </tbody>
166
+ </table>
167
+ </div>`;
168
+ }
@@ -395,6 +395,10 @@
395
395
  </header>
396
396
  <div class="action-banner" id="action-banner"></div>
397
397
  <nav>
398
+ <a href="#org-overview">Org Overview</a>
399
+ <a href="#org-runs">Org Runs</a>
400
+ <a href="#org-history">Org History</a>
401
+ <a href="#org-audit-trail">Audit Trail</a>
398
402
  <a href="#initiative">Initiative</a>
399
403
  <a href="#cross-repo">Cross-Repo</a>
400
404
  <a href="#timeline" class="active">Timeline</a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.155.73",
3
+ "version": "2.156.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -75,6 +75,7 @@
75
75
  "node": ">=18.17.0 || >=20.5.0"
76
76
  },
77
77
  "devDependencies": {
78
+ "js-yaml": "^4.1.1",
78
79
  "vitest": "^4.1.2"
79
80
  }
80
81
  }
@@ -0,0 +1,80 @@
1
+ import { buildRunExport } from '../lib/export.js';
2
+ import { loadExportArtifact } from '../lib/export-verifier.js';
3
+ import { buildGovernanceReport } from '../lib/report.js';
4
+ import {
5
+ deriveCIExitCode,
6
+ detectCIEnvironment,
7
+ formatGitHubAnnotations,
8
+ formatJUnitXml,
9
+ writeGitHubOutputVars,
10
+ } from '../lib/ci-reporter.js';
11
+
12
+ export async function ciReportCommand(options) {
13
+ const cwd = process.cwd();
14
+ const format = options.format || 'auto';
15
+
16
+ // Step 1: Build or load the governance report
17
+ let report;
18
+ if (options.input && options.input !== '-') {
19
+ const loaded = loadExportArtifact(options.input, cwd);
20
+ if (!loaded.ok) {
21
+ console.error(loaded.error);
22
+ process.exitCode = 1;
23
+ return;
24
+ }
25
+ const result = buildGovernanceReport(loaded.artifact, { input: loaded.input });
26
+ report = result.report;
27
+ } else {
28
+ // Build from current project
29
+ const exportResult = buildRunExport(cwd);
30
+ if (!exportResult.ok) {
31
+ console.error(exportResult.error);
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+ const result = buildGovernanceReport(exportResult.export, { input: 'current-project' });
36
+ report = result.report;
37
+ }
38
+
39
+ // Step 2: Detect CI environment and resolve format
40
+ const ci = detectCIEnvironment();
41
+ let resolvedFormat = format;
42
+ if (resolvedFormat === 'auto') {
43
+ if (ci?.provider === 'github_actions') {
44
+ resolvedFormat = 'github-actions';
45
+ } else {
46
+ resolvedFormat = 'json';
47
+ }
48
+ }
49
+
50
+ // Step 3: Emit formatted output
51
+ switch (resolvedFormat) {
52
+ case 'github-actions': {
53
+ const annotations = formatGitHubAnnotations(report);
54
+ console.log(annotations);
55
+
56
+ // Write output variables if $GITHUB_OUTPUT is set
57
+ const ghOutput = process.env.GITHUB_OUTPUT;
58
+ if (ghOutput) {
59
+ writeGitHubOutputVars(report, ghOutput);
60
+ }
61
+ break;
62
+ }
63
+ case 'junit-xml': {
64
+ const xml = formatJUnitXml(report);
65
+ console.log(xml);
66
+ break;
67
+ }
68
+ case 'json': {
69
+ console.log(JSON.stringify(report, null, 2));
70
+ break;
71
+ }
72
+ default:
73
+ console.error(`Unsupported ci-report format "${resolvedFormat}". Use "github-actions", "junit-xml", "json", or "auto".`);
74
+ process.exitCode = 1;
75
+ return;
76
+ }
77
+
78
+ // Step 4: Exit with CI-appropriate code
79
+ process.exitCode = deriveCIExitCode(report);
80
+ }
@@ -21,7 +21,7 @@ import { detectActiveTurnBindingDrift, detectStateBundleDesync } from '../lib/go
21
21
  import { findPendingApprovedIntents } from '../lib/intake.js';
22
22
  import { checkCleanBaseline } from '../lib/repo-observer.js';
23
23
  import { probeRuntimeSpawnContext } from '../lib/runtime-spawn-context.js';
24
- import { getClaudeSubprocessAuthIssue } from '../lib/claude-local-auth.js';
24
+ import { getClaudeSubprocessAuthIssue, isCursorLocalCliRuntime, isWindsurfLocalCliRuntime, isOpenCodeLocalCliRuntime } from '../lib/claude-local-auth.js';
25
25
 
26
26
  export async function doctorCommand(opts = {}) {
27
27
  const root = findProjectRoot(process.cwd());
@@ -502,6 +502,27 @@ async function checkRuntimeReachable(root, rtId, rt, boundRoleEntries = []) {
502
502
  case 'local_cli': {
503
503
  const probe = probeRuntimeSpawnContext(root, rt, { runtimeId: rtId });
504
504
  if (probe.ok) {
505
+ if (isCursorLocalCliRuntime(rt)) {
506
+ return attachRuntimeContract({
507
+ ...base,
508
+ level: 'pass',
509
+ detail: `${probe.detail} (Cursor IDE local_cli connector)`,
510
+ }, rtId, rt, boundRoleEntries);
511
+ }
512
+ if (isWindsurfLocalCliRuntime(rt)) {
513
+ return attachRuntimeContract({
514
+ ...base,
515
+ level: 'pass',
516
+ detail: `${probe.detail} (Windsurf IDE local_cli connector)`,
517
+ }, rtId, rt, boundRoleEntries);
518
+ }
519
+ if (isOpenCodeLocalCliRuntime(rt)) {
520
+ return attachRuntimeContract({
521
+ ...base,
522
+ level: 'pass',
523
+ detail: `${probe.detail} (OpenCode local_cli connector)`,
524
+ }, rtId, rt, boundRoleEntries);
525
+ }
505
526
  const claudeAuthIssue = await getClaudeSubprocessAuthIssue(rt);
506
527
  if (claudeAuthIssue) {
507
528
  return attachRuntimeContract({
@@ -18,6 +18,7 @@ export async function intakeApproveCommand(opts) {
18
18
  const result = approveIntent(root, opts.intent, {
19
19
  approver: opts.approver || undefined,
20
20
  reason: opts.reason || undefined,
21
+ forceScope: opts.forceScope || false,
21
22
  });
22
23
 
23
24
  if (opts.json) {
@@ -54,6 +54,7 @@ export async function replayTurnCommand(turnId, opts = {}) {
54
54
  root,
55
55
  verification: entry.verification,
56
56
  timeoutMs,
57
+ allowCommandExecution: Boolean(opts.execute),
57
58
  }),
58
59
  };
59
60
 
@@ -18,6 +18,7 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
18
18
  import { join } from 'path';
19
19
  import { loadProjectContext, loadProjectState } from '../lib/config.js';
20
20
  import { runLoop } from '../lib/run-loop.js';
21
+ import { isCredentialedExitGate } from '../lib/approval-policy.js';
21
22
  import { transitionActiveTurnLifecycle } from '../lib/runner-interface.js';
22
23
  import { buildRunExport } from '../lib/export.js';
23
24
  import { buildGovernanceReport, formatGovernanceReportMarkdown } from '../lib/report.js';
@@ -365,6 +366,12 @@ export async function executeGovernedRun(context, opts = {}) {
365
366
  onStartupHeartbeat: ({ elapsed_since_spawn_ms }) => {
366
367
  tracker.heartbeat(`Adapter keepalive (${Math.round((elapsed_since_spawn_ms || 0) / 1000)}s since spawn)`);
367
368
  },
369
+ onLivenessHeartbeat: ({ elapsed_since_spawn_ms }) => {
370
+ // RB-6: refresh last_activity_at while the subprocess is alive but
371
+ // output-silent, so the stale-turn watchdog does not misfire during
372
+ // long quiet operations (e.g. running a test suite inside the turn).
373
+ tracker.heartbeat(`Subprocess alive (${Math.round((elapsed_since_spawn_ms || 0) / 1000)}s since spawn)`);
374
+ },
368
375
  };
369
376
 
370
377
  const recordOutputActivity = (stream, text) => {
@@ -438,6 +445,37 @@ export async function executeGovernedRun(context, opts = {}) {
438
445
  const transport = runtime ? resolvePromptTransport(runtime) : 'dispatch_bundle_only';
439
446
  log(chalk.dim(` Dispatching to local CLI: ${runtime?.command || '(default)'} transport: ${transport}`));
440
447
  adapterResult = await dispatchLocalCli(projectRoot, state, cfg, adapterOpts);
448
+
449
+ // Rate-limit backoff: if adapter classified as rate_limited + retryable,
450
+ // apply exponential backoff and re-dispatch. Does NOT consume max_turn_retries.
451
+ const RATE_LIMIT_MAX_RETRIES = 5;
452
+ const RATE_LIMIT_BASE_DELAY_MS = 2000;
453
+ const RATE_LIMIT_MAX_DELAY_MS = 120_000;
454
+ const RATE_LIMIT_BACKOFF_MULTIPLIER = 2;
455
+
456
+ let rateLimitAttempt = 0;
457
+ while (
458
+ adapterResult?.classified?.error_class === 'rate_limited' &&
459
+ adapterResult?.classified?.retryable === true &&
460
+ rateLimitAttempt < RATE_LIMIT_MAX_RETRIES
461
+ ) {
462
+ rateLimitAttempt++;
463
+ const resetMs = adapterResult.classified.reset_ms;
464
+ const exponentialDelay = Math.min(
465
+ RATE_LIMIT_MAX_DELAY_MS,
466
+ RATE_LIMIT_BASE_DELAY_MS * RATE_LIMIT_BACKOFF_MULTIPLIER ** (rateLimitAttempt - 1),
467
+ );
468
+ const delayMs = resetMs != null ? Math.max(resetMs, RATE_LIMIT_BASE_DELAY_MS) : exponentialDelay;
469
+ // Add jitter: 0-25% of delay
470
+ const jitter = Math.floor(Math.random() * delayMs * 0.25);
471
+ const totalDelay = delayMs + jitter;
472
+
473
+ log(chalk.yellow(` Rate-limited by provider (attempt ${rateLimitAttempt}/${RATE_LIMIT_MAX_RETRIES}). Retrying in ${Math.ceil(totalDelay / 1000)}s...`));
474
+ await new Promise((resolve) => setTimeout(resolve, totalDelay));
475
+
476
+ log(chalk.dim(` Re-dispatching after rate-limit backoff...`));
477
+ adapterResult = await dispatchLocalCli(projectRoot, state, cfg, adapterOpts);
478
+ }
441
479
  } else if (runtimeType === 'remote_agent') {
442
480
  ensureStartingState(null);
443
481
  ensureRunningState('request');
@@ -578,6 +616,15 @@ export async function executeGovernedRun(context, opts = {}) {
578
616
 
579
617
  async approveGate(gateType, state) {
580
618
  if (autoApprove) {
619
+ // Lights-out without blind trust: a credentialed gate (protecting an
620
+ // irreversible, external, or operator-owned action) is NEVER auto-approved,
621
+ // even under --auto-approve / continuous mode. It falls through to gate_held,
622
+ // so a human must approve it (agentxchain approve-transition / approve-completion).
623
+ if (isCredentialedExitGate(config, state?.phase)) {
624
+ const gateId = config?.routing?.[state?.phase]?.exit_gate;
625
+ log(chalk.red(` Gate "${gateId}" is credentialed — auto-approval refused. A human must approve (lights-out without blind trust).`));
626
+ return false;
627
+ }
581
628
  log(chalk.yellow(` Auto-approved ${gateType} gate`));
582
629
  return true;
583
630
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * agentxchain serve — start the hosted runner HTTP server.
3
+ *
4
+ * Exposes control plane API routes for remote run management and dispatches
5
+ * governed turns to cloud agent APIs via the execution worker.
6
+ *
7
+ * Usage:
8
+ * agentxchain serve [--port 4100] [--host 127.0.0.1] [--project <path>]
9
+ */
10
+
11
+ import { resolve } from 'node:path';
12
+ import { findProjectRoot, loadProjectContext } from '../lib/config.js';
13
+ import { createHostedRunner } from '../lib/api/hosted-runner.js';
14
+
15
+ export async function serveCommand(opts) {
16
+ const rootHint = opts.project || process.cwd();
17
+ const root = findProjectRoot(resolve(rootHint));
18
+
19
+ if (!root) {
20
+ process.stderr.write(
21
+ `[agentxchain serve] Error: no agentxchain.json found at or above ${rootHint}\n`
22
+ );
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+
27
+ const ctx = loadProjectContext(root);
28
+ if (!ctx || !ctx.config) {
29
+ process.stderr.write(
30
+ `[agentxchain serve] Error: failed to load project configuration from ${root}\n`
31
+ );
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+
36
+ const port = parseInt(opts.port || '4100', 10);
37
+ const host = opts.host || '127.0.0.1';
38
+
39
+ const additionalProjects = (opts.projects || '')
40
+ .split(',')
41
+ .map(p => p.trim())
42
+ .filter(Boolean)
43
+ .map(p => resolve(p));
44
+
45
+ const runner = createHostedRunner({
46
+ root,
47
+ config: ctx.config,
48
+ port,
49
+ host,
50
+ projects: additionalProjects,
51
+ });
52
+
53
+ await runner.start();
54
+ process.stderr.write(`[agentxchain serve] Hosted runner listening on http://${host}:${port}\n`);
55
+
56
+ const shutdown = async () => {
57
+ process.stderr.write('[agentxchain serve] Shutting down...\n');
58
+ await runner.stop();
59
+ process.exit(0);
60
+ };
61
+
62
+ process.on('SIGINT', shutdown);
63
+ process.on('SIGTERM', shutdown);
64
+ }
@@ -77,6 +77,7 @@ import { consumeNextApprovedIntent } from '../lib/intake.js';
77
77
  import { failTurnStartup, reconcileStaleTurns } from '../lib/stale-turn-watchdog.js';
78
78
  import { isKnownTurnRunningProofStream } from '../lib/dispatch-streams.js';
79
79
  import { getDispatchProgressRelativePath } from '../lib/dispatch-progress.js';
80
+ import { checkpointAcceptedTurn } from '../lib/turn-checkpoint.js';
80
81
 
81
82
  export async function stepCommand(opts) {
82
83
  const context = loadProjectContext();
@@ -1002,6 +1003,21 @@ export async function stepCommand(opts) {
1002
1003
  }
1003
1004
 
1004
1005
  printAcceptSummary(acceptResult, config);
1006
+
1007
+ // Auto-checkpoint accepted turn so workspace is clean for next assignment
1008
+ if (!opts.noCheckpoint && existsSync(join(root, '.git'))) {
1009
+ const checkpoint = checkpointAcceptedTurn(root, { turnId: turn.turn_id });
1010
+ if (!checkpoint.ok) {
1011
+ console.log(` ${chalk.yellow('Checkpoint:')} accepted but checkpoint failed`);
1012
+ console.log(` ${chalk.dim('Error:')} ${checkpoint.error}`);
1013
+ console.log(` ${chalk.dim('Retry:')} agentxchain checkpoint-turn --turn ${turn.turn_id}`);
1014
+ console.log('');
1015
+ process.exit(1);
1016
+ }
1017
+ if (!checkpoint.skipped) {
1018
+ console.log(` ${chalk.dim('Checkpoint:')} ${checkpoint.checkpoint_sha}`);
1019
+ }
1020
+ }
1005
1021
  } else {
1006
1022
  // Reject and potentially retry
1007
1023
  console.log(chalk.yellow('Validation failed:'));
@@ -244,6 +244,7 @@ export async function verifyTurnCommand(turnId, opts = {}) {
244
244
  root,
245
245
  verification: turnResult.verification,
246
246
  timeoutMs,
247
+ allowCommandExecution: Boolean(opts.execute),
247
248
  }),
248
249
  };
249
250