agentxchain 0.8.6 → 0.8.8
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 +36 -38
- package/bin/agentxchain.js +62 -3
- package/package.json +3 -2
- package/scripts/agentxchain-autonudge.applescript +49 -10
- package/scripts/run-autonudge.sh +1 -1
- package/src/adapters/claude-code.js +7 -14
- package/src/adapters/cursor-local.js +26 -29
- package/src/commands/branch.js +2 -2
- package/src/commands/claim.js +86 -15
- package/src/commands/config.js +16 -0
- package/src/commands/doctor.js +9 -1
- package/src/commands/init.js +24 -5
- package/src/commands/rebind.js +77 -0
- package/src/commands/stop.js +65 -33
- package/src/commands/update.js +24 -3
- package/src/commands/watch.js +115 -34
- package/src/lib/config.js +47 -12
- package/src/lib/filter-agents.js +12 -0
- package/src/lib/generate-vscode.js +158 -51
- package/src/lib/next-owner.js +116 -0
- package/src/lib/notify.js +14 -12
- package/src/lib/prompt-core.js +108 -0
- package/src/lib/safe-write.js +44 -0
- package/src/lib/schema.js +68 -0
- package/src/lib/seed-prompt-polling.js +21 -83
- package/src/lib/seed-prompt.js +17 -63
- package/src/lib/validation.js +30 -19
- package/src/lib/verify-command.js +72 -0
package/src/commands/watch.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, appendFileSync, unlinkSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
|
|
7
|
+
import { safeWriteJson } from '../lib/safe-write.js';
|
|
7
8
|
import { notifyHuman as sendNotification } from '../lib/notify.js';
|
|
8
9
|
import { validateProject } from '../lib/validation.js';
|
|
10
|
+
import { resolveNextAgent, resolveExpectedClaimer } from '../lib/next-owner.js';
|
|
11
|
+
|
|
12
|
+
const PID_FILE = '.agentxchain-watch.pid';
|
|
9
13
|
|
|
10
14
|
export async function watchCommand(opts) {
|
|
11
15
|
if (opts.daemon && process.env.AGENTXCHAIN_WATCH_DAEMON !== '1') {
|
|
@@ -36,12 +40,15 @@ export async function watchCommand(opts) {
|
|
|
36
40
|
console.log(chalk.dim(` Poll: ${interval}ms | TTL: ${ttlMinutes}min`));
|
|
37
41
|
console.log(chalk.dim(` Mode: Local file watcher + trigger file`));
|
|
38
42
|
console.log('');
|
|
43
|
+
writePidFile(root);
|
|
44
|
+
|
|
39
45
|
console.log(chalk.cyan(' Watching lock.json... (Ctrl+C to stop)'));
|
|
40
46
|
console.log(chalk.dim(' Note: In VS Code/Cursor, the Stop hook coordinates turns automatically.'));
|
|
41
47
|
console.log(chalk.dim(' This watch process is a fallback for non-IDE environments.'));
|
|
42
48
|
console.log('');
|
|
43
49
|
|
|
44
50
|
let lastState = null;
|
|
51
|
+
let lastClaimedState = null;
|
|
45
52
|
|
|
46
53
|
const tick = async () => {
|
|
47
54
|
try {
|
|
@@ -50,17 +57,6 @@ export async function watchCommand(opts) {
|
|
|
50
57
|
|
|
51
58
|
const stateKey = `${lock.holder}:${lock.turn_number}`;
|
|
52
59
|
|
|
53
|
-
if (lock.holder && lock.holder !== 'human') {
|
|
54
|
-
const expected = pickNextAgent(lock, config);
|
|
55
|
-
if (!isValidClaimer(lock, config)) {
|
|
56
|
-
log('warn', `Illegal claim detected: holder=${lock.holder}, expected=${expected}. Handing lock to HUMAN.`);
|
|
57
|
-
blockOnIllegalClaim(root, lock, config, expected);
|
|
58
|
-
sendNotification(`Illegal claim detected (${lock.holder}). Human intervention required.`);
|
|
59
|
-
lastState = null;
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
60
|
if (lock.holder && lock.holder !== 'human' && lock.claimed_at) {
|
|
65
61
|
const elapsed = Date.now() - new Date(lock.claimed_at).getTime();
|
|
66
62
|
const ttlMs = ttlMinutes * 60 * 1000;
|
|
@@ -85,11 +81,27 @@ export async function watchCommand(opts) {
|
|
|
85
81
|
}
|
|
86
82
|
|
|
87
83
|
if (lock.holder) {
|
|
84
|
+
// Validate claim ownership only once per new claimed state.
|
|
85
|
+
// With handoff-driven routing, TALK.md may change during an active turn.
|
|
86
|
+
// Re-validating every tick can produce false "illegal claim" alerts.
|
|
87
|
+
if (stateKey !== lastClaimedState && lock.holder !== 'human') {
|
|
88
|
+
const expected = pickNextAgent(root, lock, config);
|
|
89
|
+
if (!isValidClaimer(root, lock, config)) {
|
|
90
|
+
log('warn', `Illegal claim detected: holder=${lock.holder}, expected=${expected ?? 'none'}. Handing lock to HUMAN.`);
|
|
91
|
+
blockOnIllegalClaim(root, lock, config, expected);
|
|
92
|
+
sendNotification(`Illegal claim detected (${lock.holder}). Human intervention required.`);
|
|
93
|
+
lastState = null;
|
|
94
|
+
lastClaimedState = null;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
if (stateKey !== lastState) {
|
|
89
100
|
const name = config.agents[lock.holder]?.name || lock.holder;
|
|
90
101
|
log('claimed', `${lock.holder} (${name}) working... (turn ${lock.turn_number})`);
|
|
91
102
|
lastState = stateKey;
|
|
92
103
|
}
|
|
104
|
+
lastClaimedState = stateKey;
|
|
93
105
|
return;
|
|
94
106
|
}
|
|
95
107
|
|
|
@@ -108,7 +120,15 @@ export async function watchCommand(opts) {
|
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
const
|
|
123
|
+
const resolved = resolveNextAgent(root, config, lock);
|
|
124
|
+
const next = resolved.next;
|
|
125
|
+
if (!next) {
|
|
126
|
+
log('warn', `No next owner (${resolved.source}). strict_next_owner requires TALK.md handoff. Handing lock to HUMAN.`);
|
|
127
|
+
blockOnMissingNext(root, lock, config, resolved.source);
|
|
128
|
+
sendNotification('No next owner in TALK.md. Human action required.');
|
|
129
|
+
lastState = null;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
112
132
|
log('free', `Lock free (released by ${lock.last_released_by || 'none'}). Next: ${chalk.bold(next)}.`);
|
|
113
133
|
writeTrigger(root, next, lock, config);
|
|
114
134
|
lastState = stateKey;
|
|
@@ -121,29 +141,26 @@ export async function watchCommand(opts) {
|
|
|
121
141
|
await tick();
|
|
122
142
|
const timer = setInterval(tick, interval);
|
|
123
143
|
|
|
124
|
-
|
|
144
|
+
const cleanup = () => {
|
|
125
145
|
clearInterval(timer);
|
|
146
|
+
removePidFile(root);
|
|
126
147
|
console.log('');
|
|
127
148
|
log('stop', 'Watch stopped.');
|
|
128
149
|
process.exit(0);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function pickNextAgent(lock, config) {
|
|
133
|
-
const agentIds = Object.keys(config.agents);
|
|
134
|
-
if (agentIds.length === 0) return null;
|
|
135
|
-
const lastAgent = lock.last_released_by;
|
|
150
|
+
};
|
|
136
151
|
|
|
137
|
-
|
|
152
|
+
process.on('SIGINT', cleanup);
|
|
153
|
+
process.on('SIGTERM', cleanup);
|
|
154
|
+
}
|
|
138
155
|
|
|
139
|
-
|
|
140
|
-
return
|
|
156
|
+
function pickNextAgent(root, lock, config) {
|
|
157
|
+
return resolveNextAgent(root, config, lock).next;
|
|
141
158
|
}
|
|
142
159
|
|
|
143
|
-
function isValidClaimer(lock, config) {
|
|
160
|
+
function isValidClaimer(root, lock, config) {
|
|
144
161
|
if (!lock.holder || lock.holder === 'human') return true;
|
|
145
162
|
if (!config.agents?.[lock.holder]) return false;
|
|
146
|
-
const expected =
|
|
163
|
+
const expected = resolveExpectedClaimer(root, config, lock).next;
|
|
147
164
|
return lock.holder === expected;
|
|
148
165
|
}
|
|
149
166
|
|
|
@@ -152,10 +169,10 @@ function forceRelease(root, lock, staleAgent, config) {
|
|
|
152
169
|
const newLock = {
|
|
153
170
|
holder: null,
|
|
154
171
|
last_released_by: `system:ttl:${staleAgent}`,
|
|
155
|
-
turn_number: lock.turn_number,
|
|
172
|
+
turn_number: lock.turn_number + 1,
|
|
156
173
|
claimed_at: null
|
|
157
174
|
};
|
|
158
|
-
|
|
175
|
+
safeWriteJson(lockPath, newLock);
|
|
159
176
|
|
|
160
177
|
const logFile = config.log || 'log.md';
|
|
161
178
|
const logPath = join(root, logFile);
|
|
@@ -167,12 +184,12 @@ function forceRelease(root, lock, staleAgent, config) {
|
|
|
167
184
|
function writeTrigger(root, agentId, lock, config) {
|
|
168
185
|
if (!agentId) return;
|
|
169
186
|
const triggerPath = join(root, '.agentxchain-trigger.json');
|
|
170
|
-
|
|
187
|
+
safeWriteJson(triggerPath, {
|
|
171
188
|
agent: agentId,
|
|
172
189
|
turn_number: lock.turn_number,
|
|
173
190
|
triggered_at: new Date().toISOString(),
|
|
174
191
|
project: config.project
|
|
175
|
-
}
|
|
192
|
+
});
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
function blockOnValidation(root, lock, config, validation) {
|
|
@@ -183,7 +200,7 @@ function blockOnValidation(root, lock, config, validation) {
|
|
|
183
200
|
turn_number: lock.turn_number,
|
|
184
201
|
claimed_at: new Date().toISOString()
|
|
185
202
|
};
|
|
186
|
-
|
|
203
|
+
safeWriteJson(lockPath, newLock);
|
|
187
204
|
|
|
188
205
|
const statePath = join(root, 'state.json');
|
|
189
206
|
if (existsSync(statePath)) {
|
|
@@ -195,7 +212,7 @@ function blockOnValidation(root, lock, config, validation) {
|
|
|
195
212
|
blocked: true,
|
|
196
213
|
blocked_on: `validation: ${message}`
|
|
197
214
|
};
|
|
198
|
-
|
|
215
|
+
safeWriteJson(statePath, nextState);
|
|
199
216
|
} catch {}
|
|
200
217
|
}
|
|
201
218
|
|
|
@@ -210,6 +227,39 @@ function blockOnValidation(root, lock, config, validation) {
|
|
|
210
227
|
}
|
|
211
228
|
}
|
|
212
229
|
|
|
230
|
+
function blockOnMissingNext(root, lock, config, source) {
|
|
231
|
+
const lockPath = join(root, LOCK_FILE);
|
|
232
|
+
const newLock = {
|
|
233
|
+
holder: 'human',
|
|
234
|
+
last_released_by: lock.last_released_by,
|
|
235
|
+
turn_number: lock.turn_number,
|
|
236
|
+
claimed_at: new Date().toISOString()
|
|
237
|
+
};
|
|
238
|
+
safeWriteJson(lockPath, newLock);
|
|
239
|
+
|
|
240
|
+
const statePath = join(root, 'state.json');
|
|
241
|
+
if (existsSync(statePath)) {
|
|
242
|
+
try {
|
|
243
|
+
const current = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
244
|
+
const nextState = {
|
|
245
|
+
...current,
|
|
246
|
+
blocked: true,
|
|
247
|
+
blocked_on: `missing-next-owner: ${source}`
|
|
248
|
+
};
|
|
249
|
+
safeWriteJson(statePath, nextState);
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const logFile = config.log || 'log.md';
|
|
254
|
+
const logPath = join(root, logFile);
|
|
255
|
+
if (existsSync(logPath)) {
|
|
256
|
+
appendFileSync(
|
|
257
|
+
logPath,
|
|
258
|
+
`\n---\n\n### [system] (Watch) | Turn ${lock.turn_number}\n\n**Status:** Could not resolve next agent (${source}).\n\n**Action:** Add \`Next owner: <agent_id>\` to ${config.talk_file || 'TALK.md'}, or set \`rules.strict_next_owner\` to false for cyclic fallback.\n\n`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
213
263
|
function blockOnIllegalClaim(root, lock, config, expected) {
|
|
214
264
|
const lockPath = join(root, LOCK_FILE);
|
|
215
265
|
const newLock = {
|
|
@@ -218,7 +268,7 @@ function blockOnIllegalClaim(root, lock, config, expected) {
|
|
|
218
268
|
turn_number: lock.turn_number,
|
|
219
269
|
claimed_at: new Date().toISOString()
|
|
220
270
|
};
|
|
221
|
-
|
|
271
|
+
safeWriteJson(lockPath, newLock);
|
|
222
272
|
|
|
223
273
|
const statePath = join(root, 'state.json');
|
|
224
274
|
if (existsSync(statePath)) {
|
|
@@ -229,7 +279,7 @@ function blockOnIllegalClaim(root, lock, config, expected) {
|
|
|
229
279
|
blocked: true,
|
|
230
280
|
blocked_on: `illegal-claim: expected ${expected}, got ${lock.holder}`
|
|
231
281
|
};
|
|
232
|
-
|
|
282
|
+
safeWriteJson(statePath, nextState);
|
|
233
283
|
} catch {}
|
|
234
284
|
}
|
|
235
285
|
|
|
@@ -268,8 +318,39 @@ function startWatchDaemon() {
|
|
|
268
318
|
});
|
|
269
319
|
child.unref();
|
|
270
320
|
|
|
321
|
+
const result = loadConfig();
|
|
322
|
+
if (result) {
|
|
323
|
+
writeFileSync(join(result.root, PID_FILE), String(child.pid));
|
|
324
|
+
}
|
|
325
|
+
|
|
271
326
|
console.log('');
|
|
272
327
|
console.log(chalk.green(` ✓ Watch started in daemon mode (PID: ${child.pid})`));
|
|
273
328
|
console.log(chalk.dim(' Use `agentxchain stop` or kill the PID to stop it.'));
|
|
274
329
|
console.log('');
|
|
275
330
|
}
|
|
331
|
+
|
|
332
|
+
function writePidFile(root) {
|
|
333
|
+
try {
|
|
334
|
+
writeFileSync(join(root, PID_FILE), String(process.pid));
|
|
335
|
+
} catch {}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function removePidFile(root) {
|
|
339
|
+
try {
|
|
340
|
+
const pidPath = join(root, PID_FILE);
|
|
341
|
+
if (existsSync(pidPath)) unlinkSync(pidPath);
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function getWatchPid(root) {
|
|
346
|
+
try {
|
|
347
|
+
const pidPath = join(root, PID_FILE);
|
|
348
|
+
if (!existsSync(pidPath)) return null;
|
|
349
|
+
const pid = parseInt(readFileSync(pidPath, 'utf8').trim(), 10);
|
|
350
|
+
if (!Number.isFinite(pid)) return null;
|
|
351
|
+
process.kill(pid, 0);
|
|
352
|
+
return pid;
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
package/src/lib/config.js
CHANGED
|
@@ -1,36 +1,71 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
2
|
+
import { join, parse as pathParse, resolve } from 'path';
|
|
3
|
+
import { safeParseJson, validateConfigSchema, validateLockSchema, validateStateSchema } from './schema.js';
|
|
3
4
|
|
|
4
5
|
const CONFIG_FILE = 'agentxchain.json';
|
|
5
6
|
const LOCK_FILE = 'lock.json';
|
|
6
7
|
const STATE_FILE = 'state.json';
|
|
7
8
|
|
|
8
9
|
export function findProjectRoot(startDir = process.cwd()) {
|
|
9
|
-
let dir = startDir;
|
|
10
|
-
|
|
10
|
+
let dir = resolve(startDir);
|
|
11
|
+
const { root: fsRoot } = pathParse(dir);
|
|
12
|
+
while (true) {
|
|
11
13
|
if (existsSync(join(dir, CONFIG_FILE))) return dir;
|
|
14
|
+
if (dir === fsRoot) return null;
|
|
12
15
|
dir = join(dir, '..');
|
|
13
16
|
}
|
|
14
|
-
return null;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export function loadConfig(dir = process.cwd()) {
|
|
18
20
|
const root = findProjectRoot(dir);
|
|
19
21
|
if (!root) return null;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
+
const filePath = join(root, CONFIG_FILE);
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = readFileSync(filePath, 'utf8');
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const result = safeParseJson(raw, validateConfigSchema);
|
|
30
|
+
if (!result.ok) {
|
|
31
|
+
console.error(` Warning: agentxchain.json has issues: ${result.errors.join(', ')}`);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return { root, config: result.data };
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
export function loadLock(root) {
|
|
25
|
-
const
|
|
26
|
-
if (!existsSync(
|
|
27
|
-
|
|
38
|
+
const filePath = join(root, LOCK_FILE);
|
|
39
|
+
if (!existsSync(filePath)) return null;
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = readFileSync(filePath, 'utf8');
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const result = safeParseJson(raw, validateLockSchema);
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
console.error(` Warning: lock.json has issues: ${result.errors.join(', ')}`);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return result.data;
|
|
28
52
|
}
|
|
29
53
|
|
|
30
54
|
export function loadState(root) {
|
|
31
|
-
const
|
|
32
|
-
if (!existsSync(
|
|
33
|
-
|
|
55
|
+
const filePath = join(root, STATE_FILE);
|
|
56
|
+
if (!existsSync(filePath)) return null;
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = readFileSync(filePath, 'utf8');
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const result = safeParseJson(raw, validateStateSchema);
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
console.error(` Warning: state.json has issues: ${result.errors.join(', ')}`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return result.data;
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
export { CONFIG_FILE, LOCK_FILE, STATE_FILE };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export function filterAgents(config, specificId) {
|
|
4
|
+
if (specificId) {
|
|
5
|
+
if (!config.agents[specificId]) {
|
|
6
|
+
console.log(chalk.red(` Agent "${specificId}" not found in agentxchain.json`));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
return { [specificId]: config.agents[specificId] };
|
|
10
|
+
}
|
|
11
|
+
return config.agents;
|
|
12
|
+
}
|
|
@@ -20,11 +20,13 @@ export function generateVSCodeFiles(dir, config) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
writeFileSync(join(hooksDir, 'agentxchain.json'), buildHooksJson());
|
|
23
|
+
writeFileSync(join(scriptsDir, 'agentxchain-hook-runtime.cjs'), buildHookRuntimeScript());
|
|
23
24
|
writeFileSync(join(scriptsDir, 'agentxchain-session-start.sh'), SESSION_START_SCRIPT);
|
|
24
|
-
writeFileSync(join(scriptsDir, 'agentxchain-stop.sh'), buildStopScript(
|
|
25
|
+
writeFileSync(join(scriptsDir, 'agentxchain-stop.sh'), buildStopScript());
|
|
25
26
|
writeFileSync(join(scriptsDir, 'agentxchain-pre-tool.sh'), PRE_TOOL_SCRIPT);
|
|
26
27
|
|
|
27
28
|
try {
|
|
29
|
+
chmodSync(join(scriptsDir, 'agentxchain-hook-runtime.cjs'), 0o755);
|
|
28
30
|
chmodSync(join(scriptsDir, 'agentxchain-session-start.sh'), 0o755);
|
|
29
31
|
chmodSync(join(scriptsDir, 'agentxchain-stop.sh'), 0o755);
|
|
30
32
|
chmodSync(join(scriptsDir, 'agentxchain-pre-tool.sh'), 0o755);
|
|
@@ -188,23 +190,150 @@ function buildHooksJson() {
|
|
|
188
190
|
return JSON.stringify(hooks, null, 2) + '\n';
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
193
|
+
function buildHookRuntimeScript() {
|
|
194
|
+
return `#!/usr/bin/env node
|
|
195
|
+
const fs = require('fs');
|
|
196
|
+
const path = require('path');
|
|
197
|
+
const { spawnSync } = require('child_process');
|
|
198
|
+
|
|
199
|
+
function readJson(file) {
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(fs.readFileSync(path.join(process.cwd(), file), 'utf8'));
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function splitCommand(input) {
|
|
208
|
+
const out = [];
|
|
209
|
+
let current = '';
|
|
210
|
+
let quote = null;
|
|
211
|
+
let escape = false;
|
|
212
|
+
for (const char of String(input || '')) {
|
|
213
|
+
if (escape) {
|
|
214
|
+
current += char;
|
|
215
|
+
escape = false;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (char === '\\\\') {
|
|
219
|
+
escape = true;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (quote) {
|
|
223
|
+
if (char === quote) {
|
|
224
|
+
quote = null;
|
|
225
|
+
} else {
|
|
226
|
+
current += char;
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (char === '"' || char === "'") {
|
|
231
|
+
quote = char;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (/\\s/.test(char)) {
|
|
235
|
+
if (current) {
|
|
236
|
+
out.push(current);
|
|
237
|
+
current = '';
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
current += char;
|
|
242
|
+
}
|
|
243
|
+
if (current) out.push(current);
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeVerifyCommand(config) {
|
|
248
|
+
const raw = config?.rules?.verify_command;
|
|
249
|
+
if (Array.isArray(raw) && raw.every(part => typeof part === 'string' && part.length > 0)) {
|
|
250
|
+
return raw;
|
|
251
|
+
}
|
|
252
|
+
if (typeof raw !== 'string' || !raw.trim()) return null;
|
|
253
|
+
return splitCommand(raw.trim());
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizeAgentId(raw) {
|
|
257
|
+
if (!raw) return null;
|
|
258
|
+
let value = String(raw).trim();
|
|
259
|
+
value = value.replace(/[\`*_]/g, '').trim();
|
|
260
|
+
value = value.replace(/\\(.*?\\)/g, '').trim();
|
|
261
|
+
value = value.split(/[\\s,]+/)[0];
|
|
262
|
+
value = value.toLowerCase();
|
|
263
|
+
return /^[a-z0-9_-]+$/.test(value) ? value : null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function parseNextOwnerFromTalk(talkPath, validAgentIds) {
|
|
267
|
+
try {
|
|
268
|
+
const text = fs.readFileSync(path.join(process.cwd(), talkPath), 'utf8');
|
|
269
|
+
const lines = text.split(/\\r?\\n/);
|
|
270
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
271
|
+
const line = lines[i].trim();
|
|
272
|
+
const match = line.match(/^(?:-|\\*)?\\s*\\**next\\s*owner\\**\\s*:\\s*(.+)$/i);
|
|
273
|
+
if (!match) continue;
|
|
274
|
+
const candidate = normalizeAgentId(match[1]);
|
|
275
|
+
if (candidate && validAgentIds.includes(candidate)) return candidate;
|
|
276
|
+
}
|
|
277
|
+
} catch {}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function resolveNextAgent(config, lock) {
|
|
282
|
+
const agentIds = Object.keys(config?.agents || {});
|
|
283
|
+
if (agentIds.length === 0) return null;
|
|
284
|
+
const talkFile = config?.talk_file || 'TALK.md';
|
|
285
|
+
const fromTalk = parseNextOwnerFromTalk(talkFile, agentIds);
|
|
286
|
+
if (fromTalk) return fromTalk;
|
|
287
|
+
const last = lock?.last_released_by;
|
|
288
|
+
if (last && agentIds.includes(last)) {
|
|
289
|
+
const idx = agentIds.indexOf(last);
|
|
290
|
+
return agentIds[(idx + 1) % agentIds.length];
|
|
291
|
+
}
|
|
292
|
+
return agentIds[0];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function outputJson(value) {
|
|
296
|
+
process.stdout.write(JSON.stringify(value));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const command = process.argv[2];
|
|
300
|
+
const config = readJson('agentxchain.json');
|
|
301
|
+
const lock = readJson('lock.json');
|
|
302
|
+
const state = readJson('state.json');
|
|
303
|
+
|
|
304
|
+
if (command === 'verify') {
|
|
305
|
+
const verifyArgs = normalizeVerifyCommand(config);
|
|
306
|
+
if (!verifyArgs || verifyArgs.length === 0) process.exit(0);
|
|
307
|
+
const result = spawnSync(verifyArgs[0], verifyArgs.slice(1), { stdio: 'ignore', cwd: process.cwd() });
|
|
308
|
+
process.exit(result.status === 0 ? 0 : 2);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (command === 'next') {
|
|
312
|
+
process.stdout.write(resolveNextAgent(config, lock) || '');
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (command === 'next-name') {
|
|
317
|
+
const next = resolveNextAgent(config, lock);
|
|
318
|
+
process.stdout.write(next ? (config?.agents?.[next]?.name || next) : '');
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (command === 'session-start') {
|
|
323
|
+
if (!lock || !state) {
|
|
324
|
+
outputJson({ continue: true });
|
|
325
|
+
process.exit(0);
|
|
326
|
+
}
|
|
327
|
+
const context = \`AgentXchain context: Project=\${state.project || 'unknown'} | Phase=\${state.phase || 'unknown'} | Turn=\${lock.turn_number ?? 0} | Lock=\${lock.holder ?? 'none'} | Last released by=\${lock.last_released_by ?? 'none'} | Blocked=\${state.blocked ?? false}\`;
|
|
328
|
+
outputJson({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: context } });
|
|
329
|
+
process.exit(0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
outputJson({ continue: true });
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
207
335
|
|
|
336
|
+
function buildStopScript() {
|
|
208
337
|
return `#!/bin/bash
|
|
209
338
|
INPUT=$(cat)
|
|
210
339
|
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
|
|
@@ -222,34 +351,28 @@ fi
|
|
|
222
351
|
LOCK=$(cat lock.json 2>/dev/null)
|
|
223
352
|
HOLDER=$(echo "$LOCK" | jq -r '.holder // empty')
|
|
224
353
|
TURN=$(echo "$LOCK" | jq -r '.turn_number // 0')
|
|
225
|
-
${verifyBlock}
|
|
226
354
|
if [ -z "$HOLDER" ] || [ "$HOLDER" = "null" ]; then
|
|
227
|
-
|
|
355
|
+
node "./scripts/agentxchain-hook-runtime.cjs" verify >/dev/null 2>&1
|
|
356
|
+
VERIFY_STATUS=$?
|
|
357
|
+
if [ "$VERIFY_STATUS" -eq 2 ]; then
|
|
358
|
+
echo '{"hookSpecificOutput":{"hookEventName":"Stop","decision":"block","reason":"Verification failed. Fix the issue and release the lock."}}'
|
|
359
|
+
exit 0
|
|
360
|
+
fi
|
|
361
|
+
fi
|
|
228
362
|
|
|
363
|
+
if [ -z "$HOLDER" ] || [ "$HOLDER" = "null" ]; then
|
|
229
364
|
if [ ! -f "agentxchain.json" ]; then
|
|
230
365
|
echo '{"continue":true}'
|
|
231
366
|
exit 0
|
|
232
367
|
fi
|
|
233
368
|
|
|
234
|
-
NEXT=$(node -
|
|
235
|
-
const cfg = JSON.parse(require('fs').readFileSync('agentxchain.json','utf8'));
|
|
236
|
-
const ids = Object.keys(cfg.agents);
|
|
237
|
-
const last = process.argv[1] || '';
|
|
238
|
-
const idx = ids.indexOf(last);
|
|
239
|
-
const next = ids[(idx + 1) % ids.length];
|
|
240
|
-
process.stdout.write(next);
|
|
241
|
-
" -- "$LAST" 2>/dev/null)
|
|
242
|
-
|
|
369
|
+
NEXT=$(node "./scripts/agentxchain-hook-runtime.cjs" next 2>/dev/null)
|
|
243
370
|
if [ -z "$NEXT" ]; then
|
|
244
371
|
echo '{"continue":true}'
|
|
245
372
|
exit 0
|
|
246
373
|
fi
|
|
247
374
|
|
|
248
|
-
NEXT_NAME=$(node -
|
|
249
|
-
const cfg = JSON.parse(require('fs').readFileSync('agentxchain.json','utf8'));
|
|
250
|
-
const a = cfg.agents[process.argv[1]];
|
|
251
|
-
process.stdout.write(a ? a.name : process.argv[1]);
|
|
252
|
-
" -- "$NEXT" 2>/dev/null)
|
|
375
|
+
NEXT_NAME=$(node "./scripts/agentxchain-hook-runtime.cjs" next-name 2>/dev/null)
|
|
253
376
|
|
|
254
377
|
echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"Stop\\",\\"decision\\":\\"block\\",\\"reason\\":\\"Turn $TURN complete. Next agent: $NEXT ($NEXT_NAME). Read lock.json, claim it, and do your work.\\"}}"
|
|
255
378
|
elif [ "$HOLDER" = "human" ]; then
|
|
@@ -261,24 +384,8 @@ fi
|
|
|
261
384
|
}
|
|
262
385
|
|
|
263
386
|
const SESSION_START_SCRIPT = `#!/bin/bash
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
exit 0
|
|
267
|
-
fi
|
|
268
|
-
|
|
269
|
-
LOCK=$(cat lock.json 2>/dev/null)
|
|
270
|
-
STATE=$(cat state.json 2>/dev/null)
|
|
271
|
-
|
|
272
|
-
HOLDER=$(echo "$LOCK" | jq -r '.holder // "none"')
|
|
273
|
-
TURN=$(echo "$LOCK" | jq -r '.turn_number // 0')
|
|
274
|
-
LAST=$(echo "$LOCK" | jq -r '.last_released_by // "none"')
|
|
275
|
-
PHASE=$(echo "$STATE" | jq -r '.phase // "unknown"')
|
|
276
|
-
BLOCKED=$(echo "$STATE" | jq -r '.blocked // false')
|
|
277
|
-
PROJECT=$(echo "$STATE" | jq -r '.project // "unknown"')
|
|
278
|
-
|
|
279
|
-
CONTEXT="AgentXchain context: Project=$PROJECT | Phase=$PHASE | Turn=$TURN | Lock=$HOLDER | Last released by=$LAST | Blocked=$BLOCKED"
|
|
280
|
-
|
|
281
|
-
echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"SessionStart\\",\\"additionalContext\\":\\"$CONTEXT\\"}}"
|
|
387
|
+
CONTEXT_JSON=$(node "./scripts/agentxchain-hook-runtime.cjs" session-start 2>/dev/null || echo '{"continue":true}')
|
|
388
|
+
printf '%s\n' "$CONTEXT_JSON"
|
|
282
389
|
`;
|
|
283
390
|
|
|
284
391
|
const PRE_TOOL_SCRIPT = `#!/bin/bash
|