agentxchain 2.80.0 → 2.81.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.
package/README.md CHANGED
@@ -167,6 +167,8 @@ agentxchain step
167
167
  | `approve-completion` | Approve a pending human-gated run completion |
168
168
  | `validate` | Validate governed kickoff wiring, a staged turn, or both |
169
169
  | `template validate` | Prove the template registry, workflow-kit scaffold contract, and planning artifact completeness (`--json` exposes a `workflow_kit` block) |
170
+ | `verify turn` | Replay a staged turn's declared machine-evidence commands to confirm reproducibility before acceptance |
171
+ | `replay turn` | Replay an accepted turn's machine-evidence commands from history for audit and drift detection |
170
172
  | `verify protocol` | Run the shipped protocol conformance suite against a target implementation |
171
173
  | `dashboard` | Open the local governance dashboard in your browser for repo-local runs or multi-repo coordinator initiatives, including pending gate approvals |
172
174
  | `multi init\|status\|step\|resume\|approve-gate\|resync` | Run the multi-repo coordinator lifecycle, including blocked-state recovery via `multi resume` |
@@ -60,6 +60,7 @@ 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
62
  import { verifyExportCommand, verifyProtocolCommand, verifyTurnCommand } from '../src/commands/verify.js';
63
+ import { replayTurnCommand } from '../src/commands/replay.js';
63
64
  import { kickoffCommand } from '../src/commands/kickoff.js';
64
65
  import { rebindCommand } from '../src/commands/rebind.js';
65
66
  import { branchCommand } from '../src/commands/branch.js';
@@ -388,6 +389,17 @@ verifyCmd
388
389
  .option('--format <format>', 'Output format: text or json', 'text')
389
390
  .action(verifyExportCommand);
390
391
 
392
+ const replayCmd = program
393
+ .command('replay')
394
+ .description('Replay accepted governed evidence against the current workspace');
395
+
396
+ replayCmd
397
+ .command('turn [turn_id]')
398
+ .description('Replay an accepted turn\'s declared machine-evidence commands from history')
399
+ .option('-j, --json', 'Output as JSON')
400
+ .option('--timeout <ms>', 'Per-command replay timeout in milliseconds', '30000')
401
+ .action(replayTurnCommand);
402
+
391
403
  program
392
404
  .command('migrate')
393
405
  .description('Migrate a legacy v3 project to governed format')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.80.0",
3
+ "version": "2.81.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,7 @@ import { getWatchPid } from './watch.js';
8
8
  import { loadNormalizedConfig, detectConfigVersion } from '../lib/normalized-config.js';
9
9
  import { readDaemonState, evaluateDaemonStatus } from '../lib/run-schedule.js';
10
10
  import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
11
+ import { PLUGIN_MANIFEST_FILE } from '../lib/plugins.js';
11
12
 
12
13
  export async function doctorCommand(opts = {}) {
13
14
  const root = findProjectRoot(process.cwd());
@@ -74,6 +75,7 @@ function governedDoctor(root, rawConfig, opts) {
74
75
  const check = checkRuntimeReachable(rtId, rt);
75
76
  checks.push(check);
76
77
  }
78
+ const connectorProbe = getConnectorProbeRecommendation(runtimes);
77
79
 
78
80
  // 4. State directory
79
81
  const stateDir = join(root, '.agentxchain');
@@ -130,6 +132,90 @@ function governedDoctor(root, rawConfig, opts) {
130
132
  }
131
133
  }
132
134
 
