agentxchain 2.79.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.
Files changed (28) hide show
  1. package/README.md +2 -0
  2. package/bin/agentxchain.js +19 -0
  3. package/builtin-plugins/plugin-github-issues/README.md +31 -0
  4. package/builtin-plugins/plugin-github-issues/agentxchain-plugin.json +53 -0
  5. package/builtin-plugins/plugin-github-issues/hooks/_shared.js +305 -0
  6. package/builtin-plugins/plugin-github-issues/hooks/after-acceptance.js +3 -0
  7. package/builtin-plugins/plugin-github-issues/hooks/on-escalation.js +3 -0
  8. package/builtin-plugins/plugin-github-issues/package.json +8 -0
  9. package/builtin-plugins/plugin-json-report/README.md +30 -0
  10. package/builtin-plugins/plugin-json-report/agentxchain-plugin.json +44 -0
  11. package/builtin-plugins/plugin-json-report/hooks/_shared.js +92 -0
  12. package/builtin-plugins/plugin-json-report/hooks/after-acceptance.js +4 -0
  13. package/builtin-plugins/plugin-json-report/hooks/before-gate.js +4 -0
  14. package/builtin-plugins/plugin-json-report/hooks/on-escalation.js +4 -0
  15. package/builtin-plugins/plugin-json-report/package.json +8 -0
  16. package/builtin-plugins/plugin-slack-notify/README.md +34 -0
  17. package/builtin-plugins/plugin-slack-notify/agentxchain-plugin.json +47 -0
  18. package/builtin-plugins/plugin-slack-notify/hooks/_shared.js +115 -0
  19. package/builtin-plugins/plugin-slack-notify/hooks/after-acceptance.js +16 -0
  20. package/builtin-plugins/plugin-slack-notify/hooks/before-gate.js +18 -0
  21. package/builtin-plugins/plugin-slack-notify/hooks/on-escalation.js +15 -0
  22. package/builtin-plugins/plugin-slack-notify/package.json +8 -0
  23. package/package.json +2 -1
  24. package/src/commands/doctor.js +121 -0
  25. package/src/commands/plugin.js +31 -1
  26. package/src/commands/replay.js +120 -0
  27. package/src/lib/accepted-turn-history.js +84 -0
  28. package/src/lib/plugins.js +54 -1
