agentxchain 2.65.0 → 2.67.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.65.0",
3
+ "version": "2.67.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -167,7 +167,10 @@ export async function acceptTurnCommand(opts = {}) {
167
167
  console.log(` ${chalk.dim('Cost:')} $${formatUsd(accepted.cost.usd)}`);
168
168
  }
169
169
  if (accepted?.verification_replay) {
170
- console.log(` ${chalk.dim('Replay:')} ${accepted.verification_replay.overall} (${accepted.verification_replay.matched_commands}/${accepted.verification_replay.replayed_commands})`);
170
+ const verifiedAt = accepted.verification_replay.verified_at
171
+ ? ` at ${accepted.verification_replay.verified_at}`
172
+ : '';
173
+ console.log(` ${chalk.dim('Replay:')} ${accepted.verification_replay.overall} (${accepted.verification_replay.matched_commands}/${accepted.verification_replay.replayed_commands})${verifiedAt}`);
171
174
  }
172
175
  if (result.budget_warning) {
173
176
  console.log(` ${chalk.yellow('Budget warning:')} ${result.budget_warning}`);
@@ -141,7 +141,16 @@ function renderGovernedStatus(context, opts) {
141
141
  const statusLabel = turn.status === 'conflicted'
142
142
  ? chalk.red('conflicted')
143
143
  : turn.status;
144
- console.log(` ${marker} ${turn.turn_id} ${chalk.bold(turn.assigned_role)} (${statusLabel}) [attempt ${turn.attempt}]`);
144
+ let elapsedTag = '';
145
+ if (turn.started_at) {
146
+ const elMs = Date.now() - new Date(turn.started_at).getTime();
147
+ if (elMs >= 0) {
148
+ const s = Math.floor(elMs / 1000);
149
+ const m = Math.floor(s / 60);
150
+ elapsedTag = m > 0 ? ` — ${m}m ${s % 60}s` : ` — ${s}s`;
151
+ }
152
+ }
153
+ console.log(` ${marker} ${turn.turn_id} — ${chalk.bold(turn.assigned_role)} (${statusLabel}) [attempt ${turn.attempt}]${elapsedTag}`);
145
154
  if (turn.status === 'conflicted' && turn.conflict_state) {
146
155
  const cs = turn.conflict_state;
147
156
  const files = cs.conflict_error?.conflicting_files || [];
@@ -161,6 +170,16 @@ function renderGovernedStatus(context, opts) {
161
170
  console.log(` ${chalk.dim('Role:')} ${chalk.bold(singleActiveTurn.assigned_role)} (${singleActiveTurn.status})`);
162
171
  console.log(` ${chalk.dim('Runtime:')} ${singleActiveTurn.runtime_id}`);
163
172
  console.log(` ${chalk.dim('Attempt:')} ${singleActiveTurn.attempt}`);
173
+ if (singleActiveTurn.started_at) {
174
+ const elapsedMs = Date.now() - new Date(singleActiveTurn.started_at).getTime();
175
+ if (elapsedMs >= 0) {
176
+ const secs = Math.floor(elapsedMs / 1000);
177
+ const mins = Math.floor(secs / 60);
178
+ const remainSecs = secs % 60;
179
+ const elapsed = mins > 0 ? `${mins}m ${remainSecs}s` : `${remainSecs}s`;
180
+ console.log(` ${chalk.dim('Elapsed:')} ${elapsed}`);
181
+ }
182
+ }
164
183
  if (singleActiveTurn.status === 'conflicted' && singleActiveTurn.conflict_state) {
165
184
  const cs = singleActiveTurn.conflict_state;
166
185
  const files = cs.conflict_error?.conflicting_files || [];
@@ -1024,7 +1024,10 @@ function printAcceptSummary(result) {
1024
1024
  console.log(` ${chalk.dim('Cost:')} $${(accepted.cost.usd || 0).toFixed(2)}`);
1025
1025
  }
1026
1026
  if (accepted?.verification_replay) {
1027
- console.log(` ${chalk.dim('Replay:')} ${accepted.verification_replay.overall} (${accepted.verification_replay.matched_commands}/${accepted.verification_replay.replayed_commands})`);
1027
+ const verifiedAt = accepted.verification_replay.verified_at
1028
+ ? ` at ${accepted.verification_replay.verified_at}`
1029
+ : '';
1030
+ console.log(` ${chalk.dim('Replay:')} ${accepted.verification_replay.overall} (${accepted.verification_replay.matched_commands}/${accepted.verification_replay.replayed_commands})${verifiedAt}`);
1028
1031
  }
1029
1032
  console.log('');
1030
1033
 
@@ -119,6 +119,7 @@ function buildArtifactIndex(root, turnId) {
119
119
  }
120
120
 
121
121
  function buildTurnPayload(turnId, turn, state, artifacts, assignment) {
122
+ const elapsedMs = getElapsedMs(turn.started_at);
122
123
  return {
123
124
  turn_id: turnId,
124
125
  run_id: state.run_id || assignment?.run_id || null,
@@ -127,6 +128,8 @@ function buildTurnPayload(turnId, turn, state, artifacts, assignment) {
127
128
  runtime: turn.runtime_id,
128
129
  status: turn.status,
129
130
  attempt: turn.attempt,
131
+ started_at: turn.started_at || null,
132
+ elapsed_ms: elapsedMs,
130
133
  dispatch_dir: getDispatchTurnDir(turnId),
131
134
  staging_result_path: assignment?.staging_result_path || null,
132
135
  active_turn_count: getActiveTurnCount(state),
@@ -140,6 +143,7 @@ function buildTurnPayload(turnId, turn, state, artifacts, assignment) {
140
143
  }
141
144
 
142
145
  function printTurnSummary(turnId, turn, state, artifacts, assignment) {
146
+ const elapsedMs = getElapsedMs(turn.started_at);
143
147
  console.log('');
144
148
  console.log(chalk.bold(` Turn: ${chalk.cyan(turnId)}`));
145
149
  console.log(chalk.dim(' ' + '─'.repeat(44)));
@@ -149,6 +153,12 @@ function printTurnSummary(turnId, turn, state, artifacts, assignment) {
149
153
  console.log(` ${chalk.dim('Runtime:')} ${turn.runtime_id}`);
150
154
  console.log(` ${chalk.dim('Status:')} ${turn.status}`);
151
155
  console.log(` ${chalk.dim('Attempt:')} ${turn.attempt}`);
156
+ if (turn.started_at) {
157
+ console.log(` ${chalk.dim('Started:')} ${turn.started_at}`);
158
+ }
159
+ if (elapsedMs != null) {
160
+ console.log(` ${chalk.dim('Elapsed:')} ${formatElapsed(elapsedMs)}`);
161
+ }
152
162
  console.log(` ${chalk.dim('Dispatch:')} ${getDispatchTurnDir(turnId)}`);
153
163
  if (assignment?.staging_result_path) {
154
164
  console.log(` ${chalk.dim('Staging:')} ${assignment.staging_result_path}`);
@@ -208,3 +218,22 @@ function readJsonArtifact(absPath) {
208
218
  return null;
209
219
  }
210
220
  }
221
+
222
+ function getElapsedMs(startedAt) {
223
+ if (typeof startedAt !== 'string') {
224
+ return null;
225
+ }
226
+ const started = Date.parse(startedAt);
227
+ if (!Number.isFinite(started)) {
228
+ return null;
229
+ }
230
+ const elapsed = Date.now() - started;
231
+ return elapsed >= 0 ? elapsed : null;
232
+ }
233
+
234
+ function formatElapsed(ms) {
235
+ const totalSeconds = Math.floor(ms / 1000);
236
+ const minutes = Math.floor(totalSeconds / 60);
237
+ const seconds = totalSeconds % 60;
238
+ return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
239
+ }
@@ -15,6 +15,9 @@ import { renderContextSections } from './context-section-parser.js';
15
15
  const COMPRESSION_STEPS = [
16
16
  { id: 'budget', action: 'drop' },
17
17
  { id: 'phase_gate_status', action: 'drop' },
18
+ { id: 'decision_history', action: 'drop' },
19
+ { id: 'workflow_artifacts', action: 'drop' },
20
+ { id: 'last_turn_verification', action: 'drop' },
18
21
  { id: 'gate_required_files', action: 'drop' },
19
22
  { id: 'last_turn_objections', action: 'drop' },
20
23
  { id: 'last_turn_decisions', action: 'drop' },
@@ -3,11 +3,14 @@ const CONTEXT_TITLE = '# Execution Context';
3
3
  const SECTION_DEFINITIONS = [
4
4
  { id: 'current_state', header: 'Current State', required: true },
5
5
  { id: 'budget', header: null, required: false },
6
+ { id: 'project_goal', header: 'Project Goal', required: true },
7
+ { id: 'inherited_run_context', header: 'Inherited Run Context', required: true },
6
8
  { id: 'last_turn_header', header: 'Last Accepted Turn', required: true },
7
9
  { id: 'last_turn_summary', header: null, required: false },
8
10
  { id: 'last_turn_decisions', header: null, required: false },
9
11
  { id: 'last_turn_objections', header: null, required: false },
10
12
  { id: 'last_turn_verification', header: null, required: false },
13
+ { id: 'decision_history', header: 'Decision History', required: false },
11
14
  { id: 'blockers', header: 'Blockers', required: true },
12
15
  { id: 'escalation', header: 'Escalation', required: true },
13
16
  { id: 'workflow_artifacts', header: 'Workflow Artifacts', required: false },
@@ -79,6 +82,8 @@ export function renderContextSections(sections) {
79
82
  sectionMap.get('budget')?.content,
80
83
  ]);
81
84
 
85
+ appendTopLevelSection(lines, 'Project Goal', [sectionMap.get('project_goal')?.content]);
86
+ appendTopLevelSection(lines, 'Inherited Run Context', [sectionMap.get('inherited_run_context')?.content]);
82
87
  appendTopLevelSection(lines, 'Last Accepted Turn', [
83
88
  sectionMap.get('last_turn_header')?.content,
84
89
  sectionMap.get('last_turn_summary')?.content,
@@ -87,6 +92,7 @@ export function renderContextSections(sections) {
87
92
  sectionMap.get('last_turn_verification')?.content,
88
93
  ]);
89
94
 
95
+ appendTopLevelSection(lines, 'Decision History', [sectionMap.get('decision_history')?.content]);
90
96
  appendTopLevelSection(lines, 'Blockers', [sectionMap.get('blockers')?.content]);
91
97
  appendTopLevelSection(lines, 'Escalation', [sectionMap.get('escalation')?.content]);
92
98
  appendTopLevelSection(lines, 'Workflow Artifacts', [sectionMap.get('workflow_artifacts')?.content]);
@@ -30,6 +30,8 @@ import {
30
30
  } from './turn-paths.js';
31
31
 
32
32
  const HISTORY_PATH = '.agentxchain/history.jsonl';
33
+ const LEDGER_PATH = '.agentxchain/decision-ledger.jsonl';
34
+ const DECISION_HISTORY_MAX_ENTRIES = 50;
33
35
  const FILE_PREVIEW_MAX_FILES = 5;
34
36
  const FILE_PREVIEW_MAX_LINES = 120;
35
37
  const PROPOSAL_SUMMARY_MAX_LINES = 80;
@@ -691,6 +693,12 @@ function renderContext(state, config, root, turn, role) {
691
693
  }
692
694
  }
693
695
 
696
+ // Cumulative decision history from the decision ledger
697
+ const decisionHistoryLines = renderDecisionHistory(root, warnings);
698
+ if (decisionHistoryLines.length > 0) {
699
+ lines.push(...decisionHistoryLines);
700
+ }
701
+
694
702
  // Blockers / escalation
695
703
  if (state.blocked_on) {
696
704
  lines.push('## Blockers');
@@ -1102,6 +1110,68 @@ function writeDispatchIndex(root, state, warningsByTurn = {}) {
1102
1110
  );
1103
1111
  }
1104
1112
 
1113
+ /**
1114
+ * Read agent-authored decisions from the decision ledger and render as a
1115
+ * markdown section for CONTEXT.md.
1116
+ *
1117
+ * Returns an array of markdown lines (including the ## header) or an empty
1118
+ * array when there are no agent-authored decisions.
1119
+ */
1120
+ function renderDecisionHistory(root, warnings = []) {
1121
+ const ledgerPath = join(root, LEDGER_PATH);
1122
+ if (!existsSync(ledgerPath)) return [];
1123
+
1124
+ let content;
1125
+ try {
1126
+ content = readFileSync(ledgerPath, 'utf8').trim();
1127
+ } catch (err) {
1128
+ warnings.push(`Failed to read ${LEDGER_PATH}: ${err.message}`);
1129
+ return [];
1130
+ }
1131
+ if (!content) return [];
1132
+
1133
+ // Parse all lines, skip malformed ones
1134
+ const rawLines = content.split('\n');
1135
+ const agentDecisions = [];
1136
+ for (const line of rawLines) {
1137
+ if (!line.trim()) continue;
1138
+ try {
1139
+ const entry = JSON.parse(line);
1140
+ // Agent-authored decisions have an `id` field; system entries do not
1141
+ if (entry.id) {
1142
+ agentDecisions.push(entry);
1143
+ }
1144
+ } catch {
1145
+ warnings.push(`Skipped malformed decision-ledger line`);
1146
+ }
1147
+ }
1148
+
1149
+ if (agentDecisions.length === 0) return [];
1150
+
1151
+ const totalCount = agentDecisions.length;
1152
+ const displayed = totalCount > DECISION_HISTORY_MAX_ENTRIES
1153
+ ? agentDecisions.slice(totalCount - DECISION_HISTORY_MAX_ENTRIES)
1154
+ : agentDecisions;
1155
+
1156
+ const lines = [];
1157
+ lines.push('## Decision History');
1158
+ lines.push('');
1159
+ lines.push('| ID | Phase | Role | Statement |');
1160
+ lines.push('|----|-------|------|-----------|');
1161
+ for (const d of displayed) {
1162
+ // Escape pipes in statement to avoid breaking the table
1163
+ const stmt = (d.statement || '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
1164
+ lines.push(`| ${d.id} | ${d.phase || ''} | ${d.role || ''} | ${stmt} |`);
1165
+ }
1166
+ if (totalCount > DECISION_HISTORY_MAX_ENTRIES) {
1167
+ lines.push('');
1168
+ lines.push(`_Showing ${DECISION_HISTORY_MAX_ENTRIES} of ${totalCount} decisions. Full ledger at ${LEDGER_PATH}._`);
1169
+ }
1170
+ lines.push('');
1171
+
1172
+ return lines;
1173
+ }
1174
+
1105
1175
  function readLastHistoryEntry(root, warnings = []) {
1106
1176
  const historyPath = join(root, HISTORY_PATH);
1107
1177
  if (!existsSync(historyPath)) return null;
@@ -2590,7 +2590,9 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2590
2590
  accepted_sequence: acceptedSequence,
2591
2591
  concurrent_with: Array.isArray(currentTurn.concurrent_with) ? currentTurn.concurrent_with : [],
2592
2592
  cost: turnResult.cost || {},
2593
+ ...(currentTurn.started_at ? { started_at: currentTurn.started_at } : {}),
2593
2594
  accepted_at: now,
2595
+ ...(currentTurn.started_at ? { duration_ms: Math.max(0, new Date(now).getTime() - new Date(currentTurn.started_at).getTime()) } : {}),
2594
2596
  };
2595
2597
  const nextHistoryEntries = [...historyEntries, historyEntry];
2596
2598
  // Build ledger entries for the journal
@@ -3081,11 +3083,17 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3081
3083
  }
3082
3084
 
3083
3085
  // Emit turn_accepted event to local log.
3086
+ const turnAcceptedPayload = {};
3087
+ if (currentTurn.started_at) {
3088
+ turnAcceptedPayload.started_at = currentTurn.started_at;
3089
+ turnAcceptedPayload.duration_ms = Math.max(0, new Date(now).getTime() - new Date(currentTurn.started_at).getTime());
3090
+ }
3084
3091
  emitRunEvent(root, 'turn_accepted', {
3085
3092
  run_id: updatedState.run_id,
3086
3093
  phase: updatedState.phase,
3087
3094
  status: updatedState.status,
3088
3095
  turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
3096
+ payload: turnAcceptedPayload,
3089
3097
  });
3090
3098
 
3091
3099
  if (updatedState.status === 'blocked') {
package/src/lib/report.js CHANGED
@@ -66,6 +66,25 @@ function formatUsd(value) {
66
66
  return typeof value === 'number' ? `$${value.toFixed(2)}` : 'n/a';
67
67
  }
68
68
 
69
+ function formatDurationCompact(ms) {
70
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return null;
71
+ if (ms < 1000) return `${ms}ms`;
72
+ const secs = Math.floor(ms / 1000);
73
+ if (secs < 60) return `${secs}s`;
74
+ const mins = Math.floor(secs / 60);
75
+ const remainSecs = secs % 60;
76
+ if (mins < 60) return `${mins}m ${remainSecs}s`;
77
+ const hrs = Math.floor(mins / 60);
78
+ const remainMins = mins % 60;
79
+ return `${hrs}h ${remainMins}m`;
80
+ }
81
+
82
+ function formatTurnTimelineTime(turn) {
83
+ const acceptedAt = turn.accepted_at || 'n/a';
84
+ const duration = formatDurationCompact(turn.duration_ms);
85
+ return duration ? `${acceptedAt} (${duration})` : acceptedAt;
86
+ }
87
+
69
88
  function formatStatusCounts(statusCounts) {
70
89
  const entries = Object.entries(statusCounts || {}).sort(([left], [right]) => left.localeCompare(right, 'en'));
71
90
  if (entries.length === 0) return 'none';
@@ -99,6 +118,8 @@ function extractHistoryTimeline(artifact) {
99
118
  decisions: Array.isArray(e.decisions) ? e.decisions.map((d) => d?.id || d).filter(Boolean) : [],
100
119
  objections: Array.isArray(e.objections) ? e.objections.map((o) => o?.id || o).filter(Boolean) : [],
101
120
  cost_usd: typeof e.cost?.total_usd === 'number' ? e.cost.total_usd : null,
121
+ started_at: e.started_at || null,
122
+ duration_ms: typeof e.duration_ms === 'number' ? e.duration_ms : null,
102
123
  accepted_at: e.accepted_at || null,
103
124
  }));
104
125
  }
@@ -943,7 +964,7 @@ export function formatGovernanceReportText(report) {
943
964
  const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
944
965
  const phase = t.phase_transition ? `${t.phase || '?'} -> ${t.phase_transition}` : (t.phase || '?');
945
966
  const siblingNote = Array.isArray(t.sibling_attributed_files) ? ` (${t.sibling_attributed_files.length} sibling-attributed)` : '';
946
- lines.push(` ${i + 1}. [${t.role}] ${t.summary || '(no summary)'} | phase: ${phase} | files: ${t.files_changed_count}${siblingNote} | cost: ${cost} | ${t.accepted_at || 'n/a'}`);
967
+ lines.push(` ${i + 1}. [${t.role}] ${t.summary || '(no summary)'} | phase: ${phase} | files: ${t.files_changed_count}${siblingNote} | cost: ${cost} | ${formatTurnTimelineTime(t)}`);
947
968
  }
948
969
  }
949
970
 
@@ -1353,7 +1374,7 @@ export function formatGovernanceReportMarkdown(report) {
1353
1374
  const phase = t.phase_transition ? `${t.phase || '?'} → ${t.phase_transition}` : (t.phase || '?');
1354
1375
  const summary = (t.summary || '(no summary)').replace(/\|/g, '\\|');
1355
1376
  const siblingNote = Array.isArray(t.sibling_attributed_files) ? ` (${t.sibling_attributed_files.length} sibling)` : '';
1356
- lines.push(`| ${i + 1} | ${t.role} | ${phase} | ${summary} | ${t.files_changed_count}${siblingNote} | ${cost} | ${t.accepted_at || 'n/a'} |`);
1377
+ lines.push(`| ${i + 1} | ${t.role} | ${phase} | ${summary} | ${t.files_changed_count}${siblingNote} | ${cost} | ${formatTurnTimelineTime(t).replace(/\|/g, '\\|')} |`);
1357
1378
  }
1358
1379
  }
1359
1380
 
@@ -1611,7 +1632,7 @@ export function formatGovernanceReportMarkdown(report) {
1611
1632
  const phase = t.phase_transition ? `${t.phase || '?'} → ${t.phase_transition}` : (t.phase || '?');
1612
1633
  const summary = (t.summary || '(no summary)').replace(/\|/g, '\\|');
1613
1634
  const siblingNote = Array.isArray(t.sibling_attributed_files) ? ` (${t.sibling_attributed_files.length} sibling)` : '';
1614
- repoLines.push(`| ${i + 1} | ${t.role} | ${phase} | ${summary} | ${t.files_changed_count}${siblingNote} | ${cost} | ${t.accepted_at || 'n/a'} |`);
1635
+ repoLines.push(`| ${i + 1} | ${t.role} | ${phase} | ${summary} | ${t.files_changed_count}${siblingNote} | ${cost} | ${formatTurnTimelineTime(t).replace(/\|/g, '\\|')} |`);
1615
1636
  }
1616
1637
  }
1617
1638
  if (repo.decisions && repo.decisions.length > 0) {
@@ -3,11 +3,13 @@ import { spawnSync } from 'node:child_process';
3
3
  export const DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS = 30_000;
4
4
 
5
5
  export function replayVerificationMachineEvidence({ root, verification, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS }) {
6
+ const verifiedAt = new Date().toISOString();
6
7
  const machineEvidence = Array.isArray(verification?.machine_evidence)
7
8
  ? verification.machine_evidence
8
9
  : [];
9
10
 
10
11
  const payload = {
12
+ verified_at: verifiedAt,
11
13
  timeout_ms: timeoutMs,
12
14
  overall: 'not_reproducible',
13
15
  replayed_commands: 0,
@@ -59,6 +61,7 @@ export function summarizeVerificationReplay(payload) {
59
61
  }
60
62
 
61
63
  return {
64
+ verified_at: payload.verified_at || null,
62
65
  overall: payload.overall,
63
66
  replayed_commands: payload.replayed_commands || 0,
64
67
  matched_commands: payload.matched_commands || 0,