agentxchain 2.129.0 → 2.130.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.
@@ -71,6 +71,7 @@ import { unblockCommand } from '../src/commands/unblock.js';
71
71
  import { injectCommand } from '../src/commands/inject.js';
72
72
  import { escalateCommand } from '../src/commands/escalate.js';
73
73
  import { acceptTurnCommand } from '../src/commands/accept-turn.js';
74
+ import { checkpointTurnCommand } from '../src/commands/checkpoint-turn.js';
74
75
  import { rejectTurnCommand } from '../src/commands/reject-turn.js';
75
76
  import { reissueTurnCommand } from '../src/commands/reissue-turn.js';
76
77
  import { proposalListCommand, proposalDiffCommand, proposalApplyCommand, proposalRejectCommand } from '../src/commands/proposal.js';
@@ -672,9 +673,16 @@ program
672
673
  .command('accept-turn')
673
674
  .description('Accept the currently staged governed turn result')
674
675
  .option('--turn <id>', 'Target a specific active turn when multiple turns exist')
676
+ .option('--checkpoint', 'Checkpoint the accepted turn to git immediately after acceptance')
675
677
  .option('--resolution <mode>', 'Conflict resolution mode for conflicted turns (standard, human_merge)', 'standard')
676
678
  .action(acceptTurnCommand);
677
679
 
680
+ program
681
+ .command('checkpoint-turn')
682
+ .description('Checkpoint the latest accepted turn into git so the next writable turn has a clean baseline')
683
+ .option('--turn <id>', 'Checkpoint a specific accepted turn from history')
684
+ .action(checkpointTurnCommand);
685
+
678
686
  program
679
687
  .command('reject-turn')
680
688
  .description('Reject the current governed turn result and retry or escalate')
@@ -727,6 +735,8 @@ program
727
735
  .option('--triage-approval <mode>', 'Triage policy for vision-derived intents: auto or human (default: config or auto)')
728
736
  .option('--max-idle-cycles <n>', 'Stop after N consecutive idle cycles with no derivable work (default: 3)', parseInt)
729
737
  .option('--session-budget <usd>', 'Cumulative session-level budget cap in USD for continuous mode', parseFloat)
738
+ .option('--auto-checkpoint', 'Auto-commit accepted writable turns after acceptance')
739
+ .option('--no-auto-checkpoint', 'Disable automatic checkpointing after accepted writable turns')
730
740
  .action(runCommand);
731
741
 
732
742
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.129.0",
3
+ "version": "2.130.1",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,8 @@
24
24
  "dev": "node bin/agentxchain.js",
25
25
  "test": "npm run test:vitest && npm run test:node",
26
26
  "test:vitest": "vitest run --reporter=verbose",
27
- "test:node": "node --test test/*.test.js",
27
+ "test:beta": "node --test test/beta-tester-scenarios/*.test.js",
28
+ "test:node": "node --test test/*.test.js test/beta-tester-scenarios/*.test.js",
28
29
  "preflight:release": "bash scripts/release-preflight.sh",
29
30
  "preflight:release:strict": "bash scripts/release-preflight.sh --strict",
30
31
  "check:release-alignment": "node scripts/check-release-alignment.mjs",
@@ -110,24 +110,37 @@ if [[ "$PUBLISH_GATE" -eq 1 ]]; then
110
110
  echo "[3/7] Release-gate tests (targeted subset)"
111
111
  # In publish-gate mode, run only release-critical tests to avoid CI hangs.
112
112
  # The full test suite is a pre-tag responsibility, not a publish-time gate.
