agentxchain 2.5.0 → 2.7.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/bin/agentxchain.js +26 -1
- package/package.json +1 -1
- package/src/commands/escalate.js +63 -0
- package/src/commands/export.js +63 -0
- package/src/commands/resume.js +72 -2
- package/src/commands/step.js +22 -15
- package/src/commands/verify.js +60 -0
- package/src/lib/blocked-state.js +7 -3
- package/src/lib/export-verifier.js +426 -0
- package/src/lib/export.js +305 -0
- package/src/lib/governed-state.js +255 -6
- package/src/lib/normalized-config.js +9 -0
- package/src/lib/notification-runner.js +330 -0
package/bin/agentxchain.js
CHANGED
|
@@ -59,18 +59,20 @@ import { generateCommand } from '../src/commands/generate.js';
|
|
|
59
59
|
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
|
-
import { verifyProtocolCommand } from '../src/commands/verify.js';
|
|
62
|
+
import { verifyExportCommand, verifyProtocolCommand } from '../src/commands/verify.js';
|
|
63
63
|
import { kickoffCommand } from '../src/commands/kickoff.js';
|
|
64
64
|
import { rebindCommand } from '../src/commands/rebind.js';
|
|
65
65
|
import { branchCommand } from '../src/commands/branch.js';
|
|
66
66
|
import { migrateCommand } from '../src/commands/migrate.js';
|
|
67
67
|
import { resumeCommand } from '../src/commands/resume.js';
|
|
68
|
+
import { escalateCommand } from '../src/commands/escalate.js';
|
|
68
69
|
import { acceptTurnCommand } from '../src/commands/accept-turn.js';
|
|
69
70
|
import { rejectTurnCommand } from '../src/commands/reject-turn.js';
|
|
70
71
|
import { stepCommand } from '../src/commands/step.js';
|
|
71
72
|
import { approveTransitionCommand } from '../src/commands/approve-transition.js';
|
|
72
73
|
import { approveCompletionCommand } from '../src/commands/approve-completion.js';
|
|
73
74
|
import { dashboardCommand } from '../src/commands/dashboard.js';
|
|
75
|
+
import { exportCommand } from '../src/commands/export.js';
|
|
74
76
|
import {
|
|
75
77
|
pluginInstallCommand,
|
|
76
78
|
pluginListCommand,
|
|
@@ -121,6 +123,13 @@ program
|
|
|
121
123
|
.option('-j, --json', 'Output as JSON')
|
|
122
124
|
.action(statusCommand);
|
|
123
125
|
|
|
126
|
+
program
|
|
127
|
+
.command('export')
|
|
128
|
+
.description('Export the governed run audit surface as a single artifact')
|
|
129
|
+
.option('--format <format>', 'Export format (json)', 'json')
|
|
130
|
+
.option('--output <path>', 'Write the export artifact to a file instead of stdout')
|
|
131
|
+
.action(exportCommand);
|
|
132
|
+
|
|
124
133
|
program
|
|
125
134
|
.command('start')
|
|
126
135
|
.description('Launch legacy v3 agents in your IDE')
|
|
@@ -231,6 +240,13 @@ verifyCmd
|
|
|
231
240
|
.option('--format <format>', 'Output format: text or json', 'text')
|
|
232
241
|
.action(verifyProtocolCommand);
|
|
233
242
|
|
|
243
|
+
verifyCmd
|
|
244
|
+
.command('export')
|
|
245
|
+
.description('Verify an AgentXchain export artifact against its embedded file bytes and summaries')
|
|
246
|
+
.option('--input <path>', 'Export artifact path, or "-" for stdin', '-')
|
|
247
|
+
.option('--format <format>', 'Output format: text or json', 'text')
|
|
248
|
+
.action(verifyExportCommand);
|
|
249
|
+
|
|
234
250
|
program
|
|
235
251
|
.command('migrate')
|
|
236
252
|
.description('Migrate a legacy v3 project to governed format')
|
|
@@ -245,6 +261,15 @@ program
|
|
|
245
261
|
.option('--turn <id>', 'Target a specific retained turn when multiple exist')
|
|
246
262
|
.action(resumeCommand);
|
|
247
263
|
|
|
264
|
+
program
|
|
265
|
+
.command('escalate')
|
|
266
|
+
.description('Raise an operator escalation and block the governed run intentionally')
|
|
267
|
+
.requiredOption('--reason <reason>', 'Operator escalation summary')
|
|
268
|
+
.option('--detail <detail>', 'Longer escalation detail for status and ledger surfaces')
|
|
269
|
+
.option('--action <action>', 'Override the default recovery action string')
|
|
270
|
+
.option('--turn <id>', 'Target a specific active turn when multiple turns exist')
|
|
271
|
+
.action(escalateCommand);
|
|
272
|
+
|
|
248
273
|
program
|
|
249
274
|
.command('accept-turn')
|
|
250
275
|
.description('Accept the currently staged governed turn result')
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
3
|
+
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
4
|
+
import { raiseOperatorEscalation } from '../lib/governed-state.js';
|
|
5
|
+
|
|
6
|
+
export async function escalateCommand(opts) {
|
|
7
|
+
const context = loadProjectContext();
|
|
8
|
+
if (!context) {
|
|
9
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { root, config } = context;
|
|
14
|
+
|
|
15
|
+
if (config.protocol_mode !== 'governed') {
|
|
16
|
+
console.log(chalk.red('The escalate command is only available for governed projects.'));
|
|
17
|
+
console.log(chalk.dim('Legacy projects use: agentxchain claim / release'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const state = loadProjectState(root, config);
|
|
22
|
+
if (!state) {
|
|
23
|
+
console.log(chalk.red('No governed state.json found. Run `agentxchain init --governed` first.'));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = raiseOperatorEscalation(root, config, {
|
|
28
|
+
reason: opts.reason,
|
|
29
|
+
detail: opts.detail,
|
|
30
|
+
action: opts.action,
|
|
31
|
+
turnId: opts.turn,
|
|
32
|
+
});
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
console.log(chalk.red(result.error));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const recovery = deriveRecoveryDescriptor(result.state);
|
|
39
|
+
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(chalk.yellow(' Run Escalated'));
|
|
42
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(` ${chalk.dim('Reason:')} ${result.escalation.reason}`);
|
|
45
|
+
if (result.escalation.detail && result.escalation.detail !== result.escalation.reason) {
|
|
46
|
+
console.log(` ${chalk.dim('Detail:')} ${result.escalation.detail}`);
|
|
47
|
+
}
|
|
48
|
+
console.log(` ${chalk.dim('Blocked:')} ${result.state.blocked_on}`);
|
|
49
|
+
console.log(` ${chalk.dim('Source:')} operator`);
|
|
50
|
+
console.log(` ${chalk.dim('Turn:')} ${result.escalation.from_turn_id || 'none retained'}`);
|
|
51
|
+
if (result.escalation.from_role) {
|
|
52
|
+
console.log(` ${chalk.dim('Role:')} ${result.escalation.from_role}`);
|
|
53
|
+
}
|
|
54
|
+
console.log('');
|
|
55
|
+
|
|
56
|
+
if (recovery) {
|
|
57
|
+
console.log(` ${chalk.dim('Typed:')} ${recovery.typed_reason}`);
|
|
58
|
+
console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
|
|
59
|
+
console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
|
|
60
|
+
console.log(` ${chalk.dim('Retained:')} ${recovery.turn_retained ? 'yes' : 'no'}`);
|
|
61
|
+
}
|
|
62
|
+
console.log('');
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { buildRunExport, buildCoordinatorExport } from '../lib/export.js';
|
|
5
|
+
import { COORDINATOR_CONFIG_FILE } from '../lib/coordinator-config.js';
|
|
6
|
+
import { safeWriteJson } from '../lib/safe-write.js';
|
|
7
|
+
|
|
8
|
+
function detectExportKind(cwd) {
|
|
9
|
+
// Governed project takes priority (agentxchain.json)
|
|
10
|
+
if (existsSync(join(cwd, 'agentxchain.json'))) {
|
|
11
|
+
return 'governed';
|
|
12
|
+
}
|
|
13
|
+
// Coordinator workspace (agentxchain-multi.json)
|
|
14
|
+
if (existsSync(join(cwd, COORDINATOR_CONFIG_FILE))) {
|
|
15
|
+
return 'coordinator';
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function exportCommand(options) {
|
|
21
|
+
const format = options.format || 'json';
|
|
22
|
+
if (format !== 'json') {
|
|
23
|
+
console.error(`Unsupported export format "${format}". Only "json" is supported in this slice.`);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const kind = detectExportKind(cwd);
|
|
30
|
+
|
|
31
|
+
let result;
|
|
32
|
+
try {
|
|
33
|
+
if (kind === 'governed') {
|
|
34
|
+
result = buildRunExport(cwd);
|
|
35
|
+
} else if (kind === 'coordinator') {
|
|
36
|
+
result = buildCoordinatorExport(cwd);
|
|
37
|
+
} else {
|
|
38
|
+
result = {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: 'No governed project or coordinator workspace found. Run this inside an AgentXchain governed project or coordinator workspace.',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(error.message || String(error));
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!result.ok) {
|
|
50
|
+
console.error(result.error);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (options.output) {
|
|
56
|
+
const outputPath = resolve(cwd, options.output);
|
|
57
|
+
safeWriteJson(outputPath, result.export);
|
|
58
|
+
console.log(`Exported ${kind === 'coordinator' ? 'coordinator workspace' : 'governed run'} audit to ${options.output}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(JSON.stringify(result.export, null, 2));
|
|
63
|
+
}
|
package/src/commands/resume.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
markRunBlocked,
|
|
25
25
|
getActiveTurns,
|
|
26
26
|
getActiveTurnCount,
|
|
27
|
+
reactivateGovernedRun,
|
|
27
28
|
STATE_PATH,
|
|
28
29
|
} from '../lib/governed-state.js';
|
|
29
30
|
import { writeDispatchBundle, getDispatchTurnDir, getTurnStagingResultPath } from '../lib/dispatch-bundle.js';
|
|
@@ -154,6 +155,62 @@ export async function resumeCommand(opts) {
|
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
if (state.status === 'blocked' && activeCount > 0) {
|
|
159
|
+
let retainedTurn = null;
|
|
160
|
+
if (opts.turn) {
|
|
161
|
+
retainedTurn = activeTurns[opts.turn];
|
|
162
|
+
if (!retainedTurn) {
|
|
163
|
+
console.log(chalk.red(`No active turn found for --turn ${opts.turn}`));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
} else if (activeCount > 1) {
|
|
167
|
+
console.log(chalk.red('Multiple retained turns exist. Use --turn <id> to specify which to re-dispatch.'));
|
|
168
|
+
for (const turn of Object.values(activeTurns)) {
|
|
169
|
+
console.log(` ${chalk.yellow('●')} ${turn.turn_id} — ${chalk.bold(turn.assigned_role)} (${turn.status})`);
|
|
170
|
+
}
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(chalk.dim('Example: agentxchain resume --turn <turn_id>'));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
} else {
|
|
175
|
+
retainedTurn = Object.values(activeTurns)[0];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (retainedTurn.status === 'conflicted') {
|
|
179
|
+
console.log(chalk.red(`Turn ${retainedTurn.turn_id} is conflicted. Resolve the conflict before resuming.`));
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(chalk.yellow(`Re-dispatching blocked turn: ${retainedTurn.turn_id}`));
|
|
184
|
+
console.log(` Role: ${retainedTurn.assigned_role}`);
|
|
185
|
+
console.log(` Attempt: ${retainedTurn.attempt}`);
|
|
186
|
+
console.log('');
|
|
187
|
+
|
|
188
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'resume --turn', notificationConfig: config });
|
|
189
|
+
if (!reactivated.ok) {
|
|
190
|
+
console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
state = reactivated.state;
|
|
194
|
+
|
|
195
|
+
const bundleResult = writeDispatchBundle(root, state, config, { turnId: retainedTurn.turn_id });
|
|
196
|
+
if (!bundleResult.ok) {
|
|
197
|
+
console.log(chalk.red(`Failed to write dispatch bundle: ${bundleResult.error}`));
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
printDispatchBundleWarnings(bundleResult);
|
|
201
|
+
|
|
202
|
+
const hooksConfig = config.hooks || {};
|
|
203
|
+
if (hooksConfig.after_dispatch?.length > 0) {
|
|
204
|
+
const afterDispatchResult = runAfterDispatchHooks(root, hooksConfig, state, retainedTurn, config);
|
|
205
|
+
if (!afterDispatchResult.ok) {
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
printDispatchSummary(state, config, retainedTurn);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
157
214
|
// §47: idle + no run_id → initialize new run
|
|
158
215
|
if (state.status === 'idle' && !state.run_id) {
|
|
159
216
|
const initResult = initializeGovernedRun(root, config);
|
|
@@ -165,10 +222,22 @@ export async function resumeCommand(opts) {
|
|
|
165
222
|
console.log(chalk.green(`Initialized governed run: ${state.run_id}`));
|
|
166
223
|
}
|
|
167
224
|
|
|
225
|
+
// §47: paused + run_id exists → resume same run
|
|
226
|
+
if (state.status === 'blocked' && state.run_id) {
|
|
227
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'resume', notificationConfig: config });
|
|
228
|
+
if (!reactivated.ok) {
|
|
229
|
+
console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
state = reactivated.state;
|
|
233
|
+
console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
|
|
234
|
+
}
|
|
235
|
+
|
|
168
236
|
// §47: paused + run_id exists → resume same run
|
|
169
237
|
if (state.status === 'paused' && state.run_id) {
|
|
170
238
|
state.status = 'active';
|
|
171
239
|
state.blocked_on = null;
|
|
240
|
+
state.blocked_reason = null;
|
|
172
241
|
state.escalation = null;
|
|
173
242
|
safeWriteJson(statePath, state);
|
|
174
243
|
console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
|
|
@@ -203,7 +272,7 @@ export async function resumeCommand(opts) {
|
|
|
203
272
|
const hooksConfig = config.hooks || {};
|
|
204
273
|
const turn = state.current_turn;
|
|
205
274
|
if (hooksConfig.after_dispatch?.length > 0) {
|
|
206
|
-
const afterDispatchResult = runAfterDispatchHooks(root, hooksConfig, state, turn);
|
|
275
|
+
const afterDispatchResult = runAfterDispatchHooks(root, hooksConfig, state, turn, config);
|
|
207
276
|
if (!afterDispatchResult.ok) {
|
|
208
277
|
process.exit(1);
|
|
209
278
|
}
|
|
@@ -259,7 +328,7 @@ function resolveTargetRole(opts, state, config) {
|
|
|
259
328
|
return null;
|
|
260
329
|
}
|
|
261
330
|
|
|
262
|
-
function runAfterDispatchHooks(root, hooksConfig, state, turn) {
|
|
331
|
+
function runAfterDispatchHooks(root, hooksConfig, state, turn, config) {
|
|
263
332
|
const turnId = turn.turn_id;
|
|
264
333
|
const roleId = turn.assigned_role;
|
|
265
334
|
|
|
@@ -300,6 +369,7 @@ function runAfterDispatchHooks(root, hooksConfig, state, turn) {
|
|
|
300
369
|
detail,
|
|
301
370
|
},
|
|
302
371
|
turnId,
|
|
372
|
+
notificationConfig: config,
|
|
303
373
|
});
|
|
304
374
|
|
|
305
375
|
printDispatchHookFailure({
|
package/src/commands/step.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
markRunBlocked,
|
|
33
33
|
getActiveTurnCount,
|
|
34
34
|
getActiveTurns,
|
|
35
|
+
reactivateGovernedRun,
|
|
35
36
|
STATE_PATH,
|
|
36
37
|
} from '../lib/governed-state.js';
|
|
37
38
|
import { getMaxConcurrentTurns } from '../lib/normalized-config.js';
|
|
@@ -203,8 +204,12 @@ export async function stepCommand(opts) {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
console.log(chalk.yellow(`Re-dispatching blocked turn: ${targetTurn.turn_id}`));
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'step --resume', notificationConfig: config });
|
|
208
|
+
if (!reactivated.ok) {
|
|
209
|
+
console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
state = reactivated.state;
|
|
208
213
|
skipAssignment = true;
|
|
209
214
|
|
|
210
215
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
@@ -251,8 +256,12 @@ export async function stepCommand(opts) {
|
|
|
251
256
|
|
|
252
257
|
// paused → resume
|
|
253
258
|
if (!skipAssignment && state.status === 'blocked' && state.run_id) {
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'step', notificationConfig: config });
|
|
260
|
+
if (!reactivated.ok) {
|
|
261
|
+
console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
state = reactivated.state;
|
|
256
265
|
console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
|
|
257
266
|
}
|
|
258
267
|
|
|
@@ -332,6 +341,7 @@ export async function stepCommand(opts) {
|
|
|
332
341
|
hookResults: afterDispatchHooks,
|
|
333
342
|
phase: 'after_dispatch',
|
|
334
343
|
defaultDetail: `after_dispatch hook blocked dispatch for turn ${turn.turn_id}`,
|
|
344
|
+
config,
|
|
335
345
|
});
|
|
336
346
|
printLifecycleHookFailure('Dispatch Blocked By Hook', blocked.result, {
|
|
337
347
|
turnId: turn.turn_id,
|
|
@@ -382,6 +392,7 @@ export async function stepCommand(opts) {
|
|
|
382
392
|
},
|
|
383
393
|
turnId: turn.turn_id,
|
|
384
394
|
hooksConfig,
|
|
395
|
+
notificationConfig: config,
|
|
385
396
|
});
|
|
386
397
|
if (blocked.ok) {
|
|
387
398
|
state = blocked.state;
|
|
@@ -462,6 +473,7 @@ export async function stepCommand(opts) {
|
|
|
462
473
|
},
|
|
463
474
|
turnId: turn.turn_id,
|
|
464
475
|
hooksConfig,
|
|
476
|
+
notificationConfig: config,
|
|
465
477
|
});
|
|
466
478
|
if (blocked.ok) {
|
|
467
479
|
state = blocked.state;
|
|
@@ -530,6 +542,7 @@ export async function stepCommand(opts) {
|
|
|
530
542
|
},
|
|
531
543
|
turnId: turn.turn_id,
|
|
532
544
|
hooksConfig,
|
|
545
|
+
notificationConfig: config,
|
|
533
546
|
});
|
|
534
547
|
if (blocked.ok) {
|
|
535
548
|
state = blocked.state;
|
|
@@ -556,6 +569,7 @@ export async function stepCommand(opts) {
|
|
|
556
569
|
},
|
|
557
570
|
turnId: turn.turn_id,
|
|
558
571
|
hooksConfig,
|
|
572
|
+
notificationConfig: config,
|
|
559
573
|
});
|
|
560
574
|
if (blocked.ok) {
|
|
561
575
|
state = blocked.state;
|
|
@@ -643,6 +657,7 @@ export async function stepCommand(opts) {
|
|
|
643
657
|
hookResults: beforeValidationHooks,
|
|
644
658
|
phase: 'before_validation',
|
|
645
659
|
defaultDetail: `before_validation hook blocked validation for turn ${turn.turn_id}`,
|
|
660
|
+
config,
|
|
646
661
|
});
|
|
647
662
|
printLifecycleHookFailure('Validation Blocked By Hook', blocked.result, {
|
|
648
663
|
turnId: turn.turn_id,
|
|
@@ -674,6 +689,7 @@ export async function stepCommand(opts) {
|
|
|
674
689
|
hookResults: afterValidationHooks,
|
|
675
690
|
phase: 'after_validation',
|
|
676
691
|
defaultDetail: `after_validation hook blocked acceptance for turn ${turn.turn_id}`,
|
|
692
|
+
config,
|
|
677
693
|
});
|
|
678
694
|
printLifecycleHookFailure('Validation Blocked By Hook', blocked.result, {
|
|
679
695
|
turnId: turn.turn_id,
|
|
@@ -758,7 +774,7 @@ function loadHookStagedTurn(root, stagingRel) {
|
|
|
758
774
|
}
|
|
759
775
|
}
|
|
760
776
|
|
|
761
|
-
function blockStepForHookIssue(root, turn, { hookResults, phase, defaultDetail }) {
|
|
777
|
+
function blockStepForHookIssue(root, turn, { hookResults, phase, defaultDetail, config }) {
|
|
762
778
|
const hookName = hookResults.blocker?.hook_name
|
|
763
779
|
|| hookResults.results?.find((entry) => entry.hook_name)?.hook_name
|
|
764
780
|
|| 'unknown';
|
|
@@ -777,6 +793,7 @@ function blockStepForHookIssue(root, turn, { hookResults, phase, defaultDetail }
|
|
|
777
793
|
detail,
|
|
778
794
|
},
|
|
779
795
|
turnId: turn.turn_id,
|
|
796
|
+
notificationConfig: config,
|
|
780
797
|
});
|
|
781
798
|
|
|
782
799
|
return {
|
|
@@ -860,16 +877,6 @@ function resolveTargetRole(opts, state, config) {
|
|
|
860
877
|
return null;
|
|
861
878
|
}
|
|
862
879
|
|
|
863
|
-
function clearBlockedState(state) {
|
|
864
|
-
return {
|
|
865
|
-
...state,
|
|
866
|
-
status: 'active',
|
|
867
|
-
blocked_on: null,
|
|
868
|
-
blocked_reason: null,
|
|
869
|
-
escalation: null,
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
880
|
function printRecoverySummary(state, heading) {
|
|
874
881
|
const recovery = deriveRecoveryDescriptor(state);
|
|
875
882
|
console.log(chalk.yellow(heading));
|
package/src/commands/verify.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { loadExportArtifact, verifyExportArtifact } from '../lib/export-verifier.js';
|
|
3
4
|
import { verifyProtocolConformance } from '../lib/protocol-conformance.js';
|
|
4
5
|
|
|
5
6
|
export async function verifyProtocolCommand(opts) {
|
|
@@ -35,6 +36,38 @@ export async function verifyProtocolCommand(opts) {
|
|
|
35
36
|
process.exit(result.exitCode);
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
export async function verifyExportCommand(opts) {
|
|
40
|
+
const format = opts.format || 'text';
|
|
41
|
+
const loaded = loadExportArtifact(opts.input || '-', process.cwd());
|
|
42
|
+
|
|
43
|
+
if (!loaded.ok) {
|
|
44
|
+
if (format === 'json') {
|
|
45
|
+
console.log(JSON.stringify({
|
|
46
|
+
overall: 'error',
|
|
47
|
+
input: loaded.input,
|
|
48
|
+
message: loaded.error,
|
|
49
|
+
}, null, 2));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.red(`Export verification failed: ${loaded.error}`));
|
|
52
|
+
}
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = verifyExportArtifact(loaded.artifact);
|
|
57
|
+
const report = {
|
|
58
|
+
...result.report,
|
|
59
|
+
input: loaded.input,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (format === 'json') {
|
|
63
|
+
console.log(JSON.stringify(report, null, 2));
|
|
64
|
+
} else {
|
|
65
|
+
printExportReport(report);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.exit(result.ok ? 0 : 1);
|
|
69
|
+
}
|
|
70
|
+
|
|
38
71
|
function printProtocolReport(report) {
|
|
39
72
|
console.log('');
|
|
40
73
|
console.log(chalk.bold(' AgentXchain Protocol Conformance'));
|
|
@@ -74,3 +107,30 @@ function printProtocolReport(report) {
|
|
|
74
107
|
|
|
75
108
|
console.log('');
|
|
76
109
|
}
|
|
110
|
+
|
|
111
|
+
function printExportReport(report) {
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(chalk.bold(' AgentXchain Export Verification'));
|
|
114
|
+
console.log(chalk.dim(' ' + '─'.repeat(43)));
|
|
115
|
+
console.log(chalk.dim(` Input: ${report.input}`));
|
|
116
|
+
console.log(chalk.dim(` Export kind: ${report.export_kind || 'unknown'}`));
|
|
117
|
+
console.log(chalk.dim(` Schema: ${report.schema_version || 'unknown'}`));
|
|
118
|
+
console.log('');
|
|
119
|
+
|
|
120
|
+
const overallLabel = report.overall === 'pass'
|
|
121
|
+
? chalk.green('PASS')
|
|
122
|
+
: report.overall === 'fail'
|
|
123
|
+
? chalk.red('FAIL')
|
|
124
|
+
: chalk.red('ERROR');
|
|
125
|
+
console.log(` Overall: ${overallLabel}`);
|
|
126
|
+
console.log(chalk.dim(` Files verified: ${report.file_count}`));
|
|
127
|
+
if (report.repo_count) {
|
|
128
|
+
console.log(chalk.dim(` Embedded repos: ${report.repo_count}`));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const error of report.errors || []) {
|
|
132
|
+
console.log(chalk.red(` ✗ ${error}`));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log('');
|
|
136
|
+
}
|
package/src/lib/blocked-state.js
CHANGED
|
@@ -65,12 +65,16 @@ export function deriveRecoveryDescriptor(state) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
if (state.blocked_on.startsWith('escalation:')) {
|
|
68
|
+
const isOperatorEscalation = state.blocked_on.startsWith('escalation:operator:') || state.escalation?.source === 'operator';
|
|
69
|
+
const recoveryAction = turnRetained
|
|
70
|
+
? 'Resolve the escalation, then run agentxchain step --resume'
|
|
71
|
+
: 'Resolve the escalation, then run agentxchain step';
|
|
68
72
|
return {
|
|
69
|
-
typed_reason: 'retries_exhausted',
|
|
73
|
+
typed_reason: isOperatorEscalation ? 'operator_escalation' : 'retries_exhausted',
|
|
70
74
|
owner: 'human',
|
|
71
|
-
recovery_action:
|
|
75
|
+
recovery_action: recoveryAction,
|
|
72
76
|
turn_retained: turnRetained,
|
|
73
|
-
detail: state.blocked_on,
|
|
77
|
+
detail: state.escalation?.detail || state.escalation?.reason || state.blocked_on,
|
|
74
78
|
};
|
|
75
79
|
}
|
|
76
80
|
|