agentxchain 2.14.0 → 2.15.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.
package/README.md CHANGED
@@ -100,10 +100,10 @@ For initiatives spanning multiple governed repos, use the coordinator to add cro
100
100
  npx agentxchain init --governed --template api-service --dir repos/backend -y
101
101
  npx agentxchain init --governed --template web-app --dir repos/frontend -y
102
102
  agentxchain multi init
103
- agentxchain multi step --repo backend --role pm
103
+ agentxchain multi step --json
104
104
  ```
105
105
 
106
- See the [multi-repo quickstart](https://agentxchain.dev/docs/quickstart#multi-repo-cold-start) for the full cold-start walkthrough.
106
+ If the coordinator enters `blocked`, fix the cause and run `agentxchain multi resume` before continuing with `multi step` or `multi approve-gate`. See the [multi-repo quickstart](https://agentxchain.dev/docs/quickstart#multi-repo-cold-start) for the full cold-start walkthrough.
107
107
 
108
108
  ### Migrate a legacy project
109
109
 
@@ -132,7 +132,10 @@ agentxchain step
132
132
  | `template validate` | Prove the template registry, workflow-kit scaffold contract, and planning artifact completeness (`--json` exposes a `workflow_kit` block) |
133
133
  | `verify protocol` | Run the shipped protocol conformance suite against a target implementation |
134
134
  | `dashboard` | Open the local governance dashboard in your browser for repo-local runs or multi-repo coordinator initiatives, including pending gate approvals |
135
- | `plugin install|list|remove` | Install, inspect, or remove governed hook plugins backed by `agentxchain-plugin.json` manifests |
135
+ | `multi init\|status\|step\|resume\|approve-gate\|resync` | Run the multi-repo coordinator lifecycle, including blocked-state recovery via `multi resume` |
136
+ | `intake record\|triage\|approve\|plan\|start\|scan\|resolve` | Continuous-delivery intake: turn delivery signals into governed work items |
137
+ | `intake handoff` | Bridge a planned intake intent to a coordinator workstream for multi-repo execution |
138
+ | `plugin install\|list\|remove` | Install, inspect, or remove governed hook plugins backed by `agentxchain-plugin.json` manifests |
136
139
 
137
140
  ### Shared utilities
138
141
 
@@ -197,6 +200,7 @@ TALK.md
197
200
 
198
201
  - `manual`: implemented
199
202
  - `local_cli`: implemented
203
+ - `mcp`: implemented for stdio and streamable HTTP tool-contract dispatch
200
204
  - `api_proxy`: implemented for synchronous review-only turns and stages a provider-backed result during `step`
201
205
 
202
206
  ## Legacy IDE Mode
@@ -88,6 +88,7 @@ import {
88
88
  multiInitCommand,
89
89
  multiStatusCommand,
90
90
  multiStepCommand,
91
+ multiResumeCommand,
91
92
  multiApproveGateCommand,
92
93
  multiResyncCommand,
93
94
  } from '../src/commands/multi.js';
@@ -96,6 +97,7 @@ import { intakeTriageCommand } from '../src/commands/intake-triage.js';
96
97
  import { intakeApproveCommand } from '../src/commands/intake-approve.js';
97
98
  import { intakePlanCommand } from '../src/commands/intake-plan.js';
98
99
  import { intakeStartCommand } from '../src/commands/intake-start.js';
100
+ import { intakeHandoffCommand } from '../src/commands/intake-handoff.js';
99
101
  import { intakeScanCommand } from '../src/commands/intake-scan.js';
100
102
  import { intakeResolveCommand } from '../src/commands/intake-resolve.js';
101
103
  import { intakeStatusCommand } from '../src/commands/intake-status.js';
@@ -415,6 +417,12 @@ multiCmd
415
417
  .option('-j, --json', 'Output as JSON')
416
418
  .action(multiStepCommand);
417
419
 
420
+ multiCmd
421
+ .command('resume')
422
+ .description('Clear a blocked coordinator state after operator recovery')
423
+ .option('-j, --json', 'Output as JSON')
424
+ .action(multiResumeCommand);
425
+
418
426
  multiCmd
419
427
  .command('approve-gate')
420
428
  .description('Approve a pending phase transition or completion gate')
@@ -449,7 +457,7 @@ intakeCmd
449
457
  intakeCmd
450
458
  .command('triage')
451
459
  .description('Triage a detected intent — set priority, template, charter, and acceptance')
452
- .requiredOption('--intent <id>', 'Intent ID to triage')
460
+ .option('--intent <id>', 'Intent ID to triage')
453
461
  .option('--priority <level>', 'Priority level (p0, p1, p2, p3)')
454
462
  .option('--template <id>', 'Governed template (generic, api-service, cli-tool, library, web-app)')
455
463
  .option('--charter <text>', 'Delivery charter text')
@@ -486,10 +494,19 @@ intakeCmd
486
494
  .option('-j, --json', 'Output as JSON')
487
495
  .action(intakeStartCommand);
488
496
 
497
+ intakeCmd
498
+ .command('handoff')
499
+ .description('Hand off a planned intent to a coordinator workstream')
500
+ .option('--intent <id>', 'Intent ID to hand off')
501
+ .option('--coordinator-root <path>', 'Path to the coordinator workspace root')
502
+ .option('--workstream <id>', 'Coordinator workstream ID')
503
+ .option('-j, --json', 'Output as JSON')
504
+ .action(intakeHandoffCommand);
505
+
489
506
  intakeCmd
490
507
  .command('scan')
491
508
  .description('Scan a structured source snapshot into intake events')
492
- .requiredOption('--source <id>', 'Source type: ci_failure, git_ref_change, schedule')
509
+ .option('--source <id>', 'Source type: ci_failure, git_ref_change, schedule')
493
510
  .option('--file <path>', 'Path to snapshot JSON file')
494
511
  .option('--stdin', 'Read snapshot from stdin')
495
512
  .option('-j, --json', 'Output as JSON')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,7 +47,7 @@ if [[ -z "$TARGET_VERSION" ]]; then
47
47
  fi
48
48
 
49
49
  PACKAGE_NAME="$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json', 'utf8')).name)")"
50
- HOMEBREW_FORMULA="${REPO_ROOT}/cli/homebrew/agentxchain.rb"
50
+ CANONICAL_HOMEBREW_FORMULA_URL="${AGENTXCHAIN_DOWNSTREAM_FORMULA_URL:-https://raw.githubusercontent.com/shivamtiwari93/homebrew-tap/main/Formula/agentxchain.rb}"
51
51
 
52
52
  PASS=0
53
53
  FAIL=0
@@ -57,7 +57,7 @@ fail() { FAIL=$((FAIL + 1)); echo " FAIL: $1"; }
57
57
 
58
58
  echo "AgentXchain v${TARGET_VERSION} Downstream Release Truth"
59
59
  echo "====================================="
60
- echo "Checks downstream surfaces after publish: GitHub release, Homebrew tap."
60
+ echo "Checks downstream surfaces after publish: GitHub release, canonical Homebrew tap."
61
61
  echo ""
62
62
 
63
63
  # --- Check 1: GitHub Release ---
@@ -85,38 +85,39 @@ else
85
85
  fi
86
86
 
87
87
  # --- Get registry tarball URL and compute SHA ---
88
- echo "[2/3] Homebrew tap SHA matches registry tarball"
88
+ echo "[2/3] Canonical Homebrew tap SHA matches registry tarball"
89
89
  REGISTRY_TARBALL_URL="$(npm view "${PACKAGE_NAME}@${TARGET_VERSION}" dist.tarball 2>/dev/null || true)"
90
+ FORMULA_CONTENT="$(curl -fsSL "$CANONICAL_HOMEBREW_FORMULA_URL" 2>/dev/null || true)"
90
91
  if [[ -z "$REGISTRY_TARBALL_URL" ]]; then
91
92
  fail "cannot fetch registry tarball URL for ${PACKAGE_NAME}@${TARGET_VERSION}"
93
+ elif [[ -z "$FORMULA_CONTENT" ]]; then
94
+ fail "cannot fetch canonical Homebrew formula from ${CANONICAL_HOMEBREW_FORMULA_URL}"
92
95
  else
93
96
  REGISTRY_SHA="$(curl -sL "$REGISTRY_TARBALL_URL" | shasum -a 256 | awk '{print $1}')"
94
97
  if [[ -z "$REGISTRY_SHA" ]]; then
95
98
  fail "cannot compute SHA256 of registry tarball"
96
- elif [[ ! -f "$HOMEBREW_FORMULA" ]]; then
97
- fail "Homebrew formula not found at ${HOMEBREW_FORMULA}"
98
99
  else
99
- FORMULA_SHA="$(grep -E '^\s*sha256\s+"' "$HOMEBREW_FORMULA" | sed 's/.*sha256 *"\([a-f0-9]*\)".*/\1/')"
100
+ FORMULA_SHA="$(printf '%s\n' "$FORMULA_CONTENT" | grep -E '^\s*sha256\s+"' | sed 's/.*sha256 *"\([a-f0-9]*\)".*/\1/')"
100
101
  if [[ "$REGISTRY_SHA" == "$FORMULA_SHA" ]]; then
101
- pass "Homebrew formula SHA256 matches registry tarball (${REGISTRY_SHA:0:16}...)"
102
+ pass "canonical Homebrew formula SHA256 matches registry tarball (${REGISTRY_SHA:0:16}...)"
102
103
  else
103
- fail "Homebrew formula SHA256 mismatch: formula=${FORMULA_SHA:0:16}... registry=${REGISTRY_SHA:0:16}..."
104
+ fail "canonical Homebrew formula SHA256 mismatch: formula=${FORMULA_SHA:0:16}... registry=${REGISTRY_SHA:0:16}..."
104
105
  fi
105
106
  fi
106
107
  fi
107
108
 
108
109
  # --- Check 3: Homebrew tap URL matches registry tarball URL ---
109
- echo "[3/3] Homebrew tap URL matches registry tarball"
110
+ echo "[3/3] Canonical Homebrew tap URL matches registry tarball"
110
111
  if [[ -z "$REGISTRY_TARBALL_URL" ]]; then
111
112
  fail "cannot verify URL — registry tarball URL unavailable"
112
- elif [[ ! -f "$HOMEBREW_FORMULA" ]]; then
113
- fail "Homebrew formula not found at ${HOMEBREW_FORMULA}"
113
+ elif [[ -z "$FORMULA_CONTENT" ]]; then
114
+ fail "cannot verify URL canonical Homebrew formula unavailable"
114
115
  else
115
- FORMULA_URL="$(grep -E '^\s*url\s+"' "$HOMEBREW_FORMULA" | sed 's/.*url *"\([^"]*\)".*/\1/')"
116
+ FORMULA_URL="$(printf '%s\n' "$FORMULA_CONTENT" | grep -E '^\s*url\s+"' | sed 's/.*url *"\([^"]*\)".*/\1/')"
116
117
  if [[ "$FORMULA_URL" == "$REGISTRY_TARBALL_URL" ]]; then
117
- pass "Homebrew formula URL matches registry tarball"
118
+ pass "canonical Homebrew formula URL matches registry tarball"
118
119
  else
119
- fail "Homebrew formula URL mismatch: formula=${FORMULA_URL} registry=${REGISTRY_TARBALL_URL}"
120
+ fail "canonical Homebrew formula URL mismatch: formula=${FORMULA_URL} registry=${REGISTRY_TARBALL_URL}"
120
121
  fi
121
122
  fi
122
123
 
@@ -1,17 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { approveIntent } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
4
 
5
5
  export async function intakeApproveCommand(opts) {
6
- const root = findProjectRoot(process.cwd());
7
- if (!root) {
8
- if (opts.json) {
9
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
- } else {
11
- console.log(chalk.red('agentxchain.json not found'));
12
- }
13
- process.exit(2);
14
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
15
7
 
16
8
  if (!opts.intent) {
17
9
  const msg = '--intent <id> is required';
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+ import { handoffIntent } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
+
5
+ export async function intakeHandoffCommand(opts) {
6
+ const root = requireIntakeWorkspaceOrExit(opts);
7
+
8
+ if (!opts.intent) {
9
+ const msg = '--intent <id> is required';
10
+ if (opts.json) {
11
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
12
+ } else {
13
+ console.log(chalk.red(msg));
14
+ }
15
+ process.exit(1);
16
+ }
17
+
18
+ if (!opts.coordinatorRoot) {
19
+ const msg = '--coordinator-root <path> is required';
20
+ if (opts.json) {
21
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
22
+ } else {
23
+ console.log(chalk.red(msg));
24
+ }
25
+ process.exit(1);
26
+ }
27
+
28
+ if (!opts.workstream) {
29
+ const msg = '--workstream <id> is required';
30
+ if (opts.json) {
31
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
32
+ } else {
33
+ console.log(chalk.red(msg));
34
+ }
35
+ process.exit(1);
36
+ }
37
+
38
+ const result = handoffIntent(root, opts.intent, {
39
+ coordinatorRoot: opts.coordinatorRoot,
40
+ workstreamId: opts.workstream,
41
+ });
42
+
43
+ if (opts.json) {
44
+ console.log(JSON.stringify(result, null, 2));
45
+ } else if (result.ok) {
46
+ console.log('');
47
+ console.log(chalk.green(` Handed off intent ${result.intent.intent_id}`));
48
+ console.log(` Workstream: ${result.intent.target_workstream.workstream_id}`);
49
+ console.log(` Super Run: ${result.super_run_id}`);
50
+ console.log(` Handoff: ${result.handoff_path}`);
51
+ console.log(chalk.dim(' Status: planned → executing (coordinator-managed)'));
52
+ console.log('');
53
+ } else {
54
+ console.log(chalk.red(` ${result.error}`));
55
+ }
56
+
57
+ process.exit(result.exitCode);
58
+ }
@@ -1,18 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { planIntent } from '../lib/intake.js';
4
- import { basename } from 'node:path';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
5
4
 
6
5
  export async function intakePlanCommand(opts) {
7
- const root = findProjectRoot(process.cwd());
8
- if (!root) {
9
- if (opts.json) {
10
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
11
- } else {
12
- console.log(chalk.red('agentxchain.json not found'));
13
- }
14
- process.exit(2);
15
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
16
7
 
17
8
  if (!opts.intent) {
18
9
  const msg = '--intent <id> is required';
@@ -1,19 +1,11 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { findProjectRoot } from '../lib/config.js';
5
4
  import { recordEvent } from '../lib/intake.js';
5
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
6
6
 
7
7
  export async function intakeRecordCommand(opts) {
8
- const root = findProjectRoot(process.cwd());
9
- if (!root) {
10
- if (opts.json) {
11
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
12
- } else {
13
- console.log(chalk.red('agentxchain.json not found'));
14
- }
15
- process.exit(2);
16
- }
8
+ const root = requireIntakeWorkspaceOrExit(opts);
17
9
 
18
10
  let payload;
19
11
  try {
@@ -1,17 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { resolveIntent } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
4
 
5
5
  export async function intakeResolveCommand(opts) {
6
- const root = findProjectRoot(process.cwd());
7
- if (!root) {
8
- if (opts.json) {
9
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
- } else {
11
- console.log(chalk.red('agentxchain.json not found'));
12
- }
13
- process.exit(2);
14
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
15
7
 
16
8
  if (!opts.intent) {
17
9
  const msg = '--intent <id> is required';
@@ -1,19 +1,11 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { findProjectRoot } from '../lib/config.js';
5
4
  import { scanSource, SCAN_SOURCES } from '../lib/intake.js';
5
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
6
6
 
7
7
  export async function intakeScanCommand(opts) {
8
- const root = findProjectRoot(process.cwd());
9
- if (!root) {
10
- if (opts.json) {
11
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
12
- } else {
13
- console.log(chalk.red('agentxchain.json not found'));
14
- }
15
- process.exit(2);
16
- }
8
+ const root = requireIntakeWorkspaceOrExit(opts);
17
9
 
18
10
  if (!opts.source) {
19
11
  const msg = `--source is required. Supported scan sources: ${SCAN_SOURCES.join(', ')}`;
@@ -1,17 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { startIntent } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
4
 
5
5
  export async function intakeStartCommand(opts) {
6
- const root = findProjectRoot(process.cwd());
7
- if (!root) {
8
- if (opts.json) {
9
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
- } else {
11
- console.log(chalk.red('agentxchain.json not found'));
12
- }
13
- process.exit(2);
14
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
15
7
 
16
8
  if (!opts.intent) {
17
9
  const msg = '--intent <id> is required';
@@ -1,17 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { intakeStatus } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
4
 
5
5
  export async function intakeStatusCommand(opts) {
6
- const root = findProjectRoot(process.cwd());
7
- if (!root) {
8
- if (opts.json) {
9
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
- } else {
11
- console.log(chalk.red('agentxchain.json not found'));
12
- }
13
- process.exit(2);
14
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
15
7
 
16
8
  const result = intakeStatus(root, opts.intent || null);
17
9
 
@@ -72,6 +64,10 @@ function printIntentDetail(intent, event) {
72
64
  console.log(` ${chalk.dim('Charter:')} ${intent.charter || '—'}`);
73
65
  console.log(` ${chalk.dim('Created:')} ${intent.created_at}`);
74
66
  console.log(` ${chalk.dim('Updated:')} ${intent.updated_at}`);
67
+ if (intent.target_workstream) {
68
+ console.log(` ${chalk.dim('Workstream:')} ${intent.target_workstream.workstream_id}`);
69
+ console.log(` ${chalk.dim('Super Run:')} ${intent.target_workstream.super_run_id}`);
70
+ }
75
71
 
76
72
  if (intent.acceptance_contract && intent.acceptance_contract.length > 0) {
77
73
  console.log('');
@@ -1,17 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { triageIntent } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
4
 
5
5
  export async function intakeTriageCommand(opts) {
6
- const root = findProjectRoot(process.cwd());
7
- if (!root) {
8
- if (opts.json) {
9
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
- } else {
11
- console.log(chalk.red('agentxchain.json not found'));
12
- }
13
- process.exit(2);
14
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
15
7
 
16
8
  if (!opts.intent) {
17
9
  const msg = '--intent <id> is required';
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join, parse as pathParse, resolve } from 'node:path';
4
+ import { findProjectRoot } from '../lib/config.js';
5
+ import { COORDINATOR_CONFIG_FILE } from '../lib/coordinator-config.js';
6
+
7
+ function findCoordinatorWorkspaceRoot(startDir = process.cwd()) {
8
+ let dir = resolve(startDir);
9
+ const { root: fsRoot } = pathParse(dir);
10
+
11
+ while (true) {
12
+ if (existsSync(join(dir, COORDINATOR_CONFIG_FILE))) {
13
+ return dir;
14
+ }
15
+ if (dir === fsRoot) {
16
+ return null;
17
+ }
18
+ dir = join(dir, '..');
19
+ }
20
+ }
21
+
22
+ function listCoordinatorChildRepos(coordinatorRoot) {
23
+ const configPath = join(coordinatorRoot, COORDINATOR_CONFIG_FILE);
24
+ if (!existsSync(configPath)) {
25
+ return [];
26
+ }
27
+
28
+ try {
29
+ const raw = JSON.parse(readFileSync(configPath, 'utf8'));
30
+ return Object.keys(raw?.repos || {});
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ export function requireIntakeWorkspaceOrExit(opts, startDir = process.cwd()) {
37
+ const projectRoot = findProjectRoot(startDir);
38
+ if (projectRoot) {
39
+ return projectRoot;
40
+ }
41
+
42
+ const coordinatorRoot = findCoordinatorWorkspaceRoot(startDir);
43
+ const childRepos = coordinatorRoot ? listCoordinatorChildRepos(coordinatorRoot) : [];
44
+ const repoHint = childRepos.length > 0
45
+ ? ` Available child repos: ${childRepos.join(', ')}.`
46
+ : '';
47
+ const error = coordinatorRoot
48
+ ? `intake commands are repo-local only. Found coordinator workspace at ${coordinatorRoot} (${COORDINATOR_CONFIG_FILE}). Run intake inside a child governed repo (agentxchain.json).${repoHint} Then use \`agentxchain multi step\` for cross-repo coordination.`
49
+ : 'agentxchain.json not found';
50
+
51
+ if (opts.json) {
52
+ console.log(JSON.stringify({ ok: false, error }, null, 2));
53
+ } else {
54
+ console.log(chalk.red(error));
55
+ }
56
+
57
+ process.exit(2);
58
+ }
@@ -5,6 +5,7 @@
5
5
  * multi init — bootstrap a multi-repo coordinator run
