agentxchain 2.135.1 → 2.136.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.
@@ -123,7 +123,7 @@ import { historyCommand } from '../src/commands/history.js';
123
123
  import { decisionsCommand } from '../src/commands/decisions.js';
124
124
  import { diffCommand } from '../src/commands/diff.js';
125
125
  import { eventsCommand } from '../src/commands/events.js';
126
- import { connectorCheckCommand, connectorValidateCommand } from '../src/commands/connector.js';
126
+ import { connectorCapabilitiesCommand, connectorCheckCommand, connectorValidateCommand } from '../src/commands/connector.js';
127
127
  import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
128
128
  import { chainLatestCommand, chainListCommand, chainShowCommand } from '../src/commands/chain.js';
129
129
  import { missionAttachChainCommand, missionBindCoordinatorCommand, missionListCommand, missionPlanApproveCommand, missionPlanAutopilotCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
@@ -299,6 +299,13 @@ connectorCmd
299
299
  .option('--timeout <ms>', 'Per-probe timeout in milliseconds', '8000')
300
300
  .action(connectorCheckCommand);
301
301
 
302
+ connectorCmd
303
+ .command('capabilities [runtime_id]')
304
+ .description('Show merged capability contract for a runtime or all runtimes (machine-readable handshake)')
305
+ .option('-j, --json', 'Output as JSON')
306
+ .option('--all', 'Show capabilities for all configured runtimes')
307
+ .action(connectorCapabilitiesCommand);
308
+
302
309
  connectorCmd
303
310
  .command('validate <runtime_id>')
304
311
  .description('Dispatch one synthetic governed turn through a runtime and validate the staged turn result')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.135.1",
3
+ "version": "2.136.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,9 @@
10
10
  ".": "./bin/agentxchain.js",
11
11
  "./adapter-interface": "./src/lib/adapter-interface.js",
12
12
  "./runner-interface": "./src/lib/runner-interface.js",
13
- "./run-loop": "./src/lib/run-loop.js"
13
+ "./run-loop": "./src/lib/run-loop.js",
14
+ "./schemas/agentxchain-config": "./src/lib/schemas/agentxchain-config.schema.json",
15
+ "./schemas/connector-capabilities-output": "./src/lib/schemas/connector-capabilities-output.schema.json"
14
16
  },
