agentxchain 2.110.0 → 2.112.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.
@@ -85,7 +85,7 @@ fi
85
85
  echo ""
86
86
 
87
87
  # 1. Clean working tree
88
- echo "[1/6] Git status"
88
+ echo "[1/7] Git status"
89
89
  if git diff --quiet HEAD 2>/dev/null && [ -z "$(git ls-files --others --exclude-standard 2>/dev/null)" ]; then
90
90
  pass "Working tree is clean"
91
91
  else
@@ -97,7 +97,7 @@ else
97
97
  fi
98
98
 
99
99
  # 2. Dependencies
100
- echo "[2/6] Dependencies"
100
+ echo "[2/7] Dependencies"
101
101
  if run_and_capture NPM_CI_OUTPUT npm ci --ignore-scripts; then
102
102
  pass "npm ci succeeded"
103
103
  else
@@ -107,7 +107,7 @@ fi
107
107
 
108
108
  # 3. Tests
109
109
  if [[ "$PUBLISH_GATE" -eq 1 ]]; then
110
- echo "[3/6] Release-gate tests (targeted subset)"
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
113
  GATE_TESTS=(
@@ -142,7 +142,7 @@ if [[ "$PUBLISH_GATE" -eq 1 ]]; then
142
142
  fi
143
143
  fi
144
144
  else
145
- echo "[3/6] Test suite"
145
+ echo "[3/7] Test suite"
146
146
  # Install MCP example deps — tests start example servers as subprocesses
147
147
  for example_dir in "${CLI_DIR}/../examples/mcp-echo-agent" "${CLI_DIR}/../examples/mcp-http-echo-agent"; do
148
148
  if [[ -f "${example_dir}/package.json" && ! -d "${example_dir}/node_modules" ]]; then
@@ -189,7 +189,7 @@ else
189
189
  fi
190
190
 
191
191
  # 4. CHANGELOG has target version
192
- echo "[4/6] CHANGELOG"
192
+ echo "[4/7] CHANGELOG"
193
193
  if grep -Fxq "## ${TARGET_VERSION}" CHANGELOG.md 2>/dev/null; then
194
194
  pass "CHANGELOG.md contains ${TARGET_VERSION} entry"
195
195
  else
@@ -197,7 +197,7 @@ else
197
197
  fi
198
198
 
199
199
  # 5. Package version
200
- echo "[5/6] Package version"
200
+ echo "[5/7] Package version"
201
201
  PKG_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).version)")
202
202
  echo " Current version: ${PKG_VERSION}"
203
203
  if [ "$PKG_VERSION" = "${TARGET_VERSION}" ]; then
@@ -210,8 +210,23 @@ else
210
210
  fi
211
211
  fi
212
212
 
213
- # 6. Pack dry-run
214
- echo "[6/6] npm pack --dry-run"
213
+ # 6. Release-alignment surfaces (shared manifest)
214
+ echo "[6/7] Release alignment (shared manifest)"
215
+ ALIGNMENT_SCRIPT="${SCRIPT_DIR}/check-release-alignment.mjs"
216
+ if [[ -f "$ALIGNMENT_SCRIPT" ]]; then
217
+ if run_and_capture ALIGNMENT_OUTPUT node "$ALIGNMENT_SCRIPT" --scope current --target-version "$TARGET_VERSION"; then
218
+ ALIGNED_COUNT="$(printf '%s\n' "$ALIGNMENT_OUTPUT" | awk -F'[,)]' '/surfaces/ { for (i=1;i<=NF;i++) if ($i ~ /[0-9]+ surfaces/) { gsub(/[^0-9]/,"",$i); print $i; exit } }')"
219
+ pass "Release alignment OK (${ALIGNED_COUNT:-all} surfaces)"
220
+ else
221
+ fail "Release alignment failed"
222
+ printf '%s\n' "$ALIGNMENT_OUTPUT" | head -20
223
+ fi
224
+ else
225
+ warn "check-release-alignment.mjs not found — skipping manifest validation"
226
+ fi
227
+
228
+ # 7. Pack dry-run
229
+ echo "[7/7] npm pack --dry-run"
215
230
  if run_and_capture PACK_OUTPUT npm pack --dry-run; then
216
231
  pass "npm pack --dry-run succeeded"
