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.
@@ -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
+ }