6
6
  * multi status — show coordinator status and repo-run snapshots
7
7
  * multi step — reconcile repo truth, then dispatch or request the next coordinator gate
8
+ * multi resume — clear coordinator blocked state after operator recovery
8
9
  * multi approve-gate — approve a pending phase transition or completion gate
9
10
  * multi resync — detect divergence and rebuild coordinator state from repo authority
10
11
  */
@@ -26,7 +27,11 @@ import {
26
27
  requestCoordinatorCompletion,
27
28
  requestPhaseTransition,
28
29
  } from '../lib/coordinator-gates.js';
29
- import { detectDivergence, resyncFromRepoAuthority } from '../lib/coordinator-recovery.js';
30
+ import {
31
+ detectDivergence,
32
+ resyncFromRepoAuthority,
33
+ resumeCoordinatorFromBlockedState,
34
+ } from '../lib/coordinator-recovery.js';
30
35
  import {
31
36
  fireCoordinatorHook,
32
37
  buildAssignmentPayload,
@@ -154,7 +159,7 @@ export async function multiStepCommand(options) {
154
159
  // Fire on_escalation hook (advisory — cannot block, only notifies)
155
160
  fireEscalationHook(workspacePath, configResult.config, state, state.blocked_reason || 'unknown reason');
156
161
  console.error(`Coordinator is blocked: ${state.blocked_reason || 'unknown reason'}`);
157
- console.error('Resolve the blocked state before stepping.');
162
+ console.error('Resolve the blocked state, then run `agentxchain multi resume` before stepping again.');
158
163
  process.exitCode = 1;
159
164
  return;
160
165
  }
@@ -362,6 +367,57 @@ function maybeRequestCoordinatorGate(workspacePath, state, config) {
362
367
  return { ok: false, type: 'run_completion', blockers: completionEvaluation.blockers };
363
368
  }
364
369
 
370
+ // ── multi resume ───────────────────────────────────────────────────────────
371
+
372
+ export async function multiResumeCommand(options) {
373
+ const workspacePath = process.cwd();
374
+ const configResult = loadCoordinatorConfig(workspacePath);
375
+
376
+ if (!configResult.ok) {
377
+ console.error('Coordinator config error:');
378
+ for (const err of configResult.errors || []) {
379
+ console.error(` - ${typeof err === 'string' ? err : err.message || JSON.stringify(err)}`);
380
+ }
381
+ process.exitCode = 1;
382
+ return;
383
+ }
384
+
385
+ const state = loadCoordinatorState(workspacePath);
386
+ if (!state) {
387
+ console.error('No coordinator state found. Run `agentxchain multi init` first.');
388
+ process.exitCode = 1;
389
+ return;
390
+ }
391
+
392
+ const result = resumeCoordinatorFromBlockedState(workspacePath, state, configResult.config);
393
+
394
+ if (!result.ok) {
395
+ console.error(result.error || 'Coordinator recovery failed.');
396
+ process.exitCode = 1;
397
+ return;
398
+ }
399
+
400
+ if (options.json) {
401
+ console.log(JSON.stringify({
402
+ ok: true,
403
+ previous_status: 'blocked',
404
+ resumed_status: result.resumed_status,
405
+ blocked_reason: result.blocked_reason,
406
+ pending_gate: result.state?.pending_gate || null,
407
+ resync: result.resync,
408
+ }, null, 2));
409
+ return;
410
+ }
411
+
412
+ console.log(`Coordinator resumed: ${result.resumed_status}`);
413
+ console.log(`Previous block: ${result.blocked_reason}`);
414
+ if (result.resumed_status === 'paused' && result.state?.pending_gate) {
415
+ console.log(`Next action: agentxchain multi approve-gate (${result.state.pending_gate.gate})`);
416
+ } else {
417
+ console.log('Next action: agentxchain multi step');
418
+ }
419
+ }
420
+
365
421
  // ── multi approve-gate ─────────────────────────────────────────────────────
366
422
 
367
423
  export async function multiApproveGateCommand(options) {