agentxchain 2.135.0 → 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.
@@ -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;
@@ -10,6 +10,8 @@ const SECTION_DEFINITIONS = [
10
10
  { id: 'last_turn_summary', header: null, required: false },
11
11
  { id: 'last_turn_decisions', header: null, required: false },
12
12
  { id: 'last_turn_objections', header: null, required: false },
13
+ { id: 'last_turn_files_changed', header: null, required: false },
14
+ { id: 'last_turn_changed_file_previews', header: null, required: false },
13
15
  { id: 'last_turn_verification', header: null, required: false },
14
16
  { id: 'decision_history', header: 'Decision History', required: false },
15
17
  { id: 'blockers', header: 'Blockers', required: true },
@@ -54,6 +56,8 @@ export function parseContextSections(contextMd) {
54
56
  summaryLines,
55
57
  decisionsLines,
56
58
  objectionsLines,
59
+ filesChangedLines,
60
+ changedFilePreviewLines,
57
61
  verificationLines,
58
62
  } = splitLastAcceptedTurn(lastAcceptedTurnBody);
59
63
 
@@ -61,6 +65,8 @@ export function parseContextSections(contextMd) {
61
65
  pushSection(parsedSections, 'last_turn_summary', summaryLines);
62
66
  pushSection(parsedSections, 'last_turn_decisions', decisionsLines);
63
67
  pushSection(parsedSections, 'last_turn_objections', objectionsLines);
68
+ pushSection(parsedSections, 'last_turn_files_changed', filesChangedLines);
69
+ pushSection(parsedSections, 'last_turn_changed_file_previews', changedFilePreviewLines);
64
70
  pushSection(parsedSections, 'last_turn_verification', verificationLines);
65
71
  }
66
72
 
@@ -91,6 +97,8 @@ export function renderContextSections(sections) {
91
97
  sectionMap.get('last_turn_summary')?.content,
92
98
  sectionMap.get('last_turn_decisions')?.content,
93
99
  sectionMap.get('last_turn_objections')?.content,
100
+ sectionMap.get('last_turn_files_changed')?.content,
101
+ sectionMap.get('last_turn_changed_file_previews')?.content,
94
102
  sectionMap.get('last_turn_verification')?.content,
95
103
  ]);
96
104
 
@@ -157,6 +165,8 @@ function splitLastAcceptedTurn(lines) {
157
165
  let summaryLines = [];
158
166
  let decisionsLines = [];
159
167
  let objectionsLines = [];
168
+ let filesChangedLines = [];
169
+ let changedFilePreviewLines = [];
160
170
  let verificationLines = [];
161
171
 
162
172
  let inVerification = false;
@@ -200,6 +210,20 @@ function splitLastAcceptedTurn(lines) {
200
210
  continue;
201
211
  }
202
212
 
213
+ if (line.startsWith('### Files Changed')) {
214
+ const { blockLines, nextIndex } = consumeLevel3Block(lines, index);
215
+ filesChangedLines = blockLines;
216
+ index = nextIndex - 1;
217
+ continue;
218
+ }
219
+
220
+ if (line.startsWith('### Changed File Previews')) {
221
+ const { blockLines, nextIndex } = consumeLevel3Block(lines, index);
222
+ changedFilePreviewLines = blockLines;
223
+ index = nextIndex - 1;
224
+ continue;
225
+ }
226
+
203
227
  headerLines.push(line);
204
228
  }
205
229
 
@@ -208,6 +232,8 @@ function splitLastAcceptedTurn(lines) {
208
232
  summaryLines: trimBlankLines(summaryLines),
209
233
  decisionsLines: trimBlankLines(decisionsLines),
210
234
  objectionsLines: trimBlankLines(objectionsLines),
235
+ filesChangedLines: trimBlankLines(filesChangedLines),
236
+ changedFilePreviewLines: trimBlankLines(changedFilePreviewLines),
211
237
  verificationLines: trimBlankLines(verificationLines),
212
238
  };
213
239
  }
@@ -232,6 +258,32 @@ function consumeIndentedBlock(lines, startIndex) {
232
258
  };
233
259
  }
234
260
 
261
+ function consumeLevel3Block(lines, startIndex) {
262
+ const blockLines = [lines[startIndex]];
263
+ let index = startIndex + 1;
264
+ let inCodeBlock = false;
265
+
266
+ while (index < lines.length) {
267
+ const line = lines[index];
268
+
269
+ if (line.startsWith('```')) {
270
+ inCodeBlock = !inCodeBlock;
271
+ }
272
+
273
+ if (!inCodeBlock && line.startsWith('### ')) {
274
+ break;
275
+ }
276
+
277
+ blockLines.push(line);
278
+ index += 1;
279
+ }
280
+
281
+ return {
282
+ blockLines: trimBlankLines(blockLines),
283
+ nextIndex: index,
284
+ };
285
+ }
286
+
235
287
  function pushSection(target, id, lines) {
236
288
  const normalizedLines = trimBlankLines(lines || []);
237
289
  if (!normalizedLines.length) return;
@@ -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) {
@@ -85,6 +85,15 @@ function addMissingFile(result, filePath) {
85
85
  }
86
86
  }
