agentxchain 2.133.0 → 2.134.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.133.0",
3
+ "version": "2.134.1",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,8 +2,9 @@
2
2
  # Release postflight — run this after publish succeeds.
3
3
  # Verifies: release tag exists, npm registry serves the version, metadata is present,
4
4
  # the published package resolves through npx, the published package can execute its
5
- # CLI entrypoint from the tarball, and runner package exports are importable in a
6
- # clean consumer project.
5
+ # CLI entrypoint from the tarball, runner package exports are importable in a
6
+ # clean consumer project, and the installed CLI can scaffold and validate a fresh
7
+ # governed workspace.
7
8
  # Usage: bash scripts/release-postflight.sh --target-version <semver> [--tag vX.Y.Z]
8
9
  set -uo pipefail
9
10
 
@@ -260,6 +261,64 @@ EOF
260
261
  return "$node_status"
261
262
  }
262
263
 
264
+ run_operator_frontdoor_smoke() {
265
+ if [[ -z "$TARBALL_URL" ]]; then
266
+ echo "registry tarball metadata unavailable for operator front-door smoke" >&2
267
+ return 1
268
+ fi
269
+
270
+ local smoke_root
271
+ local workspace
272
+ local bin_path
273
+ local smoke_npmrc
274
+ local install_status
275
+ local init_output
276
+ local init_status
277
+ local validate_output
278
+ local validate_status
279
+
280
+ smoke_root="$(mktemp -d "${TMPDIR:-/tmp}/agentxchain-operator-postflight.XXXXXX")"
281
+ workspace="${smoke_root}/workspace"
282
+ bin_path="${smoke_root}/bin/${PACKAGE_BIN_NAME}"
283
+ smoke_npmrc="${smoke_root}/.npmrc"
284
+ echo "registry=https://registry.npmjs.org/" > "$smoke_npmrc"
285
+
286
+ env -u NODE_AUTH_TOKEN NPM_CONFIG_USERCONFIG="$smoke_npmrc" \
287
+ npm install --global --prefix "$smoke_root" "$TARBALL_URL" >/dev/null 2>&1
288
+ install_status=$?
289
+ if [[ "$install_status" -ne 0 ]]; then
290
+ rm -rf "$smoke_root"
291
+ return "$install_status"
292
+ fi
293
+
294
+ if [[ ! -x "$bin_path" ]]; then
295
+ echo "installed binary missing at ${bin_path}" >&2
296
+ rm -rf "$smoke_root"
297
+ return 1
298
+ fi
299
+
300
+ init_output="$(
301
+ env -u NODE_AUTH_TOKEN NPM_CONFIG_USERCONFIG="$smoke_npmrc" \
302
+ "$bin_path" init --governed --template cli-tool --goal "Release operator smoke" --dir "$workspace" -y 2>&1
303
+ )"
304
+ init_status=$?
305
+ if [[ "$init_status" -ne 0 ]]; then
306
+ printf '%s\n' "$init_output"
307
+ rm -rf "$smoke_root"
308
+ return "$init_status"
309
+ fi
310
+
311
+ validate_output="$(
312
+ cd "$workspace" || exit 1
313
+ env -u NODE_AUTH_TOKEN NPM_CONFIG_USERCONFIG="$smoke_npmrc" \
314
+ "$bin_path" validate --mode kickoff --json 2>&1
315
+ )"
316
+ validate_status=$?
317
+ printf '%s\n' "$validate_output"
318
+ rm -rf "$smoke_root"
319
+ return "$validate_status"
320
+ }
321
+
263
322
  run_with_retry() {
264
323
  local __output_var="$1"
265
324
  local description="$2"
@@ -313,17 +372,17 @@ run_with_retry() {
313
372
 
314
373
  echo "AgentXchain v${TARGET_VERSION} Release Postflight"
315
374
  echo "====================================="
316
- echo "Checks release truth after publish: tag, registry visibility, metadata, npx smoke, CLI install smoke, and package export smoke."
375
+ echo "Checks release truth after publish: tag, registry visibility, metadata, npx smoke, CLI install smoke, package export smoke, and operator front-door smoke."
317
376
  echo ""
318
377
 
319
- echo "[1/7] Git tag"
378
+ echo "[1/8] Git tag"
320
379
  if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null 2>&1; then
321
380
  pass "Git tag ${TAG} exists locally"
322
381
  else
323
382
  fail "Git tag ${TAG} is missing locally"
324
383
  fi
325
384
 
326
- echo "[2/7] Registry version"
385
+ echo "[2/8] Registry version"
327
386
  if run_with_retry VERSION_OUTPUT "registry version" equals "${TARGET_VERSION}" npm view "${PACKAGE_NAME}@${TARGET_VERSION}" version; then
328
387
  PUBLISHED_VERSION="$(trim_last_line "$VERSION_OUTPUT")"
329
388
  if [[ "$PUBLISHED_VERSION" == "$TARGET_VERSION" ]]; then
@@ -336,7 +395,7 @@ else
336
395
  printf '%s\n' "$VERSION_OUTPUT" | tail -20
337
396
  fi
338
397
 
339
- echo "[3/7] Registry tarball metadata"
398
+ echo "[3/8] Registry tarball metadata"
340
399
  if run_with_retry TARBALL_OUTPUT "registry tarball metadata" nonempty "" npm view "${PACKAGE_NAME}@${TARGET_VERSION}" dist.tarball; then
341
400
  TARBALL_URL="$(trim_last_line "$TARBALL_OUTPUT")"
342
401
  if [[ -n "$TARBALL_URL" ]]; then
@@ -349,7 +408,7 @@ else
349
408
  printf '%s\n' "$TARBALL_OUTPUT" | tail -20
350
409
  fi
351
410
 
352
- echo "[4/7] Registry checksum metadata"
411
+ echo "[4/8] Registry checksum metadata"
353
412
  if run_with_retry INTEGRITY_OUTPUT "registry checksum metadata" nonempty "" npm view "${PACKAGE_NAME}@${TARGET_VERSION}" dist.integrity; then
354
413
  REGISTRY_CHECKSUM="$(trim_last_line "$INTEGRITY_OUTPUT")"
355
414
  fi
@@ -364,7 +423,7 @@ else
364
423
  fail "registry did not return checksum metadata"
365
424
  fi
366
425
 
367
- echo "[5/7] npx smoke"
426
+ echo "[5/8] npx smoke"
368
427
  if run_with_retry NPX_OUTPUT "npx smoke" nonempty "" run_npx_smoke; then
369
428
  NPX_VERSION="$(extract_matching_line "$NPX_OUTPUT" "$TARGET_VERSION" 2>/dev/null || trim_last_line "$NPX_OUTPUT")"
370
429
  if [[ "$NPX_VERSION" == "$TARGET_VERSION" ]]; then
@@ -377,7 +436,7 @@ else
377
436
  printf '%s\n' "$NPX_OUTPUT" | tail -20
378
437
  fi
379
438
 
380
- echo "[6/7] Install smoke"
439
+ echo "[6/8] Install smoke"
381
440
  if run_with_retry EXEC_OUTPUT "install smoke" nonempty "" run_install_smoke; then
382
441
  EXEC_VERSION="$(trim_last_line "$EXEC_OUTPUT")"
383
442
  if [[ "$EXEC_VERSION" == "$TARGET_VERSION" ]]; then
@@ -390,7 +449,7 @@ else
390
449
  printf '%s\n' "$EXEC_OUTPUT" | tail -20
391
450
  fi
392
451
 
393
- echo "[7/7] Package export smoke"
452
+ echo "[7/8] Package export smoke"
394
453
  if run_with_retry RUNNER_EXPORT_OUTPUT "runner export smoke" nonempty "" run_runner_export_smoke; then
395
454
  RUNNER_EXPORT_JSON="$(trim_last_line "$RUNNER_EXPORT_OUTPUT")"
396
455
  RUNNER_EXPORT_VERSION="$(printf '%s' "$RUNNER_EXPORT_JSON" | node --input-type=module -e "process.stdin.setEncoding('utf8'); let raw=''; process.stdin.on('data', (chunk) => raw += chunk); process.stdin.on('end', () => { const parsed = JSON.parse(raw); console.log(parsed.runner_interface_version || ''); });")"
@@ -410,6 +469,20 @@ else
410
469
  printf '%s\n' "$RUNNER_EXPORT_OUTPUT" | tail -20
411
470
  fi
412
471
 
472
+ echo "[8/8] Operator front-door smoke"
473
+ if run_with_retry OPERATOR_OUTPUT "operator front-door smoke" nonempty "" run_operator_frontdoor_smoke; then
474
+ OPERATOR_OK="$(printf '%s' "$OPERATOR_OUTPUT" | node --input-type=module -e "process.stdin.setEncoding('utf8'); let raw=''; process.stdin.on('data', (chunk) => raw += chunk); process.stdin.on('end', () => { const parsed = JSON.parse(raw); console.log(parsed.ok ? 'true' : 'false'); });" 2>/dev/null || true)"
475
+ OPERATOR_PROTOCOL_MODE="$(printf '%s' "$OPERATOR_OUTPUT" | node --input-type=module -e "process.stdin.setEncoding('utf8'); let raw=''; process.stdin.on('data', (chunk) => raw += chunk); process.stdin.on('end', () => { const parsed = JSON.parse(raw); console.log(parsed.protocol_mode || ''); });" 2>/dev/null || true)"
476
+ if [[ "$OPERATOR_OK" == "true" && "$OPERATOR_PROTOCOL_MODE" == "governed" ]]; then
477
+ pass "published CLI scaffolds and validates a governed workspace"
478
+ else
479
+ fail "published operator front-door smoke did not validate a governed workspace"
480
+ fi
481
+ else
482
+ fail "published operator front-door smoke failed"
483
+ printf '%s\n' "$OPERATOR_OUTPUT" | tail -20
484
+ fi
485
+
413
486
  echo ""
414
487
  echo "====================================="
415
488
  echo "Results: ${PASS} passed, ${FAIL} failed"
@@ -88,7 +88,10 @@ function printEvent(evt) {
88
88
  const coordinatorRetryDetail = evt.event_type === 'coordinator_retry' && evt.payload
89
89
  ? ` — ws ${evt.payload.workstream_id || '?'} repo ${evt.payload.repo_id || '?'} (retry of ${evt.payload.failed_turn_id || '?'})`
90
90
  : '';
91
- console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}${intentInfo}${conflictDetail}${conflictResolvedDetail}${rejectionDetail}${acceptanceFailedDetail}${phaseTransitionDetail}${gateFailedDetail}${humanEscalationDetail}${coordinatorRetryDetail}`);
91
+ const projectionWarningDetail = evt.event_type === 'coordinator_retry_projection_warning' && evt.payload
92
+ ? ` — ws ${evt.payload.workstream_id || '?'} repo ${evt.payload.repo_id || '?'} (reconciliation required)`
93
+ : '';
94
+ console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}${intentInfo}${conflictDetail}${conflictResolvedDetail}${rejectionDetail}${acceptanceFailedDetail}${phaseTransitionDetail}${gateFailedDetail}${humanEscalationDetail}${coordinatorRetryDetail}${projectionWarningDetail}`);
92
95
  }
