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.
- package/README.md +2 -0
- package/bin/agentxchain.js +19 -0
- package/builtin-plugins/plugin-github-issues/README.md +31 -0
- package/builtin-plugins/plugin-github-issues/agentxchain-plugin.json +53 -0
- package/builtin-plugins/plugin-github-issues/hooks/_shared.js +305 -0
- package/builtin-plugins/plugin-github-issues/hooks/after-acceptance.js +3 -0
- package/builtin-plugins/plugin-github-issues/hooks/on-escalation.js +3 -0
- package/builtin-plugins/plugin-github-issues/package.json +8 -0
- package/builtin-plugins/plugin-json-report/README.md +30 -0
- package/builtin-plugins/plugin-json-report/agentxchain-plugin.json +44 -0
- package/builtin-plugins/plugin-json-report/hooks/_shared.js +92 -0
- package/builtin-plugins/plugin-json-report/hooks/after-acceptance.js +4 -0
- package/builtin-plugins/plugin-json-report/hooks/before-gate.js +4 -0
- package/builtin-plugins/plugin-json-report/hooks/on-escalation.js +4 -0
- package/builtin-plugins/plugin-json-report/package.json +8 -0
- package/builtin-plugins/plugin-slack-notify/README.md +34 -0
- package/builtin-plugins/plugin-slack-notify/agentxchain-plugin.json +47 -0
- package/builtin-plugins/plugin-slack-notify/hooks/_shared.js +115 -0
- package/builtin-plugins/plugin-slack-notify/hooks/after-acceptance.js +16 -0
- package/builtin-plugins/plugin-slack-notify/hooks/before-gate.js +18 -0
- package/builtin-plugins/plugin-slack-notify/hooks/on-escalation.js +15 -0
- package/builtin-plugins/plugin-slack-notify/package.json +8 -0
- package/package.json +2 -1
- package/src/commands/doctor.js +121 -0
- package/src/commands/plugin.js +31 -1
- package/src/commands/replay.js +120 -0
- package/src/lib/accepted-turn-history.js +84 -0
- 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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentxchain",
|
|
3
|
-
"version": "2.
|
|
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": {
|
package/src/commands/doctor.js
CHANGED
|
@@ -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;
|
package/src/commands/plugin.js
CHANGED
|
@@ -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
|
+
|