agentxchain 2.53.0 → 2.55.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.
@@ -407,6 +407,7 @@ program
407
407
  .option('--no-report', 'Suppress automatic governance report after run completes')
408
408
  .option('--continue-from <run_id>', 'Continue from a prior terminal run (sets trigger=continuation)')
409
409
  .option('--recover-from <run_id>', 'Recover from a prior blocked run (sets trigger=recovery)')
410
+ .option('--inherit-context', 'Inherit read-only summary context from the parent run (requires --continue-from or --recover-from)')
410
411
  .action(runCommand);
411
412
 
412
413
  program
@@ -68,6 +68,14 @@ function truncateId(id, len = 12) {
68
68
  return id.length > len ? id.slice(0, len) + '…' : id;
69
69
  }
70
70
 
71
+ function isInheritable(entry) {
72
+ const snap = entry?.inheritance_snapshot;
73
+ if (!snap) return false;
74
+ const hasDecisions = Array.isArray(snap.recent_decisions) && snap.recent_decisions.length > 0;
75
+ const hasTurns = Array.isArray(snap.recent_accepted_turns) && snap.recent_accepted_turns.length > 0;
76
+ return hasDecisions || hasTurns;
77
+ }
78
+
71
79
  function renderRow(entry, index) {
72
80
  const rowClass = entry.status === 'blocked'
73
81
  ? ' style="border-left:3px solid var(--yellow)"'
@@ -83,10 +91,15 @@ function renderRow(entry, index) {
83
91
  ? `<div class="blocked-hint" style="font-size:0.85em;color:var(--yellow);margin-top:2px">${esc(typeof entry.blocked_reason === 'string' ? entry.blocked_reason : entry.blocked_reason?.detail || entry.blocked_reason?.category || '')}</div>`
84
92
  : '';
85
93
 
94
+ const ctxIndicator = isInheritable(entry)
95
+ ? `<span title="Has inheritance snapshot — usable by child runs" style="color:var(--green)">✓</span>`
96
+ : `<span style="color:var(--text-dim)">—</span>`;
97
+
86
98
  return `<tr${rowClass}>
87
99
  <td style="color:var(--text-dim)">${index + 1}</td>
88
100
  <td class="mono" title="${esc(entry.run_id)}">${esc(truncateId(entry.run_id))}</td>
89
101
  <td>${statusBadge(entry.status)}${blockedInfo}</td>
102
+ <td>${ctxIndicator}</td>
90
103
  <td>${phases}</td>
91
104
  <td>${entry.total_turns ?? '—'}</td>
92
105
  <td>${formatCost(entry.total_cost_usd)}</td>
@@ -125,6 +138,7 @@ export function render({ runHistory }) {
125
138
  <th>#</th>
126
139
  <th>Run ID</th>
127
140
  <th>Status</th>
141
+ <th>Ctx</th>
128
142
  <th>Phases</th>
129
143
  <th>Turns</th>
130
144
  <th>Cost</th>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.53.0",
3
+ "version": "2.55.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,7 +7,7 @@
7
7
  import { resolve } from 'path';
8
8
  import { existsSync, readFileSync } from 'fs';
9
9
  import chalk from 'chalk';
10
- import { queryRunHistory, queryRunLineage } from '../lib/run-history.js';
10
+ import { queryRunHistory, queryRunLineage, isInheritable } from '../lib/run-history.js';
11
11
  import { getRunTriggerLabel, summarizeRunProvenance } from '../lib/run-provenance.js';
12
12
 
13
13
  /**
@@ -46,11 +46,12 @@ export async function historyCommand(opts) {
46
46
  const turns = `${entry.total_turns || 0} turns`;
47
47
  const cost = entry.total_cost_usd != null ? `$${entry.total_cost_usd.toFixed(2)}` : '';
48
48
  const trigger = getRunTriggerLabel(entry.provenance);
49
+ const ctxMarker = isInheritable(entry) ? ' [ctx]' : '';
49
50
  const parentNote = entry.provenance?.parent_run_id
50
51
  ? ` from ${entry.provenance.parent_run_id.slice(0, 12)}`
51
52
  : '';
52
53
  const prefix = i === 0 ? ' ' : ' └─ ';
53
- console.log(`${prefix}${runId} ${status} ${pad(phases, 20)} ${pad(turns, 10)} ${pad(cost, 8)} (${trigger}${parentNote})`);
54
+ console.log(`${prefix}${runId} ${status} ${pad(phases, 20)} ${pad(turns, 10)} ${pad(cost, 8)} (${trigger}${parentNote})${ctxMarker}`);
54
55
  });
55
56
  return;
56
57
  }
@@ -63,7 +64,8 @@ export async function historyCommand(opts) {
63
64
  });
64
65
 
65
66
  if (opts.json) {
66
- console.log(JSON.stringify(entries, null, 2));
67
+ const enriched = entries.map(e => ({ ...e, inheritable: isInheritable(e) }));
68
+ console.log(JSON.stringify(enriched, null, 2));
67
69
  return;
68
70
  }
69
71
 
@@ -81,6 +83,7 @@ export async function historyCommand(opts) {
81
83
  pad('Run ID', 14),
82
84
  pad('Status', 11),
83
85
  pad('Trigger', 14),
86
+ pad('Ctx', 4),
84
87
  pad('Phases', 8),
85
88
  pad('Turns', 6),
86
89
  pad('Cost', 10),
@@ -96,6 +99,7 @@ export async function historyCommand(opts) {
96
99
  const runId = (entry.run_id || '—').slice(0, 12);
97
100
  const status = formatStatus(entry.status);
98
101
  const trigger = getRunTriggerLabel(entry.provenance);
102
+ const ctx = isInheritable(entry) ? '✓' : '—';
99
103
  const phases = String(entry.phases_completed?.length || 0);
100
104
  const turns = String(entry.total_turns || 0);
101
105
  const cost = entry.total_cost_usd != null
@@ -113,6 +117,7 @@ export async function historyCommand(opts) {
113
117
  pad(runId, 14),
114
118
  pad(status, 11),
115
119
  pad(trigger, 14),
120
+ pad(ctx, 4),
116
121
  pad(phases, 8),
117
122
  pad(turns, 6),
118
123
  pad(cost, 10),
@@ -33,6 +33,7 @@ import { runHooks } from '../lib/hook-runner.js';
33
33
  import { finalizeDispatchManifest } from '../lib/dispatch-manifest.js';
34
34
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
35
35
  import { resolveGovernedRole } from '../lib/role-resolution.js';
36
+ import { buildInheritedContext } from '../lib/run-context-inheritance.js';
36
37
  import {
37
38
  getDispatchAssignmentPath,
38
39
  getDispatchContextPath,
@@ -91,6 +92,25 @@ export async function executeGovernedRun(context, opts = {}) {
91
92
  };
92
93
  }
93
94
 
95
+ // ── Inherit-context validation ─────────────────────────────────────────
96
+ const inheritContext = !!opts.inheritContext;
97
+ let inheritedContext = null;
98
+ if (inheritContext) {
99
+ if (!continueFrom && !recoverFrom) {
100
+ log(chalk.red('--inherit-context requires --continue-from or --recover-from'));
101
+ log(chalk.dim('Usage: agentxchain run --continue-from <run_id> --inherit-context'));
102
+ return { exitCode: 1, result: null };
103
+ }
104
+ const parentId = continueFrom || recoverFrom;
105
+ const inheritance = buildInheritedContext(root, parentId);
106
+ inheritedContext = inheritance.inherited_context;
107
+ if (inheritance.warnings?.length) {
108
+ for (const w of inheritance.warnings) {
109
+ log(chalk.yellow(` ⚠ Inheritance: ${w}`));
110
+ }
111
+ }
112
+ }
113
+
94
114
  const maxTurns = opts.maxTurns || 50;
95
115
  const autoApprove = !!opts.autoApprove;
96
116
  const verbose = !!opts.verbose;
@@ -375,6 +395,7 @@ export async function executeGovernedRun(context, opts = {}) {
375
395
  startNewRunFromBlocked: opts.allowBlockedRestart ?? Boolean(provenance),
376
396
  };
377
397
  if (provenance) runLoopOpts.provenance = provenance;
398
+ if (inheritedContext) runLoopOpts.inheritedContext = inheritedContext;
378
399
  const result = await runLoop(root, config, callbacks, runLoopOpts);
379
400
 
380
401
  // ── Summary ─────────────────────────────────────────────────────────────
@@ -92,6 +92,7 @@ function renderGovernedStatus(context, opts) {
92
92
  config,
93
93
  state,
94
94
  provenance: state?.provenance || null,
95
+ inherited_context: state?.inherited_context || null,
95
96
  continuity,
96
97
  connector_health: connectorHealth,
97
98
  workflow_kit_artifacts: workflowKitArtifacts,
@@ -113,6 +114,9 @@ function renderGovernedStatus(context, opts) {
113
114
  if (provenanceSummary) {
114
115
  console.log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
115
116
  }
117
+ if (state?.inherited_context?.parent_run_id) {
118
+ console.log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`)}`);
119
+ }
116
120
  if (state?.accepted_integration_ref) {
117
121
  console.log(` ${chalk.dim('Accepted:')} ${state.accepted_integration_ref}`);
118
122
  }
@@ -17,6 +17,7 @@
17
17
  import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync } from 'fs';
18
18
  import { join } from 'path';
19
19
  import { getActiveTurn, getActiveTurns } from './governed-state.js';
20
+ import { renderInheritedContextMarkdown } from './run-context-inheritance.js';
20
21
  import {
21
22
  DISPATCH_INDEX_PATH,
22
23
  getDispatchAssignmentPath,
@@ -511,6 +512,16 @@ function renderContext(state, config, root, turn, role) {
511
512
  }
512
513
  lines.push('');
513
514
 
515
+ // Inherited context from parent run (when --inherit-context was used)
516
+ if (state.inherited_context) {
517
+ // First turn gets the full rendering; subsequent turns get compact
518
+ const isFirstTurn = !state.last_completed_turn_id;
519
+ const inheritedMd = renderInheritedContextMarkdown(state.inherited_context, !isFirstTurn);
520
+ if (inheritedMd) {
521
+ lines.push(inheritedMd);
522
+ }
523
+ }
524
+
514
525
  // Last accepted turn summary
515
526
  if (state.last_completed_turn_id) {
516
527
  const lastTurn = readLastHistoryEntry(root, warnings);
package/src/lib/export.js CHANGED
@@ -304,6 +304,7 @@ export function buildRunExport(startDir = process.cwd()) {
304
304
  status: state?.status || null,
305
305
  phase: state?.phase || null,
306
306
  provenance: normalizeRunProvenance(state?.provenance),
307
+ inherited_context: state?.inherited_context || null,
307
308
  active_turn_ids: activeTurns,
308
309
  retained_turn_ids: retainedTurns,
309
310
  history_entries: countJsonl(files, '.agentxchain/history.jsonl'),
@@ -1856,6 +1856,7 @@ export function initializeGovernedRun(root, config, options = {}) {
1856
1856
  remaining_usd: config.budget?.per_run_max_usd ?? null
1857
1857
  },
1858
1858
  provenance,
1859
+ inherited_context: options.inherited_context || null,
1859
1860
  };
1860
1861
 
1861
1862
  writeState(root, updatedState);
package/src/lib/report.js CHANGED
@@ -641,6 +641,7 @@ function buildRunSubject(artifact) {
641
641
  blocked_on: artifact.state?.blocked_on || null,
642
642
  blocked_reason: artifact.state?.blocked_reason || null,
643
643
  provenance: normalizeRunProvenance(artifact.summary?.provenance || artifact.state?.provenance),
644
+ inherited_context: artifact.summary?.inherited_context || artifact.state?.inherited_context || null,
644
645
  active_turn_count: activeTurns.length,
645
646
  retained_turn_count: retainedTurns.length,
646
647
  active_turn_ids: activeTurns,
@@ -904,6 +905,9 @@ export function formatGovernanceReportText(report) {
904
905
  if (summarizeRunProvenance(run.provenance)) {
905
906
  lines.push(`Provenance: ${summarizeRunProvenance(run.provenance)}`);
906
907
  }
908
+ if (run.inherited_context?.parent_run_id) {
909
+ lines.push(`Inherited from: ${run.inherited_context.parent_run_id} (${run.inherited_context.parent_status || 'unknown'})`);
910
+ }
907
911
 
908
912
  lines.push(
909
913
  `History entries: ${artifacts.history_entries}`,
@@ -1308,6 +1312,9 @@ export function formatGovernanceReportMarkdown(report) {
1308
1312
  if (summarizeRunProvenance(run.provenance)) {
1309
1313
  lines.push(`- Provenance: \`${summarizeRunProvenance(run.provenance)}\``);