15
17
  "files": [
16
18
  "bin/",
@@ -154,6 +154,8 @@ if [[ "$PUBLISH_GATE" -eq 1 ]]; then
154
154
  test/release-identity-hardening.test.js
155
155
  test/normalized-config.test.js
156
156
  test/conformance.test.js
157
+ test/beta-scenario-emission-guard.test.js
158
+ test/claim-reality-preflight.test.js
157
159
  test/beta-tester-scenarios/*.test.js
158
160
  )
159
161
  GATE_TEST_ARGS=()
@@ -3,6 +3,7 @@ import chalk from 'chalk';
3
3
  import { loadProjectContext } from '../lib/config.js';
4
4
  import { DEFAULT_VALIDATE_TIMEOUT_MS, validateConfiguredConnector } from '../lib/connector-validate.js';
5
5
  import { DEFAULT_TIMEOUT_MS, probeConfiguredConnectors } from '../lib/connector-probe.js';
6
+ import { getRuntimeCapabilityContract, getRoleRuntimeCapabilityContract, getCapabilityDeclarationWarnings } from '../lib/runtime-capabilities.js';
6
7
 
7
8
  function printJson(result, exitCode) {
8
9
  console.log(JSON.stringify(result, null, 2));
@@ -192,6 +193,141 @@ function printValidateText(result, exitCode) {
192
193
  process.exit(exitCode);
193
194
  }
194
195
 
196
+ function buildCapabilityReport(runtimeId, runtime, roles) {
197
+ const mergedContract = getRuntimeCapabilityContract(runtime);
198
+ const declaredCapabilities = (runtime.capabilities && typeof runtime.capabilities === 'object' && !Array.isArray(runtime.capabilities))
199
+ ? { ...runtime.capabilities }
200
+ : {};
201
+ const declarationWarnings = getCapabilityDeclarationWarnings(runtime);
202
+
203
+ const roleBindings = [];
204
+ for (const [roleId, role] of Object.entries(roles)) {
205
+ const boundRuntime = role.runtime_id || role.runtime;
206
+ if (boundRuntime !== runtimeId) continue;
207
+ const roleContract = getRoleRuntimeCapabilityContract(roleId, role, runtime);
208
+ roleBindings.push({
209
+ role_id: roleContract.role_id,
210
+ role_write_authority: roleContract.role_write_authority,
211
+ effective_write_path: roleContract.effective_write_path,
212
+ workflow_artifact_ownership: roleContract.workflow_artifact_ownership,
213
+ notes: roleContract.notes,
214
+ });
215
+ }
216
+
217
+ return {
218
+ runtime_id: runtimeId,
219
+ runtime_type: runtime.type || 'unknown',
220
+ declared_capabilities: declaredCapabilities,
221
+ merged_contract: mergedContract,
222
+ declaration_warnings: declarationWarnings,
223
+ role_bindings: roleBindings,
224
+ };
225
+ }
226
+
227
+ function printCapabilitiesText(report) {
228
+ console.log('');
229
+ console.log(chalk.bold(` ${report.runtime_id}`) + chalk.dim(` (${report.runtime_type})`));
230
+ console.log(chalk.dim(' ' + '-'.repeat(44)));
231
+
232
+ const c = report.merged_contract;
233
+ console.log(` ${chalk.dim('Transport:')} ${c.transport}`);
234
+ console.log(` ${chalk.dim('Write files:')} ${c.can_write_files}`);
235
+ console.log(` ${chalk.dim('Proposals:')} ${c.proposal_support}`);
236
+ console.log(` ${chalk.dim('Artifact ownership:')} ${c.workflow_artifact_ownership}`);
237
+ console.log(` ${chalk.dim('Local binary:')} ${c.requires_local_binary ? 'yes' : 'no'}`);
238
+
239
+ if (Object.keys(report.declared_capabilities).length > 0) {
240
+ console.log('');
241
+ console.log(` ${chalk.dim('Declared overrides:')}`);
242
+ for (const [k, v] of Object.entries(report.declared_capabilities)) {
243
+ console.log(` ${k}: ${v}`);
244
+ }
245
+ }
246
+
247
+ if (report.declaration_warnings.length > 0) {
248
+ console.log('');
249
+ for (const w of report.declaration_warnings) {
250
+ console.log(` ${chalk.yellow('!')} ${w}`);
251
+ }
252
+ }
253
+
254
+ if (report.role_bindings.length > 0) {
255
+ console.log('');
256
+ console.log(` ${chalk.dim('Role bindings:')}`);
257
+ for (const rb of report.role_bindings) {
258
+ const badge = rb.effective_write_path.startsWith('invalid') ? chalk.red('INVALID') : chalk.green('OK');
259
+ console.log(` ${badge} ${rb.role_id} (${rb.role_write_authority}) -> ${rb.effective_write_path}`);
260
+ for (const note of rb.notes) {
261
+ console.log(` ${chalk.dim(note)}`);
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ export async function connectorCapabilitiesCommand(runtimeId, options = {}) {
268
+ const context = loadProjectContext();
269
+ if (!context) {
270
+ const payload = { error: 'No governed agentxchain.json found.' };
271
+ if (options.json) { printJson(payload, 2); return; }
272
+ console.error(chalk.red('No governed agentxchain.json found. Run this inside a governed project.'));
273
+ process.exit(2);
274
+ }
275
+
276
+ const config = context.config;
277
+ const runtimes = config.runtimes || {};
278
+ const roles = config.roles || {};
279
+
280
+ if (options.all) {
281
+ const reports = [];
282
+ for (const [id, runtime] of Object.entries(runtimes)) {
283
+ reports.push(buildCapabilityReport(id, runtime, roles));
284
+ }
285
+ const payload = { runtimes: reports };
286
+ if (options.json) { printJson(payload, 0); return; }
287
+
288
+ console.log('');
289
+ console.log(chalk.bold(' AgentXchain Connector Capabilities'));
290
+ console.log(chalk.dim(' ' + '='.repeat(44)));
291
+ if (reports.length === 0) {
292
+ console.log(' No runtimes configured.');
293
+ } else {
294
+ for (const r of reports) { printCapabilitiesText(r); }
295
+ }
296
+ console.log('');
297
+ process.exit(0);
298
+ }
299
+
300
+ if (!runtimeId) {
301
+ const payload = { error: 'Runtime ID required. Use --all to list all runtimes.', available_runtimes: Object.keys(runtimes) };
302
+ if (options.json) { printJson(payload, 2); return; }
303
+ console.error(chalk.red('Runtime ID required. Use --all to list all runtimes.'));
304
+ if (Object.keys(runtimes).length > 0) {
305
+ console.error(chalk.dim(`Available: ${Object.keys(runtimes).join(', ')}`));
306
+ }
307
+ process.exit(2);
308
+ }
309
+
310
+ if (!runtimes[runtimeId]) {
311
+ const payload = { error: `Runtime "${runtimeId}" not found.`, available_runtimes: Object.keys(runtimes) };
312
+ if (options.json) { printJson(payload, 2); return; }
313
+ console.error(chalk.red(`Runtime "${runtimeId}" not found.`));
314
+ if (Object.keys(runtimes).length > 0) {
315
+ console.error(chalk.dim(`Available: ${Object.keys(runtimes).join(', ')}`));
316
+ }
317
+ process.exit(2);
318
+ }
319
+
320
+ const report = buildCapabilityReport(runtimeId, runtimes[runtimeId], roles);
321
+ if (options.json) { printJson(report, 0); return; }
322
+
323
+ console.log('');
324
+ console.log(chalk.bold(' AgentXchain Connector Capabilities'));
325
+ console.log(chalk.dim(' ' + '='.repeat(44)));
326
+ printCapabilitiesText(report);
327
+ console.log('');
328
+ process.exit(0);
329
+ }
330
+
195
331
  export async function connectorValidateCommand(runtimeId, options = {}) {
196
332
  const context = loadProjectContext();
197
333
  if (!context) {
@@ -326,6 +326,9 @@ export async function restartCommand(opts) {
326
326
  console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
327
327
  process.exit(1);
328
328
  }
329
+ if (reactivated.migration_notice) {
330
+ console.log(chalk.yellow(reactivated.migration_notice));
331
+ }
329
332
  }
330
333
 
331
334
  // Determine role from option or routing
@@ -144,6 +144,9 @@ export async function resumeCommand(opts) {
144
144
  process.exit(1);
145
145
  }
146
146
  state = reactivated.state;
147
+ if (reactivated.migration_notice) {
148
+ console.log(chalk.yellow(reactivated.migration_notice));
149
+ }
147
150
 
148
151
  // Write dispatch bundle for the existing turn
149
152
  const bundleResult = writeDispatchBundle(root, state, config);
@@ -204,6 +207,9 @@ export async function resumeCommand(opts) {
204
207
  process.exit(1);
205
208
  }
206
209
  state = reactivated.state;
210
+ if (reactivated.migration_notice) {
211
+ console.log(chalk.yellow(reactivated.migration_notice));
212
+ }
207
213
 
208
214
  const bundleResult = writeDispatchBundle(root, state, config, { turnId: retainedTurn.turn_id });
209
215
  if (!bundleResult.ok) {
@@ -233,6 +239,9 @@ export async function resumeCommand(opts) {
233
239
  }
234
240
  state = initResult.state;
235
241
  console.log(chalk.green(`Initialized governed run: ${state.run_id}`));
242
+ if (initResult.migration_notice) {
243
+ console.log(chalk.yellow(initResult.migration_notice));
244
+ }
236
245
  }
237
246
 
238
247
  // §47: paused + run_id exists → resume same run
@@ -244,6 +253,9 @@ export async function resumeCommand(opts) {
244
253
  }
245
254
  state = reactivated.state;
246
255
  console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
256
+ if (reactivated.migration_notice) {
257
+ console.log(chalk.yellow(reactivated.migration_notice));
258
+ }
247
259
  }
248
260
 
249
261
  // §47: paused + run_id exists → resume same run
@@ -255,6 +267,9 @@ export async function resumeCommand(opts) {
255
267
  }
256
268
  state = reactivated.state;
257
269
  console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
270
+ if (reactivated.migration_notice) {
271
+ console.log(chalk.yellow(reactivated.migration_notice));
272
+ }
258
273
  }
259
274
 
260
275
  // Print run-context header before dispatch
@@ -260,6 +260,9 @@ export async function stepCommand(opts) {
260
260
  process.exit(1);
261
261
  }
262
262
  state = reactivated.state;
263
+ if (reactivated.migration_notice) {
264
+ console.log(chalk.yellow(reactivated.migration_notice));
265
+ }
263
266
  skipAssignment = true;
264
267
 
265
268
  // BUG-1 fix: refresh baseline snapshot to capture files dirtied between assignment and dispatch
@@ -285,6 +288,9 @@ export async function stepCommand(opts) {
285
288
  }
286
289
  state = initResult.state;
287
290
  console.log(chalk.green(`Initialized governed run: ${state.run_id}`));
291
+ if (initResult.migration_notice) {
292
+ console.log(chalk.yellow(initResult.migration_notice));
293
+ }
288
294
  }
289
295
 
290
296
  // paused → resume
@@ -296,6 +302,9 @@ export async function stepCommand(opts) {
296
302
  }
297
303
  state = reactivated.state;
298
304
  console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
305
+ if (reactivated.migration_notice) {
306
+ console.log(chalk.yellow(reactivated.migration_notice));
307
+ }
299
308
  }
300
309
 
301
310
  if (!skipAssignment && state.status === 'paused' && state.run_id) {
@@ -306,6 +315,9 @@ export async function stepCommand(opts) {
306
315
  }
307
316
  state = reactivated.state;
308
317
  console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
318
+ if (reactivated.migration_notice) {
319
+ console.log(chalk.yellow(reactivated.migration_notice));
320
+ }
309
321
  }
310
322
 
311
323
  // Assign the turn
@@ -106,6 +106,12 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
106
106
  const tempBase = mkdtempSync(join(tmpdir(), 'axc-connector-validate-'));
107
107
  const scratchRoot = join(tempBase, 'workspace');
108
108
  const warnings = [...roleSelection.warnings];
109
+
110
+ // Surface capability declaration warnings for self-declared connectors
111
+ const { getCapabilityDeclarationWarnings } = await import('./runtime-capabilities.js');
112
+ const capWarnings = getCapabilityDeclarationWarnings(runtime);
113
+ warnings.push(...capWarnings);
114
+
109
115
  let keepArtifacts = options.keepArtifacts === true;
110
116
  let dispatch = null;
111
117
  let validation = null;
@@ -24,6 +24,11 @@ import {
24
24
  } from './intake.js';
25
25
  import { loadProjectState } from './config.js';
26
26
  import { safeWriteJson } from './safe-write.js';
27
+ import { emitRunEvent } from './run-events.js';
28
+ import {
29
+ archiveStaleIntentsForRun,
30
+ formatLegacyIntentMigrationNotice,
31
+ } from './intent-startup-migration.js';
27
32
 
28
33
  const CONTINUOUS_SESSION_PATH = '.agentxchain/continuous-session.json';
29
34
 
@@ -56,7 +61,7 @@ export function removeContinuousSession(root) {
56
61
  }
57
62
  }
58
63
 
59
- function createSession(visionPath, maxRuns, maxIdleCycles, perSessionMaxUsd) {
64
+ function createSession(visionPath, maxRuns, maxIdleCycles, perSessionMaxUsd, currentRunId = null) {
60
65
  return {
61
66
  session_id: `cont-${randomUUID().slice(0, 8)}`,
62
67
  started_at: new Date().toISOString(),
@@ -65,12 +70,13 @@ function createSession(visionPath, maxRuns, maxIdleCycles, perSessionMaxUsd) {
65
70
  max_runs: maxRuns,
66
71
  idle_cycles: 0,
67
72
  max_idle_cycles: maxIdleCycles,
68
- current_run_id: null,
73
+ current_run_id: currentRunId,
69
74
  current_vision_objective: null,
70
75
  status: 'running',
71
76
  per_session_max_usd: perSessionMaxUsd || null,
72
77
  cumulative_spent_usd: 0,
73
78
  budget_exhausted: false,
79
+ startup_reconciled_run_id: null,
74
80
  };
75
81
  }
76
82
 
@@ -133,8 +139,46 @@ function buildContinuousProvenance(intentId, options = {}) {
133
139
  };
134
140
  }
135
141
 
136
- export function findNextQueuedIntent(root) {
137
- return findNextDispatchableIntent(root);
142
+ export function findNextQueuedIntent(root, options = {}) {
143
+ return findNextDispatchableIntent(root, { run_id: options.run_id || null });
144
+ }
145
+
146
+ function reconcileContinuousStartupState(context, session, contOpts, log) {
147
+ const { root, config } = context;
148
+ const governedState = loadProjectState(root, config);
149
+ const scopedRunId = session.current_run_id || contOpts.continueFrom || governedState?.run_id || null;
150
+
151
+ let sessionChanged = false;
152
+ if (scopedRunId && session.current_run_id !== scopedRunId) {
153
+ session.current_run_id = scopedRunId;
154
+ sessionChanged = true;
155
+ }
156
+
157
+ if (scopedRunId && session.startup_reconciled_run_id !== scopedRunId) {
158
+ const startupIntents = archiveStaleIntentsForRun(root, scopedRunId, {
159
+ protocolVersion: governedState?.protocol_version || config?.schema_version || '2.x',
160
+ });
161
+ if (startupIntents.archived_migration_intent_ids?.length > 0) {
162
+ emitRunEvent(root, 'intents_migrated', {
163
+ run_id: scopedRunId,
164
+ phase: governedState?.phase || null,
165
+ status: governedState?.status || 'active',
166
+ payload: {
167
+ archived_count: startupIntents.archived_migration_intent_ids.length,
168
+ archived_intent_ids: startupIntents.archived_migration_intent_ids,
169
+ reason: 'pre-BUG-34 intents with approved_run_id: null archived during continuous startup',
170
+ },
171
+ });
172
+ const migrationNotice = formatLegacyIntentMigrationNotice(startupIntents.archived_migration_intent_ids);
173
+ if (migrationNotice) log(migrationNotice);
174
+ }
175
+ session.startup_reconciled_run_id = scopedRunId;
176
+ sessionChanged = true;
177
+ }
178
+
179
+ if (sessionChanged) {
180
+ writeContinuousSession(root, session);
181
+ }
138
182
  }
139
183
 
140
184
  // ---------------------------------------------------------------------------
@@ -232,6 +276,7 @@ export function resolveContinuousOptions(opts, config) {
232
276
 
233
277
  return {
234
278
  enabled: opts.continuous ?? configCont.enabled ?? false,
279
+ continueFrom: opts.continueFrom ?? null,
235
280
  visionPath: opts.vision ?? configCont.vision_path ?? '.planning/VISION.md',
236
281
  maxRuns: opts.maxRuns ?? configCont.max_runs ?? 100,
237
282
  pollSeconds: opts.pollSeconds ?? configCont.poll_seconds ?? 30,
@@ -288,6 +333,8 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
288
333
  return { ok: true, status: 'completed', action: 'session_budget_exhausted', stop_reason: 'session_budget' };
289
334
  }
290
335
 
336
+ reconcileContinuousStartupState(context, session, contOpts, log);
337
+
291
338
  // Paused-session guard: if session is paused (blocked run awaiting unblock),
292
339
  // check governed state before attempting to advance. Without this guard, the
293
340
  // loop would try to startIntent() on a blocked project, hit the blocked-state
@@ -540,7 +587,15 @@ export async function executeContinuousRun(context, contOpts, executeGovernedRun
540
587
  return { exitCode: 1, session: null };
541
588
  }
542
589
 
543
- const session = createSession(contOpts.visionPath, contOpts.maxRuns, contOpts.maxIdleCycles, contOpts.perSessionMaxUsd);
590
+ const startupState = loadProjectState(root, context.config);
591
+ const initialRunId = contOpts.continueFrom || startupState?.run_id || null;
592
+ const session = createSession(
593
+ contOpts.visionPath,
594
+ contOpts.maxRuns,
595
+ contOpts.maxIdleCycles,
596
+ contOpts.perSessionMaxUsd,
597
+ initialRunId,
598
+ );
544
599
  writeContinuousSession(root, session);
545
600
 
546
601
  // SIGINT handler
@@ -389,6 +389,20 @@ function renderPrompt(role, roleId, turn, state, config, root) {
389
389
  }
390
390
  lines.push('');
391
391
  }
392
+ if (turn.conflict_context.forward_revision_files?.length) {
393
+ lines.push('Forward-revision files already safe to carry forward:');
394
+ for (const file of turn.conflict_context.forward_revision_files) {
395
+ lines.push(`- \`${file}\``);
396
+ }
397
+ if (turn.conflict_context.forward_revision_turns_since?.length) {
398
+ lines.push('');
399
+ lines.push('Forward-revision turns since assignment:');
400
+ for (const acceptedTurn of turn.conflict_context.forward_revision_turns_since) {
401
+ lines.push(`- \`${acceptedTurn.turn_id}\` (${acceptedTurn.role}) touched: ${acceptedTurn.files_changed.join(', ') || '(none)'}`);
402
+ }
403
+ }
404
+ lines.push('');
405
+ }
392
406
  if (turn.conflict_context.non_conflicting_files_preserved?.length) {
393
407
  lines.push('Non-conflicting files to preserve from your prior attempt:');
394
408
  for (const file of turn.conflict_context.non_conflicting_files_preserved) {
@@ -46,6 +46,10 @@ import { emitRunEvent } from './run-events.js';
46
46
  import { writeSessionCheckpoint } from './session-checkpoint.js';
47
47
  import { recordRunHistory } from './run-history.js';
48
48
  import { buildDefaultRunProvenance } from './run-provenance.js';
49
+ import {
50
+ archiveStaleIntentsForRun,
51
+ formatLegacyIntentMigrationNotice,
52
+ } from './intent-startup-migration.js';
49
53
  import {
50
54
  ensureHumanEscalation,
51
55
  findCurrentHumanEscalation,
@@ -1243,6 +1247,13 @@ function buildConflictContext(turn) {
1243
1247
  files_changed: Array.isArray(entry.files_changed) ? entry.files_changed : [],
1244
1248
  }))
1245
1249
  : [];
1250
+ const forwardRevisionTurnsSince = Array.isArray(conflictError.forward_revision_turns)
1251
+ ? conflictError.forward_revision_turns.map((entry) => ({
1252
+ turn_id: entry.turn_id,
1253
+ role: entry.role,
1254
+ files_changed: Array.isArray(entry.files_changed) ? entry.files_changed : [],
1255
+ }))
1256
+ : [];
1246
1257
 
1247
1258
  return {
1248
1259
  prior_attempt_turn_id: turn.turn_id,
@@ -1250,6 +1261,8 @@ function buildConflictContext(turn) {
1250
1261
  conflict_type: conflictError.type || 'file_conflict',
1251
1262
  conflicting_files: Array.isArray(conflictError.conflicting_files) ? conflictError.conflicting_files : [],
1252
1263
  accepted_turns_since: acceptedTurnsSince,
1264
+ forward_revision_files: Array.isArray(conflictError.forward_revision_files) ? conflictError.forward_revision_files : [],
1265
+ forward_revision_turns_since: forwardRevisionTurnsSince,
1253
1266
  non_conflicting_files_preserved: Array.isArray(conflictError.non_conflicting_files)
1254
1267
  ? conflictError.non_conflicting_files
1255
1268
  : [],
@@ -2067,6 +2080,25 @@ export function reactivateGovernedRun(root, state, details = {}) {
2067
2080
 
2068
2081
  writeState(root, nextState);
2069
2082
 
2083
+ const startupIntents = nextState.run_id
2084
+ ? archiveStaleIntentsForRun(root, nextState.run_id, {
2085
+ protocolVersion: nextState.protocol_version || state.protocol_version || '2.x',
2086
+ })
2087
+ : { archived_migration_intent_ids: [], migration_notice: null };
2088
+
2089
+ if (startupIntents.archived_migration_intent_ids?.length > 0) {
2090
+ emitRunEvent(root, 'intents_migrated', {
2091
+ run_id: nextState.run_id,
2092
+ phase: nextState.phase,
2093
+ status: nextState.status,
2094
+ payload: {
2095
+ archived_count: startupIntents.archived_migration_intent_ids.length,
2096
+ archived_intent_ids: startupIntents.archived_migration_intent_ids,
2097
+ reason: 'pre-BUG-34 intents with approved_run_id: null archived during run reactivation',
2098
+ },
2099
+ });
2100
+ }
2101
+
2070
2102
  if (humanEscalation) {
2071
2103
  resolveHumanEscalation(root, humanEscalation.escalation_id, {
2072
2104
  resolved_at: now,
@@ -2108,7 +2140,11 @@ export function reactivateGovernedRun(root, state, details = {}) {
2108
2140
  });
2109
2141
  }
2110
2142
 
2111
- return { ok: true, state: attachLegacyCurrentTurnAlias(nextState) };
2143
+ return {
2144
+ ok: true,
2145
+ state: attachLegacyCurrentTurnAlias(nextState),
2146
+ migration_notice: formatLegacyIntentMigrationNotice(startupIntents.archived_migration_intent_ids),
2147
+ };
2112
2148
  }
2113
2149
 
2114
2150
  // ── Core Operations ──────────────────────────────────────────────────────────
@@ -2162,48 +2198,10 @@ export function initializeGovernedRun(root, config, options = {}) {
2162
2198
 
2163
2199
  writeState(root, updatedState);
2164
2200
 
2165
- // BUG-34 + BUG-39: retroactive migration — archive stale intents from prior runs.
2166
- // Intents with an approved_run_id from a DIFFERENT run are archived (BUG-34).
2167
- // BUG-39: Intents with approved_run_id: null are pre-BUG-34 legacy files that
2168
- // must be archived with status "archived_migration", NOT adopted into the current
2169
- // run. Silently adopting them caused continuous mode to pick up stale intents.
2170
- const archivedMigrationIntentIds = [];
2171
- try {
2172
- const intentsDir = join(root, '.agentxchain', 'intake', 'intents');
2173
- if (existsSync(intentsDir)) {
2174
- const DISPATCHABLE = new Set(['planned', 'approved']);
2175
- const intNow = new Date().toISOString();
2176
- for (const f of readdirSync(intentsDir).filter(x => x.endsWith('.json') && !x.startsWith('.tmp-'))) {
2177
- const ip = join(intentsDir, f);
2178
- try {
2179
- const intent = JSON.parse(readFileSync(ip, 'utf8'));
2180
- if (!intent || !DISPATCHABLE.has(intent.status)) continue;
2181
- if (intent.cross_run_durable === true) continue;
2182
- if (intent.approved_run_id === runId) continue;
2183
-
2184
- const prevStatus = intent.status;
2185
- if (intent.approved_run_id && intent.approved_run_id !== runId) {
2186
- // Intent from a different run — archive it (BUG-34)
2187
- intent.status = 'suppressed';
2188
- intent.updated_at = intNow;
2189
- intent.archived_reason = `stale: approved under run ${intent.approved_run_id}, archived on run ${runId} initialization`;
2190
- if (!intent.history) intent.history = [];
2191
- intent.history.push({ from: prevStatus, to: 'suppressed', at: intNow, reason: intent.archived_reason });
2192
- } else if (!intent.approved_run_id) {
2193
- // BUG-39: pre-BUG-34 legacy intent with no run binding — archive it
2194
- // with explicit migration reason. Do NOT adopt into current run.
2195
- intent.status = 'archived_migration';
2196
- intent.updated_at = intNow;
2197
- intent.archived_reason = `pre-BUG-34 intent with no run scope; archived during v${updatedState.protocol_version || '2.x'} migration on run ${runId}`;
2198
- if (!intent.history) intent.history = [];
2199
- intent.history.push({ from: prevStatus, to: 'archived_migration', at: intNow, reason: intent.archived_reason });
2200
- if (intent.intent_id) archivedMigrationIntentIds.push(intent.intent_id);
2201
- }
2202
- safeWriteJson(ip, intent);
2203
- } catch { /* non-fatal per-intent */ }
2204
- }
2205
- }
2206
- } catch { /* non-fatal — intent migration is best-effort */ }
2201
+ const startupIntents = archiveStaleIntentsForRun(root, runId, {
2202
+ protocolVersion: updatedState.protocol_version || '2.x',
2203
+ });
2204
+ const archivedMigrationIntentIds = startupIntents.archived_migration_intent_ids || [];
2207
2205
 
2208
2206
  // BUG-39: emit intents_migrated event when pre-BUG-34 intents were archived
2209
2207
  if (archivedMigrationIntentIds.length > 0) {
@@ -2226,9 +2224,7 @@ export function initializeGovernedRun(root, config, options = {}) {
2226
2224
  payload: { provenance: provenance || {} },
2227
2225
  });
2228
2226
  // BUG-39: return migration notice so callers can display it
2229
- const migrationNotice = archivedMigrationIntentIds.length > 0
2230
- ? `Archived ${archivedMigrationIntentIds.length} pre-BUG-34 intent(s). Review: agentxchain intake status --archived.`
2231
- : null;
2227
+ const migrationNotice = startupIntents.migration_notice;
2232
2228
 
2233
2229
  return { ok: true, state: attachLegacyCurrentTurnAlias(updatedState), migration_notice: migrationNotice };
2234
2230
  }