agentxchain 2.47.0 → 2.49.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 +42 -0
- package/package.json +1 -1
- package/src/commands/events.js +150 -0
- package/src/commands/run.js +97 -69
- package/src/commands/schedule.js +265 -0
- package/src/lib/export.js +4 -0
- package/src/lib/governed-state.js +107 -0
- package/src/lib/normalized-config.js +80 -0
- package/src/lib/repo-observer.js +3 -0
- package/src/lib/run-events.js +117 -0
- package/src/lib/run-schedule.js +160 -0
package/bin/agentxchain.js
CHANGED
|
@@ -106,6 +106,8 @@ import { intakeResolveCommand } from '../src/commands/intake-resolve.js';
|
|
|
106
106
|
import { intakeStatusCommand } from '../src/commands/intake-status.js';
|
|
107
107
|
import { demoCommand } from '../src/commands/demo.js';
|
|
108
108
|
import { historyCommand } from '../src/commands/history.js';
|
|
109
|
+
import { eventsCommand } from '../src/commands/events.js';
|
|
110
|
+
import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand } from '../src/commands/schedule.js';
|
|
109
111
|
|
|
110
112
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
111
113
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -257,6 +259,35 @@ program
|
|
|
257
259
|
.option('-v, --verbose', 'Show stack traces on failure')
|
|
258
260
|
.action(demoCommand);
|
|
259
261
|
|
|
262
|
+
const scheduleCmd = program
|
|
263
|
+
.command('schedule')
|
|
264
|
+
.description('Run governed schedules for repo-local lights-out execution');
|
|
265
|
+
|
|
266
|
+
scheduleCmd
|
|
267
|
+
.command('list')
|
|
268
|
+
.description('List configured governed schedules and due status')
|
|
269
|
+
.option('--schedule <id>', 'Show a single schedule')
|
|
270
|
+
.option('--at <iso8601>', 'Evaluate due status at a fixed time')
|
|
271
|
+
.option('-j, --json', 'Output as JSON')
|
|
272
|
+
.action(scheduleListCommand);
|
|
273
|
+
|
|
274
|
+
scheduleCmd
|
|
275
|
+
.command('run-due')
|
|
276
|
+
.description('Execute every due governed schedule once')
|
|
277
|
+
.option('--schedule <id>', 'Run one configured schedule only')
|
|
278
|
+
.option('--at <iso8601>', 'Evaluate due status at a fixed time')
|
|
279
|
+
.option('-j, --json', 'Output as JSON')
|
|
280
|
+
.action(scheduleRunDueCommand);
|
|
281
|
+
|
|
282
|
+
scheduleCmd
|
|
283
|
+
.command('daemon')
|
|
284
|
+
.description('Poll for due governed schedules and run them locally')
|
|
285
|
+
.option('--schedule <id>', 'Run one configured schedule only')
|
|
286
|
+
.option('--poll-seconds <n>', 'Polling interval in seconds', '60')
|
|
287
|
+
.option('--max-cycles <n>', 'Stop after N cycles (test helper)')
|
|
288
|
+
.option('-j, --json', 'Output as JSON')
|
|
289
|
+
.action(scheduleDaemonCommand);
|
|
290
|
+
|
|
260
291
|
program
|
|
261
292
|
.command('history')
|
|
262
293
|
.description('Show cross-run history of governed runs in this project')
|
|
@@ -267,6 +298,17 @@ program
|
|
|
267
298
|
.option('-d, --dir <path>', 'Project directory')
|
|
268
299
|
.action(historyCommand);
|
|
269
300
|
|
|
301
|
+
program
|
|
302
|
+
.command('events')
|
|
303
|
+
.description('Show repo-local run lifecycle events')
|
|
304
|
+
.option('-f, --follow', 'Stream events as they occur')
|
|
305
|
+
.option('-t, --type <type>', 'Filter by event type (comma-separated)')
|
|
306
|
+
.option('--since <timestamp>', 'Show events after ISO-8601 timestamp')
|
|
307
|
+
.option('-j, --json', 'Output raw JSONL')
|
|
308
|
+
.option('-l, --limit <n>', 'Max events to show (default: 50, 0 = all)')
|
|
309
|
+
.option('-d, --dir <path>', 'Project directory')
|
|
310
|
+
.action(eventsCommand);
|
|
311
|
+
|
|
270
312
|
program
|
|
271
313
|
.command('validate')
|
|
272
314
|
.description('Validate project protocol artifacts')
|
package/package.json
CHANGED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentxchain events — repo-local run event stream reader.
|
|
3
|
+
*
|
|
4
|
+
* Reads and optionally follows the `.agentxchain/events.jsonl` log,
|
|
5
|
+
* giving operators structured visibility into governed run lifecycle
|
|
6
|
+
* without requiring webhooks or a dashboard.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
import { existsSync, watchFile, unwatchFile } from 'fs';
|
|
11
|
+
import { readFileSync } from 'fs';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { readRunEvents, RUN_EVENTS_PATH, VALID_RUN_EVENTS } from '../lib/run-events.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} opts
|
|
17
|
+
* @param {boolean} [opts.follow] - Stream events as they arrive
|
|
18
|
+
* @param {string} [opts.type] - Comma-separated event types
|
|
19
|
+
* @param {string} [opts.since] - ISO-8601 timestamp filter
|
|
20
|
+
* @param {boolean} [opts.json] - Raw JSONL output
|
|
21
|
+
* @param {number} [opts.limit] - Max events to show (default 50)
|
|
22
|
+
* @param {string} [opts.dir] - Project directory
|
|
23
|
+
*/
|
|
24
|
+
export async function eventsCommand(opts) {
|
|
25
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
26
|
+
if (!root) {
|
|
27
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const limit = opts.limit != null ? parseInt(opts.limit, 10) : 50;
|
|
32
|
+
const events = readRunEvents(root, {
|
|
33
|
+
type: opts.type,
|
|
34
|
+
since: opts.since,
|
|
35
|
+
limit: limit === 0 ? undefined : limit,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (opts.json) {
|
|
39
|
+
for (const evt of events) {
|
|
40
|
+
console.log(JSON.stringify(evt));
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
if (events.length === 0 && !opts.follow) {
|
|
44
|
+
console.log(chalk.dim('No events found.'));
|
|
45
|
+
if (opts.type) console.log(chalk.dim(` (filtered by type: ${opts.type})`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const evt of events) {
|
|
49
|
+
printEvent(evt);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (opts.follow) {
|
|
54
|
+
return followEvents(root, opts);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printEvent(evt) {
|
|
59
|
+
const ts = evt.timestamp ? new Date(evt.timestamp).toLocaleTimeString() : '—';
|
|
60
|
+
const type = colorEventType(evt.event_type);
|
|
61
|
+
const runId = evt.run_id ? evt.run_id.slice(0, 12) : '—';
|
|
62
|
+
const phase = evt.phase || '—';
|
|
63
|
+
const turnInfo = evt.turn?.role_id ? ` [${evt.turn.role_id}]` : '';
|
|
64
|
+
console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function colorEventType(type) {
|
|
68
|
+
const colors = {
|
|
69
|
+
run_started: chalk.green,
|
|
70
|
+
run_completed: chalk.green.bold,
|
|
71
|
+
run_blocked: chalk.red,
|
|
72
|
+
turn_dispatched: chalk.blue,
|
|
73
|
+
turn_accepted: chalk.green,
|
|
74
|
+
turn_rejected: chalk.yellow,
|
|
75
|
+
phase_entered: chalk.magenta,
|
|
76
|
+
escalation_raised: chalk.red.bold,
|
|
77
|
+
escalation_resolved: chalk.green,
|
|
78
|
+
gate_pending: chalk.yellow,
|
|
79
|
+
gate_approved: chalk.green,
|
|
80
|
+
};
|
|
81
|
+
const colorFn = colors[type] || chalk.white;
|
|
82
|
+
return colorFn(pad(type, 22));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pad(str, len) {
|
|
86
|
+
return (str || '').padEnd(len);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function followEvents(root, opts) {
|
|
90
|
+
const filePath = resolve(root, RUN_EVENTS_PATH);
|
|
91
|
+
let lastSize = 0;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
if (existsSync(filePath)) {
|
|
95
|
+
lastSize = readFileSync(filePath).length;
|
|
96
|
+
}
|
|
97
|
+
} catch {}
|
|
98
|
+
|
|
99
|
+
console.log(chalk.dim('Watching for events... (Ctrl+C to stop)'));
|
|
100
|
+
|
|
101
|
+
return new Promise(() => {
|
|
102
|
+
const checkForNewEvents = () => {
|
|
103
|
+
try {
|
|
104
|
+
if (!existsSync(filePath)) return;
|
|
105
|
+
const content = readFileSync(filePath, 'utf8');
|
|
106
|
+
if (content.length <= lastSize) return;
|
|
107
|
+
|
|
108
|
+
const newContent = content.slice(lastSize);
|
|
109
|
+
lastSize = content.length;
|
|
110
|
+
|
|
111
|
+
const lines = newContent.split('\n').filter(Boolean);
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
try {
|
|
114
|
+
const evt = JSON.parse(line);
|
|
115
|
+
if (opts.type) {
|
|
116
|
+
const types = new Set(opts.type.split(',').map(t => t.trim()));
|
|
117
|
+
if (!types.has(evt.event_type)) continue;
|
|
118
|
+
}
|
|
119
|
+
if (opts.json) {
|
|
120
|
+
console.log(JSON.stringify(evt));
|
|
121
|
+
} else {
|
|
122
|
+
printEvent(evt);
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
} catch {}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
watchFile(filePath, { interval: 200 }, checkForNewEvents);
|
|
130
|
+
|
|
131
|
+
process.on('SIGINT', () => {
|
|
132
|
+
unwatchFile(filePath, checkForNewEvents);
|
|
133
|
+
process.exit(0);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Walk up to find the nearest directory containing agentxchain.json.
|
|
140
|
+
*/
|
|
141
|
+
function findProjectRoot(start) {
|
|
142
|
+
let dir = resolve(start);
|
|
143
|
+
while (true) {
|
|
144
|
+
if (existsSync(resolve(dir, 'agentxchain.json'))) return dir;
|
|
145
|
+
if (existsSync(resolve(dir, '.agentxchain', 'state.json'))) return dir;
|
|
146
|
+
const parent = resolve(dir, '..');
|
|
147
|
+
if (parent === dir) return null;
|
|
148
|
+
dir = parent;
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/commands/run.js
CHANGED
|
@@ -49,12 +49,18 @@ export async function runCommand(opts) {
|
|
|
49
49
|
process.exit(1);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const execution = await executeGovernedRun(context, opts);
|
|
53
|
+
process.exit(execution.exitCode);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function executeGovernedRun(context, opts = {}) {
|
|
52
57
|
const { root, config, rawConfig } = context;
|
|
58
|
+
const log = opts.log || console.log;
|
|
53
59
|
|
|
54
60
|
if (config.protocol_mode !== 'governed') {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
log(chalk.red('The run command is only available for governed projects.'));
|
|
62
|
+
log(chalk.dim('Legacy projects use: agentxchain start'));
|
|
63
|
+
return { exitCode: 1, result: null };
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
// ── Provenance flag validation ──────────────────────────────────────────
|
|
@@ -62,17 +68,21 @@ export async function runCommand(opts) {
|
|
|
62
68
|
const recoverFrom = opts.recoverFrom;
|
|
63
69
|
|
|
64
70
|
if (continueFrom && recoverFrom) {
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
log(chalk.red('Cannot specify both --continue-from and --recover-from'));
|
|
72
|
+
return { exitCode: 1, result: null };
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
let provenance =
|
|
75
|
+
let provenance = opts.provenance;
|
|
70
76
|
if (continueFrom || recoverFrom) {
|
|
77
|
+
if (provenance) {
|
|
78
|
+
log(chalk.red('Cannot combine internal provenance overrides with --continue-from or --recover-from'));
|
|
79
|
+
return { exitCode: 1, result: null };
|
|
80
|
+
}
|
|
71
81
|
const parentId = continueFrom || recoverFrom;
|
|
72
82
|
const validation = validateParentRun(root, parentId);
|
|
73
83
|
if (!validation.ok) {
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
log(chalk.red(validation.error));
|
|
85
|
+
return { exitCode: 1, result: null };
|
|
76
86
|
}
|
|
77
87
|
provenance = {
|
|
78
88
|
trigger: continueFrom ? 'continuation' : 'recovery',
|
|
@@ -89,21 +99,36 @@ export async function runCommand(opts) {
|
|
|
89
99
|
: null;
|
|
90
100
|
|
|
91
101
|
if (overrideResolution?.error) {
|
|
92
|
-
|
|
102
|
+
log(chalk.red(overrideResolution.error));
|
|
93
103
|
if (overrideResolution.availableRoles.length) {
|
|
94
|
-
|
|
104
|
+
log(chalk.dim(`Available roles: ${overrideResolution.availableRoles.join(', ')}`));
|
|
105
|
+
}
|
|
106
|
+
return { exitCode: 1, result: null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (opts.requireFreshStart) {
|
|
110
|
+
const state = loadProjectState(root, config);
|
|
111
|
+
const allowedStatuses = new Set(opts.allowedFreshStatuses || ['idle', 'completed']);
|
|
112
|
+
const currentStatus = state?.status || 'missing';
|
|
113
|
+
if (currentStatus !== 'missing' && !allowedStatuses.has(currentStatus)) {
|
|
114
|
+
return {
|
|
115
|
+
exitCode: 0,
|
|
116
|
+
skipped: true,
|
|
117
|
+
skipReason: `state_${currentStatus}`,
|
|
118
|
+
state,
|
|
119
|
+
result: null,
|
|
120
|
+
};
|
|
95
121
|
}
|
|
96
|
-
process.exit(1);
|
|
97
122
|
}
|
|
98
123
|
|
|
99
124
|
// ── Dry run ───────────────────────────────────────────────────────────────
|
|
100
125
|
if (opts.dryRun) {
|
|
101
126
|
const dryRunState = loadProjectState(root, config);
|
|
102
127
|
const roleId = overrideResolution?.roleId || resolveRole(null, dryRunState, config);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
log(chalk.cyan('Dry run — no execution'));
|
|
129
|
+
log(` First role: ${roleId || chalk.dim('(unresolved)')}`);
|
|
130
|
+
log(` Max turns: ${maxTurns}`);
|
|
131
|
+
log(` Gate mode: ${autoApprove ? 'auto-approve' : 'interactive'}`);
|
|
107
132
|
const roleIds = Object.keys(config.roles || {});
|
|
108
133
|
for (const rid of roleIds) {
|
|
109
134
|
const role = config.roles[rid];
|
|
@@ -111,9 +136,9 @@ export async function runCommand(opts) {
|
|
|
111
136
|
const rt = config.runtimes?.[rtId];
|
|
112
137
|
const rtType = rt?.type || role.runtime_class || 'manual';
|
|
113
138
|
const supported = rtType !== 'manual';
|
|
114
|
-
|
|
139
|
+
log(` ${supported ? chalk.green('✓') : chalk.red('✗')} ${rid} → ${rtType}${supported ? '' : ' (not supported in run mode)'}`);
|
|
115
140
|
}
|
|
116
|
-
|
|
141
|
+
return { exitCode: 0, result: null };
|
|
117
142
|
}
|
|
118
143
|
|
|
119
144
|
// ── SIGINT handling ─────────────────────────────────────────────────────
|
|
@@ -128,13 +153,13 @@ export async function runCommand(opts) {
|
|
|
128
153
|
}
|
|
129
154
|
aborted = true;
|
|
130
155
|
controller.abort();
|
|
131
|
-
|
|
156
|
+
log(chalk.yellow('\nSIGINT received — finishing current turn, then stopping.'));
|
|
132
157
|
});
|
|
133
158
|
|
|
134
159
|
// ── Run header ──────────────────────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
log(chalk.cyan.bold('agentxchain run'));
|
|
161
|
+
log(chalk.dim(` Max turns: ${maxTurns} Gate mode: ${autoApprove ? 'auto-approve' : 'interactive'}`));
|
|
162
|
+
log('');
|
|
138
163
|
|
|
139
164
|
// ── Track first-call for --role override ────────────────────────────────
|
|
140
165
|
let firstSelectRole = true;
|
|
@@ -165,7 +190,7 @@ export async function runCommand(opts) {
|
|
|
165
190
|
|
|
166
191
|
// Manual adapter is not supported in run mode
|
|
167
192
|
if (runtimeType === 'manual') {
|
|
168
|
-
|
|
193
|
+
log(chalk.yellow(`Skipping manual role "${roleId}" — use agentxchain step for manual dispatch.`));
|
|
169
194
|
return { accept: false, reason: 'manual adapter is not supported in run mode — use agentxchain step' };
|
|
170
195
|
}
|
|
171
196
|
|
|
@@ -204,7 +229,7 @@ export async function runCommand(opts) {
|
|
|
204
229
|
// ── Route to adapter ──────────────────────────────────────────────
|
|
205
230
|
const adapterOpts = {
|
|
206
231
|
signal: controller.signal,
|
|
207
|
-
onStatus: (msg) =>
|
|
232
|
+
onStatus: (msg) => log(chalk.dim(` ${msg}`)),
|
|
208
233
|
verifyManifest: true,
|
|
209
234
|
};
|
|
210
235
|
|
|
@@ -216,18 +241,18 @@ export async function runCommand(opts) {
|
|
|
216
241
|
let adapterResult;
|
|
217
242
|
|
|
218
243
|
if (runtimeType === 'api_proxy') {
|
|
219
|
-
|
|
244
|
+
log(chalk.dim(` Dispatching to API proxy: ${runtime?.provider || '?'} / ${runtime?.model || '?'}`));
|
|
220
245
|
adapterResult = await dispatchApiProxy(projectRoot, state, cfg, adapterOpts);
|
|
221
246
|
} else if (runtimeType === 'mcp') {
|
|
222
247
|
const transport = resolveMcpTransport(runtime);
|
|
223
|
-
|
|
248
|
+
log(chalk.dim(` Dispatching to MCP ${transport}: ${describeMcpRuntimeTarget(runtime)}`));
|
|
224
249
|
adapterResult = await dispatchMcp(projectRoot, state, cfg, adapterOpts);
|
|
225
250
|
} else if (runtimeType === 'local_cli') {
|
|
226
251
|
const transport = runtime ? resolvePromptTransport(runtime) : 'dispatch_bundle_only';
|
|
227
|
-
|
|
252
|
+
log(chalk.dim(` Dispatching to local CLI: ${runtime?.command || '(default)'} transport: ${transport}`));
|
|
228
253
|
adapterResult = await dispatchLocalCli(projectRoot, state, cfg, adapterOpts);
|
|
229
254
|
} else if (runtimeType === 'remote_agent') {
|
|
230
|
-
|
|
255
|
+
log(chalk.dim(` Dispatching to remote agent: ${describeRemoteAgentTarget(runtime)}`));
|
|
231
256
|
adapterResult = await dispatchRemoteAgent(projectRoot, state, cfg, adapterOpts);
|
|
232
257
|
} else {
|
|
233
258
|
return { accept: false, reason: `unknown runtime type "${runtimeType}"` };
|
|
@@ -286,14 +311,14 @@ export async function runCommand(opts) {
|
|
|
286
311
|
|
|
287
312
|
async approveGate(gateType, state) {
|
|
288
313
|
if (autoApprove) {
|
|
289
|
-
|
|
314
|
+
log(chalk.yellow(` Auto-approved ${gateType} gate`));
|
|
290
315
|
return true;
|
|
291
316
|
}
|
|
292
317
|
|
|
293
318
|
// Non-TTY → fail-closed
|
|
294
319
|
if (!process.stdin.isTTY) {
|
|
295
|
-
|
|
296
|
-
|
|
320
|
+
log(chalk.yellow(` Gate pause: ${gateType} — stdin is not a TTY, failing closed.`));
|
|
321
|
+
log(chalk.dim(' Use --auto-approve for non-interactive mode.'));
|
|
297
322
|
return false;
|
|
298
323
|
}
|
|
299
324
|
|
|
@@ -301,9 +326,9 @@ export async function runCommand(opts) {
|
|
|
301
326
|
? state.pending_phase_transition?.target || '(next phase)'
|
|
302
327
|
: 'run completion';
|
|
303
328
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
329
|
+
log('');
|
|
330
|
+
log(chalk.yellow.bold(`Gate pause: ${gateType}`));
|
|
331
|
+
log(chalk.dim(` Phase: ${state.phase} → ${target}`));
|
|
307
332
|
|
|
308
333
|
const answer = await promptUser(` Approve? [y/N] `);
|
|
309
334
|
const approved = /^y(es)?$/i.test(answer.trim());
|
|
@@ -313,31 +338,31 @@ export async function runCommand(opts) {
|
|
|
313
338
|
onEvent(event) {
|
|
314
339
|
switch (event.type) {
|
|
315
340
|
case 'turn_assigned':
|
|
316
|
-
|
|
341
|
+
log(chalk.cyan(`Turn assigned: ${event.turn?.turn_id} → ${event.role}`));
|
|
317
342
|
break;
|
|
318
343
|
case 'turn_accepted':
|
|
319
|
-
|
|
344
|
+
log(chalk.green(`Turn accepted: ${event.turn?.turn_id}`));
|
|
320
345
|
break;
|
|
321
346
|
case 'turn_rejected':
|
|
322
|
-
|
|
347
|
+
log(chalk.yellow(`Turn rejected: ${event.turn?.turn_id} — ${event.reason || 'no reason'}`));
|
|
323
348
|
break;
|
|
324
349
|
case 'gate_paused':
|
|
325
|
-
|
|
350
|
+
log(chalk.yellow(`Gate paused: ${event.gateType}`));
|
|
326
351
|
break;
|
|
327
352
|
case 'gate_approved':
|
|
328
|
-
|
|
353
|
+
log(chalk.green(`Gate approved: ${event.gateType}`));
|
|
329
354
|
break;
|
|
330
355
|
case 'gate_held':
|
|
331
|
-
|
|
356
|
+
log(chalk.yellow(`Gate held: ${event.gateType} — run paused`));
|
|
332
357
|
break;
|
|
333
358
|
case 'blocked':
|
|
334
|
-
|
|
359
|
+
log(chalk.red(`Run blocked`));
|
|
335
360
|
break;
|
|
336
361
|
case 'completed':
|
|
337
|
-
|
|
362
|
+
log(chalk.green.bold('Run completed'));
|
|
338
363
|
break;
|
|
339
364
|
case 'caller_stopped':
|
|
340
|
-
|
|
365
|
+
log(chalk.yellow('Run stopped by caller'));
|
|
341
366
|
break;
|
|
342
367
|
}
|
|
343
368
|
},
|
|
@@ -347,38 +372,38 @@ export async function runCommand(opts) {
|
|
|
347
372
|
const runLoopOpts = {
|
|
348
373
|
maxTurns,
|
|
349
374
|
startNewRunFromCompleted: true,
|
|
350
|
-
startNewRunFromBlocked: Boolean(provenance),
|
|
375
|
+
startNewRunFromBlocked: opts.allowBlockedRestart ?? Boolean(provenance),
|
|
351
376
|
};
|
|
352
377
|
if (provenance) runLoopOpts.provenance = provenance;
|
|
353
378
|
const result = await runLoop(root, config, callbacks, runLoopOpts);
|
|
354
379
|
|
|
355
380
|
// ── Summary ─────────────────────────────────────────────────────────────
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
381
|
+
log('');
|
|
382
|
+
log(chalk.dim('─── Run Summary ───'));
|
|
383
|
+
log(` Status: ${result.ok ? chalk.green('completed') : chalk.yellow(result.stop_reason)}`);
|
|
384
|
+
log(` Turns: ${result.turns_executed}`);
|
|
385
|
+
log(` Gates: ${result.gates_approved} approved`);
|
|
386
|
+
log(` Errors: ${result.errors.length ? chalk.red(result.errors.length) : 'none'}`);
|
|
362
387
|
|
|
363
388
|
if (result.errors.length) {
|
|
364
389
|
for (const err of result.errors) {
|
|
365
|
-
|
|
390
|
+
log(chalk.red(` ${err}`));
|
|
366
391
|
}
|
|
367
392
|
}
|
|
368
393
|
|
|
369
394
|
if (qaMissingCredentialsFallback) {
|
|
370
|
-
printManualQaFallback();
|
|
395
|
+
printManualQaFallback(log);
|
|
371
396
|
}
|
|
372
397
|
|
|
373
398
|
// Recovery guidance for blocked/rejected states
|
|
374
399
|
if (result.state && (result.stop_reason === 'blocked' || result.stop_reason === 'reject_exhausted' || result.stop_reason === 'dispatch_error')) {
|
|
375
400
|
const recovery = deriveRecoveryDescriptor(result.state);
|
|
376
401
|
if (recovery) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
402
|
+
log('');
|
|
403
|
+
log(chalk.yellow(` Recovery: ${recovery.typed_reason}`));
|
|
404
|
+
log(chalk.dim(` Action: ${recovery.recovery_action}`));
|
|
380
405
|
if (recovery.detail) {
|
|
381
|
-
|
|
406
|
+
log(chalk.dim(` Detail: ${recovery.detail}`));
|
|
382
407
|
}
|
|
383
408
|
}
|
|
384
409
|
}
|
|
@@ -399,22 +424,25 @@ export async function runCommand(opts) {
|
|
|
399
424
|
const reportPath = join(reportsDir, `report-${runId}.md`);
|
|
400
425
|
writeFileSync(reportPath, formatGovernanceReportMarkdown(reportResult.report));
|
|
401
426
|
|
|
402
|
-
|
|
403
|
-
|
|
427
|
+
log('');
|
|
428
|
+
log(chalk.dim(` Governance report: .agentxchain/reports/report-${runId}.md`));
|
|
404
429
|
} else {
|
|
405
|
-
|
|
430
|
+
log(chalk.dim(` Governance report skipped: ${exportResult.error}`));
|
|
406
431
|
}
|
|
407
432
|
} catch (err) {
|
|
408
|
-
|
|
433
|
+
log(chalk.dim(` Governance report failed: ${err.message}`));
|
|
409
434
|
}
|
|
410
435
|
}
|
|
411
436
|
|
|
412
437
|
// ── Exit code ───────────────────────────────────────────────────────────
|
|
413
438
|
const successReasons = new Set(['completed', 'gate_held', 'caller_stopped', 'max_turns_reached']);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
439
|
+
return {
|
|
440
|
+
exitCode: result.ok || successReasons.has(result.stop_reason) ? 0 : 1,
|
|
441
|
+
result,
|
|
442
|
+
skipped: false,
|
|
443
|
+
skipReason: null,
|
|
444
|
+
provenance: provenance || null,
|
|
445
|
+
};
|
|
418
446
|
}
|
|
419
447
|
|
|
420
448
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
@@ -452,10 +480,10 @@ function shouldPrintManualQaFallback({ roleId, runtimeId, classified, rawConfig
|
|
|
452
480
|
&& rawConfig?.runtimes?.['manual-qa']?.type === 'manual';
|
|
453
481
|
}
|
|
454
482
|
|
|
455
|
-
function printManualQaFallback() {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
483
|
+
function printManualQaFallback(log = console.log) {
|
|
484
|
+
log('');
|
|
485
|
+
log(chalk.dim(' No-key QA fallback:'));
|
|
486
|
+
log(chalk.dim(' - Edit agentxchain.json and change roles.qa.runtime from "api-qa" to "manual-qa"'));
|
|
487
|
+
log(chalk.dim(' - Then recover the retained QA turn with: agentxchain step --resume'));
|
|
488
|
+
log(chalk.dim(' - Guide: https://agentxchain.dev/docs/getting-started'));
|
|
461
489
|
}
|