agentxchain 2.63.0 → 2.65.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.
@@ -59,7 +59,7 @@ import { generateCommand } from '../src/commands/generate.js';
59
59
  import { doctorCommand } from '../src/commands/doctor.js';
60
60
  import { superviseCommand } from '../src/commands/supervise.js';
61
61
  import { validateCommand } from '../src/commands/validate.js';
62
- import { verifyExportCommand, verifyProtocolCommand } from '../src/commands/verify.js';
62
+ import { verifyExportCommand, verifyProtocolCommand, verifyTurnCommand } from '../src/commands/verify.js';
63
63
  import { kickoffCommand } from '../src/commands/kickoff.js';
64
64
  import { rebindCommand } from '../src/commands/rebind.js';
65
65
  import { branchCommand } from '../src/commands/branch.js';
@@ -86,6 +86,8 @@ import {
86
86
  } from '../src/commands/plugin.js';
87
87
  import { templateSetCommand } from '../src/commands/template-set.js';
88
88
  import { templateListCommand } from '../src/commands/template-list.js';
89
+ import { phaseCommand } from '../src/commands/phase.js';
90
+ import { gateCommand } from '../src/commands/gate.js';
89
91
  import { roleCommand } from '../src/commands/role.js';
90
92
  import { turnShowCommand } from '../src/commands/turn.js';
91
93
  import { templateValidateCommand } from '../src/commands/template-validate.js';
@@ -330,7 +332,14 @@ program
330
332
 
331
333
  const verifyCmd = program
332
334
  .command('verify')
333
- .description('Verify protocol conformance targets');
335
+ .description('Verify governed turns, export artifacts, and protocol conformance targets');
336
+
337
+ verifyCmd
338
+ .command('turn [turn_id]')
339
+ .description('Replay a staged turn\'s declared machine-evidence commands and compare exit codes')
340
+ .option('-j, --json', 'Output as JSON')
341
+ .option('--timeout <ms>', 'Per-command replay timeout in milliseconds', '30000')
342
+ .action(verifyTurnCommand);
334
343
 
335
344
  verifyCmd
336
345
  .command('protocol')
@@ -487,6 +496,39 @@ templateCmd
487
496
  .option('-j, --json', 'Output as JSON')
488
497
  .action(templateValidateCommand);
489
498
 
499
+ const phaseCmd = program
500
+ .command('phase')
501
+ .description('Inspect governed workflow phases');
502
+
503
+ phaseCmd
504
+ .command('list')
505
+ .description('List governed phases in routing order')
506
+ .option('-j, --json', 'Output as JSON')
507
+ .action((opts) => phaseCommand('list', null, opts));
508
+
509
+ phaseCmd
510
+ .command('show [phase]')
511
+ .description('Show one governed phase in detail')
512
+ .option('-j, --json', 'Output as JSON')
513
+ .action((phaseId, opts) => phaseCommand('show', phaseId, opts));
514
+
515
+ const gateCmd = program
516
+ .command('gate')
517
+ .description('Inspect governed gate definitions');
518
+
519
+ gateCmd
520
+ .command('list')
521
+ .description('List all defined gates with phase linkage and predicate summary')
522
+ .option('-j, --json', 'Output as JSON')
523
+ .action((opts) => gateCommand('list', null, opts));
524
+
525
+ gateCmd
526
+ .command('show <gate_id>')
527
+ .description('Show a single gate contract, predicates, and status')
528
+ .option('-j, --json', 'Output as JSON')
529
+ .option('--evaluate', 'Live-evaluate gate predicates against current filesystem')
530
+ .action((gateId, opts) => gateCommand('show', gateId, opts));
531
+
490
532
  const roleCmd = program
491
533
  .command('role')
492
534
  .description('Inspect governed role definitions');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.63.0",
3
+ "version": "2.65.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -166,6 +166,9 @@ export async function acceptTurnCommand(opts = {}) {
166
166
  if (accepted?.cost?.usd != null) {
167
167
  console.log(` ${chalk.dim('Cost:')} $${formatUsd(accepted.cost.usd)}`);
168
168
  }
169
+ if (accepted?.verification_replay) {
170
+ console.log(` ${chalk.dim('Replay:')} ${accepted.verification_replay.overall} (${accepted.verification_replay.matched_commands}/${accepted.verification_replay.replayed_commands})`);
171
+ }
169
172
  if (result.budget_warning) {
170
173
  console.log(` ${chalk.yellow('Budget warning:')} ${result.budget_warning}`);
171
174
  }
