agentxchain 2.138.0 → 2.139.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.138.0",
3
+ "version": "2.139.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,14 @@ export async function intakeApproveCommand(opts) {
24
24
  console.log(JSON.stringify(result, null, 2));
25
25
  } else if (result.ok) {
26
26
  console.log('');
27
+ if (result.superseded) {
28
+ console.log(chalk.yellow(` Superseded intent ${result.intent.intent_id}`));
29
+ console.log(chalk.dim(` Approver: ${result.intent.approved_by}`));
30
+ console.log(chalk.dim(` Status: ${result.intent.history.at(-2)?.to || 'triaged'} → superseded`));
31
+ console.log(chalk.dim(` Reason: ${result.intent.archived_reason}`));
32
+ console.log('');
33
+ process.exit(result.exitCode);
34
+ }
27
35
  console.log(chalk.green(` Approved intent ${result.intent.intent_id}`));
28
36
  console.log(chalk.dim(` Approver: ${result.intent.approved_by}`));
29
37
  console.log(chalk.dim(` Status: triaged → approved`));
@@ -68,6 +68,10 @@ import {
68
68
  } from './verification-replay.js';
69
69
  import { executeGateActions } from './gate-actions.js';
70
70
  import { detectPendingCheckpoint } from './turn-checkpoint.js';
71
+ import {
72
+ derivePhaseScopeFromIntentMetadata,
73
+ evaluateAcceptanceItemLifecycle,
74
+ } from './intent-phase-scope.js';
71
75
 
72
76
  // ── Constants ────────────────────────────────────────────────────────────────
73
77
 
@@ -78,6 +82,7 @@ const STAGING_PATH = '.agentxchain/staging/turn-result.json';
78
82
  const TALK_PATH = 'TALK.md';
79
83
  const ACCEPTANCE_LOCK_PATH = '.agentxchain/locks/accept-turn.lock';
80
84
  const ACCEPTANCE_JOURNAL_DIR = '.agentxchain/transactions/accept';
85
+ const INTAKE_INTENTS_DIR = '.agentxchain/intake/intents';
81
86
  const STALE_LOCK_TIMEOUT_MS = 30_000;
82
87
  const GOVERNED_SCHEMA_VERSION = '1.1';
83
88
 
@@ -101,6 +106,69 @@ function buildInitialPhaseGateStatus(config) {
101
106
  );
102
107
  }
103
108
 
