agentxchain 2.78.0 → 2.80.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 (24) hide show
  1. package/bin/agentxchain.js +7 -0
  2. package/builtin-plugins/plugin-github-issues/README.md +31 -0
  3. package/builtin-plugins/plugin-github-issues/agentxchain-plugin.json +53 -0
  4. package/builtin-plugins/plugin-github-issues/hooks/_shared.js +305 -0
  5. package/builtin-plugins/plugin-github-issues/hooks/after-acceptance.js +3 -0
  6. package/builtin-plugins/plugin-github-issues/hooks/on-escalation.js +3 -0
  7. package/builtin-plugins/plugin-github-issues/package.json +8 -0
  8. package/builtin-plugins/plugin-json-report/README.md +30 -0
  9. package/builtin-plugins/plugin-json-report/agentxchain-plugin.json +44 -0
  10. package/builtin-plugins/plugin-json-report/hooks/_shared.js +92 -0
  11. package/builtin-plugins/plugin-json-report/hooks/after-acceptance.js +4 -0
  12. package/builtin-plugins/plugin-json-report/hooks/before-gate.js +4 -0
  13. package/builtin-plugins/plugin-json-report/hooks/on-escalation.js +4 -0
  14. package/builtin-plugins/plugin-json-report/package.json +8 -0
  15. package/builtin-plugins/plugin-slack-notify/README.md +34 -0
  16. package/builtin-plugins/plugin-slack-notify/agentxchain-plugin.json +47 -0
  17. package/builtin-plugins/plugin-slack-notify/hooks/_shared.js +115 -0
  18. package/builtin-plugins/plugin-slack-notify/hooks/after-acceptance.js +16 -0
  19. package/builtin-plugins/plugin-slack-notify/hooks/before-gate.js +18 -0
  20. package/builtin-plugins/plugin-slack-notify/hooks/on-escalation.js +15 -0
  21. package/builtin-plugins/plugin-slack-notify/package.json +8 -0
  22. package/package.json +2 -1
  23. package/src/commands/plugin.js +31 -1
  24. package/src/lib/plugins.js +54 -1
@@ -82,6 +82,7 @@ import { reportCommand } from '../src/commands/report.js';
82
82
  import {
83
83
  pluginInstallCommand,
84
84
  pluginListCommand,
85
+ pluginListAvailableCommand,
85
86
  pluginRemoveCommand,
86
87
  pluginUpgradeCommand,
87
88
  } from '../src/commands/plugin.js';
@@ -485,6 +486,12 @@ pluginCmd
485
486
  .option('-j, --json', 'Output as JSON')
486
487
  .action(pluginListCommand);
487
488
 
489
+ pluginCmd
490
+ .command('list-available')
491
+ .description('List built-in plugins available for installation')
492
+ .option('-j, --json', 'Output as JSON')
493
+ .action(pluginListAvailableCommand);
494
+
488
495
  pluginCmd
489
496
  .command('remove <name>')
490
497
  .description('Remove an installed plugin')