93
96
 
94
97
  function formatConflictDetail(evt) {
@@ -150,6 +153,7 @@ function colorEventType(type) {
150
153
  gate_failed: chalk.red,
151
154
  budget_exceeded_warn: chalk.yellowBright,
152
155
  coordinator_retry: chalk.cyan.bold,
156
+ coordinator_retry_projection_warning: chalk.yellow.bold,
153
157
  turn_checkpointed: chalk.green,
154
158
  dispatch_progress: chalk.blue.dim,
155
159
  };
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync } from 'fs';
3
- import { resolve } from 'path';
3
+ import { join, resolve } from 'path';
4
4
  import { findProjectRoot, loadProjectContext } from '../lib/config.js';
5
5
  import {
6
6
  attachChainToMission,
@@ -37,6 +37,8 @@ import { executeChainedRun } from '../lib/run-chain.js';
37
37
  import { executeGovernedRun } from './run.js';
38
38
  import { dispatchCoordinatorTurn, selectAssignmentForWorkstream } from '../lib/coordinator-dispatch.js';
39
39
  import { loadCoordinatorState } from '../lib/coordinator-state.js';
40
+ import { projectRepoAcceptance } from '../lib/coordinator-acceptance.js';
41
+ import { emitRunEvent } from '../lib/run-events.js';
40
42
 
41
43
  export async function missionStartCommand(opts) {
42
44
  const root = findProjectRoot(opts.dir || process.cwd());
@@ -144,6 +146,50 @@ export async function missionStartCommand(opts) {
144
146
  renderMissionSnapshot(snapshot);
145
147
  }
146
148
 
149
+ function loadAcceptedRepoTurn(repoPath, turnId) {
150
+ try {
151
+ const historyPath = join(repoPath, '.agentxchain', 'history.jsonl');
152
+ const content = readFileSync(historyPath, 'utf8').trim();
153
+ if (!content) return null;
154
+ const entries = content
155
+ .split('\n')
156
+ .map((line) => JSON.parse(line))
157
+ .filter((entry) => entry?.turn_id === turnId)
158
+ .filter((entry) => Boolean(entry?.accepted_at) || entry?.status === 'accepted' || entry?.status === 'completed');
159
+ return entries.at(-1) || null;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ function projectAcceptedCoordinatorTurn(workspacePath, coordinatorConfig, repoId, turnId, workstreamId, fallbackState) {
166
+ const acceptedTurn = loadAcceptedRepoTurn(coordinatorConfig.repos?.[repoId]?.resolved_path, turnId);
167
+ if (!acceptedTurn) {
168
+ return { ok: false, error: `Accepted turn ${turnId} not found in repo-local history for ${repoId}.` };
169
+ }
170
+
171
+ const currentState = loadCoordinatorState(workspacePath) || fallbackState;
172
+ if (!currentState) {
173
+ return { ok: false, error: `Coordinator state not found at ${workspacePath}.` };
174
+ }
175
+
176
+ return projectRepoAcceptance(
177
+ workspacePath,
178
+ currentState,
179
+ coordinatorConfig,
180
+ repoId,
181
+ acceptedTurn,
182
+ workstreamId,
183
+ );
184
+ }
185
+
186
+ function buildCoordinatorProjectionWarning(message) {
187
+ return {
188
+ code: 'coordinator_acceptance_projection_incomplete',
189
+ message,
190
+ };
191
+ }
192
+
147
193
  export async function missionListCommand(opts) {
148
194
  const root = findProjectRoot(opts.dir || process.cwd());
149
195
  if (!root) {
@@ -578,6 +624,7 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
578
624
  }
579
625
 
580
626
  const executor = opts._executeGovernedRun || executeGovernedRun;
627
+ const childLog = opts.json ? noop : (opts._log || console.log);
581
628
  const repoContext = loadProjectContext(retry.retryResult.repo_path);
582
629
  if (!repoContext) {
583
630
  console.error(chalk.red(`Cannot load project context for retried repo at ${retry.retryResult.repo_path}.`));
@@ -586,6 +633,7 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
586
633
 
587
634
  const runOpts = {
588
635
  autoApprove: !!opts.autoApprove,
636
+ log: childLog,
589
637
  provenance: {
590
638
  trigger: 'manual',
591
639
  created_by: 'operator',
@@ -594,6 +642,7 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
594
642
  };
595
643
 
596
644
  let execution;
645
+ const retryWarnings = [];
597
646
  try {
598
647
  execution = await executor(repoContext, runOpts);
599
648
  } catch (error) {
@@ -619,6 +668,34 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
619
668
  process.exit(1);
620
669
  }
621
670
 
671
+ if ((execution?.exitCode ?? 0) === 0) {
672
+ const projection = projectAcceptedCoordinatorTurn(
673
+ mission.coordinator.workspace_path,
674
+ coordinatorConfigResult.config,
675
+ retry.retryResult.repo_id,
676
+ retry.retryResult.reissued_turn_id,
677
+ opts.workstream,
678
+ loadCoordinatorState(mission.coordinator.workspace_path),
679
+ );
680
+ if (!projection.ok) {
681
+ const warning = buildCoordinatorProjectionWarning(projection.error);
682
+ retryWarnings.push(warning);
683
+ console.error(chalk.yellow(`Coordinator retry projection warning: ${warning.message}`));
684
+ emitRunEvent(mission.coordinator.workspace_path, 'coordinator_retry_projection_warning', {
685
+ run_id: mission.coordinator.super_run_id,
686
+ phase: coordinatorConfigResult.config?.workstreams?.[opts.workstream]?.phase || null,
687
+ status: 'active',
688
+ payload: {
689
+ workstream_id: opts.workstream,
690
+ repo_id: retry.retryResult.repo_id,
691
+ reissued_turn_id: retry.retryResult.reissued_turn_id,
692
+ warning_code: warning.code,
693
+ warning_message: warning.message,
694
+ },
695
+ });
696
+ }
697
+ }
698
+
622
699
  const syncedRetry = synchronizeCoordinatorPlanState(root, mission, retry.plan);
623
700
  const retriedPlan = syncedRetry.ok ? syncedRetry.plan : retry.plan;
624
701
  const retriedWorkstream = retriedPlan.workstreams.find((ws) => ws.workstream_id === opts.workstream);
@@ -643,6 +720,8 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
643
720
  workstream_status: retriedWorkstream?.launch_status || 'launched',
644
721
  launch_record: retriedLaunchRecord,
645
722
  exit_code: execution?.exitCode ?? 0,
723
+ warnings: retryWarnings,
724
+ reconciliation_required: retryWarnings.length > 0,
646
725
  }, null, 2));
647
726
  if ((execution?.exitCode ?? 0) !== 0) {
648
727
  process.exit(execution.exitCode);
@@ -659,6 +738,9 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
659
738
  console.log(chalk.dim(` Old Turn: ${retry.retryResult.failed_turn_id}`));
660
739
  console.log(chalk.dim(` New Turn: ${retry.retryResult.reissued_turn_id}`));
661
740
  console.log(chalk.dim(` Workstream: ${retriedWorkstream?.launch_status || 'launched'}`));
741
+ if (retryWarnings.length > 0) {
742
+ console.log(chalk.yellow(` Warning: ${retryWarnings[0].message}`));
743
+ }
662
744
  console.log('');
663
745
  renderPlan(retriedPlan);
664
746
  if ((execution?.exitCode ?? 0) !== 0) {
@@ -1385,7 +1467,7 @@ async function dispatchAndExecuteCoordinatorWorkstream(
1385
1467
  root, mission, plan, workstreamId, coordinatorConfig, coordinatorState, opts,
1386
1468
  ) {
1387
1469
  const executor = opts._executeGovernedRun || executeGovernedRun;
1388
- const logger = opts._log || console.log;
1470
+ const logger = opts.json ? noop : (opts._log || console.log);
1389
1471
 
1390
1472
  // 1. Select assignment
1391
1473
  const assignment = selectAssignmentForWorkstream(
@@ -1456,6 +1538,26 @@ async function dispatchAndExecuteCoordinatorWorkstream(
1456
1538
  };
1457
1539
  }
1458
1540
 
1541
+ if ((execution?.exitCode ?? 0) === 0) {
1542
+ const projection = projectAcceptedCoordinatorTurn(
1543
+ mission.coordinator.workspace_path,
1544
+ coordinatorConfig,
1545
+ dispatch.repo_id,
1546
+ dispatch.turn_id,
1547
+ workstreamId,
1548
+ loadCoordinatorState(mission.coordinator.workspace_path) || coordinatorState,
1549
+ );
1550
+ if (!projection.ok) {
1551
+ return {
1552
+ ok: false,
1553
+ status: 'projection_error',
1554
+ repo_id: dispatch.repo_id,
1555
+ turn_id: dispatch.turn_id,
1556
+ error: projection.error,
1557
+ };
1558
+ }
1559
+ }
1560
+
1459
1561
  // 5. Sync plan state from coordinator (updates barriers, accepted repos, failures)
1460
1562
  const synced = synchronizeCoordinatorPlanState(root, mission, launch.plan);
1461
1563
 
@@ -2285,3 +2387,5 @@ function formatTimestamp(value) {
2285
2387
  function pad(value, width) {
2286
2388
  return String(value).padEnd(width);
2287
2389
  }
2390
+
2391
+ function noop() {}
@@ -24,6 +24,7 @@ import { getDashboardPid, getDashboardSession } from './dashboard.js';
24
24
  import { readPreemptionMarker, findPendingApprovedIntents } from '../lib/intake.js';
25
25
  import { readContinuousSession } from '../lib/continuous-run.js';
26
26
  import { readAllDispatchProgress } from '../lib/dispatch-progress.js';
27
+ import { readCoordinatorWarnings } from '../lib/coordinator-warnings.js';
27
28
 
28
29
  export async function statusCommand(opts) {
29
30
  const context = loadStatusContext();
@@ -111,10 +112,22 @@ function loadStatusContext(dir = process.cwd()) {
111
112
  return null;
112
113
  }
113
114
 
115
+ const fallbackConfig = {
116
+ ...rawConfig,
117
+ files: {
118
+ talk: rawConfig.files?.talk || 'TALK.md',
119
+ history: rawConfig.files?.history || '.agentxchain/history.jsonl',
120
+ state: rawConfig.files?.state || '.agentxchain/state.json',
121
+ log: Object.prototype.hasOwnProperty.call(rawConfig.files || {}, 'log')
122
+ ? rawConfig.files.log
123
+ : null,
124
+ },
125
+ };
126
+
114
127
  return {
115
128
  root,
116
129
  rawConfig,
117
- config: rawConfig,
130
+ config: fallbackConfig,
118
131
  version: 4,
119
132
  };
120
133
  }
@@ -122,6 +135,7 @@ function loadStatusContext(dir = process.cwd()) {
122
135
  function renderGovernedStatus(context, opts) {
123
136
  const { root, config, version } = context;
124
137
  const state = loadProjectState(root, config);
138
+ const stateRunId = state?.run_id || readRawStateRunId(root, config);
125
139
  const continuity = getContinuityStatus(root, state);
126
140
  const connectorHealth = getConnectorHealth(root, config, state);
127
141
  const recovery = deriveRecoveryDescriptor(state, config);
@@ -147,6 +161,9 @@ function renderGovernedStatus(context, opts) {
147
161
  const activeTurns = getActiveTurns(state);
148
162
  const dispatchProgress = filterDispatchProgressForActiveTurns(readAllDispatchProgress(root), activeTurns);
149
163
 
164
+ // Coordinator warning surfacing — DEC-COORD-RETRY-PROJECTION-EVENT-001
165
+ const coordinatorWarnings = readCoordinatorWarnings(root, { runId: stateRunId || null });
166
+
150
167
  if (opts.json) {
151
168
  const dashPid = getDashboardPid(root);
152
169
  const dashSession = getDashboardSession(root);
@@ -183,6 +200,7 @@ function renderGovernedStatus(context, opts) {
183
200
  dashboard_session: dashboardSessionObj,
184
201
  binding_drift: detectActiveTurnBindingDrift(state, config),
185
202
  bundle_integrity: detectStateBundleDesync(root, state),
203
+ coordinator_warnings: coordinatorWarnings,
186
204
  }, null, 2));
187
205
  return;
188
206
  }
@@ -284,6 +302,7 @@ function renderGovernedStatus(context, opts) {
284
302
  renderContinuityStatus(continuity, state);
285
303
  renderConnectorHealthStatus(connectorHealth);
286
304
  renderRecentEventSummary(recentEventSummary);
305
+ renderCoordinatorWarnings(coordinatorWarnings);
287
306
 
288
307
  // BUG-18: State/bundle integrity check
289
308
  const desync = detectStateBundleDesync(root, state);
@@ -908,3 +927,37 @@ function formatUsd(value) {
908
927
  if (typeof value !== 'number' || Number.isNaN(value)) return '0.00';
909
928
  return value.toFixed(2);
910
929
  }
930
+
931
+ /**
932
+ * Read coordinator retry projection warnings from the event log.
933
+ * Returns a structured summary for both JSON and CLI output.
934
+ */
935
+ function renderCoordinatorWarnings(warnings) {
936
+ if (!warnings || warnings.count === 0) return;
937
+ console.log(chalk.yellow.bold(` ⚠ Coordinator reconciliation required (${warnings.count} projection warning${warnings.count !== 1 ? 's' : ''})`));
938
+ for (const w of warnings.warnings) {
939
+ const wsLabel = w.workstream_id ? `ws:${w.workstream_id}` : '';
940
+ const repoLabel = w.repo_id ? `repo:${w.repo_id}` : '';
941
+ const parts = [wsLabel, repoLabel].filter(Boolean).join(' ');
942
+ console.log(` ${chalk.yellow('●')} ${parts} — ${w.warning_code}`);
943
+ }
944
+ console.log(chalk.dim(' Run `agentxchain mission plan show latest --json` to force plan sync and verify.'));
945
+ console.log('');
946
+ }
947
+
948
+ function readRawStateRunId(root, config) {
949
+ const relPath = config?.files?.state || '.agentxchain/state.json';
950
+ const filePath = join(root, relPath);
951
+ if (!existsSync(filePath)) {
952
+ return null;
953
+ }
954
+
955
+ try {
956
+ const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
957
+ return typeof parsed?.run_id === 'string' && parsed.run_id.trim().length > 0
958
+ ? parsed.run_id.trim()
959
+ : null;
960
+ } catch {
961
+ return null;
962
+ }
963
+ }
@@ -1,7 +1,7 @@
1
1
  import { appendFileSync, copyFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { loadProjectContext, loadProjectState } from './config.js';
4
- import { assignGovernedTurn, getActiveTurnCount } from './governed-state.js';
4
+ import { assignGovernedTurn, getActiveTurnCount, initializeGovernedRun } from './governed-state.js';
5
5
  import { writeDispatchBundle } from './dispatch-bundle.js';
6
6
  import { getDispatchTurnDir } from './turn-paths.js';
7
7
  import { readBarriers, readCoordinatorHistory, recordCoordinatorDecision } from './coordinator-state.js';
@@ -253,6 +253,17 @@ export function dispatchCoordinatorTurn(workspacePath, state, config, assignment
253
253
  return { ok: false, error: runtime.detail };
254
254
  }
255
255
 
256
+ let repoState = runtime.state;
257
+ if (repoState.status === 'idle' || repoState.status === 'completed') {
258
+ const initResult = initializeGovernedRun(runtime.root, runtime.config, {
259
+ allow_terminal_restart: repoState.status === 'completed',
260
+ });
261
+ if (!initResult.ok) {
262
+ return { ok: false, error: initResult.error };
263
+ }
264
+ repoState = initResult.state;
265
+ }
266
+
256
267
  const assignResult = assignGovernedTurn(runtime.root, runtime.config, assignment.role);
257
268
  if (!assignResult.ok) {
258
269
  return { ok: false, error: assignResult.error };
@@ -0,0 +1,31 @@
1
+ import { readRunEvents } from './run-events.js';
2
+
3
+ function normalizeCoordinatorWarning(event) {
4
+ return {
5
+ event_id: event.event_id,
6
+ timestamp: event.timestamp,
7
+ run_id: event.run_id || null,
8
+ workstream_id: event.payload?.workstream_id || null,
9
+ repo_id: event.payload?.repo_id || null,
10
+ reissued_turn_id: event.payload?.reissued_turn_id || null,
11
+ warning_code: event.payload?.warning_code || 'coordinator_acceptance_projection_incomplete',
12
+ warning_message: event.payload?.warning_message || null,
13
+ };
14
+ }
15
+
16
+ export function readCoordinatorWarnings(root, { runId = null } = {}) {
17
+ const events = readRunEvents(root, { type: 'coordinator_retry_projection_warning' });
18
+ const filtered = runId
19
+ ? events.filter((event) => event.run_id === runId)
20
+ : events;
21
+
22
+ if (filtered.length === 0) {
23
+ return { count: 0, reconciliation_required: false, warnings: [] };
24
+ }
25
+
26
+ return {
27
+ count: filtered.length,
28
+ reconciliation_required: true,
29
+ warnings: filtered.map(normalizeCoordinatorWarning),
30
+ };
31
+ }
@@ -7,6 +7,8 @@
7
7
 
8
8
  import { buildPlanProgressSummary, loadAllPlans } from '../mission-plans.js';
9
9
  import { loadAllMissionArtifacts } from '../missions.js';
10
+ import { loadCoordinatorState } from '../coordinator-state.js';
11
+ import { readCoordinatorWarnings } from '../coordinator-warnings.js';
10
12
 
11
13
  /**
12
14
  * Build a dashboard-ready plan snapshot across all missions.
@@ -49,12 +51,19 @@ export function readPlanSnapshot(workspacePath, { limit, missionId } = {}) {
49
51
  latestSummary = buildPlanSummary(latest);
50
52
  }
51
53
 
54
+ // Surface coordinator projection warnings for dashboard consumers
55
+ const coordinatorState = loadCoordinatorState(workspacePath);
56
+ const coordinatorWarnings = readCoordinatorWarnings(workspacePath, {
57
+ runId: coordinatorState?.super_run_id || null,
58
+ });
59
+
52
60
  return {
53
61
  ok: true,
54
62
  status: 200,
55
63
  body: {
56
64
  latest: latestSummary,
57
65
  plans: plans.map(buildPlanSummary),
66
+ coordinator_warnings: coordinatorWarnings,
58
67
  },
59
68
  };
60
69
  }
@@ -12,7 +12,7 @@ import { join } from 'path';
12
12
  import { loadChainReport } from './chain-reports.js';
13
13
  import { writeDispatchBundle } from './dispatch-bundle.js';
14
14
  import { loadProjectContext, loadProjectState } from './config.js';
15
- import { reissueTurn } from './governed-state.js';
15
+ import { reactivateGovernedRun, reissueTurn } from './governed-state.js';
16
16
  import { emitRunEvent } from './run-events.js';
17
17
  import { readBarriers, readCoordinatorHistory, recordCoordinatorDecision } from './coordinator-state.js';
18
18
  import { loadCoordinatorConfig } from './coordinator-config.js';
@@ -1074,7 +1074,19 @@ export function retryCoordinatorWorkstream(root, mission, planId, workstreamId,
1074
1074
  return { ok: false, error: reissued.error };
1075
1075
  }
1076
1076
 
1077
- const bundleResult = writeDispatchBundle(repoTurn.root, reissued.state, repoTurn.config, {
1077
+ let repoStateForBundle = reissued.state;
1078
+ if (reissued.state?.status === 'blocked' || reissued.state?.status === 'paused') {
1079
+ const reactivated = reactivateGovernedRun(repoTurn.root, reissued.state, {
1080
+ via: 'mission plan launch --retry',
1081
+ notificationConfig: repoTurn.config,
1082
+ });
1083
+ if (!reactivated.ok) {
1084
+ return { ok: false, error: `Turn reissued but blocked run could not be reactivated: ${reactivated.error}` };
1085
+ }
1086
+ repoStateForBundle = reactivated.state;
1087
+ }
1088
+
1089
+ const bundleResult = writeDispatchBundle(repoTurn.root, repoStateForBundle, repoTurn.config, {
1078
1090
  turnId: reissued.newTurn.turn_id,
1079
1091
  });
1080
1092
  if (!bundleResult.ok) {
@@ -47,6 +47,11 @@ function describeEvent(eventType, entry) {
47
47
  const retryRepo = trimToNull(entry.payload?.repo_id);
48
48
  return `${prefix}${eventType}${wsId ? ` ${wsId}` : ''}${retryRepo ? ` (${retryRepo})` : ''}`;
49
49
  }
50
+ case 'coordinator_retry_projection_warning': {
51
+ const wsIdWarn = trimToNull(entry.payload?.workstream_id);
52
+ const warnRepo = trimToNull(entry.payload?.repo_id);
53
+ return `${prefix}${eventType}${wsIdWarn ? ` ${wsIdWarn}` : ''}${warnRepo ? ` (${warnRepo})` : ''} — reconciliation required`;
54
+ }
50
55
  case 'turn_checkpointed':
51
56
  return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}`;
52
57
  case 'dispatch_progress':
@@ -23,6 +23,7 @@ export const VALID_RUN_EVENTS = [
23
23
  'turn_reissued',
24
24
  'turn_checkpointed',
25
25
  'coordinator_retry',
26
+ 'coordinator_retry_projection_warning',
26
27
  'run_blocked',
27
28
  'run_completed',
28
29
  'escalation_raised',