109
+ function listIntakeIntentFiles(root) {
110
+ const intentsDir = join(root, INTAKE_INTENTS_DIR);
111
+ if (!existsSync(intentsDir)) return [];
112
+
113
+ return readdirSync(intentsDir)
114
+ .filter((name) => name.endsWith('.json'))
115
+ .map((name) => join(intentsDir, name));
116
+ }
117
+
118
+ function retireApprovedPhaseScopedIntents(root, state, config, exitedPhase, now) {
119
+ const retired = [];
120
+
121
+ for (const intentPath of listIntakeIntentFiles(root)) {
122
+ let intent;
123
+ try {
124
+ intent = JSON.parse(readFileSync(intentPath, 'utf8'));
125
+ } catch {
126
+ continue;
127
+ }
128
+
129
+ if (!intent || intent.status !== 'approved') continue;
130
+ if (state?.run_id && intent.approved_run_id && intent.approved_run_id !== state.run_id) continue;
131
+ if (state?.run_id && !intent.approved_run_id && intent.cross_run_durable !== true) continue;
132
+
133
+ const effectivePhaseScope = derivePhaseScopeFromIntentMetadata(intent, config);
134
+ const acceptanceItems = Array.isArray(intent.acceptance_contract) ? intent.acceptance_contract : [];
135
+ const lifecycleStates = acceptanceItems.map((item) => evaluateAcceptanceItemLifecycle(
136
+ item,
137
+ { phase_scope: effectivePhaseScope },
138
+ state,
139
+ config,
140
+ ));
141
+ const retireByPhaseExit = effectivePhaseScope === exitedPhase;
142
+ const retireByGateState = acceptanceItems.length > 0
143
+ && lifecycleStates.every((entry) => entry.satisfied_by_gate_state || entry.phase_exited);
144
+
145
+ if (!retireByPhaseExit && !retireByGateState) continue;
146
+
147
+ intent.status = 'satisfied';
148
+ intent.phase_scope = effectivePhaseScope || intent.phase_scope || null;
149
+ intent.updated_at = now;
150
+ intent.satisfied_at = now;
151
+ intent.satisfied_reason = retireByPhaseExit
152
+ ? `phase ${exitedPhase} exited; ${exitedPhase}-scoped repair no longer required`
153
+ : `acceptance items satisfied by gate state after phase advance from ${exitedPhase}`;
154
+ if (!Array.isArray(intent.history)) {
155
+ intent.history = [];
156
+ }
157
+ intent.history.push({
158
+ from: 'approved',
159
+ to: 'satisfied',
160
+ at: now,
161
+ reason: intent.satisfied_reason,
162
+ exited_phase: exitedPhase,
163
+ entered_phase: state?.phase || null,
164
+ });
165
+ safeWriteJson(intentPath, intent);
166
+ retired.push(intent.intent_id);
167
+ }
168
+
169
+ return retired;
170
+ }
171
+
104
172
  function buildFreshIdleStateForNewRun(state, config) {
105
173
  return {
106
174
  schema_version: state?.schema_version || GOVERNED_SCHEMA_VERSION,
@@ -3159,7 +3227,10 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3159
3227
  // addresses each acceptance item. Default: strict for p0, lenient for others.
3160
3228
  const intakeCtx = currentTurn.intake_context;
3161
3229
  if (intakeCtx && Array.isArray(intakeCtx.acceptance_contract) && intakeCtx.acceptance_contract.length > 0) {
3162
- const intentCoverage = evaluateIntentCoverage(turnResult, intakeCtx);
3230
+ const intentCoverage = evaluateIntentCoverage(turnResult, intakeCtx, {
3231
+ state,
3232
+ config,
3233
+ });
3163
3234
  const priority = intakeCtx.priority || currentTurn.intake_context?.priority || 'p0';
3164
3235
  const intentCoverageMode = config.intent_coverage_mode
3165
3236
  || (priority === 'p0' ? 'strict' : 'lenient');
@@ -3977,6 +4048,21 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3977
4048
  [gateResult.gate_id || 'no_gate']: 'passed',
3978
4049
  };
3979
4050
  updatedState.queued_phase_transition = null;
4051
+ const retiredIntentIds = retireApprovedPhaseScopedIntents(root, updatedState, config, prevPhase, now);
4052
+ if (retiredIntentIds.length > 0) {
4053
+ emitRunEvent(root, 'intent_retired_by_phase_advance', {
4054
+ run_id: updatedState.run_id,
4055
+ phase: updatedState.phase,
4056
+ status: updatedState.status,
4057
+ turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
4058
+ payload: {
4059
+ exited_phase: prevPhase,
4060
+ entered_phase: gateResult.next_phase,
4061
+ retired_count: retiredIntentIds.length,
4062
+ retired_intent_ids: retiredIntentIds,
4063
+ },
4064
+ });
4065
+ }
3980
4066
  emitRunEvent(root, 'phase_entered', {
3981
4067
  run_id: updatedState.run_id,
3982
4068
  phase: updatedState.phase,
@@ -4019,6 +4105,21 @@ function _acceptGovernedTurnLocked(root, config, opts) {
4019
4105
  gate_id: gateResult.gate_id,
4020
4106
  timestamp: now,
4021
4107
  });
4108
+ const retiredIntentIds = retireApprovedPhaseScopedIntents(root, updatedState, config, prevPhase, now);
4109
+ if (retiredIntentIds.length > 0) {
4110
+ emitRunEvent(root, 'intent_retired_by_phase_advance', {
4111
+ run_id: updatedState.run_id,
4112
+ phase: updatedState.phase,
4113
+ status: updatedState.status,
4114
+ turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
4115
+ payload: {
4116
+ exited_phase: prevPhase,
4117
+ entered_phase: gateResult.next_phase,
4118
+ retired_count: retiredIntentIds.length,
4119
+ retired_intent_ids: retiredIntentIds,
4120
+ },
4121
+ });
4122
+ }
4022
4123
  emitRunEvent(root, 'phase_entered', {
4023
4124
  run_id: updatedState.run_id,
4024
4125
  phase: updatedState.phase,
@@ -5226,7 +5327,7 @@ function deriveNextRecommendedRole(turnResult, state, config) {
5226
5327
  // intent. Uses a hybrid approach: structural (`intent_response` field) first,
5227
5328
  // with semantic fallback scanning `summary`, `decisions`, and `files_changed`.
5228
5329
 
5229
- function evaluateIntentCoverage(turnResult, intakeContext) {
5330
+ function evaluateIntentCoverage(turnResult, intakeContext, { state = null, config = null } = {}) {
5230
5331
  const acceptanceItems = intakeContext.acceptance_contract || [];
5231
5332
  const addressed = [];
5232
5333
  const unaddressed = [];
@@ -5253,6 +5354,14 @@ function evaluateIntentCoverage(turnResult, intakeContext) {
5253
5354
 
5254
5355
  for (const item of acceptanceItems) {
5255
5356
  const normalizedItem = item.toLowerCase().trim();
5357
+ const lifecycle = state && config
5358
+ ? evaluateAcceptanceItemLifecycle(item, intakeContext, state, config)
5359
+ : null;
5360
+
5361
+ if (lifecycle?.phase_exited || lifecycle?.satisfied_by_gate_state) {
5362
+ addressed.push(item);
5363
+ continue;
5364
+ }
5256
5365
 
5257
5366
  // Check 1: Structural — intent_response field with explicit status
5258
5367
  const structuralEntry = responseMap.get(normalizedItem);
package/src/lib/intake.js CHANGED
@@ -21,7 +21,12 @@ import {
21
21
  archiveStaleIntentsForRun,
22
22
  migratePreBug34Intents,
23
23
  formatLegacyIntentMigrationNotice,
24
+ isPhantomIntent,
24
25
  } from './intent-startup-migration.js';
26
+ import {
27
+ derivePhaseScopeFromIntentMetadata,
28
+ getPhaseOrder,
29
+ } from './intent-phase-scope.js';
25
30
 
26
31
  const VALID_SOURCES = ['manual', 'ci_failure', 'git_ref_change', 'schedule', 'vision_scan'];
27
32
  const VALID_PRIORITIES = ['p0', 'p1', 'p2', 'p3'];
@@ -30,8 +35,8 @@ const INTENT_ID_RE = /^intent_\d+_[0-9a-f]{4}$/;
30
35
 
31
36
  // V3-S1 through S5 states. `failed` remains read-tolerant for historical/manual
32
37
  // intent files, but current first-party intake writers do not transition into it.
33
- const S1_STATES = new Set(['detected', 'triaged', 'approved', 'planned', 'executing', 'blocked', 'completed', 'failed', 'suppressed', 'rejected']);
34
- const TERMINAL_STATES = new Set(['suppressed', 'rejected', 'completed', 'failed']);
38
+ const S1_STATES = new Set(['detected', 'triaged', 'approved', 'planned', 'executing', 'blocked', 'completed', 'satisfied', 'failed', 'suppressed', 'rejected']);
39
+ const TERMINAL_STATES = new Set(['suppressed', 'rejected', 'completed', 'satisfied', 'failed']);
35
40
  const DISPATCHABLE_STATUSES = new Set(['planned', 'approved']);
36
41
  const PRIORITY_RANK = { p0: 0, p1: 1, p2: 2, p3: 3 };
37
42
 
@@ -282,7 +287,7 @@ export function validateEventPayload(payload) {
282
287
  return { valid: errors.length === 0, errors };
283
288
  }
284
289
 
285
- export function validateTriageFields(fields) {
290
+ export function validateTriageFields(fields, config = null) {
286
291
  const errors = [];
287
292
 
288
293
  if (!VALID_PRIORITIES.includes(fields.priority)) {
@@ -301,6 +306,17 @@ export function validateTriageFields(fields) {
301
306
  errors.push('acceptance_contract must be a non-empty array');
302
307
  }
303
308
 
309
+ if ('phase_scope' in fields && fields.phase_scope != null) {
310
+ if (typeof fields.phase_scope !== 'string' || !fields.phase_scope.trim()) {
311
+ errors.push('phase_scope must be a non-empty string when provided');
312
+ } else if (config) {
313
+ const validPhases = getPhaseOrder(config);
314
+ if (validPhases.length > 0 && !validPhases.includes(fields.phase_scope.trim())) {
315
+ errors.push(`phase_scope must be one of: ${validPhases.join(', ')}`);
316
+ }
317
+ }
318
+ }
319
+
304
320
  return { valid: errors.length === 0, errors };
305
321
  }
306
322
 
@@ -354,6 +370,7 @@ export function recordEvent(root, payload) {
354
370
  template: null,
355
371
  charter: null,
356
372
  acceptance_contract: [],
373
+ phase_scope: null,
357
374
  requires_human_start: true,
358
375
  target_run: null,
359
376
  created_at: now,
@@ -419,17 +436,25 @@ export function triageIntent(root, intentId, fields) {
419
436
  return { ok: false, error: `cannot triage from status "${intent.status}" (must be detected)`, exitCode: 1 };
420
437
  }
421
438
 
422
- const validation = validateTriageFields(fields);
439
+ const context = loadProjectContext(root);
440
+ const config = context?.config || null;
441
+ const normalizedFields = {
442
+ ...fields,
443
+ phase_scope: fields.phase_scope || derivePhaseScopeFromIntentMetadata(fields, config),
444
+ };
445
+
446
+ const validation = validateTriageFields(normalizedFields, config);
423
447
  if (!validation.valid) {
424
448
  return { ok: false, error: validation.errors.join('; '), exitCode: 1 };
425
449
  }
426
450
 
427
451
  const now = nowISO();
428
452
  intent.status = 'triaged';
429
- intent.priority = fields.priority;
430
- intent.template = fields.template;
431
- intent.charter = fields.charter;
432
- intent.acceptance_contract = fields.acceptance_contract;
453
+ intent.priority = normalizedFields.priority;
454
+ intent.template = normalizedFields.template;
455
+ intent.charter = normalizedFields.charter;
456
+ intent.acceptance_contract = normalizedFields.acceptance_contract;
457
+ intent.phase_scope = normalizedFields.phase_scope || null;
433
458
  intent.updated_at = now;
434
459
  intent.history.push({ from: 'detected', to: 'triaged', at: now, reason: 'triage completed' });
435
460
 
@@ -807,6 +832,16 @@ export function approveIntent(root, intentId, options = {}) {
807
832
  intent.status = 'approved';
808
833
  intent.approved_by = approver;
809
834
  intent.updated_at = now;
835
+
836
+ const phantomReason = 'planning artifacts for this intent already exist on disk; intent superseded during approval';
837
+ if (intent.approved_run_id && isPhantomIntent(root, intent)) {
838
+ intent.status = 'superseded';
839
+ intent.archived_reason = phantomReason;
840
+ intent.history.push({ from: previousStatus, to: 'superseded', at: now, reason: phantomReason, approver });
841
+ safeWriteJson(intentPath, intent);
842
+ return { ok: true, intent, superseded: true, exitCode: 0 };
843
+ }
844
+
810
845
  intent.history.push({ from: previousStatus, to: 'approved', at: now, reason, approver });
811
846
 
812
847
  safeWriteJson(intentPath, intent);
@@ -935,6 +970,7 @@ export function startIntent(root, intentId, options = {}) {
935
970
  category: event.category || null,
936
971
  charter: intent.charter || null,
937
972
  acceptance_contract: Array.isArray(intent.acceptance_contract) ? intent.acceptance_contract : [],
973
+ phase_scope: intent.phase_scope || null,
938
974
  };
939
975
 
940
976
  // Load governed project context
@@ -1771,6 +1807,7 @@ export function injectIntent(root, description, options = {}) {
1771
1807
  : [description.trim()];
1772
1808
  const approver = options.approver || 'human';
1773
1809
  const noApprove = options.noApprove === true;
1810
+ const phase_scope = options.phase_scope || null;
1774
1811
 
1775
1812
  // Step 1: Record event
1776
1813
  const recordResult = recordEvent(root, {
@@ -1804,6 +1841,7 @@ export function injectIntent(root, description, options = {}) {
1804
1841
  template,
1805
1842
  charter,
1806
1843
  acceptance_contract,
1844
+ phase_scope,
1807
1845
  });
1808
1846
 
1809
1847
  if (!triageResult.ok) {
@@ -0,0 +1,97 @@
1
+ function normalizeText(value) {
2
+ return typeof value === 'string' ? value.toLowerCase() : '';
3
+ }
4
+
5
+ export function getPhaseOrder(config) {
6
+ if (Array.isArray(config?.phases) && config.phases.length > 0) {
7
+ return config.phases
8
+ .map((phase) => phase?.id)
9
+ .filter((phaseId) => typeof phaseId === 'string' && phaseId.trim().length > 0);
10
+ }
11
+ return Object.keys(config?.routing || {});
12
+ }
13
+
14
+ export function buildGateToPhaseMap(config) {
15
+ const mapping = {};
16
+ for (const [phaseId, route] of Object.entries(config?.routing || {})) {
17
+ const gateId = route?.exit_gate;
18
+ if (typeof gateId === 'string' && gateId.trim().length > 0) {
19
+ mapping[gateId] = phaseId;
20
+ }
21
+ }
22
+ return mapping;
23
+ }
24
+
25
+ export function extractReferencedGateIds(text, config) {
26
+ const haystack = normalizeText(text);
27
+ if (!haystack) return [];
28
+
29
+ return Object.keys(config?.gates || {}).filter((gateId) => haystack.includes(gateId.toLowerCase()));
30
+ }
31
+
32
+ function derivePhaseScopeFromGateIds(gateIds, config) {
33
+ const gateToPhase = buildGateToPhaseMap(config);
34
+ const matchedPhases = [...new Set(
35
+ gateIds
36
+ .map((gateId) => gateToPhase[gateId] || null)
37
+ .filter(Boolean),
38
+ )];
39
+ return matchedPhases.length === 1 ? matchedPhases[0] : null;
40
+ }
41
+
42
+ export function derivePhaseScopeFromIntentMetadata(intentLike, config) {
43
+ const explicitScope = typeof intentLike?.phase_scope === 'string' && intentLike.phase_scope.trim().length > 0
44
+ ? intentLike.phase_scope.trim()
45
+ : null;
46
+ if (explicitScope) return explicitScope;
47
+ if (!config) return null;
48
+
49
+ const texts = [
50
+ intentLike?.charter || '',
51
+ ...(Array.isArray(intentLike?.acceptance_contract) ? intentLike.acceptance_contract : []),
52
+ ];
53
+ const gateIds = [...new Set(texts.flatMap((text) => extractReferencedGateIds(text, config)))];
54
+ return derivePhaseScopeFromGateIds(gateIds, config);
55
+ }
56
+
57
+ export function deriveAcceptanceItemPhaseScope(item, fallbackPhaseScope, config) {
58
+ if (!config) return fallbackPhaseScope || null;
59
+ const gateIds = extractReferencedGateIds(item, config);
60
+ const derived = derivePhaseScopeFromGateIds(gateIds, config);
61
+ return derived || fallbackPhaseScope || null;
62
+ }
63
+
64
+ export function isPhaseExited(currentPhase, scopedPhase, config) {
65
+ if (!currentPhase || !scopedPhase || !config) return false;
66
+ const order = getPhaseOrder(config);
67
+ const currentIndex = order.indexOf(currentPhase);
68
+ const scopeIndex = order.indexOf(scopedPhase);
69
+ if (currentIndex === -1 || scopeIndex === -1) return false;
70
+ return currentIndex > scopeIndex;
71
+ }
72
+
73
+ export function isAcceptanceItemSatisfiedByGateState(item, state, config) {
74
+ if (!state || !config) return false;
75
+ const itemText = normalizeText(item);
76
+ if (!itemText) return false;
77
+
78
+ const gateIds = extractReferencedGateIds(item, config);
79
+ if (gateIds.length === 0) return false;
80
+
81
+ const hasGatePassLanguage = /\b(can advance|advance to|gate can advance|gate passes|gate passed|passes|passed)\b/.test(itemText);
82
+ if (!hasGatePassLanguage) return false;
83
+
84
+ return gateIds.some((gateId) => state?.phase_gate_status?.[gateId] === 'passed');
85
+ }
86
+
87
+ export function evaluateAcceptanceItemLifecycle(item, intakeContext, state, config) {
88
+ const fallbackPhaseScope = typeof intakeContext?.phase_scope === 'string' && intakeContext.phase_scope.trim().length > 0
89
+ ? intakeContext.phase_scope.trim()
90
+ : null;
91
+ const phaseScope = deriveAcceptanceItemPhaseScope(item, fallbackPhaseScope, config);
92
+ return {
93
+ phase_scope: phaseScope,
94
+ phase_exited: isPhaseExited(state?.phase || null, phaseScope, config),
95
+ satisfied_by_gate_state: isAcceptanceItemSatisfiedByGateState(item, state, config),
96
+ };
97
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
 
4
+ import { queryAcceptedTurnHistory } from './accepted-turn-history.js';
4
5
  import { safeWriteJson } from './safe-write.js';
5
6
  import { VALID_GOVERNED_TEMPLATE_IDS, loadGovernedTemplate } from './governed-templates.js';
6
7
 
@@ -28,7 +29,37 @@ function normalizeArtifactPaths(paths) {
28
29
  )];
29
30
  }
30
31
 
31
- function readPlanningGateFiles(root) {
32
+ function parseTimestamp(value) {
33
+ if (typeof value !== 'string' || !value.trim()) return null;
34
+ const parsed = Date.parse(value);
35
+ return Number.isFinite(parsed) ? parsed : null;
36
+ }
37
+
38
+ function hasPlanningHistoryEvidence(root, intent) {
39
+ const intentId = intent?.intent_id || null;
40
+ const runId = intent?.approved_run_id || null;
41
+ const intentTimestamp = parseTimestamp(intent?.approved_at)
42
+ ?? parseTimestamp(intent?.created_at)
43
+ ?? parseTimestamp(intent?.updated_at);
44
+
45
+ for (const entry of queryAcceptedTurnHistory(root)) {
46
+ if (entry?.phase !== 'planning') continue;
47
+
48
+ if (intentId && entry.intent_id === intentId) {
49
+ return true;
50
+ }
51
+
52
+ if (!runId || entry.run_id !== runId || intentTimestamp === null) continue;
53
+ const acceptedAt = parseTimestamp(entry.accepted_at);
54
+ if (acceptedAt !== null && acceptedAt >= intentTimestamp) {
55
+ return true;
56
+ }
57
+ }
58
+
59
+ return false;
60
+ }
61
+
62
+ function readPlanningGateFiles(root, intent) {
32
63
  const configPath = join(root, 'agentxchain.json');
33
64
  if (!existsSync(configPath)) return [];
34
65
 
@@ -39,27 +70,10 @@ function readPlanningGateFiles(root) {
39
70
  return [];
40
71
  }
41
72
 
42
- // Only use planning gate requires_files for phantom detection when:
43
- // 1. The planning gate has NOT been passed yet (once passed, these files
44
- // are expected to exist from normal planning work), AND
45
- // 2. At least one turn has been completed (turn_sequence > 0). If no turns
46
- // have been completed, the files are scaffolding templates, not evidence
47
- // of completed planning work. Without this check, ANY approved intent
48
- // in a freshly scaffolded project would be falsely detected as phantom.
49
- const statePath = join(root, '.agentxchain', 'state.json');
50
- try {
51
- const state = JSON.parse(readFileSync(statePath, 'utf8'));
52
- const gateStatus = state.phase_gate_status || {};
53
- const exitGateId = config?.routing?.planning?.exit_gate;
54
- if (exitGateId && gateStatus[exitGateId] === 'passed') return [];
55
- const turnSequence = state.turn_sequence || 0;
56
- if (turnSequence === 0) return [];
57
- } catch {
58
- // If state is unreadable, fall through to check gate files
59
- }
60
-
61
73
  const exitGateId = config?.routing?.planning?.exit_gate;
62
74
  const requiresFiles = exitGateId ? config?.gates?.[exitGateId]?.requires_files : null;
75
+ if (!Array.isArray(requiresFiles) || requiresFiles.length === 0) return [];
76
+ if (!hasPlanningHistoryEvidence(root, intent)) return [];
63
77
  return normalizeArtifactPaths(requiresFiles);
64
78
  }
65
79
 
@@ -146,7 +160,7 @@ export function listExpectedPlanningArtifacts(root, intent) {
146
160
  return normalizeArtifactPaths([
147
161
  ...recordedArtifacts,
148
162
  ...templateArtifacts,
149
- ...readPlanningGateFiles(root),
163
+ ...readPlanningGateFiles(root, intent),
150
164
  ]);
151
165
  }
152
166
 
@@ -57,10 +57,15 @@ function describeEvent(eventType, entry) {
57
57
  case 'dispatch_progress':
58
58
  return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}`;
59
59
  case 'intents_migrated':
60
- case 'intents_superseded': {
60
+ case 'intents_superseded':
61
+ case 'intent_retired_by_phase_advance': {
61
62
  const count = Number.isFinite(entry.payload?.archived_count)
62
63
  ? entry.payload.archived_count
63
- : (Number.isFinite(entry.payload?.superseded_count) ? entry.payload.superseded_count : null);
64
+ : (
65
+ Number.isFinite(entry.payload?.superseded_count)
66
+ ? entry.payload.superseded_count
67
+ : (Number.isFinite(entry.payload?.retired_count) ? entry.payload.retired_count : null)
68
+ );
64
69
  return `${prefix}${eventType}${count !== null ? ` (${count})` : ''}`;
65
70
  }
66
71
  case 'run_blocked':
@@ -16,6 +16,7 @@ export const VALID_RUN_EVENTS = [
16
16
  'phase_entered',
17
17
  'intents_migrated',
18
18
  'intents_superseded',
19
+ 'intent_retired_by_phase_advance',
19
20
  'turn_dispatched',
20
21
  'turn_accepted',
21
22
  'turn_rejected',