@@ -0,0 +1,31 @@
1
+ # @agentxchain/plugin-github-issues
2
+
3
+ Built-in AgentXchain plugin that mirrors governed run status into a configured GitHub issue.
4
+
5
+ Install from this repo:
6
+
7
+ ```bash
8
+ agentxchain plugin install ./plugins/plugin-github-issues
9
+ ```
10
+
11
+ Hook phases:
12
+
13
+ - `after_acceptance`
14
+ - `on_escalation`
15
+
16
+ Required config:
17
+
18
+ - `repo`: GitHub repository in `owner/name` form
19
+ - `issue_number`: GitHub issue number
20
+
21
+ Optional config:
22
+
23
+ - `token_env`: token environment variable name (default runtime fallback: `GITHUB_TOKEN`)
24
+ - `api_base_url`: GitHub API base URL (default runtime fallback: `https://api.github.com`)
25
+ - `label_prefix`: managed label prefix (default runtime fallback: `agentxchain`)
26
+
27
+ Scope notes:
28
+
29
+ - One plugin-owned comment per run, updated in place
30
+ - Managed labels track phase or blocked state only
31
+ - This plugin does **not** close issues or claim post-gate approval state because the hook surface does not provide post-gate truth
@@ -0,0 +1,53 @@
1
+ {
2
+ "schema_version": "0.1",
3
+ "name": "@agentxchain/plugin-github-issues",
4
+ "version": "0.1.0",
5
+ "description": "Mirrors governed run status into a configured GitHub issue using advisory hooks.",
6
+ "hooks": {
7
+ "after_acceptance": [
8
+ {
9
+ "name": "github_issues_acceptance",
10
+ "type": "process",
11
+ "command": ["node", "./hooks/after-acceptance.js"],
12
+ "timeout_ms": 8000,
13
+ "mode": "advisory"
14
+ }
15
+ ],
16
+ "on_escalation": [
17
+ {
18
+ "name": "github_issues_escalation",
19
+ "type": "process",
20
+ "command": ["node", "./hooks/on-escalation.js"],
21
+ "timeout_ms": 8000,
22
+ "mode": "advisory"
23
+ }
24
+ ]
25
+ },
26
+ "config_schema": {
27
+ "type": "object",
28
+ "required": ["repo", "issue_number"],
29
+ "additionalProperties": false,
30
+ "properties": {
31
+ "repo": {
32
+ "type": "string",
33
+ "pattern": "^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$"
34
+ },
35
+ "issue_number": {
36
+ "type": "integer",
37
+ "minimum": 1
38
+ },
39
+ "token_env": {
40
+ "type": "string",
41
+ "minLength": 1
42
+ },
43
+ "api_base_url": {
44
+ "type": "string",
45
+ "minLength": 1
46
+ },
47
+ "label_prefix": {
48
+ "type": "string",
49
+ "minLength": 1
50
+ }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,305 @@
1
+ import http from 'node:http';
2
+ import https from 'node:https';
3
+ import process from 'node:process';
4
+
5
+ const DEFAULT_API_BASE = 'https://api.github.com';
6
+ const DEFAULT_TOKEN_ENV = 'GITHUB_TOKEN';
7
+ const DEFAULT_LABEL_PREFIX = 'agentxchain';
8
+
9
+ export async function readEnvelope() {
10
+ let input = '';
11
+ for await (const chunk of process.stdin) {
12
+ input += chunk;
13
+ }
14
+ return input.trim() ? JSON.parse(input) : {};
15
+ }
16
+
17
+ function writeResult(result) {
18
+ process.stdout.write(JSON.stringify(result));
19
+ }
20
+
21
+ function warn(message) {
22
+ writeResult({ verdict: 'warn', message });
23
+ }
24
+
25
+ function ok(message) {
26
+ writeResult({ verdict: 'allow', message });
27
+ }
28
+
29
+ function parsePluginConfig() {
30
+ try {
31
+ return JSON.parse(process.env.AGENTXCHAIN_PLUGIN_CONFIG || '{}');
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function normalizeConfig(raw) {
38
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
39
+ return null;
40
+ }
41
+
42
+ const repo = typeof raw.repo === 'string' ? raw.repo.trim() : '';
43
+ const issueNumber = Number.isInteger(raw.issue_number) ? raw.issue_number : null;
44
+ if (!repo || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) || !issueNumber || issueNumber < 1) {
45
+ return null;
46
+ }
47
+
48
+ const [owner, name] = repo.split('/');
49
+ return {
50
+ owner,
51
+ repo: name,
52
+ issue_number: issueNumber,
53
+ token_env: typeof raw.token_env === 'string' && raw.token_env.trim() ? raw.token_env.trim() : DEFAULT_TOKEN_ENV,
54
+ api_base_url: typeof raw.api_base_url === 'string' && raw.api_base_url.trim()
55
+ ? raw.api_base_url.trim().replace(/\/+$/, '')
56
+ : DEFAULT_API_BASE,
57
+ label_prefix: typeof raw.label_prefix === 'string' && raw.label_prefix.trim()
58
+ ? raw.label_prefix.trim()
59
+ : DEFAULT_LABEL_PREFIX,
60
+ };
61
+ }
62
+
63
+ function getToken(config) {
64
+ return process.env[config.token_env] || '';
65
+ }
66
+
67
+ function buildHeaders(token, body) {
68
+ return {
69
+ accept: 'application/vnd.github+json',
70
+ authorization: `Bearer ${token}`,
71
+ 'content-type': 'application/json',
72
+ 'content-length': Buffer.byteLength(body),
73
+ 'user-agent': '@agentxchain/plugin-github-issues',
74
+ 'x-github-api-version': '2022-11-28',
75
+ };
76
+ }
77
+
78
+ async function requestJson(config, token, method, path, bodyValue) {
79
+ const url = new URL(path, `${config.api_base_url}/`);
80
+ const body = bodyValue === undefined ? '' : JSON.stringify(bodyValue);
81
+ const transport = url.protocol === 'https:' ? https : http;
82
+
83
+ const response = await new Promise((resolve, reject) => {
84
+ const req = transport.request(url, {
85
+ method,
86
+ headers: buildHeaders(token, body),
87
+ timeout: 7000,
88
+ }, (res) => {
89
+ let data = '';
90
+ res.on('data', (chunk) => {
91
+ data += chunk;
92
+ });
93
+ res.on('end', () => {
94
+ resolve({
95
+ status: res.statusCode || 500,
96
+ body: data,
97
+ });
98
+ });
99
+ });
100
+
101
+ req.on('timeout', () => req.destroy(new Error('request timed out')));
102
+ req.on('error', reject);
103
+ if (body) {
104
+ req.write(body);
105
+ }
106
+ req.end();
107
+ });
108
+
109
+ let parsed = null;
110
+ if (response.body) {
111
+ try {
112
+ parsed = JSON.parse(response.body);
113
+ } catch {
114
+ parsed = null;
115
+ }
116
+ }
117
+
118
+ return {
119
+ ok: response.status >= 200 && response.status < 300,
120
+ status: response.status,
121
+ body: parsed,
122
+ raw: response.body,
123
+ };
124
+ }
125
+
126
+ async function listComments(config, token) {
127
+ const comments = [];
128
+ for (let page = 1; page <= 10; page += 1) {
129
+ const response = await requestJson(
130
+ config,
131
+ token,
132
+ 'GET',
133
+ `/repos/${config.owner}/${config.repo}/issues/${config.issue_number}/comments?per_page=100&page=${page}`,
134
+ );
135
+
136
+ if (!response.ok) {
137
+ return response;
138
+ }
139
+
140
+ const pageItems = Array.isArray(response.body) ? response.body : [];
141
+ comments.push(...pageItems);
142
+ if (pageItems.length < 100) {
143
+ break;
144
+ }
145
+ }
146
+
147
+ return { ok: true, body: comments };
148
+ }
149
+
150
+ function runMarker(runId) {
151
+ return `<!-- agentxchain:github-issues:run:${runId || 'unknown'} -->`;
152
+ }
153
+
154
+ function buildManagedLabels(config, event) {
155
+ const labels = [config.label_prefix];
156
+ if (event.type === 'after_acceptance' && event.phase) {
157
+ labels.push(`${config.label_prefix}:phase:${event.phase}`);
158
+ }
159
+ if (event.type === 'on_escalation') {
160
+ labels.push(`${config.label_prefix}:blocked`);
161
+ }
162
+ return labels;
163
+ }
164
+
165
+ function mergeLabels(existingLabels, managedLabels, labelPrefix) {
166
+ const retained = existingLabels
167
+ .map((entry) => (typeof entry === 'string' ? entry : entry?.name))
168
+ .filter((name) => typeof name === 'string' && name && !name.startsWith(`${labelPrefix}:`) && name !== labelPrefix);
169
+
170
+ return [...retained, ...managedLabels];
171
+ }
172
+
173
+ function buildAcceptanceBody(envelope) {
174
+ const payload = envelope.payload || {};
175
+ return [
176
+ runMarker(envelope.run_id),
177
+ '## AgentXchain Run Update',
178
+ '',
179
+ `- Run: \`${envelope.run_id || 'unknown'}\``,
180
+ '- Event: `after_acceptance`',
181
+ `- Status: \`${payload.run_status || 'active'}\``,
182
+ `- Phase: \`${payload.phase || 'unknown'}\``,
183
+ `- Latest accepted turn: \`${payload.turn_id || 'unknown'}\` (\`${payload.role_id || 'unknown'}\`)`,
184
+ `- History index: \`${payload.history_entry_index ?? 'unknown'}\``,
185
+ `- Decisions on latest turn: \`${payload.decisions_count ?? 0}\``,
186
+ `- Objections on latest turn: \`${payload.objections_count ?? 0}\``,
187
+ `- Updated: \`${envelope.timestamp || new Date().toISOString()}\``,
188
+ '',
189
+ 'Plugin-owned comment from `@agentxchain/plugin-github-issues`.',
190
+ ].join('\n');
191
+ }
192
+
193
+ function buildEscalationBody(envelope) {
194
+ const payload = envelope.payload || {};
195
+ return [
196
+ runMarker(envelope.run_id),
197
+ '## AgentXchain Run Update',
198
+ '',
199
+ `- Run: \`${envelope.run_id || 'unknown'}\``,
200
+ '- Event: `on_escalation`',
201
+ '- Status: `blocked`',
202
+ `- Blocked reason: \`${payload.blocked_reason || 'unknown'}\``,
203
+ `- Failed turn: \`${payload.failed_turn_id || 'unknown'}\` (\`${payload.failed_role || 'unknown'}\`)`,
204
+ `- Recovery action: ${payload.recovery_action || 'unknown'}`,
205
+ `- Last error: ${payload.last_error || 'unknown'}`,
206
+ `- Updated: \`${envelope.timestamp || new Date().toISOString()}\``,
207
+ '',
208
+ 'Plugin-owned comment from `@agentxchain/plugin-github-issues`.',
209
+ ].join('\n');
210
+ }
211
+
212
+ async function upsertComment(config, token, envelope, body) {
213
+ const comments = await listComments(config, token);
214
+ if (!comments.ok) {
215
+ return comments;
216
+ }
217
+
218
+ const marker = runMarker(envelope.run_id);
219
+ const existing = comments.body.find((comment) => typeof comment?.body === 'string' && comment.body.includes(marker));
220
+
221
+ if (existing) {
222
+ return requestJson(
223
+ config,
224
+ token,
225
+ 'PATCH',
226
+ `/repos/${config.owner}/${config.repo}/issues/comments/${existing.id}`,
227
+ { body },
228
+ );
229
+ }
230
+
231
+ return requestJson(
232
+ config,
233
+ token,
234
+ 'POST',
235
+ `/repos/${config.owner}/${config.repo}/issues/${config.issue_number}/comments`,
236
+ { body },
237
+ );
238
+ }
239
+
240
+ async function syncLabels(config, token, event) {
241
+ const issue = await requestJson(
242
+ config,
243
+ token,
244
+ 'GET',
245
+ `/repos/${config.owner}/${config.repo}/issues/${config.issue_number}`,
246
+ );
247
+ if (!issue.ok) {
248
+ return issue;
249
+ }
250
+
251
+ const labels = mergeLabels(issue.body?.labels || [], buildManagedLabels(config, event), config.label_prefix);
252
+ return requestJson(
253
+ config,
254
+ token,
255
+ 'PUT',
256
+ `/repos/${config.owner}/${config.repo}/issues/${config.issue_number}/labels`,
257
+ { labels },
258
+ );
259
+ }
260
+
261
+ export async function publishRunUpdate(kind) {
262
+ try {
263
+ const rawConfig = parsePluginConfig();
264
+ if (rawConfig === null) {
265
+ warn('Invalid AGENTXCHAIN_PLUGIN_CONFIG JSON');
266
+ return;
267
+ }
268
+
269
+ const config = normalizeConfig(rawConfig);
270
+ if (!config) {
271
+ warn('Missing or invalid GitHub issue plugin config');
272
+ return;
273
+ }
274
+
275
+ const token = getToken(config);
276
+ if (!token) {
277
+ warn(`Missing GitHub token env ${config.token_env}`);
278
+ return;
279
+ }
280
+
281
+ const envelope = await readEnvelope();
282
+ const body = kind === 'after_acceptance'
283
+ ? buildAcceptanceBody(envelope)
284
+ : buildEscalationBody(envelope);
285
+
286
+ const comment = await upsertComment(config, token, envelope, body);
287
+ if (!comment.ok) {
288
+ warn(`GitHub comment sync failed with HTTP ${comment.status}`);
289
+ return;
290
+ }
291
+
292
+ const labels = await syncLabels(config, token, {
293
+ type: kind,
294
+ phase: envelope.payload?.phase || null,
295
+ });
296
+ if (!labels.ok) {
297
+ warn(`GitHub label sync failed with HTTP ${labels.status}`);
298
+ return;
299
+ }
300
+
301
+ ok('GitHub issue updated');
302
+ } catch (error) {
303
+ warn(`GitHub issue sync failed: ${error.message || String(error)}`);
304
+ }
305
+ }
@@ -0,0 +1,3 @@
1
+ import { publishRunUpdate } from './_shared.js';
2
+
3
+ await publishRunUpdate('after_acceptance');
@@ -0,0 +1,3 @@
1
+ import { publishRunUpdate } from './_shared.js';
2
+
3
+ await publishRunUpdate('on_escalation');
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@agentxchain/plugin-github-issues",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Built-in AgentXchain plugin that mirrors governed run status into a GitHub issue.",
7
+ "license": "MIT"
8
+ }
@@ -0,0 +1,30 @@
1
+ # @agentxchain/plugin-json-report
2
+
3
+ Built-in AgentXchain plugin that writes structured lifecycle report artifacts into `.agentxchain/reports/`.
4
+
5
+ Install from this repo:
6
+
7
+ ```bash
8
+ agentxchain plugin install ./plugins/plugin-json-report
9
+ ```
10
+
11
+ Optional install-time config:
12
+
13
+ ```bash
14
+ agentxchain plugin install ./plugins/plugin-json-report \
15
+ --config '{"report_dir":".agentxchain/custom-reports"}'
16
+ ```
17
+
18
+ Hook phases:
19
+
20
+ - `after_acceptance`
21
+ - `before_gate`
22
+ - `on_escalation`
23
+
24
+ Outputs:
25
+
26
+ - timestamped JSON file per invocation
27
+ - `latest.json`
28
+ - `latest-<hook_phase>.json`
29
+ - default output path `.agentxchain/reports`
30
+ - `report_dir` may override the path, but it must stay inside the governed project root
@@ -0,0 +1,44 @@
1
+ {
2
+ "schema_version": "0.1",
3
+ "name": "@agentxchain/plugin-json-report",
4
+ "version": "0.1.0",
5
+ "description": "Writes structured JSON lifecycle reports into .agentxchain/reports/.",
6
+ "hooks": {
7
+ "after_acceptance": [
8
+ {
9
+ "name": "json_report_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": "json_report_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": "json_report_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
+ "report_dir": {
39
+ "type": "string",
40
+ "default": ".agentxchain/reports"
41
+ }
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,92 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { isAbsolute, join, relative, resolve } from 'node:path';
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 safeTimestamp(timestamp) {
14
+ return (timestamp || new Date().toISOString()).replace(/[:]/g, '-');
15
+ }
16
+
17
+ function parsePluginConfig() {
18
+ try {
19
+ const parsed = JSON.parse(process.env.AGENTXCHAIN_PLUGIN_CONFIG || '{}');
20
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
21
+ return null;
22
+ }
23
+ return parsed;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function resolveReportDir(projectRoot, config) {
30
+ const configuredDir = typeof config?.report_dir === 'string' && config.report_dir.trim()
31
+ ? config.report_dir.trim()
32
+ : '.agentxchain/reports';
33
+ const reportDir = resolve(projectRoot, configuredDir);
34
+ const relativePath = relative(projectRoot, reportDir);
35
+ if (relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))) {
36
+ return reportDir;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ export function writeReport(pluginName, envelope) {
42
+ const projectRoot = process.env.AGENTXCHAIN_PROJECT_ROOT;
43
+ if (!projectRoot) {
44
+ process.stdout.write(JSON.stringify({
45
+ verdict: 'warn',
46
+ message: 'Missing AGENTXCHAIN_PROJECT_ROOT',
47
+ }));
48
+ return;
49
+ }
50
+
51
+ const config = parsePluginConfig();
52
+ if (config === null) {
53
+ process.stdout.write(JSON.stringify({
54
+ verdict: 'warn',
55
+ message: 'Invalid AGENTXCHAIN_PLUGIN_CONFIG JSON',
56
+ }));
57
+ return;
58
+ }
59
+
60
+ const hookPhase = envelope.hook_phase || 'unknown';
61
+ const timestamp = envelope.timestamp || new Date().toISOString();
62
+ const reportDir = resolveReportDir(projectRoot, config);
63
+ if (!reportDir) {
64
+ process.stdout.write(JSON.stringify({
65
+ verdict: 'warn',
66
+ message: 'Configured report_dir must stay within the governed project root',
67
+ }));
68
+ return;
69
+ }
70
+
71
+ mkdirSync(reportDir, { recursive: true });
72
+
73
+ const report = {
74
+ plugin_name: pluginName,
75
+ hook_phase: hookPhase,
76
+ run_id: envelope.run_id || null,
77
+ hook_name: envelope.hook_name || null,
78
+ turn_id: envelope.payload?.turn_id || envelope.turn_id || envelope.payload?.failed_turn_id || null,
79
+ timestamp,
80
+ payload: envelope.payload || {},
81
+ };
82
+
83
+ const stampedName = `${safeTimestamp(timestamp)}-${hookPhase}.json`;
84
+ writeFileSync(join(reportDir, stampedName), JSON.stringify(report, null, 2) + '\n');
85
+ writeFileSync(join(reportDir, 'latest.json'), JSON.stringify(report, null, 2) + '\n');
86
+ writeFileSync(join(reportDir, `latest-${hookPhase}.json`), JSON.stringify(report, null, 2) + '\n');
87
+
88
+ process.stdout.write(JSON.stringify({
89
+ verdict: 'allow',
90
+ message: `Wrote ${stampedName}`,
91
+ }));
92
+ }
@@ -0,0 +1,4 @@
1
+ import { readEnvelope, writeReport } from './_shared.js';
2
+
3
+ const envelope = await readEnvelope();
4
+ writeReport('@agentxchain/plugin-json-report', envelope);
@@ -0,0 +1,4 @@
1
+ import { readEnvelope, writeReport } from './_shared.js';
2
+
3
+ const envelope = await readEnvelope();
4
+ writeReport('@agentxchain/plugin-json-report', envelope);
@@ -0,0 +1,4 @@
1
+ import { readEnvelope, writeReport } from './_shared.js';
2
+
3
+ const envelope = await readEnvelope();
4
+ writeReport('@agentxchain/plugin-json-report', envelope);
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@agentxchain/plugin-json-report",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Built-in AgentXchain plugin that writes JSON lifecycle reports for CI and operators.",
7
+ "license": "MIT"
8
+ }
@@ -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.78.0",
3
+ "version": "2.80.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": {
@@ -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) {
@@ -9,7 +9,8 @@ import {
9
9
  statSync,
10
10
  } from 'fs';
11
11
  import { createHash } from 'crypto';
12
- import { join, resolve, relative } from 'path';
12
+ import { dirname, join, resolve, relative } from 'path';
13
+ import { fileURLToPath } from 'url';
13
14
  import { tmpdir } from 'os';
14
15
  import { spawnSync } from 'child_process';
15
16
 
@@ -23,6 +24,55 @@ export const PLUGINS_DIR = '.agentxchain/plugins';
23
24
  const PLUGIN_SCHEMA_VERSION = '0.1';
24
25
  const PLUGIN_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
25
26
 
27
+ const __filename_local = fileURLToPath(import.meta.url);
28
+ const __dirname_local = dirname(__filename_local);
29
+ const BUILTIN_PLUGINS_DIR = join(__dirname_local, '../../builtin-plugins');
30
+
31
+ const BUILTIN_PLUGIN_SHORT_NAMES = {
32
+ 'slack-notify': 'plugin-slack-notify',
33
+ 'json-report': 'plugin-json-report',
34
+ 'github-issues': 'plugin-github-issues',
35
+ };
36
+
37
+ function resolveBuiltinPlugin(spec) {
38
+ const dirName = BUILTIN_PLUGIN_SHORT_NAMES[spec];
39
+ if (!dirName) return null;
40
+ const pluginDir = join(BUILTIN_PLUGINS_DIR, dirName);
41
+ if (!existsSync(pluginDir)) return null;
42
+ const root = findManifestRoot(pluginDir);
43
+ if (!root) return null;
44
+ return {
45
+ ok: true,
46
+ type: 'builtin',
47
+ root,
48
+ sourceSpec: spec,
49
+ cleanup: null,
50
+ };
51
+ }
52
+
53
+ export function listAvailablePlugins() {
54
+ const available = [];
55
+ for (const [shortName, dirName] of Object.entries(BUILTIN_PLUGIN_SHORT_NAMES)) {
56
+ const pluginDir = join(BUILTIN_PLUGINS_DIR, dirName);
57
+ if (!existsSync(pluginDir)) continue;
58
+ const manifestPath = join(pluginDir, PLUGIN_MANIFEST_FILE);
59
+ if (!existsSync(manifestPath)) continue;
60
+ try {
61
+ const manifest = readJson(manifestPath);
62
+ available.push({
63
+ short_name: shortName,
64
+ name: manifest.name,
65
+ version: manifest.version,
66
+ description: manifest.description,
67
+ install_command: `agentxchain plugin install ${shortName}`,
68
+ });
69
+ } catch {
70
+ // skip broken manifests
71
+ }
72
+ }
73
+ return { ok: true, plugins: available };
74
+ }
75
+
26
76
  function clone(value) {
27
77
  return JSON.parse(JSON.stringify(value));
28
78
  }
@@ -199,6 +249,9 @@ function resolvePluginSource(spec, startDir) {
199
249
  }
200
250
  }
201
251
 
252
+ const builtin = resolveBuiltinPlugin(spec);
253
+ if (builtin) return builtin;
254
+
202
255
  const packed = packNpmPlugin(spec);
203
256
  if (!packed.ok) {
204
257
  return packed;