217
232
  PACK_SIZE_LINE="$(printf '%s\n' "$PACK_OUTPUT" | awk '/total files:/ { print; found=1 } END { if (!found) exit 1 }')"
@@ -0,0 +1,252 @@
1
+ /**
2
+ * agentxchain chain — operator-facing read surface for chain reports.
3
+ *
4
+ * Surfaces chain report metadata so operators can inspect lights-out
5
+ * run-chaining history without opening raw JSON files.
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import { findProjectRoot } from '../lib/config.js';
10
+ import {
11
+ loadAllChainReports,
12
+ loadChainReport,
13
+ loadLatestChainReport,
14
+ } from '../lib/chain-reports.js';
15
+
16
+ /**
17
+ * agentxchain chain latest — show the most recent chain report.
18
+ *
19
+ * @param {object} opts - { json?: boolean, dir?: string }
20
+ */
21
+ export async function chainLatestCommand(opts) {
22
+ const root = findProjectRoot(opts.dir || process.cwd());
23
+ if (!root) {
24
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
25
+ process.exit(1);
26
+ }
27
+
28
+ const report = loadLatestChainReport(root);
29
+ if (!report) {
30
+ console.log(chalk.dim('No chain reports found.'));
31
+ console.log(chalk.dim(' Run `agentxchain run --chain` to enable auto-chaining.'));
32
+ return;
33
+ }
34
+
35
+ if (opts.json) {
36
+ console.log(JSON.stringify(report, null, 2));
37
+ return;
38
+ }
39
+
40
+ renderChainReport(report);
41
+ }
42
+
43
+ /**
44
+ * agentxchain chain list — list all chain reports.
45
+ *
46
+ * @param {object} opts - { json?: boolean, limit?: number, dir?: string }
47
+ */
48
+ export async function chainListCommand(opts) {
49
+ const root = findProjectRoot(opts.dir || process.cwd());
50
+ if (!root) {
51
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
52
+ process.exit(1);
53
+ }
54
+
55
+ const reports = loadAllChainReports(root);
56
+ const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
57
+ const limited = reports.slice(0, limit);
58
+
59
+ if (opts.json) {
60
+ console.log(JSON.stringify(limited, null, 2));
61
+ return;
62
+ }
63
+
64
+ if (limited.length === 0) {
65
+ console.log(chalk.dim('No chain reports found.'));
66
+ console.log(chalk.dim(' Run `agentxchain run --chain` to enable auto-chaining.'));
67
+ return;
68
+ }
69
+
70
+ // Table header
71
+ const header = [
72
+ pad('#', 4),
73
+ pad('Chain ID', 16),
74
+ pad('Runs', 6),
75
+ pad('Turns', 7),
76
+ pad('Terminal Reason', 28),
77
+ pad('Duration', 12),
78
+ pad('Started', 22),
79
+ ].join(' ');
80
+
81
+ console.log(chalk.bold(header));
82
+ console.log(chalk.dim('─'.repeat(header.length)));
83
+
84
+ limited.forEach((report, i) => {
85
+ const idx = String(i + 1);
86
+ const chainId = report.chain_id || '—';
87
+ const runs = String(report.runs?.length || 0);
88
+ const turns = String(report.total_turns || 0);
89
+ const terminal = formatTerminalReason(report.terminal_reason);
90
+ const duration = report.total_duration_ms != null
91
+ ? formatDuration(report.total_duration_ms)
92
+ : '—';
93
+ const started = report.started_at
94
+ ? new Date(report.started_at).toLocaleString()
95
+ : '—';
96
+
97
+ console.log([
98
+ pad(idx, 4),
99
+ pad(chainId, 16),
100
+ pad(runs, 6),
101
+ pad(turns, 7),
102
+ pad(terminal, 28),
103
+ pad(duration, 12),
104
+ pad(started, 22),
105
+ ].join(' '));
106
+ });
107
+
108
+ console.log(chalk.dim(`\n${limited.length} chain(s) shown${reports.length > limit ? ` (${reports.length} total)` : ''}`));
109
+ }
110
+
111
+ /**
112
+ * agentxchain chain show <chain_id> — show a specific chain report.
113
+ *
114
+ * @param {string} chainId
115
+ * @param {object} opts - { json?: boolean, dir?: string }
116
+ */
117
+ export async function chainShowCommand(chainId, opts) {
118
+ const root = findProjectRoot(opts.dir || process.cwd());
119
+ if (!root) {
120
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
121
+ process.exit(1);
122
+ }
123
+
124
+ const report = loadChainReport(root, chainId);
125
+ if (!report) {
126
+ console.error(chalk.red(`Chain report not found: ${chainId}`));
127
+ console.log(chalk.dim(' Use `agentxchain chain list` to see available chain reports.'));
128
+ process.exit(1);
129
+ }
130
+
131
+ if (opts.json) {
132
+ console.log(JSON.stringify(report, null, 2));
133
+ return;
134
+ }
135
+
136
+ renderChainReport(report);
137
+ }
138
+
139
+ // ── Rendering ─────────────────────────────────────────────────────────────────
140
+
141
+ function renderChainReport(report) {
142
+ console.log(chalk.bold(`Chain Report: ${report.chain_id}`));
143
+ console.log('');
144
+ console.log(` Total runs: ${report.runs?.length || 0}`);
145
+ console.log(` Total turns: ${report.total_turns || 0}`);
146
+ console.log(` Duration: ${formatDuration(report.total_duration_ms || 0)}`);
147
+ console.log(` Terminal: ${formatTerminalReason(report.terminal_reason)}`);
148
+ console.log(` Started: ${report.started_at || '—'}`);
149
+ console.log(` Completed: ${report.completed_at || '—'}`);
150
+ console.log('');
151
+
152
+ if (!report.runs || report.runs.length === 0) {
153
+ console.log(chalk.dim(' No runs recorded.'));
154
+ return;
155
+ }
156
+
157
+ // Run table header
158
+ const runHeader = [
159
+ pad('#', 4),
160
+ pad('Run ID', 14),
161
+ pad('Status', 12),
162
+ pad('Trigger', 14),
163
+ pad('Turns', 7),
164
+ pad('Duration', 12),
165
+ pad('Parent', 14),
166
+ pad('Ctx', 40),
167
+ ].join(' ');
168
+
169
+ console.log(chalk.bold(' Runs:'));
170
+ console.log(` ${chalk.dim(runHeader)}`);
171
+ console.log(` ${chalk.dim('─'.repeat(runHeader.length))}`);
172
+
173
+ report.runs.forEach((run, i) => {
174
+ const idx = String(i + 1);
175
+ const runId = (run.run_id || '—').slice(0, 12);
176
+ const status = formatStatus(run.status);
177
+ const trigger = run.provenance_trigger || '—';
178
+ const turns = String(run.turns || 0);
179
+ const duration = run.duration_ms != null ? formatDuration(run.duration_ms) : '—';
180
+ const parent = run.parent_run_id ? run.parent_run_id.slice(0, 12) : '—';
181
+ const ctx = formatInheritedContextSummary(run.inherited_context_summary);
182
+
183
+ console.log(` ${[
184
+ pad(idx, 4),
185
+ pad(runId, 14),
186
+ pad(status, 12),
187
+ pad(trigger, 14),
188
+ pad(turns, 7),
189
+ pad(duration, 12),
190
+ pad(parent, 14),
191
+ pad(ctx, 40),
192
+ ].join(' ')}`);
193
+ });
194
+ }
195
+
196
+ function formatInheritedContextSummary(summary) {
197
+ if (!summary) return '—';
198
+
199
+ const parts = [];
200
+ if (summary.parent_roles_used?.length) {
201
+ parts.push(`${summary.parent_roles_used.length} roles`);
202
+ }
203
+ if (summary.parent_phases_completed_count > 0) {
204
+ parts.push(`${summary.parent_phases_completed_count} phases`);
205
+ }
206
+ if (summary.recent_decisions_count > 0) {
207
+ parts.push(`${summary.recent_decisions_count} decisions`);
208
+ }
209
+ if (summary.recent_accepted_turns_count > 0) {
210
+ parts.push(`${summary.recent_accepted_turns_count} turns`);
211
+ }
212
+
213
+ return parts.length > 0 ? parts.join(', ') : '—';
214
+ }
215
+
216
+ function formatTerminalReason(reason) {
217
+ if (!reason) return '—';
218
+ switch (reason) {
219
+ case 'chain_limit_reached': return chalk.cyan('chain limit reached');
220
+ case 'non_chainable_status': return chalk.yellow('non-chainable status');
221
+ case 'operator_abort': return chalk.red('operator abort');
222
+ case 'parent_validation_failed': return chalk.red('parent validation failed');
223
+ case 'completed': return chalk.green('completed');
224
+ case 'blocked': return chalk.yellow('blocked');
225
+ default: return reason;
226
+ }
227
+ }
228
+
229
+ function formatStatus(status) {
230
+ if (status === 'completed') return chalk.green('completed');
231
+ if (status === 'blocked') return chalk.yellow('blocked');
232
+ if (status === 'failed') return chalk.red('failed');
233
+ return status || '—';
234
+ }
235
+
236
+ function formatDuration(ms) {
237
+ if (ms < 1000) return `${ms}ms`;
238
+ const seconds = Math.floor(ms / 1000);
239
+ if (seconds < 60) return `${seconds}s`;
240
+ const minutes = Math.floor(seconds / 60);
241
+ const remainingSeconds = seconds % 60;
242
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
243
+ const hours = Math.floor(minutes / 60);
244
+ const remainingMinutes = minutes % 60;
245
+ return `${hours}h ${remainingMinutes}m`;
246
+ }
247
+
248
+ function pad(str, width) {
249
+ return String(str).padEnd(width);
250
+ }
251
+
252
+ // ── Data Loading ──────────────────────────────────────────────────────────────
@@ -215,9 +215,28 @@ function formatValue(value, label = '') {
215
215
  if (typeof value === 'boolean') return value ? 'yes' : 'no';
216
216
  if (label === 'Cost' || label === 'Budget') return `$${value.toFixed(4)}`;
217
217
  if (label === 'Duration') return formatDuration(value);
218
+ if (label === 'Blocked reason' && value && typeof value === 'object') {
219
+ return formatBlockedReason(value);
220
+ }
221
+ if (typeof value === 'object') return JSON.stringify(value);
218
222
  return String(value);
219
223
  }