1310
1314
  }
1315
+ if (run.inherited_context?.parent_run_id) {
1316
+ lines.push(`- Inherited from: \`${run.inherited_context.parent_run_id}\` (${run.inherited_context.parent_status || 'unknown'})`);
1317
+ }
1311
1318
 
1312
1319
  lines.push(
1313
1320
  `- History entries: ${artifacts.history_entries}`,
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Run Context Inheritance — read-only summary bridge from parent to child run.
3
+ *
4
+ * When a child run uses `--inherit-context` with `--continue-from` or
5
+ * `--recover-from`, this module extracts a read-only summary from the
6
+ * selected parent run-history record and attaches it to the child's
7
+ * governed state.
8
+ *
9
+ * The inherited summary is:
10
+ * - read-only (never mutated by the child run)
11
+ * - a summary (not a full replay of the parent)
12
+ * - observable in status --json, report, export, and CONTEXT.md
13
+ *
14
+ * DEC-RUN-CONTEXT-INHERIT-001
15
+ * Spec: .planning/RUN_CONTEXT_INHERITANCE_SPEC.md
16
+ */
17
+
18
+ import { readFileSync, existsSync } from 'fs';
19
+ import { join } from 'path';
20
+
21
+ const RUN_HISTORY_PATH = '.agentxchain/run-history.jsonl';
22
+
23
+ /**
24
+ * Build an inherited context summary from a parent run.
25
+ *
26
+ * @param {string} root - project root directory
27
+ * @param {string} parentRunId - the parent run_id
28
+ * @returns {{ ok: boolean, inherited_context?: object, warnings?: string[] }}
29
+ */
30
+ export function buildInheritedContext(root, parentRunId) {
31
+ const warnings = [];
32
+
33
+ // 1. Read parent run-history entry
34
+ const parentEntry = findRunHistoryEntry(root, parentRunId);
35
+ if (!parentEntry) {
36
+ warnings.push(`Parent run ${parentRunId} not found in run-history.jsonl`);
37
+ return {
38
+ ok: true,
39
+ inherited_context: buildPartialContext(parentRunId, null, [], [], warnings),
40
+ warnings,
41
+ };
42
+ }
43
+
44
+ const snapshot = parentEntry.inheritance_snapshot;
45
+ const recentDecisions = Array.isArray(snapshot?.recent_decisions) ? snapshot.recent_decisions : [];
46
+ const acceptedTurns = Array.isArray(snapshot?.recent_accepted_turns) ? snapshot.recent_accepted_turns : [];
47
+
48
+ if (!snapshot) {
49
+ warnings.push('Parent run has no inheritance snapshot in run-history.jsonl — inherited context is metadata only');
50
+ }
51
+
52
+ const inherited_context = {
53
+ schema_version: '0.1',
54
+ parent_run_id: parentRunId,
55
+ parent_status: parentEntry.status,
56
+ parent_completed_at: parentEntry.completed_at || null,
57
+ parent_phases_completed: parentEntry.phases_completed || [],
58
+ parent_roles_used: parentEntry.roles_used || [],
59
+ parent_blocked_reason: parentEntry.blocked_reason || null,
60
+ recent_decisions: recentDecisions,
61
+ recent_accepted_turns: acceptedTurns,
62
+ inherited_at: new Date().toISOString(),
63
+ warnings: warnings.length > 0 ? warnings : undefined,
64
+ };
65
+
66
+ return { ok: true, inherited_context, warnings };
67
+ }
68
+
69
+ /**
70
+ * Render the inherited context as a markdown section for CONTEXT.md.
71
+ */
72
+ export function renderInheritedContextMarkdown(inheritedContext, compact = false) {
73
+ if (!inheritedContext) return '';
74
+
75
+ const lines = [];
76
+ lines.push('## Inherited Run Context');
77
+ lines.push('');
78
+ lines.push('> **This is a fresh run, not a resumed parent.** The context below is a read-only summary from the parent run to provide continuity.');
79
+ lines.push('');
80
+ lines.push(`- **Parent run:** ${inheritedContext.parent_run_id}`);
81
+ lines.push(`- **Parent status:** ${inheritedContext.parent_status || 'unknown'}`);
82
+ if (inheritedContext.parent_completed_at) {
83
+ lines.push(`- **Completed at:** ${inheritedContext.parent_completed_at}`);
84
+ }
85
+ if (inheritedContext.parent_blocked_reason) {
86
+ lines.push(`- **Blocked reason:** ${inheritedContext.parent_blocked_reason}`);
87
+ }
88
+ if (inheritedContext.parent_phases_completed?.length) {
89
+ lines.push(`- **Phases completed:** ${inheritedContext.parent_phases_completed.join(', ')}`);
90
+ }
91
+ if (inheritedContext.parent_roles_used?.length) {
92
+ lines.push(`- **Roles used:** ${inheritedContext.parent_roles_used.join(', ')}`);
93
+ }
94
+ lines.push('');
95
+
96
+ if (!compact && inheritedContext.recent_decisions?.length) {
97
+ lines.push('### Recent Decisions');
98
+ lines.push('');
99
+ for (const d of inheritedContext.recent_decisions) {
100
+ const id = d.id || '(no id)';
101
+ const stmt = d.statement || '(no statement)';
102
+ lines.push(`- **${id}:** ${stmt}`);
103
+ }
104
+ lines.push('');
105
+ }
106
+
107
+ if (!compact && inheritedContext.recent_accepted_turns?.length) {
108
+ lines.push('### Recent Accepted Turns');
109
+ lines.push('');
110
+ for (const t of inheritedContext.recent_accepted_turns) {
111
+ const role = t.role || '(unknown)';
112
+ const summary = t.summary || '(no summary)';
113
+ lines.push(`- **${role}** (${t.turn_id || '?'}): ${summary}`);
114
+ }
115
+ lines.push('');
116
+ }
117
+
118
+ if (inheritedContext.warnings?.length) {
119
+ lines.push('### Inheritance Warnings');
120
+ lines.push('');
121
+ for (const w of inheritedContext.warnings) {
122
+ lines.push(`- ⚠ ${w}`);
123
+ }
124
+ lines.push('');
125
+ }
126
+
127
+ return lines.join('\n');
128
+ }
129
+
130
+ // ── Internal ────────────────────────────────────────────────────────────────
131
+
132
+ function findRunHistoryEntry(root, runId) {
133
+ const filePath = join(root, RUN_HISTORY_PATH);
134
+ if (!existsSync(filePath)) return null;
135
+ try {
136
+ const content = readFileSync(filePath, 'utf8').trim();
137
+ if (!content) return null;
138
+ const entries = content.split('\n').filter(Boolean).map(line => {
139
+ try { return JSON.parse(line); } catch { return null; }
140
+ }).filter(Boolean);
141
+ return entries.find(e => e.run_id === runId) || null;
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ function buildPartialContext(parentRunId, parentEntry, decisions, turns, warnings) {
148
+ return {
149
+ schema_version: '0.1',
150
+ parent_run_id: parentRunId,
151
+ parent_status: parentEntry?.status || null,
152
+ parent_completed_at: parentEntry?.completed_at || null,
153
+ parent_phases_completed: parentEntry?.phases_completed || [],
154
+ parent_roles_used: parentEntry?.roles_used || [],
155
+ parent_blocked_reason: parentEntry?.blocked_reason || null,
156
+ recent_decisions: decisions,
157
+ recent_accepted_turns: turns,
158
+ inherited_at: new Date().toISOString(),
159
+ warnings: warnings.length > 0 ? warnings : undefined,
160
+ };
161
+ }
@@ -16,6 +16,8 @@ const HISTORY_PATH = '.agentxchain/history.jsonl';
16
16
  const LEDGER_PATH = '.agentxchain/decision-ledger.jsonl';
17
17
  const SCHEMA_VERSION = '0.1';
18
18
  const WRITABLE_TERMINAL_STATUSES = new Set(['completed', 'blocked']);
19
+ const MAX_INHERITANCE_DECISIONS = 5;
20
+ const MAX_INHERITANCE_TURNS = 3;
19
21
 
20
22
  /**
21
23
  * Record a run's summary into the persistent run-history ledger.
@@ -81,6 +83,10 @@ export function recordRunHistory(root, state, config, status) {
81
83
  connector_used: connectorUsed,
82
84
  model_used: modelUsed,
83
85
  provenance: normalizeRunProvenance(state?.provenance),
86
+ inheritance_snapshot: {
87
+ recent_decisions: buildRecentDecisionSnapshot(ledgerEntries),
88
+ recent_accepted_turns: buildRecentAcceptedTurnSnapshot(historyEntries),
89
+ },
84
90
  recorded_at: new Date().toISOString(),
85
91
  };
86
92
 
@@ -243,6 +249,21 @@ export function validateParentRun(root, runId) {
243
249
  return { ok: true, entry };
244
250
  }
245
251
 
252
+ /**
253
+ * Check whether a run-history entry has a usable inheritance snapshot
254
+ * (at least one decision or one accepted turn available for child runs).
255
+ *
256
+ * @param {object} entry - a run-history record
257
+ * @returns {boolean}
258
+ */
259
+ export function isInheritable(entry) {
260
+ const snap = entry?.inheritance_snapshot;
261
+ if (!snap) return false;
262
+ const hasDecisions = Array.isArray(snap.recent_decisions) && snap.recent_decisions.length > 0;
263
+ const hasTurns = Array.isArray(snap.recent_accepted_turns) && snap.recent_accepted_turns.length > 0;
264
+ return hasDecisions || hasTurns;
265
+ }
266
+
246
267
  /**
247
268
  * Get the path to the run-history file.
248
269
  */
@@ -265,3 +286,26 @@ function readJsonlSafe(root, relPath) {
265
286
  return [];
266
287
  }
267
288
  }
289
+
290
+ function buildRecentDecisionSnapshot(entries) {
291
+ return entries
292
+ .slice(-MAX_INHERITANCE_DECISIONS)
293
+ .map((entry) => ({
294
+ id: entry.id || entry.decision_id || null,
295
+ statement: entry.statement || entry.description || entry.text || null,
296
+ decided_by: entry.decided_by || entry.role || null,
297
+ phase: entry.phase || null,
298
+ }));
299
+ }
300
+
301
+ function buildRecentAcceptedTurnSnapshot(entries) {
302
+ return entries
303
+ .filter((entry) => entry.status === 'accepted')
304
+ .slice(-MAX_INHERITANCE_TURNS)
305
+ .map((entry) => ({
306
+ turn_id: entry.turn_id || null,
307
+ role: entry.role || null,
308
+ summary: entry.summary || null,
309
+ phase: entry.phase || null,
310
+ }));
311
+ }
@@ -62,6 +62,9 @@ export async function runLoop(root, config, callbacks, options = {}) {
62
62
  const shouldRestartBlocked = state?.status === 'blocked' && options.startNewRunFromBlocked === true;
63
63
  if (!state || state.status === 'idle' || shouldRestartCompleted || shouldRestartBlocked) {
64
64
  const initOpts = options.provenance ? { provenance: options.provenance } : {};
65
+ if (options.inheritedContext) {
66
+ initOpts.inherited_context = options.inheritedContext;
67
+ }
65
68
  if (shouldRestartCompleted || shouldRestartBlocked) {
66
69
  initOpts.allow_terminal_restart = true;
67
70
  }