agentxchain 2.6.0 → 2.8.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 +18 -0
- package/package.json +1 -1
- package/src/commands/escalate.js +63 -0
- package/src/commands/report.js +51 -0
- package/src/commands/resume.js +72 -2
- package/src/commands/step.js +22 -15
- package/src/lib/blocked-state.js +7 -3
- package/src/lib/export-verifier.js +4 -0
- package/src/lib/export.js +2 -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/src/lib/report.js +379 -0
package/README.md
CHANGED
|
@@ -10,8 +10,10 @@ Legacy IDE-window coordination is still shipped as a compatibility mode for team
|
|
|
10
10
|
|
|
11
11
|
- [Quickstart](https://agentxchain.dev/docs/quickstart/)
|
|
12
12
|
- [CLI reference](https://agentxchain.dev/docs/cli/)
|
|
13
|
+
- [Export schema reference](https://agentxchain.dev/docs/export-schema/)
|
|
13
14
|
- [Adapter reference](https://agentxchain.dev/docs/adapters/)
|
|
14
15
|
- [Protocol spec (v6)](https://agentxchain.dev/docs/protocol/)
|
|
16
|
+
- [Protocol reference](https://agentxchain.dev/docs/protocol-reference/)
|
|
15
17
|
- [Why governed multi-agent delivery matters](https://agentxchain.dev/why/)
|
|
16
18
|
|
|
17
19
|
## Install
|
package/bin/agentxchain.js
CHANGED
|
@@ -65,6 +65,7 @@ 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';
|
|
@@ -72,6 +73,7 @@ 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';
|
|
74
75
|
import { exportCommand } from '../src/commands/export.js';
|
|
76
|
+
import { reportCommand } from '../src/commands/report.js';
|
|
75
77
|
import {
|
|
76
78
|
pluginInstallCommand,
|
|
77
79
|
pluginListCommand,
|
|
@@ -129,6 +131,13 @@ program
|
|
|
129
131
|
.option('--output <path>', 'Write the export artifact to a file instead of stdout')
|
|
130
132
|
.action(exportCommand);
|
|
131
133
|
|
|
134
|
+
program
|
|
135
|
+
.command('report')
|
|
136
|
+
.description('Render a human-readable governance summary from an export artifact')
|
|
137
|
+
.option('--input <path>', 'Export artifact path, or "-" for stdin', '-')
|
|
138
|
+
.option('--format <format>', 'Output format: text, json, or markdown', 'text')
|
|
139
|
+
.action(reportCommand);
|
|
140
|
+
|
|
132
141
|
program
|
|
133
142
|
.command('start')
|
|
134
143
|
.description('Launch legacy v3 agents in your IDE')
|
|
@@ -260,6 +269,15 @@ program
|
|
|
260
269
|
.option('--turn <id>', 'Target a specific retained turn when multiple exist')
|
|
261
270
|
.action(resumeCommand);
|
|
262
271
|
|
|
272
|
+
program
|
|
273
|
+
.command('escalate')
|
|
274
|
+
.description('Raise an operator escalation and block the governed run intentionally')
|
|
275
|
+
.requiredOption('--reason <reason>', 'Operator escalation summary')
|
|
276
|
+
.option('--detail <detail>', 'Longer escalation detail for status and ledger surfaces')
|
|
277
|
+
.option('--action <action>', 'Override the default recovery action string')
|
|
278
|
+
.option('--turn <id>', 'Target a specific active turn when multiple turns exist')
|
|
279
|
+
.action(escalateCommand);
|
|
280
|
+
|
|
263
281
|
program
|
|
264
282
|
.command('accept-turn')
|
|
265
283
|
.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,51 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import { loadExportArtifact } from '../lib/export-verifier.js';
|
|
4
|
+
import {
|
|
5
|
+
buildGovernanceReport,
|
|
6
|
+
formatGovernanceReportMarkdown,
|
|
7
|
+
formatGovernanceReportText,
|
|
8
|
+
} from '../lib/report.js';
|
|
9
|
+
|
|
10
|
+
function printAndExit(report, format, exitCode) {
|
|
11
|
+
if (format === 'json') {
|
|
12
|
+
console.log(JSON.stringify(report, null, 2));
|
|
13
|
+
process.exit(exitCode);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (format === 'markdown') {
|
|
17
|
+
console.log(formatGovernanceReportMarkdown(report));
|
|
18
|
+
process.exit(exitCode);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (format === 'text') {
|
|
22
|
+
if (report.overall === 'error') {
|
|
23
|
+
console.log(chalk.red(formatGovernanceReportText(report)));
|
|
24
|
+
} else if (report.overall === 'fail') {
|
|
25
|
+
console.log(chalk.red(formatGovernanceReportText(report)));
|
|
26
|
+
} else {
|
|
27
|
+
console.log(formatGovernanceReportText(report));
|
|
28
|
+
}
|
|
29
|
+
process.exit(exitCode);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.error(`Unsupported report format "${format}". Use "text", "json", or "markdown".`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function reportCommand(opts) {
|
|
37
|
+
const format = opts.format || 'text';
|
|
38
|
+
const loaded = loadExportArtifact(opts.input || '-', process.cwd());
|
|
39
|
+
|
|
40
|
+
if (!loaded.ok) {
|
|
41
|
+
printAndExit({
|
|
42
|
+
overall: 'error',
|
|
43
|
+
input: loaded.input,
|
|
44
|
+
message: loaded.error,
|
|
45
|
+
}, format, 2);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = buildGovernanceReport(loaded.artifact, { input: loaded.input });
|
|
50
|
+
printAndExit(result.report, format, result.exitCode);
|
|
51
|
+
}
|
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/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
|
|
|
@@ -187,6 +187,7 @@ function verifyRunExport(artifact, errors) {
|
|
|
187
187
|
const expectedHistoryEntries = countJsonl(artifact.files, '.agentxchain/history.jsonl');
|
|
188
188
|
const expectedDecisionEntries = countJsonl(artifact.files, '.agentxchain/decision-ledger.jsonl');
|
|
189
189
|
const expectedHookAuditEntries = countJsonl(artifact.files, '.agentxchain/hook-audit.jsonl');
|
|
190
|
+
const expectedNotificationAuditEntries = countJsonl(artifact.files, '.agentxchain/notification-audit.jsonl');
|
|
190
191
|
const expectedDispatchFiles = countDirectoryFiles(artifact.files, '.agentxchain/dispatch');
|
|
191
192
|
const expectedStagingFiles = countDirectoryFiles(artifact.files, '.agentxchain/staging');
|
|
192
193
|
const expectedIntakePresent = Object.keys(artifact.files).some((path) => path.startsWith('.agentxchain/intake/'));
|
|
@@ -201,6 +202,9 @@ function verifyRunExport(artifact, errors) {
|
|
|
201
202
|
if (artifact.summary.hook_audit_entries !== expectedHookAuditEntries) {
|
|
202
203
|
addError(errors, 'summary.hook_audit_entries', 'must match .agentxchain/hook-audit.jsonl entry count');
|
|
203
204
|
}
|
|
205
|
+
if (artifact.summary.notification_audit_entries !== expectedNotificationAuditEntries) {
|
|
206
|
+
addError(errors, 'summary.notification_audit_entries', 'must match .agentxchain/notification-audit.jsonl entry count');
|
|
207
|
+
}
|
|
204
208
|
if (artifact.summary.dispatch_artifact_files !== expectedDispatchFiles) {
|
|
205
209
|
addError(errors, 'summary.dispatch_artifact_files', 'must match .agentxchain/dispatch file count');
|
|
206
210
|
}
|
package/src/lib/export.js
CHANGED
|
@@ -24,6 +24,7 @@ const INCLUDED_ROOTS = [
|
|
|
24
24
|
'.agentxchain/decision-ledger.jsonl',
|
|
25
25
|
'.agentxchain/hook-audit.jsonl',
|
|
26
26
|
'.agentxchain/hook-annotations.jsonl',
|
|
27
|
+
'.agentxchain/notification-audit.jsonl',
|
|
27
28
|
'.agentxchain/dispatch',
|
|
28
29
|
'.agentxchain/staging',
|
|
29
30
|
'.agentxchain/transactions/accept',
|
|
@@ -172,6 +173,7 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
172
173
|
history_entries: countJsonl(files, '.agentxchain/history.jsonl'),
|
|
173
174
|
decision_entries: countJsonl(files, '.agentxchain/decision-ledger.jsonl'),
|
|
174
175
|
hook_audit_entries: countJsonl(files, '.agentxchain/hook-audit.jsonl'),
|
|
176
|
+
notification_audit_entries: countJsonl(files, '.agentxchain/notification-audit.jsonl'),
|
|
175
177
|
dispatch_artifact_files: countDirectoryFiles(files, '.agentxchain/dispatch'),
|
|
176
178
|
staging_artifact_files: countDirectoryFiles(files, '.agentxchain/staging'),
|
|
177
179
|
intake_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/intake/')),
|