@@ -0,0 +1,34 @@
1
+ # @agentxchain/plugin-slack-notify
2
+
3
+ Built-in AgentXchain plugin that posts advisory lifecycle notifications to a Slack incoming webhook.
4
+
5
+ Install from this repo:
6
+
7
+ ```bash
8
+ agentxchain plugin install ./plugins/plugin-slack-notify
9
+ ```
10
+
11
+ Optional install-time config:
12
+
13
+ ```bash
14
+ agentxchain plugin install ./plugins/plugin-slack-notify \
15
+ --config '{"webhook_env":"MY_SLACK_WEBHOOK_URL","mention":"@ops"}'
16
+ ```
17
+
18
+ Runtime inputs:
19
+
20
+ - `webhook_env` (optional): which env var contains the Slack incoming webhook URL
21
+ - `mention` (optional): prefix added to each message
22
+ - default webhook lookup is `AGENTXCHAIN_SLACK_WEBHOOK_URL`, then `SLACK_WEBHOOK_URL`
23
+ - `AGENTXCHAIN_SLACK_MENTION` remains a runtime fallback when `mention` is not configured
24
+
25
+ Hook phases:
26
+
27
+ - `after_acceptance`
28
+ - `before_gate`
29
+ - `on_escalation`
30
+
31
+ Failure semantics:
32
+
33
+ - advisory only
34
+ - missing webhook config or delivery failures return `warn`, never `block`
@@ -0,0 +1,47 @@
1
+ {
2
+ "schema_version": "0.1",
3
+ "name": "@agentxchain/plugin-slack-notify",
4
+ "version": "0.1.0",
5
+ "description": "Posts governed lifecycle notifications to a Slack incoming webhook.",
6
+ "hooks": {
7
+ "after_acceptance": [
8
+ {
9
+ "name": "slack_notify_acceptance",
10
+ "type": "process",
11
+ "command": ["node", "./hooks/after-acceptance.js"],
12
+ "timeout_ms": 5000,
13
+ "mode": "advisory"
14
+ }
15
+ ],
16
+ "before_gate": [
17
+ {
18
+ "name": "slack_notify_gate",
19
+ "type": "process",
20
+ "command": ["node", "./hooks/before-gate.js"],
21
+ "timeout_ms": 5000,
22
+ "mode": "advisory"
23
+ }
24
+ ],
25
+ "on_escalation": [
26
+ {
27
+ "name": "slack_notify_escalation",
28
+ "type": "process",
29
+ "command": ["node", "./hooks/on-escalation.js"],
30
+ "timeout_ms": 5000,
31
+ "mode": "advisory"
32
+ }
33
+ ]
34
+ },
35
+ "config_schema": {
36
+ "type": "object",
37
+ "properties": {
38
+ "webhook_env": {
39
+ "type": "string",
40
+ "default": "AGENTXCHAIN_SLACK_WEBHOOK_URL"
41
+ },
42
+ "mention": {
43
+ "type": "string"
44
+ }
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,115 @@
1
+ import http from 'node:http';
2
+ import https from 'node:https';
3
+ import process from 'node:process';
4
+
5
+ export async function readEnvelope() {
6
+ let input = '';
7
+ for await (const chunk of process.stdin) {
8
+ input += chunk;
9
+ }
10
+ return input.trim() ? JSON.parse(input) : {};
11
+ }
12
+
13
+ function parsePluginConfig() {
14
+ try {
15
+ const parsed = JSON.parse(process.env.AGENTXCHAIN_PLUGIN_CONFIG || '{}');
16
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
17
+ return null;
18
+ }
19
+ return parsed;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function resolveWebhookSetting(config) {
26
+ const configuredEnv = typeof config?.webhook_env === 'string' ? config.webhook_env.trim() : '';
27
+ if (configuredEnv) {
28
+ return {
29
+ envName: configuredEnv,
30
+ url: process.env[configuredEnv] || '',
31
+ };
32
+ }
33
+
34
+ return {
35
+ envName: 'AGENTXCHAIN_SLACK_WEBHOOK_URL or SLACK_WEBHOOK_URL',
36
+ url: process.env.AGENTXCHAIN_SLACK_WEBHOOK_URL || process.env.SLACK_WEBHOOK_URL || '',
37
+ };
38
+ }
39
+
40
+ function buildText(title, lines, config) {
41
+ const mention = typeof config?.mention === 'string' && config.mention.trim()
42
+ ? config.mention.trim()
43
+ : (process.env.AGENTXCHAIN_SLACK_MENTION || '');
44
+ return [mention, title, ...lines.filter(Boolean)].filter(Boolean).join('\n');
45
+ }
46
+
47
+ export async function sendSlackMessage(title, lines) {
48
+ const config = parsePluginConfig();
49
+ if (config === null) {
50
+ return {
51
+ verdict: 'warn',
52
+ message: 'Invalid AGENTXCHAIN_PLUGIN_CONFIG JSON',
53
+ };
54
+ }
55
+
56
+ const webhookSetting = resolveWebhookSetting(config);
57
+ const webhookUrl = webhookSetting.url;
58
+ if (!webhookUrl) {
59
+ return {
60
+ verdict: 'warn',
61
+ message: `Missing Slack webhook env ${webhookSetting.envName}`,
62
+ };
63
+ }
64
+
65
+ const body = JSON.stringify({
66
+ text: buildText(title, lines, config),
67
+ });
68
+
69
+ const url = new URL(webhookUrl);
70
+ const transport = url.protocol === 'https:' ? https : http;
71
+ let response;
72
+ try {
73
+ response = await new Promise((resolve, reject) => {
74
+ const req = transport.request(url, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'content-type': 'application/json',
78
+ 'content-length': Buffer.byteLength(body),
79
+ },
80
+ timeout: 4000,
81
+ }, (res) => {
82
+ res.resume();
83
+ res.on('end', () => resolve({ ok: (res.statusCode || 500) >= 200 && (res.statusCode || 500) < 300, status: res.statusCode || 500 }));
84
+ });
85
+
86
+ req.on('timeout', () => {
87
+ req.destroy(new Error('request timed out'));
88
+ });
89
+ req.on('error', reject);
90
+ req.write(body);
91
+ req.end();
92
+ });
93
+ } catch (error) {
94
+ return {
95
+ verdict: 'warn',
96
+ message: `Slack webhook request failed: ${error.message}`,
97
+ };
98
+ }
99
+
100
+ if (!response.ok) {
101
+ return {
102
+ verdict: 'warn',
103
+ message: `Slack webhook failed with HTTP ${response.status}`,
104
+ };
105
+ }
106
+
107
+ return {
108
+ verdict: 'allow',
109
+ message: 'Slack notification delivered',
110
+ };
111
+ }
112
+
113
+ export function writeResult(result) {
114
+ process.stdout.write(JSON.stringify(result));
115
+ }
@@ -0,0 +1,16 @@
1
+ import { readEnvelope, sendSlackMessage, writeResult } from './_shared.js';
2
+
3
+ const envelope = await readEnvelope();
4
+ const payload = envelope.payload || {};
5
+
6
+ const result = await sendSlackMessage('AgentXchain accepted turn', [
7
+ `run: ${envelope.run_id || 'unknown'}`,
8
+ `phase: ${payload.phase || 'unknown'}`,
9
+ `role: ${payload.role_id || 'unknown'}`,
10
+ `turn: ${payload.turn_id || 'unknown'}`,
11
+ `decisions: ${payload.decisions_count ?? 0}`,
12
+ `objections: ${payload.objections_count ?? 0}`,
13
+ `status: ${payload.run_status || 'unknown'}`,
14
+ ]);
15
+
16
+ writeResult(result);
@@ -0,0 +1,18 @@
1
+ import { readEnvelope, sendSlackMessage, writeResult } from './_shared.js';
2
+
3
+ const envelope = await readEnvelope();
4
+ const payload = envelope.payload || {};
5
+
6
+ const title = payload.gate_type === 'run_completion'
7
+ ? 'AgentXchain run completion awaiting approval'
8
+ : 'AgentXchain phase gate awaiting approval';
9
+
10
+ const result = await sendSlackMessage(title, [
11
+ `run: ${envelope.run_id || 'unknown'}`,
12
+ `gate type: ${payload.gate_type || 'unknown'}`,
13
+ `current phase: ${payload.current_phase || 'unknown'}`,
14
+ `target phase: ${payload.target_phase || 'n/a'}`,
15
+ `history length: ${payload.history_length ?? 0}`,
16
+ ]);
17
+
18
+ writeResult(result);
@@ -0,0 +1,15 @@
1
+ import { readEnvelope, sendSlackMessage, writeResult } from './_shared.js';
2
+
3
+ const envelope = await readEnvelope();
4
+ const payload = envelope.payload || {};
5
+
6
+ const result = await sendSlackMessage('AgentXchain escalation', [
7
+ `run: ${envelope.run_id || 'unknown'}`,
8
+ `blocked reason: ${payload.blocked_reason || 'unknown'}`,
9
+ `recovery: ${payload.recovery_action || 'unknown'}`,
10
+ `role: ${payload.failed_role || 'unknown'}`,
11
+ `turn: ${payload.failed_turn_id || 'unknown'}`,
12
+ `error: ${payload.last_error || 'unknown'}`,
13
+ ]);
14
+
15
+ writeResult(result);
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@agentxchain/plugin-slack-notify",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Built-in AgentXchain plugin that posts advisory lifecycle notifications to Slack.",
7
+ "license": "MIT"
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.79.0",
3
+ "version": "2.81.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "src/",
18
18
  "dashboard/",
19
19
  "scripts/",
20
+ "builtin-plugins/",
20
21
  "README.md"
21
22
  ],
22
23
  "scripts": {
@@ -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;
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
- import { installPlugin, listInstalledPlugins, removePlugin, upgradePlugin } from '../lib/plugins.js';
4
+ import { installPlugin, listInstalledPlugins, listAvailablePlugins, removePlugin, upgradePlugin } from '../lib/plugins.js';
5
5
 
6
6
  function parsePluginConfigOptions(options) {
7
7
  if (options.config && options.configFile) {
@@ -117,6 +117,36 @@ export async function pluginRemoveCommand(name, options) {
117
117
  console.log(` Path: ${result.install_path}`);
118
118
  }
119
119
 
120
+ export async function pluginListAvailableCommand(options) {
121
+ const result = listAvailablePlugins();
122
+
123
+ if (!result.ok) {
124
+ console.error(result.error);
125
+ if (options.json) {
126
+ console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
127
+ }
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+
132
+ if (options.json) {
133
+ console.log(JSON.stringify({ plugins: result.plugins }, null, 2));
134
+ return;
135
+ }
136
+
137
+ if (result.plugins.length === 0) {
138
+ console.log('No built-in plugins available.');
139
+ return;
140
+ }
141
+
142
+ console.log(`Available built-in plugins: ${result.plugins.length}`);
143
+ for (const plugin of result.plugins) {
144
+ console.log(` ${plugin.short_name}`);
145
+ console.log(` ${plugin.description}`);
146
+ console.log(` Install: ${plugin.install_command}`);
147
+ }
148
+ }
149
+
120
150
  export async function pluginUpgradeCommand(name, source, options) {
121
151
  const config = parsePluginConfigOptions(options);
122
152
  if (!config.ok) {
@@ -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
+