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.
Files changed (94) hide show
  1. package/README.md +123 -154
  2. package/bin/agentxchain.js +240 -8
  3. package/dashboard/app.js +305 -0
  4. package/dashboard/components/blocked.js +145 -0
  5. package/dashboard/components/cross-repo.js +126 -0
  6. package/dashboard/components/gate.js +311 -0
  7. package/dashboard/components/hooks.js +177 -0
  8. package/dashboard/components/initiative.js +147 -0
  9. package/dashboard/components/ledger.js +165 -0
  10. package/dashboard/components/timeline.js +222 -0
  11. package/dashboard/index.html +352 -0
  12. package/package.json +16 -7
  13. package/scripts/agentxchain-autonudge.applescript +32 -5
  14. package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
  15. package/scripts/publish-from-tag.sh +88 -0
  16. package/scripts/release-postflight.sh +231 -0
  17. package/scripts/release-preflight.sh +167 -0
  18. package/scripts/run-autonudge.sh +1 -1
  19. package/src/adapters/claude-code.js +7 -14
  20. package/src/adapters/cursor-local.js +17 -16
  21. package/src/commands/accept-turn.js +160 -0
  22. package/src/commands/approve-completion.js +80 -0
  23. package/src/commands/approve-transition.js +85 -0
  24. package/src/commands/branch.js +2 -2
  25. package/src/commands/claim.js +84 -9
  26. package/src/commands/config.js +16 -0
  27. package/src/commands/dashboard.js +70 -0
  28. package/src/commands/doctor.js +9 -1
  29. package/src/commands/init.js +540 -5
  30. package/src/commands/migrate.js +348 -0
  31. package/src/commands/multi.js +549 -0
  32. package/src/commands/plugin.js +157 -0
  33. package/src/commands/reject-turn.js +204 -0
  34. package/src/commands/resume.js +389 -0
  35. package/src/commands/status.js +196 -3
  36. package/src/commands/step.js +947 -0
  37. package/src/commands/stop.js +65 -33
  38. package/src/commands/template-list.js +33 -0
  39. package/src/commands/template-set.js +279 -0
  40. package/src/commands/update.js +24 -3
  41. package/src/commands/validate.js +20 -11
  42. package/src/commands/verify.js +71 -0
  43. package/src/commands/watch.js +112 -25
  44. package/src/lib/adapters/api-proxy-adapter.js +1076 -0
  45. package/src/lib/adapters/local-cli-adapter.js +337 -0
  46. package/src/lib/adapters/manual-adapter.js +169 -0
  47. package/src/lib/blocked-state.js +94 -0
  48. package/src/lib/config.js +143 -12
  49. package/src/lib/context-compressor.js +121 -0
  50. package/src/lib/context-section-parser.js +220 -0
  51. package/src/lib/coordinator-acceptance.js +428 -0
  52. package/src/lib/coordinator-config.js +461 -0
  53. package/src/lib/coordinator-dispatch.js +276 -0
  54. package/src/lib/coordinator-gates.js +487 -0
  55. package/src/lib/coordinator-hooks.js +239 -0
  56. package/src/lib/coordinator-recovery.js +523 -0
  57. package/src/lib/coordinator-state.js +365 -0
  58. package/src/lib/cross-repo-context.js +247 -0
  59. package/src/lib/dashboard/bridge-server.js +284 -0
  60. package/src/lib/dashboard/file-watcher.js +93 -0
  61. package/src/lib/dashboard/state-reader.js +96 -0
  62. package/src/lib/dispatch-bundle.js +568 -0
  63. package/src/lib/dispatch-manifest.js +252 -0
  64. package/src/lib/filter-agents.js +12 -0
  65. package/src/lib/gate-evaluator.js +285 -0
  66. package/src/lib/generate-vscode.js +158 -68
  67. package/src/lib/governed-state.js +2139 -0
  68. package/src/lib/governed-templates.js +145 -0
  69. package/src/lib/hook-runner.js +788 -0
  70. package/src/lib/next-owner.js +61 -6
  71. package/src/lib/normalized-config.js +539 -0
  72. package/src/lib/notify.js +14 -12
  73. package/src/lib/plugin-config-schema.js +192 -0
  74. package/src/lib/plugins.js +692 -0
  75. package/src/lib/prompt-core.js +108 -0
  76. package/src/lib/protocol-conformance.js +291 -0
  77. package/src/lib/reference-conformance-adapter.js +717 -0
  78. package/src/lib/repo-observer.js +597 -0
  79. package/src/lib/repo.js +0 -31
  80. package/src/lib/safe-write.js +44 -0
  81. package/src/lib/schema.js +189 -0
  82. package/src/lib/schemas/turn-result.schema.json +205 -0
  83. package/src/lib/seed-prompt-polling.js +15 -73
  84. package/src/lib/seed-prompt.js +17 -63
  85. package/src/lib/token-budget.js +206 -0
  86. package/src/lib/token-counter.js +27 -0
  87. package/src/lib/turn-paths.js +67 -0
  88. package/src/lib/turn-result-validator.js +496 -0
  89. package/src/lib/validation.js +167 -19
  90. package/src/lib/verify-command.js +72 -0
  91. package/src/templates/governed/api-service.json +31 -0
  92. package/src/templates/governed/cli-tool.json +30 -0
  93. package/src/templates/governed/generic.json +10 -0
  94. package/src/templates/governed/web-app.json +30 -0
@@ -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 next = pickNextAgent(root, lock, config);
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
- process.on('SIGINT', () => {
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 = pickNextAgent(root, lock, config);
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
- writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
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
- writeFileSync(triggerPath, JSON.stringify({
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
- }, null, 2) + '\n');
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
- writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
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
- writeFileSync(statePath, JSON.stringify(nextState, null, 2) + '\n');
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
- writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
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
- writeFileSync(statePath, JSON.stringify(nextState, null, 2) + '\n');
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
+ }