@@ -0,0 +1,315 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import chalk from 'chalk';
4
+ import { loadProjectContext, loadProjectState } from '../lib/config.js';
5
+ import {
6
+ evaluateArtifactSemantics,
7
+ evaluateWorkflowGateSemantics,
8
+ getSemanticIdForPath,
9
+ } from '../lib/workflow-gate-semantics.js';
10
+ import { getEffectiveGateArtifacts, hasRoleParticipationInPhase } from '../lib/gate-evaluator.js';
11
+
12
+ export function gateCommand(subcommand, gateId, opts) {
13
+ const context = loadProjectContext();
14
+ if (!context) {
15
+ console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
16
+ process.exit(1);
17
+ }
18
+
19
+ const { root, config, version } = context;
20
+ if (version !== 4 || config.protocol_mode !== 'governed') {
21
+ console.log(chalk.red(' Not a governed AgentXchain project (requires v4 config).'));
22
+ process.exit(1);
23
+ }
24
+
25
+ const gateIds = Object.keys(config.gates || {});
26
+ if (gateIds.length === 0) {
27
+ console.log(chalk.red(' No gates defined in config.'));
28
+ process.exit(1);
29
+ }
30
+
31
+ const state = loadProjectState(root, config);
32
+
33
+ if (subcommand === 'show') {
34
+ return showGate(gateId, { root, config, state, gateIds, opts });
35
+ }
36
+
37
+ return listGates({ root, config, state, gateIds, opts });
38
+ }
39
+
40
+ function findLinkedPhase(gateId, routing) {
41
+ for (const [phaseId, route] of Object.entries(routing || {})) {
42
+ if (route.exit_gate === gateId) return phaseId;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function buildGateRecord(root, config, state, gateId, evaluate) {
48
+ const gateDef = config.gates[gateId] || {};
49
+ const linkedPhase = findLinkedPhase(gateId, config.routing);
50
+ const effectiveArtifacts = getEffectiveGateArtifacts(config, gateDef, linkedPhase);
51
+ const requiresFiles = Array.isArray(gateDef.requires_files) ? gateDef.requires_files : [];
52
+ const requiresVerification = gateDef.requires_verification_pass === true;
53
+ const requiresHumanApproval = gateDef.requires_human_approval === true;
54
+ const status = state?.phase_gate_status?.[gateId] || null;
55
+
56
+ const lastFailure = state?.last_gate_failure?.gate_id === gateId
57
+ ? state.last_gate_failure
58
+ : null;
59
+
60
+ const record = {
61
+ id: gateId,
62
+ linked_phase: linkedPhase,
63
+ requires_files: requiresFiles,
64
+ effective_artifacts: effectiveArtifacts.map(normalizeEffectiveArtifact),
65
+ requires_verification_pass: requiresVerification,
66
+ requires_human_approval: requiresHumanApproval,
67
+ status,
68
+ last_failure: lastFailure
69
+ ? {
70
+ gate_type: lastFailure.gate_type,
71
+ phase: lastFailure.phase,
72
+ failed_at: lastFailure.failed_at,
73
+ reasons: lastFailure.reasons || [],
74
+ missing_files: lastFailure.missing_files || [],
75
+ }
76
+ : null,
77
+ };
78
+
79
+ if (evaluate) {
80
+ record.evaluation = evaluateGateSnapshot({
81
+ root,
82
+ state,
83
+ linkedPhase,
84
+ requiresVerification,
85
+ effectiveArtifacts,
86
+ });
87
+ }
88
+
89
+ return record;
90
+ }
91
+
92
+ function normalizeEffectiveArtifact(artifact) {
93
+ return {
94
+ path: artifact.path,
95
+ required: artifact.required !== false,
96
+ owned_by: artifact.owned_by || null,
97
+ legacy_semantics: artifact.useLegacySemantics ? getSemanticIdForPath(artifact.path) : null,
98
+ semantic_checks: Array.isArray(artifact.semanticChecks) ? artifact.semanticChecks : [],
99
+ };
100
+ }
101
+
102
+ function getLatestAcceptedTurnForPhase(state, phase) {
103
+ if (!phase || !Array.isArray(state?.history)) {
104
+ return null;
105
+ }
106
+
107
+ for (let index = state.history.length - 1; index >= 0; index--) {
108
+ const entry = state.history[index];
109
+ if (entry?.phase === phase) {
110
+ return entry;
111
+ }
112
+ }
113
+
114
+ return null;
115
+ }
116
+
117
+ function buildOwnershipFailure(artifact, linkedPhase) {
118
+ if (!artifact.owned_by) {
119
+ return null;
120
+ }
121
+
122
+ if (!linkedPhase) {
123
+ return `Gate is not linked to a routing phase, so ownership for "${artifact.path}" cannot be evaluated.`;
124
+ }
125
+
126
+ return `"${artifact.path}" requires participation from role "${artifact.owned_by}" in phase "${linkedPhase}", but no accepted turn from that role was found`;
127
+ }
128
+
129
+ function evaluateGateSnapshot({ root, state, linkedPhase, requiresVerification, effectiveArtifacts }) {
130
+ const missingFiles = [];
131
+ const semanticFailures = [];
132
+ const ownershipFailures = [];
133
+
134
+ const artifacts = effectiveArtifacts.map((artifact) => {
135
+ const exists = existsSync(join(root, artifact.path));
136
+ const failures = [];
137
+
138
+ if (!exists && artifact.required) {
139
+ const reason = `Required file missing: ${artifact.path}`;
140
+ missingFiles.push(artifact.path);
141
+ failures.push(reason);
142
+ }
143
+
144
+ if (exists && artifact.useLegacySemantics) {
145
+ const semanticCheck = evaluateWorkflowGateSemantics(root, artifact.path);
146
+ if (semanticCheck && !semanticCheck.ok) {
147
+ semanticFailures.push(semanticCheck.reason);
148
+ failures.push(semanticCheck.reason);
149
+ }
150
+ }
151
+
152
+ if (exists) {
153
+ for (const semantic of artifact.semanticChecks || []) {
154
+ const semanticCheck = evaluateArtifactSemantics(root, {
155
+ path: artifact.path,
156
+ semantics: semantic.semantics,
157
+ semantics_config: semantic.semantics_config,
158
+ });
159
+ if (semanticCheck && !semanticCheck.ok) {
160
+ semanticFailures.push(semanticCheck.reason);
161
+ failures.push(semanticCheck.reason);
162
+ }
163
+ }
164
+ }
165
+
166
+ const ownershipSatisfied = artifact.owned_by && linkedPhase
167
+ ? hasRoleParticipationInPhase(state, linkedPhase, artifact.owned_by)
168
+ : null;
169
+ if (artifact.owned_by && linkedPhase && ownershipSatisfied === false) {
170
+ const reason = buildOwnershipFailure(artifact, linkedPhase);
171
+ ownershipFailures.push(reason);
172
+ failures.push(reason);
173
+ }
174
+
175
+ return {
176
+ ...normalizeEffectiveArtifact(artifact),
177
+ exists,
178
+ ownership_satisfied: ownershipSatisfied,
179
+ failures,
180
+ };
181
+ });
182
+
183
+ const latestAcceptedTurn = getLatestAcceptedTurnForPhase(state, linkedPhase);
184
+ const verificationStatus = latestAcceptedTurn?.verification?.status || null;
185
+ const verificationPassed = verificationStatus === 'pass' || verificationStatus === 'attested_pass';
186
+ const reasons = [
187
+ ...artifacts.flatMap((artifact) => artifact.failures),
188
+ ];
189
+
190
+ if (requiresVerification && !verificationPassed) {
191
+ reasons.push(`Verification status is "${verificationStatus || 'missing'}", requires "pass" or "attested_pass"`);
192
+ }
193
+
194
+ return {
195
+ phase: linkedPhase,
196
+ passed: reasons.length === 0,
197
+ reasons,
198
+ missing_files: missingFiles,
199
+ semantic_failures: semanticFailures,
200
+ ownership_failures: ownershipFailures,
201
+ artifacts,
202
+ verification: {
203
+ required: requiresVerification,
204
+ source_turn_id: latestAcceptedTurn?.turn_id || null,
205
+ status: verificationStatus,
206
+ passed: requiresVerification ? verificationPassed : null,
207
+ },
208
+ };
209
+ }
210
+
211
+ function listGates({ root, config, state, gateIds, opts }) {
212
+ const gates = gateIds.map((id) => buildGateRecord(root, config, state, id, false));
213
+
214
+ if (opts.json) {
215
+ console.log(JSON.stringify({ gates }, null, 2));
216
+ return;
217
+ }
218
+
219
+ console.log(chalk.bold(`\n Gates (${gates.length}):\n`));
220
+ for (const gate of gates) {
221
+ const phase = gate.linked_phase || chalk.dim('orphaned');
222
+ const approval = gate.requires_human_approval ? chalk.yellow('human-approval') : 'auto';
223
+ const predicates = [];
224
+ if (gate.effective_artifacts.length > 0) predicates.push(`${gate.effective_artifacts.length} artifact${gate.effective_artifacts.length > 1 ? 's' : ''}`);
225
+ if (gate.requires_verification_pass) predicates.push('verification');
226
+ const predicateStr = predicates.length > 0 ? predicates.join(' + ') : 'none';
227
+ const statusStr = gate.status ? ` [${gate.status}]` : '';
228
+ console.log(` ${chalk.cyan(gate.id)}${statusStr} — phase ${chalk.bold(phase)}, ${approval}, requires ${predicateStr}`);
229
+ }
230
+ console.log('');
231
+ console.log(chalk.dim(' Usage: agentxchain gate show <gate_id>\n'));
232
+ }
233
+
234
+ function showGate(requestedGateId, { root, config, state, gateIds, opts }) {
235
+ if (!requestedGateId) {
236
+ console.log(chalk.red(' Gate ID is required.'));
237
+ console.log(chalk.dim(` Available: ${gateIds.join(', ')}`));
238
+ process.exit(1);
239
+ }
240
+
241
+ if (!config.gates[requestedGateId]) {
242
+ console.log(chalk.red(` Unknown gate: ${requestedGateId}`));
243
+ console.log(chalk.dim(` Available: ${gateIds.join(', ')}`));
244
+ process.exit(1);
245
+ }
246
+
247
+ const gate = buildGateRecord(root, config, state, requestedGateId, opts.evaluate);
248
+
249
+ if (opts.json) {
250
+ console.log(JSON.stringify(gate, null, 2));
251
+ return;
252
+ }
253
+
254
+ console.log(chalk.bold(`\n Gate: ${chalk.cyan(gate.id)}\n`));
255
+ console.log(` Linked phase: ${gate.linked_phase || chalk.dim('none (orphaned)')}`);
256
+ console.log(` Human approval: ${gate.requires_human_approval ? chalk.yellow('yes') : 'no'}`);
257
+ console.log(` Verification: ${gate.requires_verification_pass ? 'required' : 'not required'}`);
258
+ if (gate.status) {
259
+ const statusColor = gate.status === 'passed' ? chalk.green : gate.status === 'failed' ? chalk.red : chalk.yellow;
260
+ console.log(` Status: ${statusColor(gate.status)}`);
261
+ }
262
+ if (gate.evaluation) {
263
+ console.log(` Evaluation: ${gate.evaluation.passed ? chalk.green('pass') : chalk.red('fail')}`);
264
+ }
265
+ console.log('');
266
+
267
+ if (gate.effective_artifacts.length === 0) {
268
+ console.log(` ${chalk.dim('Effective artifacts:')} none\n`);
269
+ } else {
270
+ console.log(` ${chalk.dim('Effective artifacts:')}`);
271
+ const evaluatedArtifacts = gate.evaluation?.artifacts || [];
272
+ for (const artifact of gate.effective_artifacts) {
273
+ const artifactEval = evaluatedArtifacts.find((entry) => entry.path === artifact.path);
274
+ const icon = artifactEval
275
+ ? artifactEval.failures.length === 0
276
+ ? chalk.green('\u2713')
277
+ : chalk.red('\u2717')
278
+ : chalk.dim('-');
279
+ const owner = artifact.owned_by || 'none';
280
+ const semantics = [
281
+ artifact.legacy_semantics,
282
+ ...artifact.semantic_checks.map((entry) => entry.semantics),
283
+ ].filter(Boolean);
284
+ console.log(` ${icon} ${artifact.path} [${artifact.required ? 'required' : 'optional'}] [owner: ${owner}] [semantics: ${semantics.length > 0 ? semantics.join(', ') : 'none'}]`);
285
+ if (artifactEval?.failures.length) {
286
+ for (const failure of artifactEval.failures) {
287
+ console.log(` ${chalk.dim(`- ${failure}`)}`);
288
+ }
289
+ }
290
+ }
291
+ console.log('');
292
+ }
293
+
294
+ if (gate.evaluation && gate.requires_verification_pass) {
295
+ const vpIcon = gate.evaluation.verification.passed ? chalk.green('\u2713') : chalk.red('\u2717');
296
+ const source = gate.evaluation.verification.source_turn_id
297
+ ? ` (${gate.evaluation.verification.source_turn_id})`
298
+ : '';
299
+ console.log(` Verification pass: ${vpIcon} ${gate.evaluation.verification.passed ? 'yes' : 'no'}${source}\n`);
300
+ }
301
+
302
+ if (gate.last_failure) {
303
+ console.log(chalk.dim(' Last failure:'));
304
+ console.log(` Type: ${gate.last_failure.gate_type}`);
305
+ console.log(` Phase: ${gate.last_failure.phase}`);
306
+ console.log(` At: ${gate.last_failure.failed_at}`);
307
+ if (gate.last_failure.reasons.length > 0) {
308
+ console.log(` Reasons: ${gate.last_failure.reasons.join('; ')}`);
309
+ }
310
+ if (gate.last_failure.missing_files.length > 0) {
311
+ console.log(` Missing: ${gate.last_failure.missing_files.join(', ')}`);
312
+ }
313
+ console.log('');
314
+ }
315
+ }
@@ -0,0 +1,159 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import chalk from 'chalk';
4
+ import { loadProjectContext, loadProjectState } from '../lib/config.js';
5
+ import { getNextPhase } from '../lib/gate-evaluator.js';
6
+ import { getMaxConcurrentTurns } from '../lib/normalized-config.js';
7
+
8
+ export function phaseCommand(subcommand, phaseId, opts) {
9
+ const context = loadProjectContext();
10
+ if (!context) {
11
+ console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
12
+ process.exit(1);
13
+ }
14
+
15
+ const { root, config, rawConfig, version } = context;
16
+ if (version !== 4 || config.protocol_mode !== 'governed') {
17
+ console.log(chalk.red(' Not a governed AgentXchain project (requires v4 config).'));
18
+ process.exit(1);
19
+ }
20
+
21
+ const phaseIds = Object.keys(config.routing || {});
22
+ if (phaseIds.length === 0) {
23
+ console.log(chalk.red(' No governed phases are defined in routing.'));
24
+ process.exit(1);
25
+ }
26
+
27
+ const state = loadProjectState(root, config);
28
+
29
+ if (subcommand === 'show') {
30
+ return showPhase(phaseId, { root, config, rawConfig, state, phaseIds, opts });
31
+ }
32
+
33
+ return listPhases({ root, config, rawConfig, state, phaseIds, opts });
34
+ }
35
+
36
+ function listPhases({ root, config, rawConfig, state, phaseIds, opts }) {
37
+ const phases = phaseIds.map((phaseId) => buildPhaseRecord(root, config, rawConfig, state, phaseId));
38
+
39
+ if (opts.json) {
40
+ console.log(JSON.stringify({
41
+ current_phase: state?.phase || null,
42
+ phases,
43
+ }, null, 2));
44
+ return;
45
+ }
46
+
47
+ console.log(chalk.bold(`\n Phases (${phases.length}):\n`));
48
+ for (const phase of phases) {
49
+ const current = phase.is_current ? chalk.green(' [current]') : '';
50
+ const entry = phase.entry_role || 'none';
51
+ const gate = phase.exit_gate || 'open';
52
+ const next = phase.next_phase || 'final';
53
+ console.log(` ${chalk.cyan(phase.id)}${current} — entry ${chalk.bold(entry)}, gate ${gate}, next ${next}, artifacts ${phase.workflow_kit.artifacts.length}`);
54
+ }
55
+ console.log('');
56
+ console.log(chalk.dim(' Usage: agentxchain phase show <phase>\n'));
57
+ }
58
+
59
+ function showPhase(requestedPhaseId, { root, config, rawConfig, state, phaseIds, opts }) {
60
+ const phaseId = requestedPhaseId || state?.phase || phaseIds[0];
61
+ if (!config.routing?.[phaseId]) {
62
+ console.log(chalk.red(` Unknown phase: ${phaseId}`));
63
+ console.log(chalk.dim(` Available: ${phaseIds.join(', ')}`));
64
+ process.exit(1);
65
+ }
66
+
67
+ const phase = buildPhaseRecord(root, config, rawConfig, state, phaseId);
68
+
69
+ if (opts.json) {
70
+ console.log(JSON.stringify(phase, null, 2));
71
+ return;
72
+ }
73
+
74
+ console.log(chalk.bold(`\n Phase: ${chalk.cyan(phase.id)}${phase.is_current ? chalk.green(' [current]') : ''}\n`));
75
+ console.log(` Entry role: ${phase.entry_role || chalk.dim('none')}`);
76
+ console.log(` Exit gate: ${phase.exit_gate || chalk.dim('open')}`);
77
+ if (phase.exit_gate_status) {
78
+ console.log(` Gate status: ${phase.exit_gate_status}`);
79
+ }
80
+ console.log(` Next phase: ${phase.next_phase || chalk.dim('final')}`);
81
+ console.log(` Next roles: ${phase.allowed_next_roles.length > 0 ? phase.allowed_next_roles.join(', ') : chalk.dim('none')}`);
82
+ console.log(` Max turns: ${phase.max_concurrent_turns}`);
83
+ if (phase.timeout_minutes != null || phase.timeout_action) {
84
+ console.log(` Timeout override: ${phase.timeout_minutes != null ? `${phase.timeout_minutes}m` : chalk.dim('default')} / ${phase.timeout_action || chalk.dim('default')}`);
85
+ }
86
+ console.log(` Workflow source: ${phase.workflow_kit.source}`);
87
+ if (phase.workflow_kit.template) {
88
+ console.log(` Workflow template: ${phase.workflow_kit.template}`);
89
+ }
90
+ console.log('');
91
+
92
+ if (phase.workflow_kit.artifacts.length === 0) {
93
+ console.log(` ${chalk.dim('Artifacts:')} none declared\n`);
94
+ return;
95
+ }
96
+
97
+ console.log(` ${chalk.dim('Artifacts:')}`);
98
+ for (const artifact of phase.workflow_kit.artifacts) {
99
+ const icon = artifact.exists ? chalk.green('✓') : (artifact.required ? chalk.red('✗') : chalk.yellow('○'));
100
+ const ownerLabel = artifact.owner_resolution === 'explicit'
101
+ ? artifact.owned_by
102
+ : artifact.owner_resolution === 'entry_role'
103
+ ? `${chalk.dim(artifact.owned_by + ' (hint, not enforced)')}`
104
+ : 'none';
105
+ const semantics = artifact.semantics || 'none';
106
+ const required = artifact.required ? 'required' : 'optional';
107
+ console.log(` ${icon} ${artifact.path} [${required}] [owner: ${ownerLabel}] [semantics: ${semantics}]`);
108
+ }
109
+ if (phase.workflow_kit.artifacts.some((artifact) => artifact.owner_resolution === 'entry_role')) {
110
+ console.log(` ${chalk.dim('Hint: inferred ownership from entry_role is display-only. Only explicit owned_by is enforced at gate evaluation.')}`);
111
+ }
112
+ console.log('');
113
+ }
114
+
115
+ function buildPhaseRecord(root, config, rawConfig, state, phaseId) {
116
+ const route = config.routing?.[phaseId] || {};
117
+ const normalizedPhaseKit = config.workflow_kit?.phases?.[phaseId] || null;
118
+ const rawWorkflowKit = rawConfig.workflow_kit;
119
+ const rawPhaseKit = rawWorkflowKit?.phases?.[phaseId] || null;
120
+ const hasExplicitWorkflowKit = rawWorkflowKit !== undefined && rawWorkflowKit !== null;
121
+
122
+ const workflowSource = !hasExplicitWorkflowKit
123
+ ? 'default'
124
+ : rawPhaseKit
125
+ ? 'explicit'
126
+ : 'not_declared';
127
+
128
+ const artifacts = (normalizedPhaseKit?.artifacts || []).map((artifact) => {
129
+ const hasExplicitOwner = typeof artifact.owned_by === 'string' && artifact.owned_by.length > 0;
130
+ const ownedBy = hasExplicitOwner ? artifact.owned_by : (route.entry_role || null);
131
+ return {
132
+ path: artifact.path,
133
+ required: artifact.required !== false,
134
+ semantics: artifact.semantics || null,
135
+ owned_by: ownedBy,
136
+ owner_resolution: hasExplicitOwner ? 'explicit' : (ownedBy ? 'entry_role' : 'unowned'),
137
+ ownership_enforced: hasExplicitOwner,
138
+ exists: existsSync(join(root, artifact.path)),
139
+ };
140
+ });
141
+
142
+ return {
143
+ id: phaseId,
144
+ is_current: state?.phase === phaseId,
145
+ entry_role: route.entry_role || null,
146
+ exit_gate: route.exit_gate || null,
147
+ exit_gate_status: route.exit_gate ? (state?.phase_gate_status?.[route.exit_gate] || null) : null,
148
+ next_phase: getNextPhase(phaseId, config.routing || {}),
149
+ allowed_next_roles: Array.isArray(route.allowed_next_roles) ? route.allowed_next_roles : [],
150
+ timeout_minutes: typeof route.timeout_minutes === 'number' ? route.timeout_minutes : null,
151
+ timeout_action: typeof route.timeout_action === 'string' ? route.timeout_action : null,
152
+ max_concurrent_turns: getMaxConcurrentTurns(config, phaseId),
153
+ workflow_kit: {
154
+ source: workflowSource,
155
+ template: typeof rawPhaseKit?.template === 'string' ? rawPhaseKit.template : null,
156
+ artifacts,
157
+ },
158
+ };
159
+ }
@@ -1023,6 +1023,9 @@ function printAcceptSummary(result) {
1023
1023
  if (accepted?.cost?.usd != null) {
1024
1024
  console.log(` ${chalk.dim('Cost:')} $${(accepted.cost.usd || 0).toFixed(2)}`);
1025
1025
  }
1026
+ if (accepted?.verification_replay) {
1027
+ console.log(` ${chalk.dim('Replay:')} ${accepted.verification_replay.overall} (${accepted.verification_replay.matched_commands}/${accepted.verification_replay.replayed_commands})`);
1028
+ }
1026
1029
  console.log('');
1027
1030
 
1028
1031
  if (result.state?.status === 'completed') {
@@ -1,7 +1,16 @@
1
1
  import chalk from 'chalk';
2
+ import { loadProjectContext, loadProjectState } from '../lib/config.js';
3
+ import { getActiveTurns } from '../lib/governed-state.js';
4
+ import { normalizeVerification } from '../lib/repo-observer.js';
5
+ import { validateStagedTurnResult } from '../lib/turn-result-validator.js';
6
+ import { getTurnStagingResultPath } from '../lib/turn-paths.js';
2
7
  import { resolve } from 'node:path';
3
8
  import { loadExportArtifact, verifyExportArtifact } from '../lib/export-verifier.js';
4
9
  import { verifyProtocolConformance } from '../lib/protocol-conformance.js';
10
+ import {
11
+ DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS,
12
+ replayVerificationMachineEvidence,
13
+ } from '../lib/verification-replay.js';
5
14
 
6
15
  export async function verifyProtocolCommand(opts, command) {
7
16
  const requestedTier = Number.parseInt(String(opts.tier || '1'), 10);
@@ -95,6 +104,71 @@ export async function verifyExportCommand(opts) {
95
104
  process.exit(result.ok ? 0 : 1);
96
105
  }
97
106
 
107
+ export async function verifyTurnCommand(turnId, opts = {}) {
108
+ const context = loadProjectContext();
109
+ if (!context) {
110
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
111
+ process.exit(2);
112
+ }
113
+
114
+ if (context.config.protocol_mode !== 'governed' || context.version !== 4) {
115
+ console.log(chalk.red('verify turn is only available in governed v4 projects.'));
116
+ process.exit(2);
117
+ }
118
+
119
+ const timeoutMs = Number.parseInt(String(opts.timeout || String(DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS)), 10);
120
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
121
+ console.log(chalk.red('verify turn requires a positive integer --timeout in milliseconds.'));
122
+ process.exit(2);
123
+ }
124
+
125
+ const { root, config } = context;
126
+ const state = loadProjectState(root, config);
127
+ const activeTurns = getActiveTurns(state);
128
+ const selectedTurnId = resolveTargetTurnId(turnId, activeTurns);
129
+ const selectedTurn = activeTurns[selectedTurnId];
130
+ const stagingPath = getTurnStagingResultPath(selectedTurnId);
131
+ const validationState = {
132
+ ...state,
133
+ active_turns: {
134
+ [selectedTurnId]: selectedTurn,
135
+ },
136
+ };
137
+ const validation = validateStagedTurnResult(root, validationState, config, { stagingPath });
138
+
139
+ if (!validation.ok) {
140
+ emitTurnValidationFailure(validation, opts.json);
141
+ process.exit(1);
142
+ }
143
+
144
+ const turnResult = validation.turnResult;
145
+ const runtimeType = config.runtimes?.[selectedTurn.runtime_id]?.type || 'manual';
146
+ const declaredStatus = turnResult.verification?.status || 'skipped';
147
+ const normalizedStatus = normalizeVerification(turnResult.verification, runtimeType).status;
148
+ const payload = {
149
+ turn_id: selectedTurnId,
150
+ role: selectedTurn.assigned_role,
151
+ runtime_id: selectedTurn.runtime_id,
152
+ runtime_type: runtimeType,
153
+ staging_path: stagingPath,
154
+ declared_status: declaredStatus,
155
+ normalized_status: normalizedStatus,
156
+ validation: {
157
+ ok: true,
158
+ warnings: validation.warnings || [],
159
+ },
160
+ timeout_ms: timeoutMs,
161
+ ...replayVerificationMachineEvidence({
162
+ root,
163
+ verification: turnResult.verification,
164
+ timeoutMs,
165
+ }),
166
+ };
167
+
168
+ emitTurnVerification(payload, opts.json);
169
+ process.exit(payload.overall === 'match' ? 0 : 1);
170
+ }
171
+
98
172
  function printProtocolReport(report) {
99
173
  console.log('');
100
174
  console.log(chalk.bold(' AgentXchain Protocol Conformance'));
@@ -162,3 +236,118 @@ function printExportReport(report) {
162
236
 
163
237
  console.log('');
164
238
  }
239
+
240
+ function resolveTargetTurnId(requestedTurnId, activeTurns) {
241
+ const turnIds = Object.keys(activeTurns);
242
+
243
+ if (requestedTurnId) {
244
+ if (!activeTurns[requestedTurnId]) {
245
+ console.log(chalk.red(`Unknown active turn: ${requestedTurnId}`));
246
+ if (turnIds.length > 0) {
247
+ console.log(chalk.dim(` Available: ${turnIds.join(', ')}`));
248
+ }
249
+ process.exit(2);
250
+ }
251
+ return requestedTurnId;
252
+ }
253
+
254
+ if (turnIds.length === 0) {
255
+ console.log(chalk.red('No active turn found.'));
256
+ process.exit(2);
257
+ }
258
+
259
+ if (turnIds.length > 1) {
260
+ console.log(chalk.red('Multiple active turns are present. Re-run with `agentxchain verify turn <turn_id>`.'));
261
+ console.log(chalk.dim(` Available: ${turnIds.join(', ')}`));
262
+ process.exit(2);
263
+ }
264
+
265
+ return turnIds[0];
266
+ }
267
+
268
+ function emitTurnValidationFailure(validation, jsonMode) {
269
+ const payload = {
270
+ overall: 'validation_failed',
271
+ validation: {
272
+ ok: false,
273
+ stage: validation.stage,
274
+ error_class: validation.error_class,
275
+ errors: validation.errors || [],
276
+ warnings: validation.warnings || [],
277
+ },
278
+ };
279
+
280
+ if (jsonMode) {
281
+ console.log(JSON.stringify(payload, null, 2));
282
+ return;
283
+ }
284
+
285
+ console.log('');
286
+ console.log(chalk.red(' Turn Verification Blocked By Validation'));
287
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
288
+ console.log(` ${chalk.dim('Stage:')} ${validation.stage || 'unknown'}`);
289
+ console.log(` ${chalk.dim('Reason:')} ${validation.error_class || 'validation_error'}`);
290
+ for (const error of validation.errors || []) {
291
+ console.log(` ${chalk.dim('Error:')} ${error}`);
292
+ }
293
+ if ((validation.warnings || []).length > 0) {
294
+ console.log('');
295
+ for (const warning of validation.warnings || []) {
296
+ console.log(` ${chalk.dim('Warning:')} ${warning}`);
297
+ }
298
+ }
299
+ console.log('');
300
+ }
301
+
302
+ function emitTurnVerification(payload, jsonMode) {
303
+ if (jsonMode) {
304
+ console.log(JSON.stringify(payload, null, 2));
305
+ return;
306
+ }
307
+
308
+ console.log('');
309
+ console.log(chalk.bold(` Verify Turn: ${chalk.cyan(payload.turn_id)}`));
310
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
311
+ console.log(` ${chalk.dim('Role:')} ${payload.role}`);
312
+ console.log(` ${chalk.dim('Runtime:')} ${payload.runtime_id} (${payload.runtime_type})`);
313
+ console.log(` ${chalk.dim('Staging:')} ${payload.staging_path}`);
314
+ console.log(` ${chalk.dim('Declared:')} ${payload.declared_status}`);
315
+ console.log(` ${chalk.dim('Normalized:')} ${payload.normalized_status}`);
316
+ console.log(` ${chalk.dim('Outcome:')} ${formatOutcome(payload.overall)}`);
317
+
318
+ for (const warning of payload.validation?.warnings || []) {
319
+ console.log(` ${chalk.dim('Warning:')} ${warning}`);
320
+ }
321
+
322
+ if (payload.reason) {
323
+ console.log(` ${chalk.dim('Reason:')} ${payload.reason}`);
324
+ console.log('');
325
+ return;
326
+ }
327
+
328
+ console.log('');
329
+ for (const command of payload.commands || []) {
330
+ const marker = command.matched ? chalk.green('match') : chalk.red('mismatch');
331
+ console.log(` [${marker}] ${command.command}`);
332
+ console.log(` declared=${command.declared_exit_code} actual=${command.actual_exit_code == null ? 'null' : command.actual_exit_code}`);
333
+ if (command.signal) {
334
+ console.log(` signal=${command.signal}`);
335
+ }
336
+ if (command.timed_out) {
337
+ console.log(' timed_out=true');
338
+ }
339
+ if (command.error) {
340
+ console.log(` error=${command.error}`);
341
+ }
342
+ }
343
+
344
+ console.log('');
345
+ console.log(chalk.dim(' Replay uses the current workspace and shell environment. It verifies declared exit-code reproducibility, not historical stdout/stderr identity.'));
346
+ console.log('');
347
+ }
348
+
349
+ function formatOutcome(outcome) {
350
+ if (outcome === 'match') return chalk.green('match');
351
+ if (outcome === 'mismatch') return chalk.red('mismatch');
352
+ return chalk.yellow('not_reproducible');
353
+ }
@@ -30,7 +30,7 @@ function getWorkflowArtifactsForPhase(config, phase) {
30
30
  return Array.isArray(artifacts) ? artifacts : [];
31
31
  }
32
32
 
33
- function buildEffectiveGateArtifacts(config, gateDef, phase) {
33
+ export function getEffectiveGateArtifacts(config, gateDef, phase) {
34
34
  const byPath = new Map();
35
35
 
36
36
  if (Array.isArray(gateDef?.requires_files)) {
@@ -92,7 +92,7 @@ function prefixSemanticReason(filePath, reason) {
92
92
  return `${filePath}: ${reason}`;
93
93
  }
94
94
 
95
- function hasRoleParticipationInPhase(state, phase, roleId) {
95
+ export function hasRoleParticipationInPhase(state, phase, roleId) {
96
96
  const history = state?.history;
97
97
  if (!Array.isArray(history)) {
98
98
  return false;
@@ -104,7 +104,7 @@ function hasRoleParticipationInPhase(state, phase, roleId) {
104
104
 
105
105
  function evaluateGateArtifacts({ root, config, gateDef, phase, result, state }) {
106
106
  const failures = [];
107
- const artifacts = buildEffectiveGateArtifacts(config, gateDef, phase);
107
+ const artifacts = getEffectiveGateArtifacts(config, gateDef, phase);
108
108
 
109
109
  for (const artifact of artifacts) {
110
110
  const absPath = join(root, artifact.path);
@@ -44,6 +44,10 @@ import { emitRunEvent } from './run-events.js';
44
44
  import { writeSessionCheckpoint } from './session-checkpoint.js';
45
45
  import { recordRunHistory } from './run-history.js';
46
46
  import { buildDefaultRunProvenance } from './run-provenance.js';
47
+ import {
48
+ replayVerificationMachineEvidence,
49
+ summarizeVerificationReplay,
50
+ } from './verification-replay.js';
47
51
 
48
52
  // ── Constants ────────────────────────────────────────────────────────────────
49
53
 
@@ -2367,6 +2371,9 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2367
2371
  const normalizedVerification = normalizeVerification(turnResult.verification, runtimeType);
2368
2372
  const artifactType = turnResult.artifact?.type || 'review';
2369
2373
  const derivedRef = deriveAcceptedRef(observation, artifactType, state.accepted_integration_ref);
2374
+ const verificationReplay = (config.policies || []).some((policy) => policy?.rule === 'require_reproducible_verification')
2375
+ ? replayVerificationMachineEvidence({ root, verification: turnResult.verification })
2376
+ : null;
2370
2377
 
2371
2378
  // Policy evaluation — declarative governance rules (spec: POLICY_ENGINE_SPEC.md)
2372
2379
  const policyResult = evaluatePolicies(config.policies || [], {
@@ -2375,6 +2382,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2375
2382
  turnStatus: turnResult.status,
2376
2383
  turnCostUsd: readTurnCostUsd(turnResult),
2377
2384
  history: historyEntries,
2385
+ verificationReplay,
2378
2386
  });
2379
2387
 
2380
2388
  if (policyResult.blocks.length > 0) {
@@ -2572,6 +2580,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2572
2580
  artifacts_created: turnResult.artifacts_created || [],
2573
2581
  verification: turnResult.verification || {},
2574
2582
  normalized_verification: normalizedVerification,
2583
+ ...(verificationReplay ? { verification_replay: summarizeVerificationReplay(verificationReplay) } : {}),
2575
2584
  artifact: turnResult.artifact || {},
2576
2585
  observed_artifact: observedArtifact,
2577
2586
  proposed_next_role: turnResult.proposed_next_role,
@@ -3176,6 +3185,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3176
3185
  hookResults,
3177
3186
  ...(budgetWarning ? { budget_warning: budgetWarning } : {}),
3178
3187
  ...(policyResult.warnings.length > 0 ? { policy_warnings: policyResult.warnings } : {}),
3188
+ ...(verificationReplay ? { verification_replay: summarizeVerificationReplay(verificationReplay) } : {}),
3179
3189
  };
3180
3190
  }
3181
3191
 
@@ -78,6 +78,32 @@ const RULE_EVALUATORS = {
78
78
  }
79
79
  return { triggered: false, message: '' };
80
80
  },
81
+
82
+ require_reproducible_verification: (_params, ctx) => {
83
+ const replay = ctx.verificationReplay;
84
+ if (!replay) {
85
+ return {
86
+ triggered: true,
87
+ message: 'verification replay context is missing at acceptance time',
88
+ };
89
+ }
90
+
91
+ if (replay.overall === 'match') {
92
+ return { triggered: false, message: '' };
93
+ }
94
+
95
+ if (replay.overall === 'not_reproducible') {
96
+ return {
97
+ triggered: true,
98
+ message: replay.reason || 'verification.machine_evidence did not provide executable proof',
99
+ };
100
+ }
101
+
102
+ return {
103
+ triggered: true,
104
+ message: `verification replay mismatch: ${replay.matched_commands || 0}/${replay.replayed_commands || 0} commands matched declared exit codes`,
105
+ };
106
+ },
81
107
  };
82
108
 
83
109
  export const VALID_POLICY_RULES = Object.keys(RULE_EVALUATORS);
@@ -185,6 +211,12 @@ function validatePolicyParams(rule, params, prefix) {
185
211
  }
186
212
  }
187
213
  break;
214
+
215
+ case 'require_reproducible_verification':
216
+ if (params != null && typeof params !== 'object') {
217
+ errors.push(`${prefix}: params must be an object when provided`);
218
+ }
219
+ break;
188
220
  }
189
221
 
190
222
  return errors;
@@ -0,0 +1,68 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ export const DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS = 30_000;
4
+
5
+ export function replayVerificationMachineEvidence({ root, verification, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS }) {
6
+ const machineEvidence = Array.isArray(verification?.machine_evidence)
7
+ ? verification.machine_evidence
8
+ : [];
9
+
10
+ const payload = {
11
+ timeout_ms: timeoutMs,
12
+ overall: 'not_reproducible',
13
+ replayed_commands: 0,
14
+ matched_commands: 0,
15
+ commands: [],
16
+ };
17
+
18
+ if (machineEvidence.length === 0) {
19
+ payload.reason = 'No verification.machine_evidence commands were declared. commands/evidence_summary are not executable proof.';
20
+ return payload;
21
+ }
22
+
23
+ payload.commands = machineEvidence.map((entry, index) => replayEvidenceCommand(root, entry, index, timeoutMs));
24
+ payload.replayed_commands = payload.commands.length;
25
+ payload.matched_commands = payload.commands.filter((entry) => entry.matched).length;
26
+ payload.overall = payload.commands.every((entry) => entry.matched) ? 'match' : 'mismatch';
27
+
28
+ return payload;
29
+ }
30
+
31
+ export function replayEvidenceCommand(root, entry, index, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS) {
32
+ const result = spawnSync(entry.command, {
33
+ cwd: root,
34
+ encoding: 'utf8',
35
+ shell: true,
36
+ timeout: timeoutMs,
37
+ maxBuffer: 1024 * 1024,
38
+ });
39
+
40
+ const timedOut = result.error?.code === 'ETIMEDOUT';
41
+ const actualExitCode = Number.isInteger(result.status) ? result.status : null;
42
+ const errorMessage = result.error?.message || null;
43
+
44
+ return {
45
+ index,
46
+ command: entry.command,
47
+ declared_exit_code: entry.exit_code,
48
+ actual_exit_code: actualExitCode,
49
+ matched: actualExitCode === entry.exit_code,
50
+ timed_out: timedOut,
51
+ signal: result.signal || null,
52
+ error: errorMessage,
53
+ };
54
+ }
55
+
56
+ export function summarizeVerificationReplay(payload) {
57
+ if (!payload) {
58
+ return null;
59
+ }
60
+
61
+ return {
62
+ overall: payload.overall,
63
+ replayed_commands: payload.replayed_commands || 0,
64
+ matched_commands: payload.matched_commands || 0,
65
+ timeout_ms: payload.timeout_ms || DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS,
66
+ ...(payload.reason ? { reason: payload.reason } : {}),
67
+ };
68
+ }