135
+ // 8. Installed plugin health (only when plugins are installed)
136
+ const installedPlugins = rawConfig.plugins || {};
137
+ const pluginNames = Object.keys(installedPlugins);
138
+ if (pluginNames.length > 0) {
139
+ for (const pluginName of pluginNames) {
140
+ const meta = installedPlugins[pluginName];
141
+ const checkId = `plugin_${pluginName.replace(/[^a-z0-9_-]/gi, '_')}`;
142
+
143
+ // Check install path exists
144
+ if (!meta.install_path) {
145
+ checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: 'No install_path recorded', plugin_name: pluginName });
146
+ continue;
147
+ }
148
+ const installAbsPath = join(root, meta.install_path);
149
+ if (!existsSync(installAbsPath)) {
150
+ checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: `Install path missing: ${meta.install_path}`, plugin_name: pluginName });
151
+ continue;
152
+ }
153
+
154
+ // Check manifest exists and is valid
155
+ const manifestPath = join(installAbsPath, PLUGIN_MANIFEST_FILE);
156
+ if (!existsSync(manifestPath)) {
157
+ checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: 'Manifest file missing', plugin_name: pluginName });
158
+ continue;
159
+ }
160
+ let manifest;
161
+ try {
162
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
163
+ } catch (err) {
164
+ checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: `Manifest is corrupt JSON: ${err.message}`, plugin_name: pluginName });
165
+ continue;
166
+ }
167
+
168
+ // Check hook files exist
169
+ const hookErrors = [];
170
+ if (manifest.hooks && typeof manifest.hooks === 'object') {
171
+ for (const [hookName, hookDef] of Object.entries(manifest.hooks)) {
172
+ if (!hookDef) continue;
173
+ const commands = Array.isArray(hookDef) ? hookDef : (hookDef.command ? [hookDef] : []);
174
+ for (const cmd of commands) {
175
+ const cmdArgs = cmd.command || cmd;
176
+ if (Array.isArray(cmdArgs) && cmdArgs.length > 0) {
177
+ const firstArg = cmdArgs[0];
178
+ if (typeof firstArg === 'string' && (firstArg.startsWith('./') || firstArg.startsWith('../'))) {
179
+ const hookFilePath = join(installAbsPath, firstArg);
180
+ if (!existsSync(hookFilePath)) {
181
+ hookErrors.push(`${hookName}: ${firstArg}`);
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ if (hookErrors.length > 0) {
189
+ checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: `Missing hook files: ${hookErrors.join(', ')}`, plugin_name: pluginName });
190
+ continue;
191
+ }
192
+
193
+ // Check config env vars (warn only)
194
+ const envWarnings = [];
195
+ const pluginConfig = meta.config || {};
196
+ for (const [key, value] of Object.entries(pluginConfig)) {
197
+ if (typeof value === 'string' && value.startsWith('$')) {
198
+ const envVar = value.slice(1);
199
+ if (!process.env[envVar]) {
200
+ envWarnings.push(envVar);
201
+ }
202
+ }
203
+ }
204
+ // Also check webhook_env pattern from config
205
+ if (pluginConfig.webhook_env && !process.env[pluginConfig.webhook_env]) {
206
+ if (!envWarnings.includes(pluginConfig.webhook_env)) {
207
+ envWarnings.push(pluginConfig.webhook_env);
208
+ }
209
+ }
210
+
211
+ if (envWarnings.length > 0) {
212
+ checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'warn', detail: `Env var(s) not set: ${envWarnings.join(', ')}`, plugin_name: pluginName });
213
+ } else {
214
+ checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'pass', detail: `v${manifest.version || '?'}, ${Object.keys(manifest.hooks || {}).length} hooks`, plugin_name: pluginName });
215
+ }
216
+ }
217
+ }
218
+
133
219
  // Compute summary
134
220
  const failCount = checks.filter(c => c.level === 'fail').length;
135
221
  const warnCount = checks.filter(c => c.level === 'warn').length;
@@ -143,6 +229,9 @@ function governedDoctor(root, rawConfig, opts) {
143
229
  ...versionSurface,
144
230
  config_version: versionSurface.config_generation,
145
231
  overall,
232
+ connector_probe_recommended: connectorProbe.recommended,
233
+ connector_probe_runtime_ids: connectorProbe.runtimeIds,
234
+ connector_probe_detail: connectorProbe.detail,
146
235
  checks,
147
236
  fail_count: failCount,
148
237
  warn_count: warnCount,
@@ -173,6 +262,9 @@ function governedDoctor(root, rawConfig, opts) {
173
262
  } else {
174
263
  console.log(chalk.red(` Not ready: ${failCount} failure${failCount > 1 ? 's' : ''}, ${warnCount} warning${warnCount > 1 ? 's' : ''}.`));
175
264
  }
265
+ if (failCount === 0 && connectorProbe.recommended) {
266
+ console.log(chalk.dim(` Next: ${connectorProbe.detail}`));
267
+ }
176
268
  console.log('');
177
269
  }
178
270
 
@@ -236,6 +328,35 @@ function checkRuntimeReachable(rtId, rt) {
236
328
  }
237
329
  }
238
330
 
