agentxchain 2.128.0 → 2.129.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 +28 -4
- package/package.json +1 -1
- package/scripts/verify-post-publish.sh +55 -5
- package/src/commands/connector.js +17 -2
- package/src/commands/doctor.js +122 -1
- package/src/commands/events.js +7 -1
- package/src/commands/init.js +42 -11
- package/src/commands/inject.js +1 -1
- package/src/commands/mission.js +142 -0
- package/src/commands/reissue-turn.js +122 -0
- package/src/commands/reject-turn.js +24 -4
- package/src/commands/restart.js +9 -2
- package/src/commands/resume.js +20 -9
- package/src/commands/status.js +46 -4
- package/src/commands/step.js +49 -10
- package/src/commands/validate.js +78 -20
- package/src/lib/cli-version.js +106 -0
- package/src/lib/connector-probe.js +146 -5
- package/src/lib/continuous-run.js +14 -86
- package/src/lib/dispatch-bundle.js +39 -0
- package/src/lib/governed-state.js +474 -10
- package/src/lib/governed-templates.js +1 -0
- package/src/lib/intake.js +221 -77
- package/src/lib/missions.js +56 -4
- package/src/lib/normalized-config.js +50 -15
- package/src/lib/repo-observer.js +7 -2
- package/src/lib/run-events.js +4 -0
- package/src/lib/run-loop.js +5 -0
- package/src/lib/runner-interface.js +2 -0
- package/src/lib/session-checkpoint.js +18 -2
- package/src/templates/governed/full-local-cli.json +71 -0
package/src/commands/mission.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolve } from 'path';
|
|
|
4
4
|
import { findProjectRoot, loadProjectContext } from '../lib/config.js';
|
|
5
5
|
import {
|
|
6
6
|
attachChainToMission,
|
|
7
|
+
bindCoordinatorToMission,
|
|
7
8
|
buildMissionListSummary,
|
|
8
9
|
buildMissionSnapshot,
|
|
9
10
|
createMission,
|
|
@@ -12,6 +13,8 @@ import {
|
|
|
12
13
|
loadMissionArtifact,
|
|
13
14
|
loadMissionSnapshot,
|
|
14
15
|
} from '../lib/missions.js';
|
|
16
|
+
import { loadCoordinatorConfig } from '../lib/coordinator-config.js';
|
|
17
|
+
import { initializeCoordinatorRun } from '../lib/coordinator-state.js';
|
|
15
18
|
import {
|
|
16
19
|
approvePlanArtifact,
|
|
17
20
|
createPlanArtifact,
|
|
@@ -48,6 +51,22 @@ export async function missionStartCommand(opts) {
|
|
|
48
51
|
process.exit(1);
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
// Multi-repo: validate coordinator config before creating mission (fail-fast)
|
|
55
|
+
let coordinatorConfig = null;
|
|
56
|
+
let coordinatorWorkspacePath = null;
|
|
57
|
+
if (opts.multi) {
|
|
58
|
+
coordinatorWorkspacePath = resolve(opts.coordinatorWorkspace || opts.coordinatorConfig || root);
|
|
59
|
+
coordinatorConfig = loadCoordinatorConfig(coordinatorWorkspacePath);
|
|
60
|
+
if (!coordinatorConfig.ok) {
|
|
61
|
+
console.error(chalk.red('Coordinator config validation failed:'));
|
|
62
|
+
console.error(chalk.dim(` Expected agentxchain-multi.json at: ${coordinatorWorkspacePath}`));
|
|
63
|
+
for (const err of coordinatorConfig.errors || []) {
|
|
64
|
+
console.error(chalk.red(` ${err}`));
|
|
65
|
+
}
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
const result = createMission(root, {
|
|
52
71
|
missionId: opts.id,
|
|
53
72
|
title,
|
|
@@ -58,6 +77,37 @@ export async function missionStartCommand(opts) {
|
|
|
58
77
|
process.exit(1);
|
|
59
78
|
}
|
|
60
79
|
|
|
80
|
+
// Multi-repo: initialize coordinator and bind to mission
|
|
81
|
+
if (opts.multi && coordinatorConfig) {
|
|
82
|
+
const initResult = initializeCoordinatorRun(coordinatorWorkspacePath, coordinatorConfig.config);
|
|
83
|
+
if (!initResult.ok) {
|
|
84
|
+
// Atomic rollback: delete the mission artifact
|
|
85
|
+
const { getMissionsDir } = await import('../lib/missions.js');
|
|
86
|
+
const { unlinkSync, existsSync: fileExists } = await import('fs');
|
|
87
|
+
const { join: joinPath } = await import('path');
|
|
88
|
+
const missionFile = joinPath(getMissionsDir(root), `${result.mission.mission_id}.json`);
|
|
89
|
+
if (fileExists(missionFile)) {
|
|
90
|
+
try { unlinkSync(missionFile); } catch { /* best effort */ }
|
|
91
|
+
}
|
|
92
|
+
console.error(chalk.red('Coordinator initialization failed:'));
|
|
93
|
+
for (const err of initResult.errors || []) {
|
|
94
|
+
console.error(chalk.red(` ${err}`));
|
|
95
|
+
}
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const bindResult = bindCoordinatorToMission(root, result.mission.mission_id, {
|
|
100
|
+
super_run_id: initResult.super_run_id,
|
|
101
|
+
config_path: opts.coordinatorConfig || null,
|
|
102
|
+
workspace_path: coordinatorWorkspacePath,
|
|
103
|
+
});
|
|
104
|
+
if (!bindResult.ok) {
|
|
105
|
+
console.error(chalk.yellow(`Mission created but coordinator binding failed: ${bindResult.error}`));
|
|
106
|
+
} else {
|
|
107
|
+
result.mission = bindResult.mission;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
61
111
|
const snapshot = buildMissionSnapshot(root, result.mission);
|
|
62
112
|
if (opts.plan) {
|
|
63
113
|
try {
|
|
@@ -1288,6 +1338,54 @@ function renderMissionPlanError(error) {
|
|
|
1288
1338
|
}
|
|
1289
1339
|
}
|
|
1290
1340
|
|
|
1341
|
+
// ── Mission Bind-Coordinator Command ───────────────────────────────────────
|
|
1342
|
+
|
|
1343
|
+
export async function missionBindCoordinatorCommand(missionId, opts) {
|
|
1344
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
1345
|
+
if (!root) {
|
|
1346
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
1347
|
+
process.exit(1);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
const superRunId = String(opts.superRunId || '').trim();
|
|
1351
|
+
const configPath = String(opts.coordinatorConfig || '').trim();
|
|
1352
|
+
if (!superRunId) {
|
|
1353
|
+
console.error(chalk.red('--super-run-id is required.'));
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const mission = missionId
|
|
1358
|
+
? loadMissionArtifact(root, missionId)
|
|
1359
|
+
: loadLatestMissionArtifact(root);
|
|
1360
|
+
if (!mission) {
|
|
1361
|
+
console.error(chalk.red(missionId ? `Mission not found: ${missionId}` : 'No mission found.'));
|
|
1362
|
+
process.exit(1);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const workspacePath = resolve(opts.coordinatorWorkspace || root);
|
|
1366
|
+
const result = bindCoordinatorToMission(root, mission.mission_id, {
|
|
1367
|
+
super_run_id: superRunId,
|
|
1368
|
+
config_path: configPath || null,
|
|
1369
|
+
workspace_path: workspacePath,
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
if (!result.ok) {
|
|
1373
|
+
console.error(chalk.red(result.error));
|
|
1374
|
+
process.exit(1);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const snapshot = buildMissionSnapshot(root, result.mission);
|
|
1378
|
+
if (opts.json) {
|
|
1379
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
console.log(chalk.green(`Bound coordinator ${superRunId} to ${snapshot.mission_id}`));
|
|
1384
|
+
renderMissionSnapshot(snapshot);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// ── Rendering ──────────────────────────────────────────────────────────────
|
|
1388
|
+
|
|
1291
1389
|
function renderMissionSnapshot(snapshot) {
|
|
1292
1390
|
const latestPlan = snapshot.latest_plan || null;
|
|
1293
1391
|
|
|
@@ -1309,6 +1407,36 @@ function renderMissionSnapshot(snapshot) {
|
|
|
1309
1407
|
console.log(` Missing chains: ${snapshot.missing_chain_ids.join(', ')}`);
|
|
1310
1408
|
}
|
|
1311
1409
|
|
|
1410
|
+
// Coordinator (multi-repo) section
|
|
1411
|
+
if (snapshot.coordinator_status) {
|
|
1412
|
+
const cs = snapshot.coordinator_status;
|
|
1413
|
+
console.log('');
|
|
1414
|
+
console.log(chalk.bold(' Coordinator (multi-repo):'));
|
|
1415
|
+
if (cs.unreachable) {
|
|
1416
|
+
console.log(` Super Run: ${cs.super_run_id || '—'}`);
|
|
1417
|
+
console.log(` Status: ${chalk.red('unreachable')}`);
|
|
1418
|
+
} else {
|
|
1419
|
+
console.log(` Super Run: ${cs.super_run_id || '—'}`);
|
|
1420
|
+
console.log(` Status: ${formatCoordinatorStatus(cs.status)}`);
|
|
1421
|
+
console.log(` Phase: ${cs.phase || '—'}`);
|
|
1422
|
+
if (cs.repo_runs && Object.keys(cs.repo_runs).length > 0) {
|
|
1423
|
+
console.log(' Repos:');
|
|
1424
|
+
for (const [repoId, repo] of Object.entries(cs.repo_runs)) {
|
|
1425
|
+
console.log(` ${pad(repoId, 16)} ${pad(repo.status || '—', 14)} ${pad(repo.phase || '—', 18)} ${repo.run_id || '—'}`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (cs.pending_barriers && cs.pending_barriers.length > 0) {
|
|
1429
|
+
console.log(' Barriers:');
|
|
1430
|
+
for (const barrier of cs.pending_barriers) {
|
|
1431
|
+
console.log(` ${pad(barrier.id, 28)} ${pad(barrier.type || '—', 24)} ${barrier.status || '—'}`);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (cs.blocked_reason) {
|
|
1435
|
+
console.log(` Blocked: ${chalk.yellow(cs.blocked_reason)}`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1312
1440
|
if (latestPlan) {
|
|
1313
1441
|
console.log('');
|
|
1314
1442
|
console.log(chalk.bold(' Latest plan:'));
|
|
@@ -1374,6 +1502,20 @@ function formatMissionStatus(status) {
|
|
|
1374
1502
|
}
|
|
1375
1503
|
}
|
|
1376
1504
|
|
|
1505
|
+
function formatCoordinatorStatus(status) {
|
|
1506
|
+
if (!status) return '—';
|
|
1507
|
+
switch (status) {
|
|
1508
|
+
case 'active':
|
|
1509
|
+
return chalk.green('active');
|
|
1510
|
+
case 'blocked':
|
|
1511
|
+
return chalk.red('blocked');
|
|
1512
|
+
case 'completed':
|
|
1513
|
+
return chalk.cyan('completed');
|
|
1514
|
+
default:
|
|
1515
|
+
return status;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1377
1519
|
function formatTimestamp(value) {
|
|
1378
1520
|
if (!value) return '—';
|
|
1379
1521
|
try {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reissue-turn command — unified turn invalidation + reissue against current state.
|
|
3
|
+
*
|
|
4
|
+
* Covers all drift recovery scenarios:
|
|
5
|
+
* - Baseline drift (HEAD changed after dispatch)
|
|
6
|
+
* - Runtime drift (agentxchain.json rebinding after dispatch)
|
|
7
|
+
* - Authority drift (write_authority changed on assigned role)
|
|
8
|
+
* - Operator-initiated (explicit redo from current state)
|
|
9
|
+
*
|
|
10
|
+
* BUG-7 fix: single command, multiple trigger reasons.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { readFileSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
17
|
+
import {
|
|
18
|
+
getActiveTurns,
|
|
19
|
+
getActiveTurn,
|
|
20
|
+
reissueTurn,
|
|
21
|
+
} from '../lib/governed-state.js';
|
|
22
|
+
import { writeDispatchBundle } from '../lib/dispatch-bundle.js';
|
|
23
|
+
|
|
24
|
+
export async function reissueTurnCommand(opts) {
|
|
25
|
+
const context = loadProjectContext();
|
|
26
|
+
if (!context) {
|
|
27
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { root, config } = context;
|
|
32
|
+
|
|
33
|
+
if (config.protocol_mode !== 'governed') {
|
|
34
|
+
console.log(chalk.red('The reissue-turn command is only available for governed projects.'));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let state = loadProjectState(root, config);
|
|
39
|
+
if (!state) {
|
|
40
|
+
console.log(chalk.red('No governed state.json found.'));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Resolve target turn
|
|
45
|
+
const activeTurns = getActiveTurns(state);
|
|
46
|
+
const activeCount = Object.keys(activeTurns).length;
|
|
47
|
+
|
|
48
|
+
if (activeCount === 0) {
|
|
49
|
+
console.log(chalk.red('No active turns to reissue.'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let targetTurn;
|
|
54
|
+
if (opts.turn) {
|
|
55
|
+
targetTurn = activeTurns[opts.turn];
|
|
56
|
+
if (!targetTurn) {
|
|
57
|
+
console.log(chalk.red(`No active turn found for --turn ${opts.turn}`));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
} else if (activeCount === 1) {
|
|
61
|
+
targetTurn = Object.values(activeTurns)[0];
|
|
62
|
+
} else {
|
|
63
|
+
console.log(chalk.red('Multiple active turns exist. Use --turn <id> to specify which to reissue.'));
|
|
64
|
+
for (const turn of Object.values(activeTurns)) {
|
|
65
|
+
console.log(` ${chalk.yellow('●')} ${turn.turn_id} — ${chalk.bold(turn.assigned_role)} (${turn.status})`);
|
|
66
|
+
}
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const reason = opts.reason || 'operator-initiated reissue';
|
|
71
|
+
|
|
72
|
+
console.log(chalk.cyan(`Reissuing turn: ${targetTurn.turn_id} (${targetTurn.assigned_role})`));
|
|
73
|
+
console.log(chalk.dim(`Reason: ${reason}`));
|
|
74
|
+
|
|
75
|
+
const result = reissueTurn(root, config, {
|
|
76
|
+
turnId: targetTurn.turn_id,
|
|
77
|
+
reason,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
console.log(chalk.red(`Failed to reissue turn: ${result.error}`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Write dispatch bundle for the reissued turn
|
|
86
|
+
const bundleResult = writeDispatchBundle(root, result.state, config, {
|
|
87
|
+
turnId: result.newTurn.turn_id,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!bundleResult.ok) {
|
|
91
|
+
console.log(chalk.red(`Turn reissued but dispatch bundle failed: ${bundleResult.error}`));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Print summary
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(chalk.green(' Turn Reissued'));
|
|
98
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(` ${chalk.dim('Old turn:')} ${targetTurn.turn_id}`);
|
|
101
|
+
console.log(` ${chalk.dim('New turn:')} ${result.newTurn.turn_id}`);
|
|
102
|
+
console.log(` ${chalk.dim('Role:')} ${result.newTurn.assigned_role}`);
|
|
103
|
+
console.log(` ${chalk.dim('Attempt:')} ${result.newTurn.attempt}`);
|
|
104
|
+
console.log(` ${chalk.dim('Reason:')} ${reason}`);
|
|
105
|
+
|
|
106
|
+
// Show baseline delta
|
|
107
|
+
if (result.baselineDelta) {
|
|
108
|
+
const delta = result.baselineDelta;
|
|
109
|
+
if (delta.head_changed) {
|
|
110
|
+
console.log(` ${chalk.dim('HEAD:')} ${chalk.yellow(delta.old_head?.slice(0, 12) || '?')} → ${chalk.green(delta.new_head?.slice(0, 12) || '?')}`);
|
|
111
|
+
}
|
|
112
|
+
if (delta.runtime_changed) {
|
|
113
|
+
console.log(` ${chalk.dim('Runtime:')} ${chalk.yellow(delta.old_runtime || '?')} → ${chalk.green(delta.new_runtime || '?')}`);
|
|
114
|
+
}
|
|
115
|
+
if (delta.dirty_files_changed) {
|
|
116
|
+
console.log(` ${chalk.dim('Workspace:')} ${delta.added_dirty_files?.length || 0} new dirty file(s), ${delta.removed_dirty_files?.length || 0} resolved`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log(chalk.dim('Run: agentxchain step --resume to dispatch the reissued turn.'));
|
|
122
|
+
}
|
|
@@ -107,11 +107,31 @@ function buildRejectionValidation(root, state, config, opts) {
|
|
|
107
107
|
return resolution;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// BUG-9 fix: --reassign should work for any rejected turn, not just conflicted ones.
|
|
111
|
+
// For drift-induced failures with no conflict_state, redirect to reissue-turn.
|
|
110
112
|
if (opts.reassign && !resolution.turn.conflict_state) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
// Detect if baseline drift exists
|
|
114
|
+
const currentHead = (() => {
|
|
115
|
+
try {
|
|
116
|
+
return require('child_process').execSync('git rev-parse HEAD', {
|
|
117
|
+
cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
|
+
}).trim();
|
|
119
|
+
} catch { return null; }
|
|
120
|
+
})();
|
|
121
|
+
const turnHead = resolution.turn.baseline?.head_ref;
|
|
122
|
+
const hasDrift = currentHead && turnHead && currentHead !== turnHead;
|
|
123
|
+
|
|
124
|
+
if (hasDrift) {
|
|
125
|
+
console.log(chalk.yellow(`Baseline drift detected: HEAD moved from ${turnHead?.slice(0, 12)} to ${currentHead?.slice(0, 12)}.`));
|
|
126
|
+
console.log(chalk.dim(`Use: agentxchain reissue-turn --turn ${resolution.turn.turn_id} --reason "baseline drift"`));
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
error: `--reassign detected baseline drift. Use reissue-turn instead for a clean reissue from current HEAD.`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// No drift, no conflict_state — just do a normal reject + reassign
|
|
134
|
+
// (treat it as a fresh retry with refreshed baseline, which BUG-8 now handles)
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
if (resolution.turn.conflict_state) {
|
package/src/commands/restart.js
CHANGED
|
@@ -265,10 +265,17 @@ export async function restartCommand(opts) {
|
|
|
265
265
|
console.log(chalk.yellow(`Warning: ${activeTurnCount} turn(s) were assigned but never completed: ${turnIds.join(', ')}`));
|
|
266
266
|
console.log(chalk.dim('These turns will be available for the next agent to complete.'));
|
|
267
267
|
|
|
268
|
-
// Fail closed if retained turn + irreconcilable drift
|
|
268
|
+
// Fail closed if retained turn + irreconcilable drift — BUG-10 fix: surface actionable recovery
|
|
269
269
|
if (driftWarnings.length > 0) {
|
|
270
270
|
console.log(chalk.yellow('Active turns exist with repo drift since checkpoint. Reconnecting with warnings.'));
|
|
271
|
-
console.log(
|
|
271
|
+
console.log('');
|
|
272
|
+
console.log(chalk.dim('Recovery options:'));
|
|
273
|
+
for (const turnId of turnIds) {
|
|
274
|
+
const turn = activeTurns[turnId];
|
|
275
|
+
console.log(` ${chalk.cyan(`agentxchain reissue-turn --turn ${turnId} --reason "baseline drift"`)} — reissue ${turn.assigned_role} from current HEAD`);
|
|
276
|
+
}
|
|
277
|
+
console.log(` ${chalk.cyan('agentxchain reject-turn --reason "baseline drift"')} — reject and retry with refreshed baseline`);
|
|
278
|
+
console.log(` ${chalk.dim('Continue as-is if the drift does not affect the retained turns.')}`);
|
|
272
279
|
}
|
|
273
280
|
}
|
|
274
281
|
|
package/src/commands/resume.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
40
40
|
import { runHooks } from '../lib/hook-runner.js';
|
|
41
41
|
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
42
|
+
import { consumeNextApprovedIntent } from '../lib/intake.js';
|
|
42
43
|
|
|
43
44
|
export async function resumeCommand(opts) {
|
|
44
45
|
const context = loadProjectContext();
|
|
@@ -265,17 +266,27 @@ export async function resumeCommand(opts) {
|
|
|
265
266
|
process.exit(1);
|
|
266
267
|
}
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
const shouldBindIntent = opts.intent !== false;
|
|
270
|
+
const consumed = shouldBindIntent ? consumeNextApprovedIntent(root, { role: roleId }) : { ok: false };
|
|
271
|
+
if (consumed.ok) {
|
|
272
|
+
state = loadProjectState(root, config);
|
|
273
|
+
if (!state) {
|
|
274
|
+
console.log(chalk.red('Failed to reload governed state after intake binding.'));
|
|
275
|
+
process.exit(1);
|
|
273
276
|
}
|
|
274
|
-
console.log(chalk.
|
|
275
|
-
|
|
277
|
+
console.log(chalk.green(`Bound approved intent to next turn: ${consumed.intentId}`));
|
|
278
|
+
} else {
|
|
279
|
+
const assignResult = assignGovernedTurn(root, config, roleId);
|
|
280
|
+
if (!assignResult.ok) {
|
|
281
|
+
if (assignResult.error_code?.startsWith('hook_') || assignResult.error_code === 'hook_blocked') {
|
|
282
|
+
printAssignmentHookFailure(assignResult, roleId, config);
|
|
283
|
+
}
|
|
284
|
+
console.log(chalk.red(`Failed to assign turn: ${assignResult.error}`));
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
printAssignmentWarnings(assignResult);
|
|
288
|
+
state = assignResult.state;
|
|
276
289
|
}
|
|
277
|
-
printAssignmentWarnings(assignResult);
|
|
278
|
-
state = assignResult.state;
|
|
279
290
|
|
|
280
291
|
// Write dispatch bundle
|
|
281
292
|
const bundleResult = writeDispatchBundle(root, state, config);
|
package/src/commands/status.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
deriveRecoveryDescriptor,
|
|
9
9
|
deriveRuntimeBlockedGuidance,
|
|
10
10
|
} from '../lib/blocked-state.js';
|
|
11
|
-
import { getActiveTurn, getActiveTurnCount, getActiveTurns } from '../lib/governed-state.js';
|
|
11
|
+
import { getActiveTurn, getActiveTurnCount, getActiveTurns, detectActiveTurnBindingDrift } from '../lib/governed-state.js';
|
|
12
12
|
import { getContinuityStatus } from '../lib/continuity-status.js';
|
|
13
13
|
import { getConnectorHealth } from '../lib/connector-health.js';
|
|
14
14
|
import { readRepoDecisions, summarizeRepoDecisions } from '../lib/repo-decisions.js';
|
|
@@ -21,7 +21,7 @@ import { deriveConflictedTurnResolutionActions } from '../lib/conflict-actions.j
|
|
|
21
21
|
import { summarizeLatestGateActionAttempt } from '../lib/gate-actions.js';
|
|
22
22
|
import { findCurrentHumanEscalation } from '../lib/human-escalations.js';
|
|
23
23
|
import { getDashboardPid, getDashboardSession } from './dashboard.js';
|
|
24
|
-
import { readPreemptionMarker } from '../lib/intake.js';
|
|
24
|
+
import { readPreemptionMarker, findPendingApprovedIntents } from '../lib/intake.js';
|
|
25
25
|
import { readContinuousSession } from '../lib/continuous-run.js';
|
|
26
26
|
import { readAllDispatchProgress } from '../lib/dispatch-progress.js';
|
|
27
27
|
|
|
@@ -133,6 +133,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
133
133
|
const workflowKitArtifacts = deriveWorkflowKitArtifacts(root, config, state);
|
|
134
134
|
const humanEscalation = findCurrentHumanEscalation(root, state);
|
|
135
135
|
const preemptionMarker = readPreemptionMarker(root);
|
|
136
|
+
const pendingIntents = findPendingApprovedIntents(root);
|
|
136
137
|
const continuousSession = readContinuousSession(root);
|
|
137
138
|
const gateActionAttempt = state?.pending_phase_transition
|
|
138
139
|
? summarizeLatestGateActionAttempt(root, 'phase_transition', state.pending_phase_transition.gate)
|
|
@@ -175,10 +176,12 @@ function renderGovernedStatus(context, opts) {
|
|
|
175
176
|
dispatch_progress: dispatchProgress,
|
|
176
177
|
human_escalation: humanEscalation,
|
|
177
178
|
preemption_marker: preemptionMarker,
|
|
179
|
+
pending_intents: pendingIntents,
|
|
178
180
|
continuous_session: continuousSession,
|
|
179
181
|
gate_action_attempt: gateActionAttempt,
|
|
180
182
|
workflow_kit_artifacts: workflowKitArtifacts,
|
|
181
183
|
dashboard_session: dashboardSessionObj,
|
|
184
|
+
binding_drift: detectActiveTurnBindingDrift(state, config),
|
|
182
185
|
}, null, 2));
|
|
183
186
|
return;
|
|
184
187
|
}
|
|
@@ -204,6 +207,21 @@ function renderGovernedStatus(context, opts) {
|
|
|
204
207
|
console.log('');
|
|
205
208
|
}
|
|
206
209
|
|
|
210
|
+
// Pending injected intents (BUG-15)
|
|
211
|
+
if (pendingIntents.length > 0) {
|
|
212
|
+
console.log(chalk.yellow.bold(' 📋 Pending injected intents (will drive next turn):'));
|
|
213
|
+
for (const pi of pendingIntents) {
|
|
214
|
+
const priorityColor = pi.priority === 'p0' ? chalk.red.bold : pi.priority === 'p1' ? chalk.yellow.bold : chalk.dim;
|
|
215
|
+
const charterSnippet = pi.charter
|
|
216
|
+
? (pi.charter.length > 60 ? pi.charter.slice(0, 57) + '...' : pi.charter)
|
|
217
|
+
: '(no charter)';
|
|
218
|
+
console.log(` ${priorityColor(`[${pi.priority}]`)} ${chalk.dim(pi.intent_id)} — ${charterSnippet}`);
|
|
219
|
+
console.log(chalk.dim(` Acceptance: ${pi.acceptance_count} item${pi.acceptance_count !== 1 ? 's' : ''}`));
|
|
220
|
+
}
|
|
221
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
222
|
+
console.log('');
|
|
223
|
+
}
|
|
224
|
+
|
|
207
225
|
// Continuous session banner
|
|
208
226
|
if (continuousSession) {
|
|
209
227
|
console.log(chalk.cyan.bold(' 🔄 Continuous Vision-Driven Session'));
|
|
@@ -272,12 +290,14 @@ function renderGovernedStatus(context, opts) {
|
|
|
272
290
|
if (activeTurnCount > 1) {
|
|
273
291
|
console.log(` ${chalk.dim('Turns:')} ${activeTurnCount} active`);
|
|
274
292
|
for (const turn of Object.values(activeTurns)) {
|
|
275
|
-
const marker = turn.status === 'conflicted'
|
|
293
|
+
const marker = (turn.status === 'conflicted' || turn.status === 'failed_acceptance')
|
|
276
294
|
? chalk.red('✗')
|
|
277
295
|
: chalk.yellow('●');
|
|
278
296
|
const statusLabel = turn.status === 'conflicted'
|
|
279
297
|
? chalk.red('conflicted')
|
|
280
|
-
: turn.status
|
|
298
|
+
: turn.status === 'failed_acceptance'
|
|
299
|
+
? chalk.red('failed_acceptance')
|
|
300
|
+
: turn.status;
|
|
281
301
|
let elapsedTag = '';
|
|
282
302
|
if (turn.started_at) {
|
|
283
303
|
const elMs = Date.now() - new Date(turn.started_at).getTime();
|
|
@@ -318,6 +338,11 @@ function renderGovernedStatus(context, opts) {
|
|
|
318
338
|
console.log(` ${chalk.dim('Resolve:')} ${chalk.cyan(reassignAction.command)}`);
|
|
319
339
|
console.log(` ${chalk.dim(' or:')} ${chalk.cyan(mergeAction.command)}`);
|
|
320
340
|
}
|
|
341
|
+
if (turn.status === 'failed_acceptance') {
|
|
342
|
+
console.log(` ${chalk.dim('Reason:')} ${turn.failure_reason || 'unknown'}`);
|
|
343
|
+
console.log(` ${chalk.dim('Recover:')} ${chalk.cyan(`agentxchain reject-turn --turn ${turn.turn_id}`)} — reject and retry`);
|
|
344
|
+
console.log(` ${chalk.dim(' or:')} ${chalk.cyan(`agentxchain accept-turn --turn ${turn.turn_id}`)} — re-attempt acceptance`);
|
|
345
|
+
}
|
|
321
346
|
}
|
|
322
347
|
} else if (singleActiveTurn) {
|
|
323
348
|
console.log(` ${chalk.dim('Turn:')} ${singleActiveTurn.turn_id}`);
|
|
@@ -372,6 +397,23 @@ function renderGovernedStatus(context, opts) {
|
|
|
372
397
|
console.log(` ${chalk.dim('Turn:')} ${chalk.yellow('No active turn')}`);
|
|
373
398
|
}
|
|
374
399
|
|
|
400
|
+
// Runtime/authority binding drift detection (B-7)
|
|
401
|
+
const bindingDrifts = detectActiveTurnBindingDrift(state, config);
|
|
402
|
+
if (bindingDrifts.length > 0) {
|
|
403
|
+
console.log('');
|
|
404
|
+
console.log(` ${chalk.red.bold('⚠ Stale binding detected')}`);
|
|
405
|
+
for (const drift of bindingDrifts) {
|
|
406
|
+
if (drift.runtime_changed) {
|
|
407
|
+
console.log(` ${chalk.dim('Turn:')} ${drift.turn_id} (${drift.role_id})`);
|
|
408
|
+
console.log(` ${chalk.dim('Runtime:')} ${chalk.yellow(drift.old_runtime)} → ${chalk.green(drift.new_runtime)} (config changed)`);
|
|
409
|
+
}
|
|
410
|
+
if (drift.authority_changed) {
|
|
411
|
+
console.log(` ${chalk.dim('Authority:')} ${chalk.yellow(drift.old_authority)} → ${chalk.green(drift.new_authority)} (config changed)`);
|
|
412
|
+
}
|
|
413
|
+
console.log(` ${chalk.dim('Recover:')} ${chalk.cyan(drift.recovery_command)}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
375
417
|
// Queued phase/completion requests
|
|
376
418
|
if (state?.queued_phase_transition) {
|
|
377
419
|
const qt = state.queued_phase_transition;
|
package/src/commands/step.js
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
getActiveTurnCount,
|
|
36
36
|
getActiveTurns,
|
|
37
37
|
reactivateGovernedRun,
|
|
38
|
+
refreshTurnBaselineSnapshot,
|
|
38
39
|
STATE_PATH,
|
|
39
40
|
} from '../lib/governed-state.js';
|
|
40
41
|
import { getMaxConcurrentTurns } from '../lib/normalized-config.js';
|
|
@@ -68,6 +69,7 @@ import { finalizeDispatchManifest, verifyDispatchManifest } from '../lib/dispatc
|
|
|
68
69
|
import { resolveGovernedRole } from '../lib/role-resolution.js';
|
|
69
70
|
import { shouldSuggestManualQaFallback } from '../lib/manual-qa-fallback.js';
|
|
70
71
|
import { evaluateApprovalSlaReminders } from '../lib/notification-runner.js';
|
|
72
|
+
import { consumeNextApprovedIntent } from '../lib/intake.js';
|
|
71
73
|
|
|
72
74
|
export async function stepCommand(opts) {
|
|
73
75
|
const context = loadProjectContext();
|
|
@@ -213,6 +215,17 @@ export async function stepCommand(opts) {
|
|
|
213
215
|
process.exit(1);
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
// If the target turn failed acceptance, print recovery guidance (BUG-3 fix)
|
|
219
|
+
if (targetTurn.status === 'failed_acceptance') {
|
|
220
|
+
console.log(chalk.red(`Turn ${targetTurn.turn_id} (${targetTurn.assigned_role}) failed acceptance.`));
|
|
221
|
+
console.log(chalk.dim(`Reason: ${targetTurn.failure_reason || 'unknown'}`));
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(chalk.dim('Recovery options:'));
|
|
224
|
+
console.log(` ${chalk.cyan(`agentxchain reject-turn --turn ${targetTurn.turn_id}`)} — reject and retry`);
|
|
225
|
+
console.log(` ${chalk.cyan(`agentxchain accept-turn --turn ${targetTurn.turn_id}`)} — re-attempt acceptance after fixing`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
216
229
|
console.log(chalk.yellow(`Re-dispatching blocked turn: ${targetTurn.turn_id}`));
|
|
217
230
|
const reactivated = reactivateGovernedRun(root, state, { via: 'step --resume', notificationConfig: config });
|
|
218
231
|
if (!reactivated.ok) {
|
|
@@ -222,6 +235,10 @@ export async function stepCommand(opts) {
|
|
|
222
235
|
state = reactivated.state;
|
|
223
236
|
skipAssignment = true;
|
|
224
237
|
|
|
238
|
+
// BUG-1 fix: refresh baseline snapshot to capture files dirtied between assignment and dispatch
|
|
239
|
+
refreshTurnBaselineSnapshot(root, targetTurn.turn_id);
|
|
240
|
+
state = JSON.parse(readFileSync(join(root, '.agentxchain/state.json'), 'utf8'));
|
|
241
|
+
|
|
225
242
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
226
243
|
if (!bundleResult.ok) {
|
|
227
244
|
console.log(chalk.red(`Failed to write dispatch bundle: ${bundleResult.error}`));
|
|
@@ -245,6 +262,10 @@ export async function stepCommand(opts) {
|
|
|
245
262
|
state = reactivated.state;
|
|
246
263
|
skipAssignment = true;
|
|
247
264
|
|
|
265
|
+
// BUG-1 fix: refresh baseline snapshot to capture files dirtied between assignment and dispatch
|
|
266
|
+
refreshTurnBaselineSnapshot(root, pausedTurn.turn_id);
|
|
267
|
+
state = JSON.parse(readFileSync(join(root, '.agentxchain/state.json'), 'utf8'));
|
|
268
|
+
|
|
248
269
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
249
270
|
if (!bundleResult.ok) {
|
|
250
271
|
console.log(chalk.red(`Failed to write dispatch bundle: ${bundleResult.error}`));
|
|
@@ -294,16 +315,26 @@ export async function stepCommand(opts) {
|
|
|
294
315
|
process.exit(1);
|
|
295
316
|
}
|
|
296
317
|
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
318
|
+
const shouldBindIntent = opts.intent !== false;
|
|
319
|
+
const consumed = shouldBindIntent ? consumeNextApprovedIntent(root, { role: roleId }) : { ok: false };
|
|
320
|
+
if (consumed.ok) {
|
|
321
|
+
state = loadProjectState(root, config);
|
|
322
|
+
if (!state) {
|
|
323
|
+
console.log(chalk.red('Failed to reload governed state after intake binding.'));
|
|
324
|
+
process.exit(1);
|
|
301
325
|
}
|
|
302
|
-
|
|
303
|
-
|
|
326
|
+
} else {
|
|
327
|
+
const assignResult = assignGovernedTurn(root, config, roleId);
|
|
328
|
+
if (!assignResult.ok) {
|
|
329
|
+
if (assignResult.error_code?.startsWith('hook_') || assignResult.error_code === 'hook_blocked') {
|
|
330
|
+
printAssignmentHookFailure(assignResult, roleId, config);
|
|
331
|
+
}
|
|
332
|
+
console.log(chalk.red(`Failed to assign turn: ${assignResult.error}`));
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
printAssignmentWarnings(assignResult);
|
|
336
|
+
state = assignResult.state;
|
|
304
337
|
}
|
|
305
|
-
printAssignmentWarnings(assignResult);
|
|
306
|
-
state = assignResult.state;
|
|
307
338
|
|
|
308
339
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
309
340
|
if (!bundleResult.ok) {
|
|
@@ -587,13 +618,21 @@ export async function stepCommand(opts) {
|
|
|
587
618
|
console.log(chalk.yellow(`The subprocess must independently read from .agentxchain/dispatch/turns/${turn.turn_id}/PROMPT.md`));
|
|
588
619
|
console.log(chalk.dim('To enable automatic prompt delivery, set prompt_transport to "argv" or "stdin" in the runtime config.'));
|
|
589
620
|
}
|
|
621
|
+
// BUG-6: always show log file path so operators know where to watch
|
|
622
|
+
const logPath = `.agentxchain/dispatch/turns/${turn.turn_id}/stdout.log`;
|
|
623
|
+
console.log(chalk.dim(`Log: ${logPath}`));
|
|
624
|
+
if (!opts.stream && !opts.verbose) {
|
|
625
|
+
console.log(chalk.dim(` Watch live: tail -f ${logPath}`));
|
|
626
|
+
}
|
|
590
627
|
console.log(chalk.dim('Press Ctrl+C to abort and leave the turn assigned.'));
|
|
591
628
|
console.log('');
|
|
592
629
|
|
|
630
|
+
// BUG-6: stream subprocess output by default (--stream or --verbose), suppress with --quiet
|
|
631
|
+
const shouldStream = opts.stream || opts.verbose || false;
|
|
593
632
|
const cliResult = await dispatchLocalCli(root, state, config, {
|
|
594
633
|
signal: controller.signal,
|
|
595
|
-
onStdout:
|
|
596
|
-
onStderr:
|
|
634
|
+
onStdout: shouldStream ? (text) => process.stdout.write(chalk.dim(text)) : undefined,
|
|
635
|
+
onStderr: shouldStream ? (text) => process.stderr.write(chalk.yellow(text)) : undefined,
|
|
597
636
|
verifyManifest: true,
|
|
598
637
|
});
|
|
599
638
|
|