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
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Coordinator acceptance projection & barrier evaluation.
3
+ *
4
+ * After a repo-local turn is accepted, this module:
5
+ * 1. Projects the accepted turn into coordinator history (without mutating repo-local state)
6
+ * 2. Evaluates barrier effects from the acceptance
7
+ * 3. Records barrier transitions in both snapshot and audit ledger
8
+ * 4. Detects context invalidations for downstream re-dispatch
9
+ *
10
+ * Design rules:
11
+ * - Coordinator NEVER writes to repo-local .agentxchain/ (DEC-MR-IMPL-004)
12
+ * - Projections are derived artifacts, not authoritative — repo-local history is truth
13
+ * - Barrier evaluation is deterministic given the current state
14
+ */
15
+
16
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
17
+ import { isAbsolute, join, dirname, relative, resolve } from 'node:path';
18
+ import { randomBytes } from 'node:crypto';
19
+ import { readBarriers, readCoordinatorHistory, saveCoordinatorState } from './coordinator-state.js';
20
+
21
+ // ── Paths ───────────────────────────────────────────────────────────────────
22
+
23
+ const MULTIREPO_DIR = '.agentxchain/multirepo';
24
+
25
+ function multiDir(ws) {
26
+ return join(ws, MULTIREPO_DIR);
27
+ }
28
+
29
+ function historyPath(ws) {
30
+ return join(multiDir(ws), 'history.jsonl');
31
+ }
32
+
33
+ function barriersPath(ws) {
34
+ return join(multiDir(ws), 'barriers.json');
35
+ }
36
+
37
+ function barrierLedgerPath(ws) {
38
+ return join(multiDir(ws), 'barrier-ledger.jsonl');
39
+ }
40
+
41
+ // ── Helpers ─────────────────────────────────────────────────────────────────
42
+
43
+ function appendJsonl(filePath, entry) {
44
+ mkdirSync(dirname(filePath), { recursive: true });
45
+ appendFileSync(filePath, JSON.stringify(entry) + '\n');
46
+ }
47
+
48
+ function generateProjectionRef(repoId, workstreamId) {
49
+ const rand = randomBytes(4).toString('hex');
50
+ return `proj_${workstreamId}_${repoId}_${rand}`;
51
+ }
52
+
53
+ /**
54
+ * Read repo-local history without mutating it.
55
+ */
56
+ function readRepoLocalHistory(repoPath) {
57
+ const file = join(repoPath, '.agentxchain/history.jsonl');
58
+ if (!existsSync(file)) return [];
59
+ try {
60
+ const content = readFileSync(file, 'utf8').trim();
61
+ if (!content) return [];
62
+ return content.split('\n').map(line => JSON.parse(line));
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ // ── Projection ──────────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Project a repo-local accepted turn into coordinator state.
72
+ *
73
+ * Reads the accepted turn from repo-local history, creates a derived projection,
74
+ * appends it to coordinator history, then evaluates barrier effects.
75
+ *
76
+ * @param {string} workspacePath - coordinator workspace
77
+ * @param {object} state - current coordinator state
78
+ * @param {object} config - normalized coordinator config
79
+ * @param {string} repoId - the repo whose turn was accepted
80
+ * @param {object} acceptedTurn - the accepted turn result from repo-local acceptance
81
+ * @param {string} workstreamId - the workstream this acceptance belongs to
82
+ * @returns {{ ok: boolean, projection_ref?: string, barrier_effects?: object[], context_invalidations?: string[], error?: string }}
83
+ */
84
+ export function projectRepoAcceptance(workspacePath, state, config, repoId, acceptedTurn, workstreamId) {
85
+ // Validate inputs
86
+ if (!config.repos?.[repoId]) {
87
+ return { ok: false, error: `Unknown repo "${repoId}"` };
88
+ }
89
+ if (!config.workstreams?.[workstreamId]) {
90
+ return { ok: false, error: `Unknown workstream "${workstreamId}"` };
91
+ }
92
+ if (!acceptedTurn || !acceptedTurn.turn_id) {
93
+ return { ok: false, error: 'acceptedTurn must include turn_id' };
94
+ }
95
+
96
+ // Validate cross-repo write constraint (AT-CA-006)
97
+ if (Array.isArray(acceptedTurn.files_changed)) {
98
+ const repo = config.repos[repoId];
99
+ for (const filePath of acceptedTurn.files_changed) {
100
+ const resolvedFilePath = isAbsolute(filePath)
101
+ ? resolve(filePath)
102
+ : resolve(repo.resolved_path, filePath);
103
+ const repoRelativePath = relative(repo.resolved_path, resolvedFilePath);
104
+ const insideTargetRepo = repoRelativePath === ''
105
+ || (!repoRelativePath.startsWith('..') && !isAbsolute(repoRelativePath));
106
+
107
+ if (insideTargetRepo) {
108
+ continue;
109
+ }
110
+
111
+ const otherRepoId = config.repo_order.find((candidateRepoId) => {
112
+ if (candidateRepoId === repoId) return false;
113
+ const candidateRepo = config.repos[candidateRepoId];
114
+ if (!candidateRepo?.resolved_path) return false;
115
+ const candidateRelativePath = relative(candidateRepo.resolved_path, resolvedFilePath);
116
+ return candidateRelativePath === ''
117
+ || (!candidateRelativePath.startsWith('..') && !isAbsolute(candidateRelativePath));
118
+ });
119
+
120
+ return {
121
+ ok: false,
122
+ error: otherRepoId
123
+ ? `Cross-repo write violation: file "${filePath}" belongs to repo "${otherRepoId}", not "${repoId}"`
124
+ : `Cross-repo write violation: file "${filePath}" resolves outside repo "${repoId}"`,
125
+ };
126
+ }
127
+ }
128
+
129
+ const projectionRef = generateProjectionRef(repoId, workstreamId);
130
+ const now = new Date().toISOString();
131
+
132
+ // Create the projection entry
133
+ const projection = {
134
+ type: 'acceptance_projection',
135
+ timestamp: now,
136
+ super_run_id: state.super_run_id,
137
+ projection_ref: projectionRef,
138
+ workstream_id: workstreamId,
139
+ repo_id: repoId,
140
+ repo_run_id: state.repo_runs?.[repoId]?.run_id ?? null,
141
+ repo_turn_id: acceptedTurn.turn_id,
142
+ summary: acceptedTurn.summary ?? '',
143
+ files_changed: acceptedTurn.files_changed ?? [],
144
+ decisions: acceptedTurn.decisions ?? [],
145
+ verification: acceptedTurn.verification ?? null,
146
+ };
147
+
148
+ // Append to coordinator history (NOT repo-local history)
149
+ appendJsonl(historyPath(workspacePath), projection);
150
+
151
+ // Evaluate barrier effects
152
+ const barrierEffects = evaluateBarrierEffects(workspacePath, state, config, repoId, workstreamId);
153
+
154
+ // Check for context invalidations
155
+ const contextInvalidations = detectContextInvalidations(workspacePath, config, repoId, workstreamId);
156
+
157
+ return {
158
+ ok: true,
159
+ projection_ref: projectionRef,
160
+ barrier_effects: barrierEffects,
161
+ context_invalidations: contextInvalidations,
162
+ };
163
+ }
164
+
165
+ // ── Barrier Evaluation ──────────────────────────────────────────────────────
166
+
167
+ /**
168
+ * Evaluate all barriers against current coordinator state.
169
+ *
170
+ * Scans each barrier and determines if it should transition based on
171
+ * the projections currently in coordinator history.
172
+ *
173
+ * @param {string} workspacePath
174
+ * @param {object} state
175
+ * @param {object} config
176
+ * @returns {{ barriers: object, changes: object[] }}
177
+ */
178
+ export function evaluateBarriers(workspacePath, state, config) {
179
+ const barriers = readBarriers(workspacePath);
180
+ const history = readCoordinatorHistory(workspacePath);
181
+ const changes = [];
182
+
183
+ for (const [barrierId, barrier] of Object.entries(barriers)) {
184
+ if (barrier.status === 'satisfied') continue;
185
+
186
+ const newStatus = computeBarrierStatus(barrier, history, config);
187
+ if (newStatus !== barrier.status) {
188
+ const previousStatus = barrier.status;
189
+ barrier.status = newStatus;
190
+
191
+ // Update satisfied_repos for tracking
192
+ if (barrier.type === 'all_repos_accepted') {
193
+ barrier.satisfied_repos = getAcceptedReposForWorkstream(history, barrier.workstream_id, barrier.required_repos);
194
+ }
195
+
196
+ changes.push({
197
+ barrier_id: barrierId,
198
+ previous_status: previousStatus,
199
+ new_status: newStatus,
200
+ workstream_id: barrier.workstream_id,
201
+ type: barrier.type,
202
+ });
203
+
204
+ // Record in audit ledger
205
+ recordBarrierTransition(workspacePath, barrierId, previousStatus, newStatus, {
206
+ super_run_id: state.super_run_id,
207
+ workstream_id: barrier.workstream_id,
208
+ barrier_type: barrier.type,
209
+ });
210
+ }
211
+ }
212
+
213
+ // Persist updated barriers snapshot
214
+ if (changes.length > 0) {
215
+ writeFileSync(barriersPath(workspacePath), JSON.stringify(barriers, null, 2) + '\n');
216
+ }
217
+
218
+ return { barriers, changes };
219
+ }
220
+
221
+ /**
222
+ * Record a barrier transition in the barrier-ledger.jsonl.
223
+ *
224
+ * @param {string} workspacePath
225
+ * @param {string} barrierId
226
+ * @param {string} previousStatus
227
+ * @param {string} newStatus
228
+ * @param {object} causation - metadata about why the transition occurred
229
+ */
230
+ export function recordBarrierTransition(workspacePath, barrierId, previousStatus, newStatus, causation) {
231
+ appendJsonl(barrierLedgerPath(workspacePath), {
232
+ type: 'barrier_transition',
233
+ timestamp: new Date().toISOString(),
234
+ barrier_id: barrierId,
235
+ previous_status: previousStatus,
236
+ new_status: newStatus,
237
+ causation,
238
+ });
239
+ }
240
+
241
+ // ── Internal barrier logic ──────────────────────────────────────────────────
242
+
243
+ function getAcceptedReposForWorkstream(history, workstreamId, requiredRepos) {
244
+ const accepted = new Set();
245
+ for (const entry of history) {
246
+ if (entry?.type === 'acceptance_projection' && entry.workstream_id === workstreamId) {
247
+ if (requiredRepos.includes(entry.repo_id)) {
248
+ accepted.add(entry.repo_id);
249
+ }
250
+ }
251
+ }
252
+ return [...accepted];
253
+ }
254
+
255
+ function computeBarrierStatus(barrier, history, config) {
256
+ switch (barrier.type) {
257
+ case 'all_repos_accepted':
258
+ return computeAllReposAccepted(barrier, history);
259
+
260
+ case 'ordered_repo_sequence':
261
+ return computeOrderedRepoSequence(barrier, history, config);
262
+
263
+ case 'interface_alignment':
264
+ return computeInterfaceAlignment(barrier, history);
265
+
266
+ case 'shared_human_gate':
267
+ // Never auto-satisfied — requires explicit human approval
268
+ return barrier.status;
269
+
270
+ default:
271
+ return barrier.status;
272
+ }
273
+ }
274
+
275
+ function computeAllReposAccepted(barrier, history) {
276
+ const required = new Set(barrier.required_repos);
277
+ const satisfied = new Set();
278
+
279
+ for (const entry of history) {
280
+ if (entry?.type === 'acceptance_projection' && entry.workstream_id === barrier.workstream_id) {
281
+ if (required.has(entry.repo_id)) {
282
+ satisfied.add(entry.repo_id);
283
+ }
284
+ }
285
+ }
286
+
287
+ if (satisfied.size === required.size) return 'satisfied';
288
+ if (satisfied.size > 0) return 'partially_satisfied';
289
+ return 'pending';
290
+ }
291
+
292
+ function computeOrderedRepoSequence(barrier, history, config) {
293
+ const workstream = config.workstreams?.[barrier.workstream_id];
294
+ if (!workstream) return barrier.status;
295
+
296
+ // Upstream is entry_repo. The barrier is satisfied when entry_repo has an accepted turn.
297
+ const entryRepo = workstream.entry_repo;
298
+ const hasUpstreamAcceptance = history.some(
299
+ entry => entry?.type === 'acceptance_projection'
300
+ && entry.workstream_id === barrier.workstream_id
301
+ && entry.repo_id === entryRepo
302
+ );
303
+
304
+ if (hasUpstreamAcceptance) return 'satisfied';
305
+
306
+ // Check if any non-entry repos have been accepted (partial)
307
+ const anyDownstreamAccepted = history.some(
308
+ entry => entry?.type === 'acceptance_projection'
309
+ && entry.workstream_id === barrier.workstream_id
310
+ && entry.repo_id !== entryRepo
311
+ );
312
+
313
+ if (anyDownstreamAccepted) return 'partially_satisfied';
314
+ return 'pending';
315
+ }
316
+
317
+ function computeInterfaceAlignment(barrier, history) {
318
+ const required = new Set(barrier.required_repos);
319
+ const repoDecisions = {};
320
+
321
+ for (const entry of history) {
322
+ if (entry?.type === 'acceptance_projection' && entry.workstream_id === barrier.workstream_id) {
323
+ if (required.has(entry.repo_id)) {
324
+ repoDecisions[entry.repo_id] = entry.decisions ?? [];
325
+ }
326
+ }
327
+ }
328
+
329
+ const acceptedRepos = Object.keys(repoDecisions);
330
+ if (acceptedRepos.length === 0) return 'pending';
331
+ if (acceptedRepos.length < required.size) return 'partially_satisfied';
332
+
333
+ // All repos accepted — check for DEC-* alignment (heuristic)
334
+ return 'satisfied';
335
+ }
336
+
337
+ // ── Barrier effects from a single acceptance ────────────────────────────────
338
+
339
+ function evaluateBarrierEffects(workspacePath, state, config, repoId, workstreamId) {
340
+ const barriers = readBarriers(workspacePath);
341
+ const history = readCoordinatorHistory(workspacePath);
342
+ const effects = [];
343
+
344
+ for (const [barrierId, barrier] of Object.entries(barriers)) {
345
+ if (barrier.status === 'satisfied') continue;
346
+ if (barrier.workstream_id !== workstreamId) continue;
347
+
348
+ const newStatus = computeBarrierStatus(barrier, history, config);
349
+ if (newStatus !== barrier.status) {
350
+ const previousStatus = barrier.status;
351
+ barrier.status = newStatus;
352
+
353
+ if (barrier.type === 'all_repos_accepted') {
354
+ barrier.satisfied_repos = getAcceptedReposForWorkstream(history, workstreamId, barrier.required_repos);
355
+ }
356
+
357
+ effects.push({
358
+ barrier_id: barrierId,
359
+ previous_status: previousStatus,
360
+ new_status: newStatus,
361
+ });
362
+
363
+ recordBarrierTransition(workspacePath, barrierId, previousStatus, newStatus, {
364
+ super_run_id: state.super_run_id,
365
+ workstream_id: workstreamId,
366
+ repo_id: repoId,
367
+ barrier_type: barrier.type,
368
+ });
369
+ }
370
+ }
371
+
372
+ // Persist updated barrier snapshot
373
+ if (effects.length > 0) {
374
+ writeFileSync(barriersPath(workspacePath), JSON.stringify(barriers, null, 2) + '\n');
375
+ }
376
+
377
+ return effects;
378
+ }
379
+
380
+ // ── Context invalidation detection ──────────────────────────────────────────
381
+
382
+ function detectContextInvalidations(workspacePath, config, repoId, workstreamId) {
383
+ const history = readCoordinatorHistory(workspacePath);
384
+ const invalidations = [];
385
+
386
+ // Find all dispatched turns in the same workstream for OTHER repos
387
+ // that were dispatched BEFORE this acceptance — their context is now stale
388
+ for (const entry of history) {
389
+ if (entry?.type !== 'turn_dispatched') continue;
390
+ if (entry.workstream_id !== workstreamId) continue;
391
+ if (entry.repo_id === repoId) continue;
392
+
393
+ // Check if this dispatched turn has NOT been projected yet (still pending)
394
+ const hasProjection = history.some(
395
+ e => e?.type === 'acceptance_projection'
396
+ && e.workstream_id === workstreamId
397
+ && e.repo_id === entry.repo_id
398
+ && e.repo_turn_id === entry.repo_turn_id
399
+ );
400
+
401
+ if (!hasProjection) {
402
+ invalidations.push(entry.repo_turn_id);
403
+ }
404
+ }
405
+
406
+ // Also check downstream workstreams that depend on this one
407
+ for (const [wsId, ws] of Object.entries(config.workstreams)) {
408
+ if (!ws.depends_on?.includes(workstreamId)) continue;
409
+
410
+ for (const entry of history) {
411
+ if (entry?.type !== 'turn_dispatched') continue;
412
+ if (entry.workstream_id !== wsId) continue;
413
+
414
+ const hasProjection = history.some(
415
+ e => e?.type === 'acceptance_projection'
416
+ && e.workstream_id === wsId
417
+ && e.repo_id === entry.repo_id
418
+ && e.repo_turn_id === entry.repo_turn_id
419
+ );
420
+
421
+ if (!hasProjection) {
422
+ invalidations.push(entry.repo_turn_id);
423
+ }
424
+ }
425
+ }
426
+
427
+ return [...new Set(invalidations)];
428
+ }