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
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` |
|
package/bin/agentxchain.js
CHANGED
|
@@ -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';
|
|
@@ -82,6 +83,7 @@ import { reportCommand } from '../src/commands/report.js';
|
|
|
82
83
|
import {
|
|
83
84
|
pluginInstallCommand,
|
|
84
85
|
pluginListCommand,
|
|
86
|
+
pluginListAvailableCommand,
|
|
85
87
|
pluginRemoveCommand,
|
|
86
88
|
pluginUpgradeCommand,
|
|
87
89
|
} from '../src/commands/plugin.js';
|
|
@@ -387,6 +389,17 @@ verifyCmd
|
|
|
387
389
|
.option('--format <format>', 'Output format: text or json', 'text')
|
|
388
390
|
.action(verifyExportCommand);
|
|
389
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
|
+
|
|
390
403
|
program
|
|
391
404
|
.command('migrate')
|
|
392
405
|
.description('Migrate a legacy v3 project to governed format')
|
|
@@ -485,6 +498,12 @@ pluginCmd
|
|
|
485
498
|
.option('-j, --json', 'Output as JSON')
|
|
486
499
|
.action(pluginListCommand);
|
|
487
500
|
|
|
501
|
+
pluginCmd
|
|
502
|
+
.command('list-available')
|
|
503
|
+
.description('List built-in plugins available for installation')
|
|
504
|
+
.option('-j, --json', 'Output as JSON')
|
|
505
|
+
.action(pluginListAvailableCommand);
|
|
506
|
+
|
|
488
507
|
pluginCmd
|
|
489
508
|
.command('remove <name>')
|
|
490
509
|
.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,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
|
+
}
|