331
+ function getConnectorProbeRecommendation(runtimes) {
332
+ const runtimeIds = [];
333
+
334
+ for (const [rtId, rt] of Object.entries(runtimes || {})) {
335
+ if (!rt || typeof rt !== 'object') continue;
336
+ if (rt.type === 'api_proxy' || rt.type === 'remote_agent') {
337
+ runtimeIds.push(rtId);
338
+ continue;
339
+ }
340
+ if (rt.type === 'mcp' && (rt.transport || 'stdio') === 'streamable_http') {
341
+ runtimeIds.push(rtId);
342
+ }
343
+ }
344
+
345
+ if (runtimeIds.length === 0) {
346
+ return {
347
+ recommended: false,
348
+ runtimeIds: [],
349
+ detail: null,
350
+ };
351
+ }
352
+
353
+ return {
354
+ recommended: true,
355
+ runtimeIds,
356
+ detail: 'run `agentxchain connector check` to live-probe api / remote HTTP runtimes before the first governed turn.',
357
+ };
358
+ }
359
+
239
360
  function getCurrentPhase(root) {
240
361
  const statePath = join(root, '.agentxchain', 'state.json');
241
362
  if (!existsSync(statePath)) return null;
@@ -0,0 +1,120 @@
1
+ import chalk from 'chalk';
2
+
3
+ import { loadProjectContext } from '../lib/config.js';
4
+ import { normalizeVerification } from '../lib/repo-observer.js';
5
+ import { resolveAcceptedTurnHistoryReference } from '../lib/accepted-turn-history.js';
6
+ import {
7
+ DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS,
8
+ replayVerificationMachineEvidence,
9
+ } from '../lib/verification-replay.js';
10
+
11
+ export async function replayTurnCommand(turnId, opts = {}) {
12
+ const context = loadProjectContext();
13
+ if (!context) {
14
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
15
+ process.exit(2);
16
+ }
17
+
18
+ if (context.config.protocol_mode !== 'governed' || context.version !== 4) {
19
+ console.log(chalk.red('replay turn is only available in governed v4 projects.'));
20
+ process.exit(2);
21
+ }
22
+
23
+ const timeoutMs = Number.parseInt(String(opts.timeout || String(DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS)), 10);
24
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
25
+ console.log(chalk.red('replay turn requires a positive integer --timeout in milliseconds.'));
26
+ process.exit(2);
27
+ }
28
+
29
+ const { root, config } = context;
30
+ const resolved = resolveAcceptedTurnHistoryReference(root, turnId);
31
+ if (!resolved.ok) {
32
+ console.log(chalk.red(resolved.error));
33
+ process.exit(2);
34
+ }
35
+
36
+ const entry = resolved.entry;
37
+ const runtimeType = config.runtimes?.[entry.runtime_id]?.type || 'unknown';
38
+ const payload = {
39
+ source: 'history',
40
+ match_kind: resolved.match_kind,
41
+ turn_id: entry.turn_id,
42
+ resolved_turn_id: resolved.resolved_ref,
43
+ run_id: entry.run_id || null,
44
+ role: entry.role || null,
45
+ phase: entry.phase || null,
46
+ runtime_id: entry.runtime_id || null,
47
+ runtime_type: runtimeType,
48
+ accepted_at: entry.accepted_at || null,
49
+ declared_status: entry.verification?.status || 'skipped',
50
+ normalized_status: normalizeVerification(entry.verification, runtimeType).status,
51
+ timeout_ms: timeoutMs,
52
+ prior_verification_replay: entry.verification_replay || null,
53
+ ...replayVerificationMachineEvidence({
54
+ root,
55
+ verification: entry.verification,
56
+ timeoutMs,
57
+ }),
58
+ };
59
+
60
+ emitReplayTurn(payload, opts.json);
61
+ process.exit(payload.overall === 'match' ? 0 : 1);
62
+ }
63
+
64
+ function emitReplayTurn(payload, jsonMode) {
65
+ if (jsonMode) {
66
+ console.log(JSON.stringify(payload, null, 2));
67
+ return;
68
+ }
69
+
70
+ console.log('');
71
+ console.log(chalk.bold(` Replay Turn: ${chalk.cyan(payload.turn_id)}`));
72
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
73
+ console.log(` ${chalk.dim('Source:')} accepted history (${payload.match_kind})`);
74
+ console.log(` ${chalk.dim('Run:')} ${payload.run_id || '—'}`);
75
+ console.log(` ${chalk.dim('Role:')} ${payload.role || '—'}`);
76
+ console.log(` ${chalk.dim('Phase:')} ${payload.phase || '—'}`);
77
+ console.log(` ${chalk.dim('Runtime:')} ${payload.runtime_id || '—'} (${payload.runtime_type})`);
78
+ console.log(` ${chalk.dim('Accepted:')} ${payload.accepted_at || '—'}`);
79
+ console.log(` ${chalk.dim('Declared:')} ${payload.declared_status}`);
80
+ console.log(` ${chalk.dim('Normalized:')} ${payload.normalized_status}`);
81
+ if (payload.prior_verification_replay) {
82
+ const prior = payload.prior_verification_replay;
83
+ const verifiedAt = prior.verified_at ? ` at ${prior.verified_at}` : '';
84
+ console.log(` ${chalk.dim('Prior replay:')} ${prior.overall} (${prior.matched_commands || 0}/${prior.replayed_commands || 0})${verifiedAt}`);
85
+ }
86
+ console.log(` ${chalk.dim('Outcome:')} ${formatOutcome(payload.overall)}`);
87
+
88
+ if (payload.reason) {
89
+ console.log(` ${chalk.dim('Reason:')} ${payload.reason}`);
90
+ console.log('');
91
+ return;
92
+ }
93
+
94
+ console.log('');
95
+ for (const command of payload.commands || []) {
96
+ const marker = command.matched ? chalk.green('match') : chalk.red('mismatch');
97
+ console.log(` [${marker}] ${command.command}`);
98
+ console.log(` declared=${command.declared_exit_code} actual=${command.actual_exit_code == null ? 'null' : command.actual_exit_code}`);
99
+ if (command.signal) {
100
+ console.log(` signal=${command.signal}`);
101
+ }
102
+ if (command.timed_out) {
103
+ console.log(' timed_out=true');
104
+ }
105
+ if (command.error) {
106
+ console.log(` error=${command.error}`);
107
+ }
108
+ }
109
+
110
+ console.log('');
111
+ console.log(chalk.dim(' Replay uses the current workspace and shell environment. It verifies declared exit-code reproducibility, not historical stdout/stderr identity.'));
112
+ console.log('');
113
+ }
114
+
115
+ function formatOutcome(outcome) {
116
+ if (outcome === 'match') return chalk.green('match');
117
+ if (outcome === 'mismatch') return chalk.red('mismatch');
118
+ return chalk.yellow('not_reproducible');
119
+ }
120
+
@@ -0,0 +1,84 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ const HISTORY_PATH = '.agentxchain/history.jsonl';
5
+
6
+ export function queryAcceptedTurnHistory(root) {
7
+ const filePath = join(root, HISTORY_PATH);
8
+ if (!existsSync(filePath)) return [];
9
+
10
+ let content;
11
+ try {
12
+ content = readFileSync(filePath, 'utf8').trim();
13
+ } catch {
14
+ return [];
15
+ }
16
+
17
+ if (!content) return [];
18
+
19
+ return content
20
+ .split('\n')
21
+ .filter(Boolean)
22
+ .map((line) => {
23
+ try {
24
+ return JSON.parse(line);
25
+ } catch {
26
+ return null;
27
+ }
28
+ })
29
+ .filter((entry) => entry && typeof entry.turn_id === 'string')
30
+ .reverse();
31
+ }
32
+
33
+ export function resolveAcceptedTurnHistoryReference(root, ref) {
34
+ const entries = queryAcceptedTurnHistory(root);
35
+
36
+ if (entries.length === 0) {
37
+ return {
38
+ ok: false,
39
+ error: 'No accepted turn history found. Accept at least one governed turn first.',
40
+ };
41
+ }
42
+
43
+ if (!ref) {
44
+ return {
45
+ ok: true,
46
+ entry: entries[0],
47
+ resolved_ref: entries[0].turn_id,
48
+ match_kind: 'latest',
49
+ };
50
+ }
51
+
52
+ const exact = entries.find((entry) => entry.turn_id === ref);
53
+ if (exact) {
54
+ return {
55
+ ok: true,
56
+ entry: exact,
57
+ resolved_ref: exact.turn_id,
58
+ match_kind: 'exact',
59
+ };
60
+ }
61
+
62
+ const prefixMatches = entries.filter((entry) => entry.turn_id.startsWith(ref));
63
+ if (prefixMatches.length === 1) {
64
+ return {
65
+ ok: true,
66
+ entry: prefixMatches[0],
67
+ resolved_ref: prefixMatches[0].turn_id,
68
+ match_kind: 'prefix',
69
+ };
70
+ }
71
+
72
+ if (prefixMatches.length > 1) {
73
+ return {
74
+ ok: false,
75
+ error: `Turn reference "${ref}" is ambiguous. Matches: ${prefixMatches.map((entry) => entry.turn_id).join(', ')}`,
76
+ };
77
+ }
78
+
79
+ return {
80
+ ok: false,
81
+ error: `Accepted turn ${ref} not found in history.`,
82
+ };
83
+ }
84
+