agentxchain 0.8.7 → 2.1.1
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 +123 -154
- package/bin/agentxchain.js +240 -8
- package/dashboard/app.js +305 -0
- package/dashboard/components/blocked.js +145 -0
- package/dashboard/components/cross-repo.js +126 -0
- package/dashboard/components/gate.js +311 -0
- package/dashboard/components/hooks.js +177 -0
- package/dashboard/components/initiative.js +147 -0
- package/dashboard/components/ledger.js +165 -0
- package/dashboard/components/timeline.js +222 -0
- package/dashboard/index.html +352 -0
- package/package.json +16 -7
- package/scripts/agentxchain-autonudge.applescript +32 -5
- package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
- package/scripts/publish-from-tag.sh +88 -0
- package/scripts/release-postflight.sh +231 -0
- package/scripts/release-preflight.sh +167 -0
- package/scripts/run-autonudge.sh +1 -1
- package/src/adapters/claude-code.js +7 -14
- package/src/adapters/cursor-local.js +17 -16
- package/src/commands/accept-turn.js +160 -0
- package/src/commands/approve-completion.js +80 -0
- package/src/commands/approve-transition.js +85 -0
- package/src/commands/branch.js +2 -2
- package/src/commands/claim.js +84 -9
- package/src/commands/config.js +16 -0
- package/src/commands/dashboard.js +70 -0
- package/src/commands/doctor.js +9 -1
- package/src/commands/init.js +540 -5
- package/src/commands/migrate.js +348 -0
- package/src/commands/multi.js +549 -0
- package/src/commands/plugin.js +157 -0
- package/src/commands/reject-turn.js +204 -0
- package/src/commands/resume.js +389 -0
- package/src/commands/status.js +196 -3
- package/src/commands/step.js +947 -0
- package/src/commands/stop.js +65 -33
- package/src/commands/template-list.js +33 -0
- package/src/commands/template-set.js +279 -0
- package/src/commands/update.js +24 -3
- package/src/commands/validate.js +20 -11
- package/src/commands/verify.js +71 -0
- package/src/commands/watch.js +112 -25
- package/src/lib/adapters/api-proxy-adapter.js +1076 -0
- package/src/lib/adapters/local-cli-adapter.js +337 -0
- package/src/lib/adapters/manual-adapter.js +169 -0
- package/src/lib/blocked-state.js +94 -0
- package/src/lib/config.js +143 -12
- package/src/lib/context-compressor.js +121 -0
- package/src/lib/context-section-parser.js +220 -0
- package/src/lib/coordinator-acceptance.js +428 -0
- package/src/lib/coordinator-config.js +461 -0
- package/src/lib/coordinator-dispatch.js +276 -0
- package/src/lib/coordinator-gates.js +487 -0
- package/src/lib/coordinator-hooks.js +239 -0
- package/src/lib/coordinator-recovery.js +523 -0
- package/src/lib/coordinator-state.js +365 -0
- package/src/lib/cross-repo-context.js +247 -0
- package/src/lib/dashboard/bridge-server.js +284 -0
- package/src/lib/dashboard/file-watcher.js +93 -0
- package/src/lib/dashboard/state-reader.js +96 -0
- package/src/lib/dispatch-bundle.js +568 -0
- package/src/lib/dispatch-manifest.js +252 -0
- package/src/lib/filter-agents.js +12 -0
- package/src/lib/gate-evaluator.js +285 -0
- package/src/lib/generate-vscode.js +158 -68
- package/src/lib/governed-state.js +2139 -0
- package/src/lib/governed-templates.js +145 -0
- package/src/lib/hook-runner.js +788 -0
- package/src/lib/next-owner.js +61 -6
- package/src/lib/normalized-config.js +539 -0
- package/src/lib/notify.js +14 -12
- package/src/lib/plugin-config-schema.js +192 -0
- package/src/lib/plugins.js +692 -0
- package/src/lib/prompt-core.js +108 -0
- package/src/lib/protocol-conformance.js +291 -0
- package/src/lib/reference-conformance-adapter.js +717 -0
- package/src/lib/repo-observer.js +597 -0
- package/src/lib/repo.js +0 -31
- package/src/lib/safe-write.js +44 -0
- package/src/lib/schema.js +189 -0
- package/src/lib/schemas/turn-result.schema.json +205 -0
- package/src/lib/seed-prompt-polling.js +15 -73
- package/src/lib/seed-prompt.js +17 -63
- package/src/lib/token-budget.js +206 -0
- package/src/lib/token-counter.js +27 -0
- package/src/lib/turn-paths.js +67 -0
- package/src/lib/turn-result-validator.js +496 -0
- package/src/lib/validation.js +167 -19
- package/src/lib/verify-command.js +72 -0
- package/src/templates/governed/api-service.json +31 -0
- package/src/templates/governed/cli-tool.json +30 -0
- package/src/templates/governed/generic.json +10 -0
- package/src/templates/governed/web-app.json +30 -0
package/src/commands/watch.js
CHANGED
|
@@ -1,12 +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';
|
|
9
|
-
import { resolveNextAgent } from '../lib/next-owner.js';
|
|
10
|
+
import { resolveNextAgent, resolveExpectedClaimer } from '../lib/next-owner.js';
|
|
11
|
+
|
|
12
|
+
const PID_FILE = '.agentxchain-watch.pid';
|
|
10
13
|
|
|
11
14
|
export async function watchCommand(opts) {
|
|
12
15
|
if (opts.daemon && process.env.AGENTXCHAIN_WATCH_DAEMON !== '1') {
|
|
@@ -37,12 +40,15 @@ export async function watchCommand(opts) {
|
|
|
37
40
|
console.log(chalk.dim(` Poll: ${interval}ms | TTL: ${ttlMinutes}min`));
|
|
38
41
|
console.log(chalk.dim(` Mode: Local file watcher + trigger file`));
|
|
39
42
|
console.log('');
|
|
43
|
+
writePidFile(root);
|
|
44
|
+
|
|
40
45
|
console.log(chalk.cyan(' Watching lock.json... (Ctrl+C to stop)'));
|
|
41
46
|
console.log(chalk.dim(' Note: In VS Code/Cursor, the Stop hook coordinates turns automatically.'));
|
|
42
47
|
console.log(chalk.dim(' This watch process is a fallback for non-IDE environments.'));
|
|
43
48
|
console.log('');
|
|
44
49
|
|
|
45
50
|
let lastState = null;
|
|
51
|
+
let lastClaimedState = null;
|
|
46
52
|
|
|
47
53
|
const tick = async () => {
|
|
48
54
|
try {
|
|
@@ -51,17 +57,6 @@ export async function watchCommand(opts) {
|
|
|
51
57
|
|
|
52
58
|
const stateKey = `${lock.holder}:${lock.turn_number}`;
|
|
53
59
|
|
|
54
|
-
if (lock.holder && lock.holder !== 'human') {
|
|
55
|
-
const expected = pickNextAgent(root, lock, config);
|
|
56
|
-
if (!isValidClaimer(root, lock, config)) {
|
|
57
|
-
log('warn', `Illegal claim detected: holder=${lock.holder}, expected=${expected}. Handing lock to HUMAN.`);
|
|
58
|
-
blockOnIllegalClaim(root, lock, config, expected);
|
|
59
|
-
sendNotification(`Illegal claim detected (${lock.holder}). Human intervention required.`);
|
|
60
|
-
lastState = null;
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
60
|
if (lock.holder && lock.holder !== 'human' && lock.claimed_at) {
|
|
66
61
|
const elapsed = Date.now() - new Date(lock.claimed_at).getTime();
|
|
67
62
|
const ttlMs = ttlMinutes * 60 * 1000;
|
|
@@ -86,11 +81,27 @@ export async function watchCommand(opts) {
|
|
|
86
81
|
}
|
|
87
82
|
|
|
88
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
|
+
|
|
89
99
|
if (stateKey !== lastState) {
|
|
90
100
|
const name = config.agents[lock.holder]?.name || lock.holder;
|
|
91
101
|
log('claimed', `${lock.holder} (${name}) working... (turn ${lock.turn_number})`);
|
|
92
102
|
lastState = stateKey;
|
|
93
103
|
}
|
|
104
|
+
lastClaimedState = stateKey;
|
|
94
105
|
return;
|
|
95
106
|
}
|
|
96
107
|
|
|
@@ -109,7 +120,15 @@ export async function watchCommand(opts) {
|
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
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
|
+
}
|
|
113
132
|
log('free', `Lock free (released by ${lock.last_released_by || 'none'}). Next: ${chalk.bold(next)}.`);
|
|
114
133
|
writeTrigger(root, next, lock, config);
|
|
115
134
|
lastState = stateKey;
|
|
@@ -122,12 +141,16 @@ export async function watchCommand(opts) {
|
|
|
122
141
|
await tick();
|
|
123
142
|
const timer = setInterval(tick, interval);
|
|
124
143
|
|
|
125
|
-
|
|
144
|
+
const cleanup = () => {
|
|
126
145
|
clearInterval(timer);
|
|
146
|
+
removePidFile(root);
|
|
127
147
|
console.log('');
|
|
128
148
|
log('stop', 'Watch stopped.');
|
|
129
149
|
process.exit(0);
|
|
130
|
-
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
process.on('SIGINT', cleanup);
|
|
153
|
+
process.on('SIGTERM', cleanup);
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
function pickNextAgent(root, lock, config) {
|
|
@@ -137,7 +160,7 @@ function pickNextAgent(root, lock, config) {
|
|
|
137
160
|
function isValidClaimer(root, lock, config) {
|
|
138
161
|
if (!lock.holder || lock.holder === 'human') return true;
|
|
139
162
|
if (!config.agents?.[lock.holder]) return false;
|
|
140
|
-
const expected =
|
|
163
|
+
const expected = resolveExpectedClaimer(root, config, lock).next;
|
|
141
164
|
return lock.holder === expected;
|
|
142
165
|
}
|
|
143
166
|
|
|
@@ -146,10 +169,10 @@ function forceRelease(root, lock, staleAgent, config) {
|
|
|
146
169
|
const newLock = {
|
|
147
170
|
holder: null,
|
|
148
171
|
last_released_by: `system:ttl:${staleAgent}`,
|
|
149
|
-
turn_number: lock.turn_number,
|
|
172
|
+
turn_number: lock.turn_number + 1,
|
|
150
173
|
claimed_at: null
|
|
151
174
|
};
|
|
152
|
-
|
|
175
|
+
safeWriteJson(lockPath, newLock);
|
|
153
176
|
|
|
154
177
|
const logFile = config.log || 'log.md';
|
|
155
178
|
const logPath = join(root, logFile);
|
|
@@ -161,12 +184,12 @@ function forceRelease(root, lock, staleAgent, config) {
|
|
|
161
184
|
function writeTrigger(root, agentId, lock, config) {
|
|
162
185
|
if (!agentId) return;
|
|
163
186
|
const triggerPath = join(root, '.agentxchain-trigger.json');
|
|
164
|
-
|
|
187
|
+
safeWriteJson(triggerPath, {
|
|
165
188
|
agent: agentId,
|
|
166
189
|
turn_number: lock.turn_number,
|
|
167
190
|
triggered_at: new Date().toISOString(),
|
|
168
191
|
project: config.project
|
|
169
|
-
}
|
|
192
|
+
});
|
|
170
193
|
}
|
|
171
194
|
|
|
172
195
|
function blockOnValidation(root, lock, config, validation) {
|
|
@@ -177,7 +200,7 @@ function blockOnValidation(root, lock, config, validation) {
|
|
|
177
200
|
turn_number: lock.turn_number,
|
|
178
201
|
claimed_at: new Date().toISOString()
|
|
179
202
|
};
|
|
180
|
-
|
|
203
|
+
safeWriteJson(lockPath, newLock);
|
|
181
204
|
|
|
182
205
|
const statePath = join(root, 'state.json');
|
|
183
206
|
if (existsSync(statePath)) {
|
|
@@ -189,7 +212,7 @@ function blockOnValidation(root, lock, config, validation) {
|
|
|
189
212
|
blocked: true,
|
|
190
213
|
blocked_on: `validation: ${message}`
|
|
191
214
|
};
|
|
192
|
-
|
|
215
|
+
safeWriteJson(statePath, nextState);
|
|
193
216
|
} catch {}
|
|
194
217
|
}
|
|
195
218
|
|
|
@@ -204,6 +227,39 @@ function blockOnValidation(root, lock, config, validation) {
|
|
|
204
227
|
}
|
|
205
228
|
}
|
|
206
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
|
+
|
|
207
263
|
function blockOnIllegalClaim(root, lock, config, expected) {
|
|
208
264
|
const lockPath = join(root, LOCK_FILE);
|
|
209
265
|
const newLock = {
|
|
@@ -212,7 +268,7 @@ function blockOnIllegalClaim(root, lock, config, expected) {
|
|
|
212
268
|
turn_number: lock.turn_number,
|
|
213
269
|
claimed_at: new Date().toISOString()
|
|
214
270
|
};
|
|
215
|
-
|
|
271
|
+
safeWriteJson(lockPath, newLock);
|
|
216
272
|
|
|
217
273
|
const statePath = join(root, 'state.json');
|
|
218
274
|
if (existsSync(statePath)) {
|
|
@@ -223,7 +279,7 @@ function blockOnIllegalClaim(root, lock, config, expected) {
|
|
|
223
279
|
blocked: true,
|
|
224
280
|
blocked_on: `illegal-claim: expected ${expected}, got ${lock.holder}`
|
|
225
281
|
};
|
|
226
|
-
|
|
282
|
+
safeWriteJson(statePath, nextState);
|
|
227
283
|
} catch {}
|
|
228
284
|
}
|
|
229
285
|
|
|
@@ -262,8 +318,39 @@ function startWatchDaemon() {
|
|
|
262
318
|
});
|
|
263
319
|
child.unref();
|
|
264
320
|
|
|
321
|
+
const result = loadConfig();
|
|
322
|
+
if (result) {
|
|
323
|
+
writeFileSync(join(result.root, PID_FILE), String(child.pid));
|
|
324
|
+
}
|
|
325
|
+
|
|
265
326
|
console.log('');
|
|
266
327
|
console.log(chalk.green(` ✓ Watch started in daemon mode (PID: ${child.pid})`));
|
|
267
328
|
console.log(chalk.dim(' Use `agentxchain stop` or kill the PID to stop it.'));
|
|
268
329
|
console.log('');
|
|
269
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
|
+
}
|