agentxchain 2.129.0 → 2.130.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 +10 -0
- package/package.json +1 -1
- package/src/commands/accept-turn.js +14 -0
- package/src/commands/checkpoint-turn.js +35 -0
- package/src/commands/doctor.js +31 -2
- package/src/commands/mission.js +661 -7
- package/src/commands/reject-turn.js +36 -2
- package/src/commands/restart.js +72 -8
- package/src/commands/run.js +13 -0
- package/src/commands/status.js +13 -1
- package/src/lib/continuous-run.js +8 -1
- package/src/lib/coordinator-dispatch.js +25 -0
- package/src/lib/governed-state.js +150 -1
- package/src/lib/intake.js +12 -0
- package/src/lib/mission-plans.js +510 -6
- package/src/lib/missions.js +9 -2
- package/src/lib/repo-observer.js +1 -0
- package/src/lib/run-events.js +1 -0
- package/src/lib/run-loop.js +20 -0
- package/src/lib/turn-checkpoint.js +221 -0
package/src/lib/run-loop.js
CHANGED
|
@@ -396,6 +396,16 @@ async function executeParallelTurns(root, config, state, maxConcurrent, callback
|
|
|
396
396
|
|
|
397
397
|
acceptedCount++;
|
|
398
398
|
history.push({ role: roleId, turn_id: turn.turn_id, accepted: true });
|
|
399
|
+
if (callbacks.afterAccept) {
|
|
400
|
+
const afterAcceptResult = await callbacks.afterAccept({ turn, acceptResult });
|
|
401
|
+
if (afterAcceptResult?.ok === false) {
|
|
402
|
+
errors.push(`afterAccept(${roleId}): ${afterAcceptResult.error}`);
|
|
403
|
+
if (afterAcceptResult.state) {
|
|
404
|
+
emit({ type: 'blocked', state: afterAcceptResult.state, reason: 'after_accept_failed' });
|
|
405
|
+
}
|
|
406
|
+
return { terminal: true, ok: false, stop_reason: 'blocked', history, acceptedCount };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
399
409
|
emit({ type: 'turn_accepted', turn, role: roleId, state: acceptResult.state });
|
|
400
410
|
} else {
|
|
401
411
|
const validationResult = {
|
|
@@ -512,6 +522,16 @@ async function dispatchAndProcess(root, config, turn, assignState, callbacks, em
|
|
|
512
522
|
}
|
|
513
523
|
|
|
514
524
|
history.push({ role: roleId, turn_id: turn.turn_id, accepted: true });
|
|
525
|
+
if (callbacks.afterAccept) {
|
|
526
|
+
const afterAcceptResult = await callbacks.afterAccept({ turn, acceptResult });
|
|
527
|
+
if (afterAcceptResult?.ok === false) {
|
|
528
|
+
errors.push(`afterAccept(${roleId}): ${afterAcceptResult.error}`);
|
|
529
|
+
if (afterAcceptResult.state) {
|
|
530
|
+
emit({ type: 'blocked', state: afterAcceptResult.state, reason: 'after_accept_failed' });
|
|
531
|
+
}
|
|
532
|
+
return { terminal: true, ok: false, stop_reason: 'blocked', history };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
515
535
|
emit({ type: 'turn_accepted', turn, role: roleId, state: acceptResult.state });
|
|
516
536
|
return { terminal: false, accepted: true, history };
|
|
517
537
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { resolveAcceptedTurnHistoryReference } from './accepted-turn-history.js';
|
|
5
|
+
import { emitRunEvent } from './run-events.js';
|
|
6
|
+
import { safeWriteJson } from './safe-write.js';
|
|
7
|
+
|
|
8
|
+
const STATE_PATH = '.agentxchain/state.json';
|
|
9
|
+
const HISTORY_PATH = '.agentxchain/history.jsonl';
|
|
10
|
+
|
|
11
|
+
function readState(root) {
|
|
12
|
+
const filePath = join(root, STATE_PATH);
|
|
13
|
+
if (!existsSync(filePath)) return null;
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeState(root, state) {
|
|
22
|
+
safeWriteJson(join(root, STATE_PATH), state);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readHistoryEntries(root) {
|
|
26
|
+
const filePath = join(root, HISTORY_PATH);
|
|
27
|
+
if (!existsSync(filePath)) return [];
|
|
28
|
+
const raw = readFileSync(filePath, 'utf8').trim();
|
|
29
|
+
if (!raw) return [];
|
|
30
|
+
return raw.split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeHistoryEntries(root, entries) {
|
|
34
|
+
const filePath = join(root, HISTORY_PATH);
|
|
35
|
+
const content = entries.map((entry) => JSON.stringify(entry)).join('\n');
|
|
36
|
+
writeFileSync(filePath, content ? `${content}\n` : '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function git(root, args) {
|
|
40
|
+
return execFileSync('git', args, {
|
|
41
|
+
cwd: root,
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
}).trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isGitRepo(root) {
|
|
48
|
+
try {
|
|
49
|
+
return git(root, ['rev-parse', '--is-inside-work-tree']) === 'true';
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeFilesChanged(filesChanged) {
|
|
56
|
+
return [...new Set(
|
|
57
|
+
(Array.isArray(filesChanged) ? filesChanged : [])
|
|
58
|
+
.filter((value) => typeof value === 'string')
|
|
59
|
+
.map((value) => value.trim())
|
|
60
|
+
.filter(Boolean),
|
|
61
|
+
)];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractGitError(err) {
|
|
65
|
+
const stderr = typeof err?.stderr === 'string' ? err.stderr.trim() : '';
|
|
66
|
+
const stdout = typeof err?.stdout === 'string' ? err.stdout.trim() : '';
|
|
67
|
+
return stderr || stdout || err?.message || 'git command failed';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildCheckpointCommit(entry) {
|
|
71
|
+
const subject = `checkpoint: ${entry.turn_id} (role=${entry.role}, phase=${entry.phase})`;
|
|
72
|
+
const bodyLines = [
|
|
73
|
+
`Summary: ${entry.summary || '(none)'}`,
|
|
74
|
+
`Turn-ID: ${entry.turn_id}`,
|
|
75
|
+
`Role: ${entry.role || '(unknown)'}`,
|
|
76
|
+
`Phase: ${entry.phase || '(unknown)'}`,
|
|
77
|
+
`Runtime: ${entry.runtime_id || '(unknown)'}`,
|
|
78
|
+
];
|
|
79
|
+
if (entry.intent_id) bodyLines.push(`Intent-ID: ${entry.intent_id}`);
|
|
80
|
+
if (entry.accepted_at) bodyLines.push(`Accepted-At: ${entry.accepted_at}`);
|
|
81
|
+
return { subject, body: bodyLines.join('\n') };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function detectPendingCheckpoint(root, dirtyFiles = []) {
|
|
85
|
+
const actorDirtyFiles = normalizeFilesChanged(dirtyFiles);
|
|
86
|
+
if (actorDirtyFiles.length === 0) return { required: false };
|
|
87
|
+
|
|
88
|
+
const resolved = resolveAcceptedTurnHistoryReference(root, null);
|
|
89
|
+
if (!resolved.ok || !resolved.entry) return { required: false };
|
|
90
|
+
|
|
91
|
+
const entry = resolved.entry;
|
|
92
|
+
if (entry.checkpoint_sha) return { required: false };
|
|
93
|
+
|
|
94
|
+
const turnFiles = normalizeFilesChanged(entry.files_changed);
|
|
95
|
+
if (turnFiles.length === 0) return { required: false };
|
|
96
|
+
|
|
97
|
+
const dirtyOutsideTurn = actorDirtyFiles.filter((file) => !turnFiles.includes(file));
|
|
98
|
+
if (dirtyOutsideTurn.length > 0) return { required: false };
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
required: true,
|
|
102
|
+
turn_id: entry.turn_id,
|
|
103
|
+
message: `Accepted turn ${entry.turn_id} is not checkpointed yet. Run agentxchain checkpoint-turn --turn ${entry.turn_id} before assigning the next code-writing turn.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function checkpointAcceptedTurn(root, opts = {}) {
|
|
108
|
+
if (!isGitRepo(root)) {
|
|
109
|
+
return { ok: false, error: 'checkpoint-turn requires a git repository.' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const resolved = resolveAcceptedTurnHistoryReference(root, opts.turnId || opts.turn || null);
|
|
113
|
+
if (!resolved.ok || !resolved.entry) {
|
|
114
|
+
return { ok: false, error: resolved.error || 'Accepted turn not found.' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const entry = resolved.entry;
|
|
118
|
+
if (entry.checkpoint_sha) {
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
already_checkpointed: true,
|
|
122
|
+
turn: entry,
|
|
123
|
+
checkpoint_sha: entry.checkpoint_sha,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const filesChanged = normalizeFilesChanged(entry.files_changed);
|
|
128
|
+
if (filesChanged.length === 0) {
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
skipped: true,
|
|
132
|
+
turn: entry,
|
|
133
|
+
reason: 'Accepted turn has no writable files_changed paths to checkpoint.',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
git(root, ['add', '-A', '--', ...filesChanged]);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
turn: entry,
|
|
143
|
+
error: `Failed to stage accepted files for checkpoint: ${extractGitError(err)}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let staged = [];
|
|
148
|
+
try {
|
|
149
|
+
staged = git(root, ['diff', '--cached', '--name-only', '--', ...filesChanged])
|
|
150
|
+
.split('\n')
|
|
151
|
+
.map((value) => value.trim())
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
turn: entry,
|
|
157
|
+
error: `Failed to inspect staged checkpoint diff: ${extractGitError(err)}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (staged.length === 0) {
|
|
162
|
+
return {
|
|
163
|
+
ok: true,
|
|
164
|
+
skipped: true,
|
|
165
|
+
turn: entry,
|
|
166
|
+
reason: `Accepted turn ${entry.turn_id} has no staged repo changes to checkpoint.`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const commit = buildCheckpointCommit(entry);
|
|
171
|
+
try {
|
|
172
|
+
git(root, ['commit', '-m', commit.subject, '-m', commit.body]);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
turn: entry,
|
|
177
|
+
error: `Checkpoint commit failed: ${extractGitError(err)}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const checkpointSha = git(root, ['rev-parse', 'HEAD']);
|
|
182
|
+
const checkpointedAt = new Date().toISOString();
|
|
183
|
+
|
|
184
|
+
const historyEntries = readHistoryEntries(root).map((historyEntry) => (
|
|
185
|
+
historyEntry.turn_id === entry.turn_id
|
|
186
|
+
? { ...historyEntry, checkpoint_sha: checkpointSha, checkpointed_at: checkpointedAt }
|
|
187
|
+
: historyEntry
|
|
188
|
+
));
|
|
189
|
+
writeHistoryEntries(root, historyEntries);
|
|
190
|
+
|
|
191
|
+
const state = readState(root);
|
|
192
|
+
if (state) {
|
|
193
|
+
writeState(root, {
|
|
194
|
+
...state,
|
|
195
|
+
last_completed_turn: {
|
|
196
|
+
turn_id: entry.turn_id,
|
|
197
|
+
role: entry.role || null,
|
|
198
|
+
phase: entry.phase || null,
|
|
199
|
+
checkpoint_sha: checkpointSha,
|
|
200
|
+
checkpointed_at: checkpointedAt,
|
|
201
|
+
intent_id: entry.intent_id || null,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
emitRunEvent(root, 'turn_checkpointed', {
|
|
206
|
+
run_id: state.run_id || null,
|
|
207
|
+
phase: state.phase || null,
|
|
208
|
+
status: state.status || null,
|
|
209
|
+
turn: { turn_id: entry.turn_id, role_id: entry.role || null },
|
|
210
|
+
intent_id: entry.intent_id || null,
|
|
211
|
+
payload: { checkpoint_sha: checkpointSha, checkpointed_at: checkpointedAt },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
turn: entry,
|
|
218
|
+
checkpoint_sha: checkpointSha,
|
|
219
|
+
checkpointed_at: checkpointedAt,
|
|
220
|
+
};
|
|
221
|
+
}
|