agentxchain 2.135.1 → 2.136.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.
@@ -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
  }
package/src/lib/intake.js CHANGED
@@ -17,6 +17,11 @@ import { getDispatchTurnDir } from './turn-paths.js';
17
17
  import { loadCoordinatorConfig } from './coordinator-config.js';
18
18
  import { loadCoordinatorState, readBarriers } from './coordinator-state.js';
19
19
  import { writeCoordinatorHandoff } from './intake-handoff.js';
20
+ import {
21
+ archiveStaleIntentsForRun,
22
+ migratePreBug34Intents,
23
+ formatLegacyIntentMigrationNotice,
24
+ } from './intent-startup-migration.js';
20
25
 
21
26
  const VALID_SOURCES = ['manual', 'ci_failure', 'git_ref_change', 'schedule', 'vision_scan'];
22
27
  const VALID_PRIORITIES = ['p0', 'p1', 'p2', 'p3'];
@@ -516,16 +521,14 @@ export function findNextDispatchableIntent(root, options = {}) {
516
521
  .filter((intent) => intent && DISPATCHABLE_STATUSES.has(intent.status));
517
522
 
518
523
  // BUG-34: when run_id scoping is active, filter out intents that belong to
519
- // a different run. An intent belongs to the current run if:
520
- // (a) it has approved_run_id matching the current run, OR
521
- // (b) it has no approved_run_id AND is marked cross_run_durable, OR
522
- // (c) it was injected in the current run (approved_run_id matches)
523
- // Legacy intents (no approved_run_id, no cross_run_durable) are excluded
524
- // because they are stale leftovers from prior runs.
524
+ // a different run. `cross_run_durable` is only a pre-run holding state for
525
+ // freshly approved idle intents. Once a run starts, those intents must be
526
+ // rebound onto that run and the flag cleared; it must never override an
527
+ // existing run binding.
525
528
  if (scopeRunId) {
526
529
  intents = intents.filter((intent) => {
527
530
  if (intent.approved_run_id === scopeRunId) return true;
528
- if (intent.cross_run_durable === true) return true;
531
+ if (!intent.approved_run_id && intent.cross_run_durable === true) return true;
529
532
  // Legacy intent with no run binding — stale, skip it
530
533
  if (!intent.approved_run_id) return false;
531
534
  // Intent bound to a different run — stale, skip it
@@ -579,10 +582,11 @@ export function findPendingApprovedIntents(root, options = {}) {
579
582
  return readJsonDir(dirs.intents)
580
583
  .filter((intent) => {
581
584
  if (!intent || intent.status !== 'approved') return false;
582
- // BUG-34: run_id scoping — same logic as findNextDispatchableIntent
585
+ // BUG-34: run_id scoping — same logic as findNextDispatchableIntent.
586
+ // `cross_run_durable` only applies before the first run binding exists.
583
587
  if (scopeRunId) {
584
588
  if (intent.approved_run_id === scopeRunId) return true;
585
- if (intent.cross_run_durable === true) return true;
589
+ if (!intent.approved_run_id && intent.cross_run_durable === true) return true;
586
590
  return false;
587
591
  }
588
592
  return true;
@@ -616,51 +620,7 @@ export function findPendingApprovedIntents(root, options = {}) {
616
620
  * @returns {{ archived: number }}
617
621
  */
618
622
  export function archiveStaleIntents(root, newRunId) {
619
- const dirs = intakeDirs(root);
620
- if (!existsSync(dirs.intents)) return { archived: 0, adopted: 0 };
621
-
622
- const now = nowISO();
623
- let archived = 0;
624
- let adopted = 0;
625
-
626
- const files = readdirSync(dirs.intents).filter(f => f.endsWith('.json') && !f.startsWith('.tmp-'));
627
- for (const file of files) {
628
- const intentPath = join(dirs.intents, file);
629
- let intent;
630
- try {
631
- intent = JSON.parse(readFileSync(intentPath, 'utf8'));
632
- } catch {
633
- continue;
634
- }
635
-
636
- if (!intent || !DISPATCHABLE_STATUSES.has(intent.status)) continue;
637
- if (intent.cross_run_durable === true) continue;
638
- if (intent.approved_run_id === newRunId) continue;
639
-
640
- if (intent.approved_run_id && intent.approved_run_id !== newRunId) {
641
- // Intent from a different run — archive it
642
- intent.status = 'suppressed';
643
- intent.updated_at = now;
644
- intent.archived_reason = `stale: approved under run ${intent.approved_run_id}, archived on run ${newRunId} initialization`;
645
- if (!intent.history) intent.history = [];
646
- intent.history.push({ from: 'approved', to: 'suppressed', at: now, reason: intent.archived_reason });
647
- safeWriteJson(intentPath, intent);
648
- archived++;
649
- } else if (!intent.approved_run_id) {
650
- // BUG-39: pre-BUG-34 legacy intent with no run binding — archive it
651
- // with explicit migration reason. Do NOT adopt into current run.
652
- const prevStatus = intent.status;
653
- intent.status = 'archived_migration';
654
- intent.updated_at = now;
655
- intent.archived_reason = `pre-BUG-34 intent with no run scope; archived during migration on run ${newRunId}`;
656
- if (!intent.history) intent.history = [];
657
- intent.history.push({ from: prevStatus, to: 'archived_migration', at: now, reason: intent.archived_reason });
658
- safeWriteJson(intentPath, intent);
659
- archived++;
660
- }
661
- }
662
-
663
- return { archived, adopted };
623
+ return archiveStaleIntentsForRun(root, newRunId);
664
624
  }
665
625
 
666
626
  /**
@@ -823,9 +783,9 @@ export function approveIntent(root, intentId, options = {}) {
823
783
 
824
784
  // BUG-34/39: stamp the current run_id on approval so the intent is scoped
825
785
  // to the run that approved it. When approval happens before any governed
826
- // run exists, mark the intent as cross_run_durable so the next run init
827
- // preserves this freshly approved work instead of archiving it as legacy
828
- // migration debt.
786
+ // run exists, hold the intent in a pre-run durable state so the *next* run
787
+ // initialization can bind it to that run. The flag is not an evergreen
788
+ // cross-run bypass; it exists only until first run binding happens.
829
789
  if (!intent.approved_run_id) {
830
790
  const statePath = join(root, '.agentxchain', 'state.json');
831
791
  if (existsSync(statePath)) {
@@ -0,0 +1,151 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import { safeWriteJson } from './safe-write.js';
5
+
6
+ const DISPATCHABLE_STATUSES = new Set(['planned', 'approved']);
7
+
8
+ function nowISO() {
9
+ return new Date().toISOString();
10
+ }
11
+
12
+ function getIntentsDir(root) {
13
+ return join(root, '.agentxchain', 'intake', 'intents');
14
+ }
15
+
16
+ function listIntentFiles(intentsDir) {
17
+ if (!existsSync(intentsDir)) return [];
18
+ return readdirSync(intentsDir).filter((file) => file.endsWith('.json') && !file.startsWith('.tmp-'));
19
+ }
20
+
21
+ export function formatLegacyIntentMigrationNotice(intentIds) {
22
+ if (!Array.isArray(intentIds) || intentIds.length === 0) return null;
23
+ return `Archived ${intentIds.length} pre-BUG-34 intent(s): ${intentIds.join(', ')}`;
24
+ }
25
+
26
+ export function migratePreBug34Intents(root, runId, options = {}) {
27
+ const intentsDir = getIntentsDir(root);
28
+ if (!existsSync(intentsDir)) {
29
+ return {
30
+ archived_migration_count: 0,
31
+ archived_migration_intent_ids: [],
32
+ migration_notice: null,
33
+ };
34
+ }
35
+
36
+ const now = nowISO();
37
+ const archivedMigrationIntentIds = [];
38
+ const protocolVersion = options.protocolVersion || '2.x';
39
+
40
+ for (const file of listIntentFiles(intentsDir)) {
41
+ const intentPath = join(intentsDir, file);
42
+ let intent;
43
+ try {
44
+ intent = JSON.parse(readFileSync(intentPath, 'utf8'));
45
+ } catch {
46
+ continue;
47
+ }
48
+
49
+ if (!intent || !DISPATCHABLE_STATUSES.has(intent.status)) continue;
50
+ if (intent.cross_run_durable === true) continue;
51
+ if (intent.approved_run_id) continue;
52
+
53
+ const prevStatus = intent.status;
54
+ intent.status = 'archived_migration';
55
+ intent.updated_at = now;
56
+ intent.archived_reason = `pre-BUG-34 intent with no run scope; archived during v${protocolVersion} migration on run ${runId}`;
57
+ if (!intent.history) intent.history = [];
58
+ intent.history.push({
59
+ from: prevStatus,
60
+ to: 'archived_migration',
61
+ at: now,
62
+ reason: intent.archived_reason,
63
+ });
64
+ safeWriteJson(intentPath, intent);
65
+ if (intent.intent_id) archivedMigrationIntentIds.push(intent.intent_id);
66
+ }
67
+
68
+ return {
69
+ archived_migration_count: archivedMigrationIntentIds.length,
70
+ archived_migration_intent_ids: archivedMigrationIntentIds,
71
+ migration_notice: formatLegacyIntentMigrationNotice(archivedMigrationIntentIds),
72
+ };
73
+ }
74
+
75
+ export function archiveStaleIntentsForRun(root, runId, options = {}) {
76
+ const intentsDir = getIntentsDir(root);
77
+ if (!existsSync(intentsDir)) {
78
+ return {
79
+ archived: 0,
80
+ adopted: 0,
81
+ archived_migration_count: 0,
82
+ archived_migration_intent_ids: [],
83
+ migration_notice: null,
84
+ };
85
+ }
86
+
87
+ const now = nowISO();
88
+ let archived = 0;
89
+ let adopted = 0;
90
+
91
+ for (const file of listIntentFiles(intentsDir)) {
92
+ const intentPath = join(intentsDir, file);
93
+ let intent;
94
+ try {
95
+ intent = JSON.parse(readFileSync(intentPath, 'utf8'));
96
+ } catch {
97
+ continue;
98
+ }
99
+
100
+ if (!intent || !DISPATCHABLE_STATUSES.has(intent.status)) continue;
101
+
102
+ if (intent.cross_run_durable === true && !intent.approved_run_id) {
103
+ intent.approved_run_id = runId;
104
+ delete intent.cross_run_durable;
105
+ intent.updated_at = now;
106
+ if (!intent.history) intent.history = [];
107
+ intent.history.push({
108
+ from: intent.status,
109
+ to: intent.status,
110
+ at: now,
111
+ reason: `pre-run durable approval bound to run ${runId}`,
112
+ });
113
+ safeWriteJson(intentPath, intent);
114
+ adopted += 1;
115
+ continue;
116
+ }
117
+
118
+ if (intent.cross_run_durable === true && intent.approved_run_id === runId) {
119
+ delete intent.cross_run_durable;
120
+ intent.updated_at = now;
121
+ safeWriteJson(intentPath, intent);
122
+ continue;
123
+ }
124
+
125
+ if (intent.approved_run_id && intent.approved_run_id !== runId) {
126
+ const prevStatus = intent.status;
127
+ intent.status = 'suppressed';
128
+ intent.updated_at = now;
129
+ intent.archived_reason = `stale: approved under run ${intent.approved_run_id}, archived on run ${runId} initialization`;
130
+ if (!intent.history) intent.history = [];
131
+ intent.history.push({
132
+ from: prevStatus,
133
+ to: 'suppressed',
134
+ at: now,
135
+ reason: intent.archived_reason,
136
+ });
137
+ safeWriteJson(intentPath, intent);
138
+ archived += 1;
139
+ }
140
+ }
141
+
142
+ const migration = migratePreBug34Intents(root, runId, options);
143
+
144
+ return {
145
+ archived,
146
+ adopted,
147
+ archived_migration_count: migration.archived_migration_count,
148
+ archived_migration_intent_ids: migration.archived_migration_intent_ids,
149
+ migration_notice: migration.migration_notice,
150
+ };
151
+ }