87
87
 
88
+ function addFailingFile(result, filePath) {
89
+ if (!filePath) {
90
+ return;
91
+ }
92
+ if (!result.failing_files.includes(filePath)) {
93
+ result.failing_files.push(filePath);
94
+ }
95
+ }
96
+
88
97
  function prefixSemanticReason(filePath, reason) {
89
98
  if (!reason || reason.includes(filePath)) {
90
99
  return reason;
@@ -111,6 +120,7 @@ function evaluateGateArtifacts({ root, config, gateDef, phase, result, state })
111
120
  if (!existsSync(absPath)) {
112
121
  if (artifact.required) {
113
122
  addMissingFile(result, artifact.path);
123
+ addFailingFile(result, artifact.path);
114
124
  failures.push(`Required file missing: ${artifact.path}`);
115
125
  }
116
126
  continue;
@@ -119,6 +129,7 @@ function evaluateGateArtifacts({ root, config, gateDef, phase, result, state })
119
129
  if (artifact.useLegacySemantics) {
120
130
  const semanticCheck = evaluateWorkflowGateSemantics(root, artifact.path);
121
131
  if (semanticCheck && !semanticCheck.ok) {
132
+ addFailingFile(result, artifact.path);
122
133
  failures.push(semanticCheck.reason);
123
134
  }
124
135
  }
@@ -130,12 +141,14 @@ function evaluateGateArtifacts({ root, config, gateDef, phase, result, state })
130
141
  semantics_config: semantic.semantics_config,
131
142
  });
132
143
  if (semanticCheck && !semanticCheck.ok) {
144
+ addFailingFile(result, artifact.path);
133
145
  failures.push(prefixSemanticReason(artifact.path, semanticCheck.reason));
134
146
  }
135
147
  }
136
148
 
137
149
  // Charter enforcement: verify owning role participated in this phase
138
150
  if (artifact.owned_by && !hasRoleParticipationInPhase(state, phase, artifact.owned_by)) {
151
+ addFailingFile(result, artifact.path);
139
152
  failures.push(
140
153
  `"${artifact.path}" requires participation from role "${artifact.owned_by}" in phase "${phase}", but no accepted turn from that role was found`,
141
154
  );
@@ -161,11 +174,12 @@ function evaluateGateArtifacts({ root, config, gateDef, phase, result, state })
161
174
  * @property {boolean} blocked_by_human_approval - gate passed structurally but needs human sign-off
162
175
  * @property {string[]} reasons - human-readable failure reasons
163
176
  * @property {string[]} missing_files - files required by gate but not found
177
+ * @property {string[]} failing_files - files tied to gate failures
164
178
  * @property {boolean} missing_verification - verification required but not passed
165
179
  * @property {string|null} next_phase - the target phase if transition was requested and gate passed
166
180
  * @property {string|null} transition_request - the raw phase_transition_request value
167
181
  * @property {'no_request'|'unknown_phase'|'gate_failed'|'advance'|'awaiting_human_approval'|'no_gate'} action
168
- */
182
+ */
169
183
  export function evaluatePhaseExit({ state, config, acceptedTurn, root }) {
170
184
  const currentPhase = state.phase;
171
185
  const transitionRequest = acceptedTurn.phase_transition_request || null;
@@ -176,6 +190,7 @@ export function evaluatePhaseExit({ state, config, acceptedTurn, root }) {
176
190
  blocked_by_human_approval: false,
177
191
  reasons: [],
178
192
  missing_files: [],
193
+ failing_files: [],
179
194
  missing_verification: false,
180
195
  next_phase: null,
181
196
  transition_request: transitionRequest,
@@ -303,6 +318,7 @@ export function evaluatePhaseExit({ state, config, acceptedTurn, root }) {
303
318
  * @property {boolean} blocked_by_human_approval - gate passed structurally but needs human sign-off
304
319
  * @property {string[]} reasons - human-readable failure reasons
305
320
  * @property {string[]} missing_files - files required by gate but not found
321
+ * @property {string[]} failing_files - files tied to gate failures
306
322
  * @property {boolean} missing_verification - verification required but not passed
307
323
  * @property {'no_request'|'not_final_phase'|'gate_failed'|'complete'|'awaiting_human_approval'} action
308
324
  */
@@ -313,6 +329,7 @@ export function evaluateRunCompletion({ state, config, acceptedTurn, root }) {
313
329
  blocked_by_human_approval: false,
314
330
  reasons: [],
315
331
  missing_files: [],
332
+ failing_files: [],
316
333
  missing_verification: false,
317
334
  action: 'no_request',
318
335
  };