220
224
 
225
+ function formatBlockedReason(reason) {
226
+ const category = reason.category || 'unknown';
227
+ const gateAction = reason.gate_action;
228
+ if (category === 'gate_action_failed' && gateAction) {
229
+ const actionLabel = gateAction.action_label || gateAction.command || 'unknown action';
230
+ if (gateAction.timed_out) {
231
+ return `gate_action_failed: ${actionLabel} timed out after ${gateAction.timeout_ms}ms`;
232
+ }
233
+ const exit = gateAction.exit_code != null ? ` (exit ${gateAction.exit_code})` : '';
234
+ return `gate_action_failed: ${actionLabel} failed${exit}`;
235
+ }
236
+ const detail = reason.detail || reason.recovery?.detail || '';
237
+ return detail ? `${category}: ${detail}` : category;
238
+ }
239
+
221
240
  function formatDelta(delta, label) {
222
241
  if (delta == null || delta === 0) return '';
223
242
  if (label === 'Cost' || label === 'Budget') {
@@ -0,0 +1,252 @@
1
+ import chalk from 'chalk';
2
+ import { findProjectRoot } from '../lib/config.js';
3
+ import {
4
+ attachChainToMission,
5
+ buildMissionListSummary,
6
+ buildMissionSnapshot,
7
+ createMission,
8
+ loadLatestMissionArtifact,
9
+ loadLatestMissionSnapshot,
10
+ loadMissionArtifact,
11
+ loadMissionSnapshot,
12
+ } from '../lib/missions.js';
13
+
14
+ export async function missionStartCommand(opts) {
15
+ const root = findProjectRoot(opts.dir || process.cwd());
16
+ if (!root) {
17
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
18
+ process.exit(1);
19
+ }
20
+
21
+ const title = String(opts.title || '').trim();
22
+ const goal = String(opts.goal || '').trim();
23
+ if (!title) {
24
+ console.error(chalk.red('Mission title is required. Use --title <text>.'));
25
+ process.exit(1);
26
+ }
27
+ if (!goal) {
28
+ console.error(chalk.red('Mission goal is required. Use --goal <text>.'));
29
+ process.exit(1);
30
+ }
31
+
32
+ const result = createMission(root, {
33
+ missionId: opts.id,
34
+ title,
35
+ goal,
36
+ });
37
+ if (!result.ok) {
38
+ console.error(chalk.red(result.error));
39
+ process.exit(1);
40
+ }
41
+
42
+ const snapshot = buildMissionSnapshot(root, result.mission);
43
+ if (opts.json) {
44
+ console.log(JSON.stringify(snapshot, null, 2));
45
+ return;
46
+ }
47
+
48
+ console.log(chalk.green(`Created mission ${snapshot.mission_id}`));
49
+ console.log(chalk.dim(` Goal: ${snapshot.goal}`));
50
+ renderMissionSnapshot(snapshot);
51
+ }
52
+
53
+ export async function missionListCommand(opts) {
54
+ const root = findProjectRoot(opts.dir || process.cwd());
55
+ if (!root) {
56
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
57
+ process.exit(1);
58
+ }
59
+
60
+ const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
61
+ const missions = buildMissionListSummary(root, limit);
62
+
63
+ if (opts.json) {
64
+ console.log(JSON.stringify(missions, null, 2));
65
+ return;
66
+ }
67
+
68
+ if (missions.length === 0) {
69
+ console.log(chalk.dim('No missions found.'));
70
+ console.log(chalk.dim(' Run `agentxchain mission start --title "..." --goal "..."` to create one.'));
71
+ return;
72
+ }
73
+
74
+ const header = [
75
+ pad('#', 4),
76
+ pad('Mission ID', 28),
77
+ pad('Status', 18),
78
+ pad('Chains', 8),
79
+ pad('Runs', 7),
80
+ pad('Turns', 7),
81
+ pad('Decisions', 10),
82
+ pad('Updated', 22),
83
+ 'Title',
84
+ ].join(' ');
85
+
86
+ console.log(chalk.bold(header));
87
+ console.log(chalk.dim('─'.repeat(header.length)));
88
+
89
+ missions.forEach((mission, index) => {
90
+ console.log([
91
+ pad(String(index + 1), 4),
92
+ pad(mission.mission_id || '—', 28),
93
+ pad(formatMissionStatus(mission.derived_status), 18),
94
+ pad(String(mission.chain_count || 0), 8),
95
+ pad(String(mission.total_runs || 0), 7),
96
+ pad(String(mission.total_turns || 0), 7),
97
+ pad(String(mission.active_repo_decisions_count || 0), 10),
98
+ pad(formatTimestamp(mission.updated_at), 22),
99
+ mission.title || '—',
100
+ ].join(' '));
101
+ });
102
+
103
+ console.log(chalk.dim(`\n${missions.length} mission(s) shown`));
104
+ }
105
+
106
+ export async function missionShowCommand(missionId, opts) {
107
+ const root = findProjectRoot(opts.dir || process.cwd());
108
+ if (!root) {
109
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
110
+ process.exit(1);
111
+ }
112
+
113
+ const snapshot = missionId
114
+ ? loadMissionSnapshot(root, missionId)
115
+ : loadLatestMissionSnapshot(root);
116
+ if (!snapshot) {
117
+ if (missionId) {
118
+ console.error(chalk.red(`Mission not found: ${missionId}`));
119
+ process.exit(1);
120
+ }
121
+ console.log(chalk.dim('No missions found.'));
122
+ console.log(chalk.dim(' Run `agentxchain mission start --title "..." --goal "..."` to create one.'));
123
+ return;
124
+ }
125
+
126
+ if (opts.json) {
127
+ console.log(JSON.stringify(snapshot, null, 2));
128
+ return;
129
+ }
130
+
131
+ renderMissionSnapshot(snapshot);
132
+ }
133
+
134
+ export async function missionAttachChainCommand(chainId, opts) {
135
+ const root = findProjectRoot(opts.dir || process.cwd());
136
+ if (!root) {
137
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
138
+ process.exit(1);
139
+ }
140
+
141
+ const mission = opts.mission
142
+ ? loadMissionArtifact(root, opts.mission)
143
+ : loadLatestMissionArtifact(root);
144
+ if (!mission) {
145
+ console.error(chalk.red('No mission found to attach to.'));
146
+ console.error(chalk.dim(' Use `agentxchain mission start --title "..." --goal "..."` first.'));
147
+ process.exit(1);
148
+ }
149
+
150
+ const result = attachChainToMission(root, mission.mission_id, chainId || 'latest');
151
+ if (!result.ok) {
152
+ console.error(chalk.red(result.error));
153
+ process.exit(1);
154
+ }
155
+
156
+ const snapshot = buildMissionSnapshot(root, result.mission);
157
+ if (opts.json) {
158
+ console.log(JSON.stringify(snapshot, null, 2));
159
+ return;
160
+ }
161
+
162
+ console.log(chalk.green(`Attached ${result.chain.chain_id} to ${snapshot.mission_id}`));
163
+ renderMissionSnapshot(snapshot);
164
+ }
165
+
166
+ function renderMissionSnapshot(snapshot) {
167
+ console.log(chalk.bold(`Mission: ${snapshot.mission_id}`));
168
+ console.log('');
169
+ console.log(` Title: ${snapshot.title || '—'}`);
170
+ console.log(` Goal: ${snapshot.goal || '—'}`);
171
+ console.log(` Status: ${formatMissionStatus(snapshot.derived_status)}`);
172
+ console.log(` Chains: ${snapshot.chain_count || 0}`);
173
+ console.log(` Total runs: ${snapshot.total_runs || 0}`);
174
+ console.log(` Total turns: ${snapshot.total_turns || 0}`);
175
+ console.log(` Active repo decisions: ${snapshot.active_repo_decisions_count || 0}`);
176
+ console.log(` Latest chain: ${snapshot.latest_chain_id || '—'}`);
177
+ console.log(` Latest terminal: ${snapshot.latest_terminal_reason || '—'}`);
178
+ console.log(` Created: ${snapshot.created_at || '—'}`);
179
+ console.log(` Updated: ${snapshot.updated_at || '—'}`);
180
+
181
+ if (snapshot.missing_chain_ids?.length) {
182
+ console.log(` Missing chains: ${snapshot.missing_chain_ids.join(', ')}`);
183
+ }
184
+
185
+ if (!snapshot.chains || snapshot.chains.length === 0) {
186
+ console.log('');
187
+ console.log(chalk.dim(' No chains attached.'));
188
+ console.log(chalk.dim(' Use `agentxchain mission attach-chain latest` after a chained run.'));
189
+ return;
190
+ }
191
+
192
+ const header = [
193
+ pad('#', 4),
194
+ pad('Chain ID', 16),
195
+ pad('Runs', 6),
196
+ pad('Turns', 7),
197
+ pad('Terminal', 26),
198
+ pad('Started', 22),
199
+ ].join(' ');
200
+
201
+ console.log('');
202
+ console.log(chalk.bold(' Chains:'));
203
+ console.log(` ${chalk.dim(header)}`);
204
+ console.log(` ${chalk.dim('─'.repeat(header.length))}`);
205
+
206
+ snapshot.chains.forEach((chain, index) => {
207
+ console.log(` ${[
208
+ pad(String(index + 1), 4),
209
+ pad(chain.chain_id || '—', 16),
210
+ pad(String(chain.runs?.length || 0), 6),
211
+ pad(String(chain.total_turns || 0), 7),
212
+ pad(formatTerminal(chain.terminal_reason), 26),
213
+ pad(formatTimestamp(chain.started_at), 22),
214
+ ].join(' ')}`);
215
+ });
216
+ }
217
+
218
+ function formatTerminal(reason) {
219
+ if (!reason) return '—';
220
+ if (reason === 'chain_limit_reached') return 'chain limit reached';
221
+ if (reason === 'non_chainable_status') return 'non-chainable status';
222
+ return reason.replace(/_/g, ' ');
223
+ }
224
+
225
+ function formatMissionStatus(status) {
226
+ if (!status) return '—';
227
+ switch (status) {
228
+ case 'planned':
229
+ return chalk.blue('planned');
230
+ case 'progressing':
231
+ return chalk.green('progressing');
232
+ case 'needs_attention':
233
+ return chalk.yellow('needs_attention');
234
+ case 'degraded':
235
+ return chalk.red('degraded');
236
+ default:
237
+ return status;
238
+ }
239
+ }
240
+
241
+ function formatTimestamp(value) {
242
+ if (!value) return '—';
243
+ try {
244
+ return new Date(value).toLocaleString();
245
+ } catch {
246
+ return value;
247
+ }
248
+ }
249
+
250
+ function pad(value, width) {
251
+ return String(value).padEnd(width);
252
+ }