113
- GATE_TESTS=(
113
+ GATE_TEST_PATTERNS=(
114
114
  test/release-preflight.test.js
115
115
  test/release-docs-content.test.js
116
116
  test/release-notes-gate.test.js
117
117
  test/release-identity-hardening.test.js
118
118
  test/normalized-config.test.js
119
119
  test/conformance.test.js
120
+ test/beta-tester-scenarios/*.test.js
120
121
  )
121
122
  GATE_TEST_ARGS=()
122
- for t in "${GATE_TESTS[@]}"; do
123
- if [[ -f "$t" ]]; then
123
+ shopt -s nullglob
124
+ for pattern in "${GATE_TEST_PATTERNS[@]}"; do
125
+ for t in $pattern; do
124
126
  GATE_TEST_ARGS+=("$t")
125
- fi
127
+ done
126
128
  done
129
+ shopt -u nullglob
127
130
  if [[ ${#GATE_TEST_ARGS[@]} -eq 0 ]]; then
128
131
  fail "No release-gate test files found"
129
132
  else
130
- if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 node --test "${GATE_TEST_ARGS[@]}"; then
133
+ BETA_TEST_COUNT=0
134
+ for t in "${GATE_TEST_ARGS[@]}"; do
135
+ if [[ "$t" == test/beta-tester-scenarios/*.test.js ]]; then
136
+ BETA_TEST_COUNT=$((BETA_TEST_COUNT + 1))
137
+ fi
138
+ done
139
+ if [[ "$BETA_TEST_COUNT" -eq 0 ]]; then
140
+ fail "No beta-tester scenario tests found for release-gate verification"
141
+ TEST_OUTPUT=""
142
+ TEST_STATUS=1
143
+ elif run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 node --test "${GATE_TEST_ARGS[@]}"; then
131
144
  TEST_STATUS=0
132
145
  else
133
146
  TEST_STATUS=$?
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { loadProjectContext } from '../lib/config.js';
3
3
  import { acceptGovernedTurn } from '../lib/governed-state.js';
4
4
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
5
+ import { checkpointAcceptedTurn } from '../lib/turn-checkpoint.js';
5
6
 
6
7
  export async function acceptTurnCommand(opts = {}) {
7
8
  const context = loadProjectContext();
@@ -166,6 +167,19 @@ export async function acceptTurnCommand(opts = {}) {
166
167
  if (accepted?.cost?.usd != null) {
167
168
  console.log(` ${chalk.dim('Cost:')} $${formatUsd(accepted.cost.usd)}`);
168
169
  }
170
+ if (opts.checkpoint) {
171
+ const checkpoint = checkpointAcceptedTurn(root, { turnId });
172
+ if (!checkpoint.ok) {
173
+ console.log(` ${chalk.yellow('Checkpoint:')} accepted but checkpoint failed`);
174
+ console.log(` ${chalk.dim('Action:')} ${checkpoint.error}`);
175
+ console.log(` ${chalk.dim('Retry:')} agentxchain checkpoint-turn --turn ${turnId}`);
176
+ console.log('');
177
+ process.exit(1);
178
+ }
179
+ if (!checkpoint.skipped) {
180
+ console.log(` ${chalk.dim('Checkpoint:')} ${checkpoint.checkpoint_sha}`);
181
+ }
182
+ }
169
183
  if (accepted?.verification_replay) {
170
184
  const verifiedAt = accepted.verification_replay.verified_at
171
185
  ? ` at ${accepted.verification_replay.verified_at}`
@@ -0,0 +1,35 @@
1
+ import chalk from 'chalk';
2
+ import { loadProjectContext } from '../lib/config.js';
3
+ import { checkpointAcceptedTurn } from '../lib/turn-checkpoint.js';
4
+
5
+ export async function checkpointTurnCommand(opts = {}) {
6
+ const context = loadProjectContext();
7
+ if (!context) {
8
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
9
+ process.exit(1);
10
+ }
11
+
12
+ const { root, config } = context;
13
+ if (config.protocol_mode !== 'governed') {
14
+ console.log(chalk.red('The checkpoint-turn command is only available for governed projects.'));
15
+ process.exit(1);
16
+ }
17
+
18
+ const result = checkpointAcceptedTurn(root, { turnId: opts.turn });
19
+ if (!result.ok) {
20
+ console.log(chalk.red(result.error || 'Failed to checkpoint accepted turn.'));
21
+ process.exit(1);
22
+ }
23
+
24
+ if (result.already_checkpointed) {
25
+ console.log(chalk.yellow(`Turn ${result.turn.turn_id} already has checkpoint ${result.checkpoint_sha}.`));
26
+ return;
27
+ }
28
+
29
+ if (result.skipped) {
30
+ console.log(chalk.dim(result.reason));
31
+ return;
32
+ }
33
+
34
+ console.log(chalk.green(`Checkpointed ${result.turn.turn_id} at ${result.checkpoint_sha}.`));
35
+ }
@@ -105,6 +105,7 @@ export async function connectorCheckCommand(runtimeId, options = {}) {
105
105
  const result = await probeConfiguredConnectors(context.config, {
106
106
  runtimeId: runtimeId || null,
107
107
  timeoutMs,
108
+ root: context.root,
108
109
  onProbeStart: options.json ? null : (probeRuntimeId, runtime) => {
109
110
  console.log(` ${chalk.dim('…')} Probing ${chalk.bold(probeRuntimeId)} ${chalk.dim(`(${runtime.type})`)}`);
110
111
  },
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'fs';
2
- import { execFileSync, execSync } from 'child_process';
2
+ import { execFileSync } from 'child_process';
3
3
  import { join } from 'path';
4
4
  import chalk from 'chalk';
5
5
  import { loadConfig, loadLock, findProjectRoot, loadProjectState } from '../lib/config.js';
@@ -17,9 +17,10 @@ import {
17
17
  summarizeRoleRuntimeCapability,
18
18
  summarizeRuntimeCapabilityContract,
19
19
  } from '../lib/runtime-capabilities.js';
20
- import { detectActiveTurnBindingDrift } from '../lib/governed-state.js';
20
+ import { detectActiveTurnBindingDrift, detectStateBundleDesync } from '../lib/governed-state.js';
21
21
  import { findPendingApprovedIntents } from '../lib/intake.js';
22
22
  import { checkCleanBaseline } from '../lib/repo-observer.js';
23
+ import { probeRuntimeSpawnContext } from '../lib/runtime-spawn-context.js';
23
24
 
24
25
  export async function doctorCommand(opts = {}) {
25
26
  const root = findProjectRoot(process.cwd());
@@ -90,7 +91,7 @@ function governedDoctor(root, rawConfig, opts) {
90
91
  const runtimes = (normalized && normalized.runtimes) || rawConfig.runtimes || {};
91
92
  const rolesByRuntime = buildRolesByRuntime(normalized?.roles || {});
92
93
  for (const [rtId, rt] of Object.entries(runtimes)) {
93
- const check = checkRuntimeReachable(rtId, rt, rolesByRuntime[rtId] || []);
94
+ const check = checkRuntimeReachable(root, rtId, rt, rolesByRuntime[rtId] || []);
94
95
  checks.push(check);
95
96
  }
96
97
  const connectorProbe = getConnectorProbeRecommendation(runtimes);
@@ -152,7 +153,36 @@ function governedDoctor(root, rawConfig, opts) {
152
153
  }
153
154
  }
154
155
 
155
- // 5c. Clean working tree pre-flight for authoritative/proposed roles
156
+ // 5c. BUG-18: State/bundle integrity active turns must have dispatch bundles
157
+ if (normalized && existsSync(join(root, '.agentxchain', 'state.json'))) {
158
+ try {
159
+ const stateData = loadProjectState(root, normalized);
160
+ const desync = detectStateBundleDesync(root, stateData);
161
+ if (!desync.ok) {
162
+ const desyncSummary = desync.desynced
163
+ .map(d => `${d.turn_id} (${d.role}): missing ${d.expected_path}`)
164
+ .join('; ');
165
+ checks.push({
166
+ id: 'bundle_integrity',
167
+ name: 'Dispatch bundle integrity',
168
+ level: 'fail',
169
+ detail: `Ghost turn(s): ${desyncSummary}. Run: agentxchain reissue-turn`,
170
+ desynced: desync.desynced,
171
+ });
172
+ } else if (Object.keys(stateData?.active_turns || {}).length > 0) {
173
+ checks.push({
174
+ id: 'bundle_integrity',
175
+ name: 'Dispatch bundle integrity',
176
+ level: 'pass',
177
+ detail: 'All active turns have dispatch bundles on disk',
178
+ });
179
+ }
180
+ } catch {
181
+ // State couldn't be loaded — skip integrity check
182
+ }
183
+ }
184
+
185
+ // 5d. Clean working tree pre-flight for authoritative/proposed roles
156
186
  if (normalized?.roles) {
157
187
  const writableRoles = Object.entries(normalized.roles)
158
188
  .filter(([, role]) => role.write_authority === 'authoritative' || role.write_authority === 'proposed')
@@ -455,7 +485,7 @@ function buildCliVersionCheck(cliVersionHealth) {
455
485
  };
456
486
  }
457
487
 
458
- function checkRuntimeReachable(rtId, rt, boundRoleEntries = []) {
488
+ function checkRuntimeReachable(root, rtId, rt, boundRoleEntries = []) {
459
489
  const base = { id: `runtime_${rtId}`, name: `Runtime: ${rtId}` };
460
490
 
461
491
  if (!rt || !rt.type) {
@@ -467,14 +497,8 @@ function checkRuntimeReachable(rtId, rt, boundRoleEntries = []) {
467
497
  return attachRuntimeContract({ ...base, level: 'pass', detail: 'Manual runtime (no binary needed)' }, rtId, rt, boundRoleEntries);
468
498
 
469
499
  case 'local_cli': {
470
- const cmd = Array.isArray(rt.command) ? rt.command[0] : (typeof rt.command === 'string' ? rt.command.split(/\s+/)[0] : null);
471
- if (!cmd) return attachRuntimeContract({ ...base, level: 'warn', detail: 'No command configured' }, rtId, rt, boundRoleEntries);
472
- try {
473
- execSync(`command -v ${cmd}`, { stdio: 'ignore' });
474
- return attachRuntimeContract({ ...base, level: 'pass', detail: `${cmd} binary found` }, rtId, rt, boundRoleEntries);
475
- } catch {
476
- return attachRuntimeContract({ ...base, level: 'fail', detail: `${cmd} not found in PATH` }, rtId, rt, boundRoleEntries);
477
- }
500
+ const probe = probeRuntimeSpawnContext(root, rt, { runtimeId: rtId });
501
+ return attachRuntimeContract({ ...base, level: probe.ok ? 'pass' : 'fail', detail: probe.detail }, rtId, rt, boundRoleEntries);
478
502
  }
479
503
 
480
504
  case 'api_proxy': {
@@ -494,14 +518,8 @@ function checkRuntimeReachable(rtId, rt, boundRoleEntries = []) {
494
518
  if (transport === 'streamable_http') {
495
519
  return attachRuntimeContract({ ...base, level: 'warn', detail: 'Remote MCP endpoint (cannot verify at doctor time)' }, rtId, rt, boundRoleEntries);
496
520
  }
497
- const cmd = Array.isArray(rt.command) ? rt.command[0] : (typeof rt.command === 'string' ? rt.command.split(/\s+/)[0] : null);
498
- if (!cmd) return attachRuntimeContract({ ...base, level: 'warn', detail: 'No MCP command configured' }, rtId, rt, boundRoleEntries);
499
- try {
500
- execSync(`command -v ${cmd}`, { stdio: 'ignore' });
501
- return attachRuntimeContract({ ...base, level: 'pass', detail: `${cmd} binary found` }, rtId, rt, boundRoleEntries);
502
- } catch {
503
- return attachRuntimeContract({ ...base, level: 'fail', detail: `${cmd} not found in PATH` }, rtId, rt, boundRoleEntries);
504
- }
521
+ const probe = probeRuntimeSpawnContext(root, rt, { runtimeId: rtId });
522
+ return attachRuntimeContract({ ...base, level: probe.ok ? 'pass' : 'fail', detail: probe.detail }, rtId, rt, boundRoleEntries);
505
523
  }
506
524
 
507
525
  case 'remote_agent':