agentxchain 0.8.8 → 2.2.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.
Files changed (74) hide show
  1. package/README.md +136 -136
  2. package/bin/agentxchain.js +186 -5
  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 +14 -6
  13. package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
  14. package/scripts/publish-from-tag.sh +88 -0
  15. package/scripts/release-postflight.sh +231 -0
  16. package/scripts/release-preflight.sh +167 -0
  17. package/src/commands/accept-turn.js +160 -0
  18. package/src/commands/approve-completion.js +80 -0
  19. package/src/commands/approve-transition.js +85 -0
  20. package/src/commands/dashboard.js +70 -0
  21. package/src/commands/init.js +516 -0
  22. package/src/commands/migrate.js +348 -0
  23. package/src/commands/multi.js +549 -0
  24. package/src/commands/plugin.js +157 -0
  25. package/src/commands/reject-turn.js +204 -0
  26. package/src/commands/resume.js +389 -0
  27. package/src/commands/status.js +196 -3
  28. package/src/commands/step.js +947 -0
  29. package/src/commands/template-list.js +33 -0
  30. package/src/commands/template-set.js +279 -0
  31. package/src/commands/validate.js +20 -11
  32. package/src/commands/verify.js +71 -0
  33. package/src/lib/adapters/api-proxy-adapter.js +1076 -0
  34. package/src/lib/adapters/local-cli-adapter.js +337 -0
  35. package/src/lib/adapters/manual-adapter.js +169 -0
  36. package/src/lib/blocked-state.js +94 -0
  37. package/src/lib/config.js +97 -1
  38. package/src/lib/context-compressor.js +121 -0
  39. package/src/lib/context-section-parser.js +220 -0
  40. package/src/lib/coordinator-acceptance.js +428 -0
  41. package/src/lib/coordinator-config.js +461 -0
  42. package/src/lib/coordinator-dispatch.js +276 -0
  43. package/src/lib/coordinator-gates.js +487 -0
  44. package/src/lib/coordinator-hooks.js +239 -0
  45. package/src/lib/coordinator-recovery.js +523 -0
  46. package/src/lib/coordinator-state.js +365 -0
  47. package/src/lib/cross-repo-context.js +247 -0
  48. package/src/lib/dashboard/bridge-server.js +284 -0
  49. package/src/lib/dashboard/file-watcher.js +93 -0
  50. package/src/lib/dashboard/state-reader.js +96 -0
  51. package/src/lib/dispatch-bundle.js +568 -0
  52. package/src/lib/dispatch-manifest.js +252 -0
  53. package/src/lib/gate-evaluator.js +285 -0
  54. package/src/lib/governed-state.js +2139 -0
  55. package/src/lib/governed-templates.js +145 -0
  56. package/src/lib/hook-runner.js +788 -0
  57. package/src/lib/normalized-config.js +539 -0
  58. package/src/lib/plugin-config-schema.js +192 -0
  59. package/src/lib/plugins.js +692 -0
  60. package/src/lib/protocol-conformance.js +291 -0
  61. package/src/lib/reference-conformance-adapter.js +858 -0
  62. package/src/lib/repo-observer.js +597 -0
  63. package/src/lib/repo.js +0 -31
  64. package/src/lib/schema.js +121 -0
  65. package/src/lib/schemas/turn-result.schema.json +205 -0
  66. package/src/lib/token-budget.js +206 -0
  67. package/src/lib/token-counter.js +27 -0
  68. package/src/lib/turn-paths.js +67 -0
  69. package/src/lib/turn-result-validator.js +496 -0
  70. package/src/lib/validation.js +137 -0
  71. package/src/templates/governed/api-service.json +31 -0
  72. package/src/templates/governed/cli-tool.json +30 -0
  73. package/src/templates/governed/generic.json +10 -0
  74. package/src/templates/governed/web-app.json +30 -0
@@ -0,0 +1,549 @@
1
+ /**
2
+ * CLI commands for multi-repo coordinator orchestration (Slice 6).
3
+ *
4
+ * Subcommands:
5
+ * multi init — bootstrap a multi-repo coordinator run
6
+ * multi status — show coordinator status and repo-run snapshots
7
+ * multi step — reconcile repo truth, then dispatch or request the next coordinator gate
8
+ * multi approve-gate — approve a pending phase transition or completion gate
9
+ * multi resync — detect divergence and rebuild coordinator state from repo authority
10
+ */
11
+
12
+ import { loadCoordinatorConfig } from '../lib/coordinator-config.js';
13
+ import {
14
+ initializeCoordinatorRun,
15
+ loadCoordinatorState,
16
+ getCoordinatorStatus,
17
+ readBarriers,
18
+ saveCoordinatorState,
19
+ } from '../lib/coordinator-state.js';
20
+ import { selectNextAssignment, dispatchCoordinatorTurn } from '../lib/coordinator-dispatch.js';
21
+ import {
22
+ evaluateCompletionGate,
23
+ evaluatePhaseGate,
24
+ approveCoordinatorPhaseTransition,
25
+ approveCoordinatorCompletion,
26
+ requestCoordinatorCompletion,
27
+ requestPhaseTransition,
28
+ } from '../lib/coordinator-gates.js';
29
+ import { detectDivergence, resyncFromRepoAuthority } from '../lib/coordinator-recovery.js';
30
+ import {
31
+ fireCoordinatorHook,
32
+ buildAssignmentPayload,
33
+ buildAcceptancePayload,
34
+ buildGatePayload,
35
+ buildEscalationPayload,
36
+ } from '../lib/coordinator-hooks.js';
37
+ import { computeContextInvalidations } from '../lib/cross-repo-context.js';
38
+
39
+ // ── multi init ─────────────────────────────────────────────────────────────
40
+
41
+ export async function multiInitCommand(options) {
42
+ const workspacePath = process.cwd();
43
+ const configResult = loadCoordinatorConfig(workspacePath);
44
+
45
+ if (!configResult.ok) {
46
+ console.error('Coordinator config error:');
47
+ for (const err of configResult.errors || []) {
48
+ console.error(` - ${typeof err === 'string' ? err : err.message || JSON.stringify(err)}`);
49
+ }
50
+ process.exitCode = 1;
51
+ return;
52
+ }
53
+
54
+ const result = initializeCoordinatorRun(workspacePath, configResult.config);
55
+
56
+ if (!result.ok) {
57
+ console.error('Failed to initialize coordinator run:');
58
+ for (const err of result.errors || []) {
59
+ console.error(` - ${err}`);
60
+ }
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+
65
+ if (options.json) {
66
+ console.log(JSON.stringify({
67
+ super_run_id: result.super_run_id,
68
+ repo_runs: result.repo_runs,
69
+ }, null, 2));
70
+ return;
71
+ }
72
+
73
+ console.log(`Coordinator run initialized: ${result.super_run_id}`);
74
+ console.log('');
75
+ for (const [repoId, info] of Object.entries(result.repo_runs || {})) {
76
+ const label = info.initialized_by_coordinator ? 'initialized' : 'linked';
77
+ console.log(` ${repoId}: ${info.run_id} (${label})`);
78
+ }
79
+ }
80
+
81
+ // ── multi status ───────────────────────────────────────────────────────────
82
+
83
+ export async function multiStatusCommand(options) {
84
+ const workspacePath = process.cwd();
85
+ const state = loadCoordinatorState(workspacePath);
86
+
87
+ if (!state) {
88
+ console.error('No coordinator state found. Run `agentxchain multi init` first.');
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+
93
+ const status = getCoordinatorStatus(workspacePath);
94
+ const barriers = readBarriers(workspacePath);
95
+
96
+ if (options.json) {
97
+ console.log(JSON.stringify({ ...status, barriers }, null, 2));
98
+ return;
99
+ }
100
+
101
+ console.log(`Super Run: ${status.super_run_id}`);
102
+ console.log(`Status: ${status.status}`);
103
+ console.log(`Phase: ${status.phase}`);
104
+
105
+ if (status.pending_gate) {
106
+ console.log(`Pending Gate: ${status.pending_gate.gate} (${status.pending_gate.gate_type})`);
107
+ }
108
+
109
+ console.log('');
110
+ console.log('Repos:');
111
+ for (const [repoId, info] of Object.entries(status.repo_runs || {})) {
112
+ const phase = info.phase ? ` [${info.phase}]` : '';
113
+ console.log(` ${repoId}: ${info.status || 'unknown'}${phase} (run: ${info.run_id})`);
114
+ }
115
+
116
+ const barrierEntries = Object.entries(barriers || {});
117
+ if (barrierEntries.length > 0) {
118
+ console.log('');
119
+ console.log('Barriers:');
120
+ for (const [barrierId, barrier] of barrierEntries) {
121
+ console.log(` ${barrierId}: ${barrier.status} (${barrier.type})`);
122
+ }
123
+ }
124
+ }
125
+
126
+ // ── multi step ─────────────────────────────────────────────────────────────
127
+
128
+ export async function multiStepCommand(options) {
129
+ const workspacePath = process.cwd();
130
+ const configResult = loadCoordinatorConfig(workspacePath);
131
+
132
+ if (!configResult.ok) {
133
+ console.error('Coordinator config error:');
134
+ for (const err of configResult.errors || []) {
135
+ console.error(` - ${typeof err === 'string' ? err : err.message || JSON.stringify(err)}`);
136
+ }
137
+ process.exitCode = 1;
138
+ return;
139
+ }
140
+
141
+ let state = loadCoordinatorState(workspacePath);
142
+ if (!state) {
143
+ console.error('No coordinator state found. Run `agentxchain multi init` first.');
144
+ process.exitCode = 1;
145
+ return;
146
+ }
147
+
148
+ if (state.status === 'completed') {
149
+ console.log('Coordinator run is already completed.');
150
+ return;
151
+ }
152
+
153
+ if (state.status === 'blocked') {
154
+ // Fire on_escalation hook (advisory — cannot block, only notifies)
155
+ fireEscalationHook(workspacePath, configResult.config, state, state.blocked_reason || 'unknown reason');
156
+ console.error(`Coordinator is blocked: ${state.blocked_reason || 'unknown reason'}`);
157
+ console.error('Resolve the blocked state before stepping.');
158
+ process.exitCode = 1;
159
+ return;
160
+ }
161
+
162
+ if (state.pending_gate) {
163
+ console.error(`Coordinator has a pending gate: ${state.pending_gate.gate}`);
164
+ console.error('Approve the gate with `agentxchain multi approve-gate` before stepping.');
165
+ process.exitCode = 1;
166
+ return;
167
+ }
168
+
169
+ const divergence = detectDivergence(workspacePath, state, configResult.config);
170
+ if (divergence.diverged) {
171
+ const resync = resyncFromRepoAuthority(workspacePath, state, configResult.config);
172
+ state = loadCoordinatorState(workspacePath) || state;
173
+
174
+ if (!resync.ok) {
175
+ // Fire on_escalation for the blocked resync
176
+ fireEscalationHook(workspacePath, configResult.config, state, resync.blocked_reason || 'resync failure');
177
+ console.error(`Coordinator resync entered blocked state: ${resync.blocked_reason || 'unknown reason'}`);
178
+ process.exitCode = 1;
179
+ return;
180
+ }
181
+
182
+ // Fire after_acceptance hooks only for newly projected acceptances.
183
+ if ((resync.projected_acceptances || []).length > 0) {
184
+ for (const projection of resync.projected_acceptances) {
185
+ // Compute real context invalidation signals for this acceptance
186
+ const contextInvalidations = computeContextInvalidations(
187
+ workspacePath,
188
+ projection.repo_id,
189
+ projection.workstream_id,
190
+ projection.files_changed || [],
191
+ );
192
+
193
+ const acceptancePayload = buildAcceptancePayload(
194
+ {
195
+ projection_ref: projection.projection_ref,
196
+ repo_turn_id: projection.repo_turn_id,
197
+ summary: projection.summary,
198
+ files_changed: projection.files_changed || [],
199
+ decisions: projection.decisions || [],
200
+ verification: projection.verification ?? null,
201
+ barrier_effects: resync.barrier_changes.filter(
202
+ (change) => change.workstream_id === projection.workstream_id,
203
+ ),
204
+ context_invalidations: contextInvalidations,
205
+ },
206
+ projection.repo_id,
207
+ projection.workstream_id,
208
+ state,
209
+ );
210
+ const acceptanceHook = fireCoordinatorHook(workspacePath, configResult.config, 'after_acceptance', acceptancePayload, {
211
+ super_run_id: state.super_run_id,
212
+ });
213
+
214
+ if (!acceptanceHook.ok) {
215
+ const reason = acceptanceHook.error || `after_acceptance hook failed for repo "${projection.repo_id}"`;
216
+ const blockedState = blockCoordinator(workspacePath, state, `coordinator_hook_violation: ${reason}`);
217
+ fireEscalationHook(workspacePath, configResult.config, blockedState, blockedState.blocked_reason);
218
+ console.error(`Coordinator blocked by after_acceptance hook: ${reason}`);
219
+ process.exitCode = 1;
220
+ return;
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ // Select next assignment
227
+ const assignment = selectNextAssignment(workspacePath, state, configResult.config);
228
+
229
+ if (!assignment.ok) {
230
+ const gate = maybeRequestCoordinatorGate(workspacePath, state, configResult.config);
231
+ if (gate.ok) {
232
+ if (options.json) {
233
+ console.log(JSON.stringify(gate.payload, null, 2));
234
+ return;
235
+ }
236
+
237
+ if (gate.type === 'phase_transition') {
238
+ console.log(`Phase gate requested: ${gate.payload.gate}`);
239
+ console.log(` Transition: ${gate.payload.from} → ${gate.payload.to}`);
240
+ } else {
241
+ console.log(`Completion gate requested: ${gate.payload.gate}`);
242
+ }
243
+ console.log('Approve the gate with `agentxchain multi approve-gate` to continue.');
244
+ return;
245
+ }
246
+
247
+ console.error(`No assignable workstream: ${assignment.reason}`);
248
+ if (assignment.detail) {
249
+ console.error(` ${assignment.detail}`);
250
+ }
251
+ if (gate.blockers.length > 0) {
252
+ console.error(`Coordinator ${gate.type === 'phase_transition' ? 'phase' : 'completion'} gate is not ready:`);
253
+ for (const blocker of gate.blockers) {
254
+ console.error(` - ${blocker.message}`);
255
+ }
256
+ }
257
+ process.exitCode = 1;
258
+ return;
259
+ }
260
+
261
+ // Fire before_assignment hook (blocking — can prevent dispatch)
262
+ const assignmentPayload = buildAssignmentPayload(assignment, state);
263
+ const assignmentHook = fireCoordinatorHook(workspacePath, configResult.config, 'before_assignment', assignmentPayload, {
264
+ super_run_id: state.super_run_id,
265
+ });
266
+
267
+ if (assignmentHook.blocked) {
268
+ const blocker = assignmentHook.verdicts.find(v => v.verdict === 'block');
269
+ const reason = blocker?.message || 'before_assignment hook blocked dispatch';
270
+ console.error(`Assignment blocked by hook: ${reason}`);
271
+ if (options.json) {
272
+ console.log(JSON.stringify({ blocked: true, hook_phase: 'before_assignment', reason }, null, 2));
273
+ }
274
+ process.exitCode = 1;
275
+ return;
276
+ }
277
+
278
+ if (!assignmentHook.ok) {
279
+ console.error(`Assignment hook failed: ${assignmentHook.error || 'unknown hook failure'}`);
280
+ if (options.json) {
281
+ console.log(JSON.stringify({
282
+ blocked: true,
283
+ hook_phase: 'before_assignment',
284
+ reason: assignmentHook.error || 'unknown hook failure',
285
+ }, null, 2));
286
+ }
287
+ process.exitCode = 1;
288
+ return;
289
+ }
290
+
291
+ // Dispatch the turn
292
+ const dispatch = dispatchCoordinatorTurn(workspacePath, state, configResult.config, assignment);
293
+
294
+ if (!dispatch.ok) {
295
+ console.error(`Dispatch failed: ${dispatch.error}`);
296
+ process.exitCode = 1;
297
+ return;
298
+ }
299
+
300
+ if (options.json) {
301
+ console.log(JSON.stringify({
302
+ repo_id: dispatch.repo_id,
303
+ turn_id: dispatch.turn_id,
304
+ workstream_id: assignment.workstream_id,
305
+ role: assignment.role,
306
+ bundle_path: dispatch.bundle_path,
307
+ }, null, 2));
308
+ return;
309
+ }
310
+
311
+ console.log(`Dispatched turn to ${dispatch.repo_id}:`);
312
+ console.log(` Turn ID: ${dispatch.turn_id}`);
313
+ console.log(` Workstream: ${assignment.workstream_id}`);
314
+ console.log(` Role: ${assignment.role}`);
315
+ console.log(` Bundle: ${dispatch.bundle_path}`);
316
+ console.log('');
317
+ console.log('The agent turn is now active in the target repo.');
318
+ console.log('Use `agentxchain step` in the target repo to execute the agent, then');
319
+ console.log('use `agentxchain accept-turn` or `agentxchain reject-turn` to complete the turn.');
320
+ }
321
+
322
+ function maybeRequestCoordinatorGate(workspacePath, state, config) {
323
+ const phaseEvaluation = evaluatePhaseGate(workspacePath, state, config);
324
+ if (phaseEvaluation.ready) {
325
+ const request = requestPhaseTransition(workspacePath, state, config, phaseEvaluation.target_phase);
326
+ if (request.ok) {
327
+ return {
328
+ ok: true,
329
+ type: 'phase_transition',
330
+ payload: {
331
+ action: 'phase_transition_requested',
332
+ gate: request.gate.gate,
333
+ gate_type: request.gate.gate_type,
334
+ from: request.gate.from,
335
+ to: request.gate.to,
336
+ },
337
+ };
338
+ }
339
+ }
340
+
341
+ const phaseIsFinal = phaseEvaluation.blockers.some((blocker) => blocker.code === 'no_next_phase');
342
+ if (!phaseIsFinal) {
343
+ return { ok: false, type: 'phase_transition', blockers: phaseEvaluation.blockers };
344
+ }
345
+
346
+ const completionEvaluation = evaluateCompletionGate(workspacePath, state, config);
347
+ if (completionEvaluation.ready) {
348
+ const request = requestCoordinatorCompletion(workspacePath, state, config);
349
+ if (request.ok) {
350
+ return {
351
+ ok: true,
352
+ type: 'run_completion',
353
+ payload: {
354
+ action: 'run_completion_requested',
355
+ gate: request.gate.gate,
356
+ gate_type: request.gate.gate_type,
357
+ },
358
+ };
359
+ }
360
+ }
361
+
362
+ return { ok: false, type: 'run_completion', blockers: completionEvaluation.blockers };
363
+ }
364
+
365
+ // ── multi approve-gate ─────────────────────────────────────────────────────
366
+
367
+ export async function multiApproveGateCommand(options) {
368
+ const workspacePath = process.cwd();
369
+ const configResult = loadCoordinatorConfig(workspacePath);
370
+
371
+ if (!configResult.ok) {
372
+ console.error('Coordinator config error:');
373
+ for (const err of configResult.errors || []) {
374
+ console.error(` - ${typeof err === 'string' ? err : err.message || JSON.stringify(err)}`);
375
+ }
376
+ process.exitCode = 1;
377
+ return;
378
+ }
379
+
380
+ const state = loadCoordinatorState(workspacePath);
381
+ if (!state) {
382
+ console.error('No coordinator state found. Run `agentxchain multi init` first.');
383
+ process.exitCode = 1;
384
+ return;
385
+ }
386
+
387
+ if (!state.pending_gate) {
388
+ console.error('No pending gate to approve.');
389
+ process.exitCode = 1;
390
+ return;
391
+ }
392
+
393
+ // Fire before_gate hook (blocking — can prevent gate approval)
394
+ const gatePayload = buildGatePayload(state.pending_gate, state);
395
+ const gateHook = fireCoordinatorHook(workspacePath, configResult.config, 'before_gate', gatePayload, {
396
+ super_run_id: state.super_run_id,
397
+ });
398
+
399
+ if (gateHook.blocked) {
400
+ const blocker = gateHook.verdicts.find(v => v.verdict === 'block');
401
+ const reason = blocker?.message || 'before_gate hook blocked approval';
402
+ console.error(`Gate approval blocked by hook: ${reason}`);
403
+ if (options.json) {
404
+ console.log(JSON.stringify({ blocked: true, hook_phase: 'before_gate', reason }, null, 2));
405
+ }
406
+ process.exitCode = 1;
407
+ return;
408
+ }
409
+
410
+ if (!gateHook.ok) {
411
+ console.error(`Gate hook failed: ${gateHook.error || 'unknown hook failure'}`);
412
+ if (options.json) {
413
+ console.log(JSON.stringify({
414
+ blocked: true,
415
+ hook_phase: 'before_gate',
416
+ reason: gateHook.error || 'unknown hook failure',
417
+ }, null, 2));
418
+ }
419
+ process.exitCode = 1;
420
+ return;
421
+ }
422
+
423
+ const gateType = state.pending_gate.gate_type;
424
+ let result;
425
+
426
+ if (gateType === 'phase_transition') {
427
+ result = approveCoordinatorPhaseTransition(workspacePath, state, configResult.config);
428
+ } else if (gateType === 'run_completion') {
429
+ result = approveCoordinatorCompletion(workspacePath, state, configResult.config);
430
+ } else {
431
+ console.error(`Unknown gate type: "${gateType}"`);
432
+ process.exitCode = 1;
433
+ return;
434
+ }
435
+
436
+ if (!result.ok) {
437
+ console.error(`Gate approval failed: ${result.error}`);
438
+ process.exitCode = 1;
439
+ return;
440
+ }
441
+
442
+ if (options.json) {
443
+ console.log(JSON.stringify(result, null, 2));
444
+ return;
445
+ }
446
+
447
+ if (gateType === 'phase_transition') {
448
+ console.log(`Phase transition approved: ${result.transition?.from} → ${result.transition?.to}`);
449
+ } else {
450
+ console.log('Run completion approved. Coordinator run is now complete.');
451
+ }
452
+ }
453
+
454
+ // ── multi resync ───────────────────────────────────────────────────────────
455
+
456
+ export async function multiResyncCommand(options) {
457
+ const workspacePath = process.cwd();
458
+ const configResult = loadCoordinatorConfig(workspacePath);
459
+
460
+ if (!configResult.ok) {
461
+ console.error('Coordinator config error:');
462
+ for (const err of configResult.errors || []) {
463
+ console.error(` - ${typeof err === 'string' ? err : err.message || JSON.stringify(err)}`);
464
+ }
465
+ process.exitCode = 1;
466
+ return;
467
+ }
468
+
469
+ const state = loadCoordinatorState(workspacePath);
470
+ if (!state) {
471
+ console.error('No coordinator state found. Run `agentxchain multi init` first.');
472
+ process.exitCode = 1;
473
+ return;
474
+ }
475
+
476
+ // Step 1: Detect divergence
477
+ const divergence = detectDivergence(workspacePath, state, configResult.config);
478
+
479
+ if (!divergence.diverged) {
480
+ if (options.json) {
481
+ console.log(JSON.stringify({ diverged: false, mismatches: [] }, null, 2));
482
+ } else {
483
+ console.log('No divergence detected. Coordinator state is consistent with repos.');
484
+ }
485
+ return;
486
+ }
487
+
488
+ if (options.dryRun) {
489
+ if (options.json) {
490
+ console.log(JSON.stringify({ diverged: true, mismatches: divergence.mismatches }, null, 2));
491
+ } else {
492
+ console.log(`Divergence detected (${divergence.mismatches.length} mismatch(es)):`);
493
+ for (const m of divergence.mismatches) {
494
+ console.log(` [${m.type}] ${m.detail}`);
495
+ }
496
+ console.log('');
497
+ console.log('Run without --dry-run to resync.');
498
+ }
499
+ return;
500
+ }
501
+
502
+ // Step 2: Resync
503
+ const result = resyncFromRepoAuthority(workspacePath, state, configResult.config);
504
+
505
+ if (options.json) {
506
+ console.log(JSON.stringify(result, null, 2));
507
+ return;
508
+ }
509
+
510
+ if (result.ok) {
511
+ console.log('Resync complete.');
512
+ if (result.resynced_repos.length > 0) {
513
+ console.log(` Repos resynced: ${result.resynced_repos.join(', ')}`);
514
+ }
515
+ if (result.barrier_changes.length > 0) {
516
+ console.log(' Barrier changes:');
517
+ for (const bc of result.barrier_changes) {
518
+ console.log(` ${bc.barrier_id}: ${bc.previous_status} → ${bc.new_status}`);
519
+ }
520
+ }
521
+ } else {
522
+ console.error('Resync completed with blocked state:');
523
+ console.error(` Reason: ${result.blocked_reason}`);
524
+ process.exitCode = 1;
525
+ }
526
+ }
527
+
528
+ // ── Hook helpers ──────────────────────────────────────────────────────────
529
+
530
+ /**
531
+ * Fire the on_escalation coordinator hook (advisory — never blocks).
532
+ * Used when the coordinator enters a blocked state from any path.
533
+ */
534
+ function fireEscalationHook(workspacePath, config, state, blockedReason) {
535
+ const payload = buildEscalationPayload(blockedReason, state);
536
+ return fireCoordinatorHook(workspacePath, config, 'on_escalation', payload, {
537
+ super_run_id: state.super_run_id,
538
+ });
539
+ }
540
+
541
+ function blockCoordinator(workspacePath, state, blockedReason) {
542
+ const blockedState = {
543
+ ...state,
544
+ status: 'blocked',
545
+ blocked_reason: blockedReason,
546
+ };
547
+ saveCoordinatorState(workspacePath, blockedState);
548
+ return blockedState;
549
+ }
@@ -0,0 +1,157 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ import { installPlugin, listInstalledPlugins, removePlugin, upgradePlugin } from '../lib/plugins.js';
5
+
6
+ function parsePluginConfigOptions(options) {
7
+ if (options.config && options.configFile) {
8
+ return { ok: false, error: 'Use either --config or --config-file, not both.' };
9
+ }
10
+
11
+ if (!options.config && !options.configFile) {
12
+ return { ok: true, config: undefined };
13
+ }
14
+
15
+ try {
16
+ const raw = options.configFile
17
+ ? readFileSync(resolve(process.cwd(), options.configFile), 'utf8')
18
+ : options.config;
19
+ return { ok: true, config: JSON.parse(raw) };
20
+ } catch (error) {
21
+ return { ok: false, error: `Invalid plugin config JSON: ${error.message || String(error)}` };
22
+ }
23
+ }
24
+
25
+ export async function pluginInstallCommand(spec, options) {
26
+ const config = parsePluginConfigOptions(options);
27
+ if (!config.ok) {
28
+ console.error(config.error);
29
+ if (options.json) {
30
+ console.log(JSON.stringify({ ok: false, error: config.error }, null, 2));
31
+ }
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+
36
+ const result = installPlugin(spec, process.cwd(), { config: config.config });
37
+
38
+ if (!result.ok) {
39
+ console.error(result.error);
40
+ if (options.json) {
41
+ console.log(JSON.stringify({ ok: false, error: result.error, collisions: result.collisions || [] }, null, 2));
42
+ }
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+
47
+ if (options.json) {
48
+ console.log(JSON.stringify(result, null, 2));
49
+ return;
50
+ }
51
+
52
+ console.log(`Installed plugin: ${result.name}@${result.version}`);
53
+ console.log(` Source: ${result.source.type} (${result.source.spec})`);
54
+ console.log(` Path: ${result.install_path}`);
55
+ if (result.config !== undefined) {
56
+ console.log(` Config: ${JSON.stringify(result.config)}`);
57
+ }
58
+ console.log(' Hooks:');
59
+ for (const [phase, hookNames] of Object.entries(result.hooks || {})) {
60
+ console.log(` ${phase}: ${hookNames.join(', ')}`);
61
+ }
62
+ }
63
+
64
+ export async function pluginListCommand(options) {
65
+ const result = listInstalledPlugins(process.cwd());
66
+
67
+ if (!result.ok) {
68
+ console.error(result.error);
69
+ if (options.json) {
70
+ console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
71
+ }
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+
76
+ if (options.json) {
77
+ console.log(JSON.stringify({ plugins: result.plugins }, null, 2));
78
+ return;
79
+ }
80
+
81
+ if (result.plugins.length === 0) {
82
+ console.log('No plugins installed.');
83
+ return;
84
+ }
85
+
86
+ console.log(`Installed plugins: ${result.plugins.length}`);
87
+ for (const plugin of result.plugins) {
88
+ console.log(` ${plugin.name}@${plugin.version || 'unknown'}`);
89
+ console.log(` Path: ${plugin.install_path || 'unknown'}`);
90
+ console.log(` Source: ${plugin.source?.type || 'unknown'} (${plugin.source?.spec || 'unknown'})`);
91
+ console.log(` Present: ${plugin.installed ? 'yes' : 'no'}`);
92
+ const bindings = Object.entries(plugin.hooks || {})
93
+ .map(([phase, hooks]) => `${phase}: ${hooks.join(', ')}`)
94
+ .join(' | ');
95
+ console.log(` Hooks: ${bindings || 'none'}`);
96
+ }
97
+ }
98
+
99
+ export async function pluginRemoveCommand(name, options) {
100
+ const result = removePlugin(name, process.cwd());
101
+
102
+ if (!result.ok) {
103
+ console.error(result.error);
104
+ if (options.json) {
105
+ console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
106
+ }
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+
111
+ if (options.json) {
112
+ console.log(JSON.stringify(result, null, 2));
113
+ return;
114
+ }
115
+
116
+ console.log(`Removed plugin: ${result.name}`);
117
+ console.log(` Path: ${result.install_path}`);
118
+ }
119
+
120
+ export async function pluginUpgradeCommand(name, source, options) {
121
+ const config = parsePluginConfigOptions(options);
122
+ if (!config.ok) {
123
+ console.error(config.error);
124
+ if (options.json) {
125
+ console.log(JSON.stringify({ ok: false, error: config.error }, null, 2));
126
+ }
127
+ process.exitCode = 1;
128
+ return;
129
+ }
130
+
131
+ const result = upgradePlugin(name, source, process.cwd(), { config: config.config });
132
+
133
+ if (!result.ok) {
134
+ console.error(result.error);
135
+ if (options.json) {
136
+ console.log(JSON.stringify({ ok: false, error: result.error, collisions: result.collisions || [] }, null, 2));
137
+ }
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+
142
+ if (options.json) {
143
+ console.log(JSON.stringify(result, null, 2));
144
+ return;
145
+ }
146
+
147
+ console.log(`Upgraded plugin: ${result.name}@${result.version}`);
148
+ console.log(` Source: ${result.source.type} (${result.source.spec})`);
149
+ console.log(` Path: ${result.install_path}`);
150
+ if (result.config !== undefined) {
151
+ console.log(` Config: ${JSON.stringify(result.config)}`);
152
+ }
153
+ console.log(' Hooks:');
154
+ for (const [phase, hookNames] of Object.entries(result.hooks || {})) {
155
+ console.log(` ${phase}: ${hookNames.join(', ')}`);
156
+ }
157
+ }