agentxchain 2.82.0 → 2.83.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.82.0",
3
+ "version": "2.83.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@ export async function intakeStatusCommand(opts) {
19
19
 
20
20
  // Detail mode: single intent
21
21
  if (result.intent) {
22
- printIntentDetail(result.intent, result.event);
22
+ printIntentDetail(result.intent, result.event, result.next_action);
23
23
  process.exit(0);
24
24
  }
25
25
 
@@ -46,14 +46,17 @@ function printSummary(summary) {
46
46
  const pri = i.priority ? i.priority.padEnd(3) : '---';
47
47
  const tpl = (i.template || '---').padEnd(12);
48
48
  const st = statusColor(i.status);
49
- console.log(` ${chalk.dim(i.intent_id)} ${pri} ${tpl} ${st} ${chalk.dim(i.updated_at)}`);
49
+ const actionHint = i.next_action?.action_required && i.next_action?.label !== 'none'
50
+ ? ` ${chalk.dim(`→ ${i.next_action.label}`)}`
51
+ : '';
52
+ console.log(` ${chalk.dim(i.intent_id)} ${pri} ${tpl} ${st} ${chalk.dim(i.updated_at)}${actionHint}`);
50
53
  }
51
54
  }
52
55
 
53
56
  console.log('');
54
57
  }
55
58
 
56
- function printIntentDetail(intent, event) {
59
+ function printIntentDetail(intent, event, nextAction) {
57
60
  console.log('');
58
61
  console.log(chalk.bold(` Intent: ${intent.intent_id}`));
59
62
  console.log(chalk.dim(' ' + '─'.repeat(44)));
@@ -93,6 +96,21 @@ function printIntentDetail(intent, event) {
93
96
  console.log(` ${chalk.dim('Signal:')} ${JSON.stringify(event.signal)}`);
94
97
  }
95
98
 
99
+ if (nextAction) {
100
+ console.log('');
101
+ console.log(chalk.dim(' Next Action:'));
102
+ console.log(` ${nextAction.summary}`);
103
+ if (nextAction.command) {
104
+ console.log(` ${chalk.dim('Command:')} ${nextAction.command}`);
105
+ }
106
+ for (const alternative of nextAction.alternatives || []) {
107
+ console.log(` ${chalk.dim('Alternative:')} ${alternative}`);
108
+ }
109
+ if (nextAction.recovery) {
110
+ console.log(` ${chalk.dim('Recovery:')} ${nextAction.recovery}`);
111
+ }
112
+ }
113
+
96
114
  console.log('');
97
115
  }
98
116
 
@@ -167,7 +167,7 @@ export async function restartCommand(opts) {
167
167
  // Load state
168
168
  const statePath = join(root, STATE_PATH);
169
169
  if (!existsSync(statePath)) {
170
- console.log(chalk.red('No governed run found. Use `agentxchain resume` or `agentxchain run` to start.'));
170
+ console.log(chalk.red('No governed run found. Use `agentxchain run` to start a governed run.'));
171
171
  process.exit(1);
172
172
  }
173
173
 
@@ -38,6 +38,7 @@ import {
38
38
  } from '../lib/turn-paths.js';
39
39
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
40
40
  import { runHooks } from '../lib/hook-runner.js';
41
+ import { summarizeRunProvenance } from '../lib/run-provenance.js';
41
42
 
42
43
  export async function resumeCommand(opts) {
43
44
  const context = loadProjectContext();
@@ -128,6 +129,7 @@ export async function resumeCommand(opts) {
128
129
 
129
130
  const turnStatus = retainedTurn.status;
130
131
  if (turnStatus === 'failed' || turnStatus === 'retrying') {
132
+ printResumeRunContext({ root, state, config });
131
133
  console.log(chalk.yellow(`Re-dispatching failed turn: ${retainedTurn.turn_id}`));
132
134
  console.log(` Role: ${retainedTurn.assigned_role}`);
133
135
  console.log(` Attempt: ${retainedTurn.attempt}`);
@@ -187,6 +189,7 @@ export async function resumeCommand(opts) {
187
189
  process.exit(1);
188
190
  }
189
191
 
192
+ printResumeRunContext({ root, state, config });
190
193
  console.log(chalk.yellow(`Re-dispatching blocked turn: ${retainedTurn.turn_id}`));
191
194
  console.log(` Role: ${retainedTurn.assigned_role}`);
192
195
  console.log(` Attempt: ${retainedTurn.attempt}`);
@@ -251,6 +254,9 @@ export async function resumeCommand(opts) {
251
254
  console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
252
255
  }
253
256
 
257
+ // Print run-context header before dispatch
258
+ printResumeRunContext({ root, state, config });
259
+
254
260
  // Resolve target role
255
261
  const roleId = resolveTargetRole(opts, state, config);
256
262
  if (!roleId) {
@@ -302,6 +308,53 @@ export async function resumeCommand(opts) {
302
308
 
303
309
  // ── Helpers ─────────────────────────────────────────────────────────────────
304
310
 
311
+ function printResumeRunContext({ root, state, config }) {
312
+ console.log('');
313
+ console.log(chalk.cyan.bold('agentxchain resume'));
314
+ console.log(` ${chalk.dim('Run:')} ${state?.run_id || '(uninitialized)'}`);
315
+ console.log(` ${chalk.dim('Phase:')} ${state?.phase || '(unknown)'}`);
316
+
317
+ const provenanceSummary = summarizeRunProvenance(state?.provenance);
318
+ if (provenanceSummary) {
319
+ console.log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
320
+ }
321
+
322
+ if (state?.inherited_context?.parent_run_id) {
323
+ console.log(
324
+ ` ${chalk.dim('Inherits:')} ${chalk.magenta(
325
+ `parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`
326
+ )}`
327
+ );
328
+ }
329
+
330
+ const activeGate = config?.routing?.[state?.phase]?.exit_gate || null;
331
+ if (activeGate) {
332
+ const gateStatus = state?.phase_gate_status?.[activeGate] || 'pending';
333
+ console.log(` ${chalk.dim('Gate:')} ${activeGate} (${gateStatus})`);
334
+
335
+ if (gateStatus !== 'passed') {
336
+ const gateDef = config?.gates?.[activeGate];
337
+ if (Array.isArray(gateDef?.requires_files) && gateDef.requires_files.length > 0) {
338
+ const fileChecks = gateDef.requires_files.map((filePath) => {
339
+ const exists = existsSync(join(root, filePath));
340
+ const shortPath = filePath.replace(/^\.planning\//, '');
341
+ return exists ? chalk.green(shortPath) : chalk.red(shortPath);
342
+ });
343
+ console.log(` ${chalk.dim('Files:')} ${fileChecks.join(chalk.dim(', '))}`);
344
+ }
345
+
346
+ const requirements = [];
347
+ if (gateDef?.requires_human_approval) requirements.push('human approval');
348
+ if (gateDef?.requires_verification_pass) requirements.push('verification pass');
349
+ if (requirements.length > 0) {
350
+ console.log(` ${chalk.dim('Needs:')} ${requirements.join(', ')}`);
351
+ }
352
+ }
353
+ }
354
+
355
+ console.log('');
356
+ }
357
+
305
358
  function resolveTargetRole(opts, state, config) {
306
359
  const phase = state.phase;
307
360
  const routing = config.routing?.[phase];
@@ -32,6 +32,7 @@ import { dispatchRemoteAgent, describeRemoteAgentTarget } from '../lib/adapters/
32
32
  import { runHooks } from '../lib/hook-runner.js';
33
33
  import { finalizeDispatchManifest } from '../lib/dispatch-manifest.js';
34
34
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
35
+ import { summarizeRunProvenance } from '../lib/run-provenance.js';
35
36
  import { resolveGovernedRole } from '../lib/role-resolution.js';
36
37
  import { buildInheritedContext } from '../lib/run-context-inheritance.js';
37
38
  import {
@@ -179,6 +180,24 @@ export async function executeGovernedRun(context, opts = {}) {
179
180
  // ── Run header ──────────────────────────────────────────────────────────
180
181
  log(chalk.cyan.bold('agentxchain run'));
181
182
  log(chalk.dim(` Max turns: ${maxTurns} Gate mode: ${autoApprove ? 'auto-approve' : 'interactive'}`));
183
+ if (provenance) {
184
+ const provenanceSummary = summarizeRunProvenance(provenance);
185
+ if (provenanceSummary) {
186
+ log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
187
+ }
188
+ }
189
+ if (inheritedContext) {
190
+ const ic = inheritedContext;
191
+ const phasesCount = ic.parent_phases_completed?.length || 0;
192
+ const decisionsCount = ic.recent_decisions?.length || 0;
193
+ const turnsCount = ic.recent_accepted_turns?.length || 0;
194
+ const parts = [];
195
+ if (phasesCount) parts.push(`${phasesCount} phase${phasesCount !== 1 ? 's' : ''}`);
196
+ if (decisionsCount) parts.push(`${decisionsCount} decision${decisionsCount !== 1 ? 's' : ''}`);
197
+ if (turnsCount) parts.push(`${turnsCount} turn${turnsCount !== 1 ? 's' : ''}`);
198
+ const detail = parts.length ? ` — ${parts.join(', ')}` : '';
199
+ log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${ic.parent_run_id} (${ic.parent_status || 'unknown'})${detail}`)}`);
200
+ }
182
201
  log('');
183
202
 
184
203
  // ── Track first-call for --role override ────────────────────────────────
@@ -1,4 +1,6 @@
1
1
  import chalk from 'chalk';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
2
4
  import { loadConfig, loadLock, loadProjectContext, loadProjectState, loadState } from '../lib/config.js';
3
5
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
4
6
  import { getActiveTurn, getActiveTurnCount, getActiveTurns } from '../lib/governed-state.js';
@@ -267,9 +269,29 @@ function renderGovernedStatus(context, opts) {
267
269
  if (state?.phase_gate_status) {
268
270
  console.log('');
269
271
  console.log(` ${chalk.dim('Gates:')}`);
272
+ const activePhase = state.phase;
273
+ const activeRouting = config.routing?.[activePhase];
274
+ const activeExitGate = activeRouting?.exit_gate || null;
270
275
  for (const [gate, status] of Object.entries(state.phase_gate_status)) {
271
276
  const icon = status === 'passed' ? chalk.green('✓') : chalk.dim('○');
272
277
  console.log(` ${icon} ${gate}: ${status}`);
278
+ if (status !== 'passed' && gate === activeExitGate && config.gates?.[gate]) {
279
+ const gateDef = config.gates[gate];
280
+ if (Array.isArray(gateDef.requires_files) && gateDef.requires_files.length > 0) {
281
+ const fileChecks = gateDef.requires_files.map(f => {
282
+ const exists = existsSync(join(root, f));
283
+ const short = f.replace(/^\.planning\//, '');
284
+ return exists ? chalk.green(short) : chalk.red(short);
285
+ });
286
+ console.log(` ${chalk.dim('Files:')} ${fileChecks.join(chalk.dim(', '))}`);
287
+ }
288
+ const reqs = [];
289
+ if (gateDef.requires_human_approval) reqs.push('human approval');
290
+ if (gateDef.requires_verification_pass) reqs.push('verification pass');
291
+ if (reqs.length > 0) {
292
+ console.log(` ${chalk.dim('Needs:')} ${reqs.join(', ')}`);
293
+ }
294
+ }
273
295
  }
274
296
  }
275
297
 
@@ -425,7 +447,7 @@ function renderWorkflowKitArtifactsSection(wkData) {
425
447
 
426
448
  function renderLastGateFailure(failure, config) {
427
449
  const entryRole = config?.routing?.[failure.phase]?.entry_role || null;
428
- const suggestedCommand = entryRole ? `agentxchain assign ${entryRole}` : 'agentxchain assign <role>';
450
+ const suggestedCommand = entryRole ? `agentxchain step --role ${entryRole}` : 'agentxchain step --role <role>';
429
451
  const requestLabel = failure.gate_type === 'run_completion'
430
452
  ? 'Run completion'
431
453
  : `${failure.from_phase || failure.phase} -> ${failure.to_phase || 'unknown'}`;
@@ -51,6 +51,7 @@ import {
51
51
  } from '../lib/adapters/local-cli-adapter.js';
52
52
  import { describeMcpRuntimeTarget, dispatchMcp, resolveMcpTransport } from '../lib/adapters/mcp-adapter.js';
53
53
  import { dispatchRemoteAgent, describeRemoteAgentTarget } from '../lib/adapters/remote-agent-adapter.js';
54
+ import { summarizeRunProvenance } from '../lib/run-provenance.js';
54
55
  import {
55
56
  getDispatchAssignmentPath,
56
57
  getDispatchContextPath,
@@ -326,6 +327,8 @@ export async function stepCommand(opts) {
326
327
  const runtimeType = runtime?.type || role?.runtime_class || 'manual';
327
328
  const hooksConfig = config.hooks || {};
328
329
 
330
+ printStepRunContext({ root, state, config });
331
+
329
332
  if (bundleWritten && hooksConfig.after_dispatch?.length > 0) {
330
333
  const afterDispatchHooks = runHooks(root, hooksConfig, 'after_dispatch', {
331
334
  turn_id: turn.turn_id,
@@ -945,6 +948,53 @@ function printRecoverySummary(state, heading) {
945
948
  }
946
949
  }
947
950
 
951
+ function printStepRunContext({ root, state, config }) {
952
+ console.log('');
953
+ console.log(chalk.cyan.bold('agentxchain step'));
954
+ console.log(` ${chalk.dim('Run:')} ${state?.run_id || '(uninitialized)'}`);
955
+ console.log(` ${chalk.dim('Phase:')} ${state?.phase || '(unknown)'}`);
956
+
957
+ const provenanceSummary = summarizeRunProvenance(state?.provenance);
958
+ if (provenanceSummary) {
959
+ console.log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
960
+ }
961
+
962
+ if (state?.inherited_context?.parent_run_id) {
963
+ console.log(
964
+ ` ${chalk.dim('Inherits:')} ${chalk.magenta(
965
+ `parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`
966
+ )}`
967
+ );
968
+ }
969
+
970
+ const activeGate = config?.routing?.[state?.phase]?.exit_gate || null;
971
+ if (activeGate) {
972
+ const gateStatus = state?.phase_gate_status?.[activeGate] || 'pending';
973
+ console.log(` ${chalk.dim('Gate:')} ${activeGate} (${gateStatus})`);
974
+
975
+ if (gateStatus !== 'passed') {
976
+ const gateDef = config?.gates?.[activeGate];
977
+ if (Array.isArray(gateDef?.requires_files) && gateDef.requires_files.length > 0) {
978
+ const fileChecks = gateDef.requires_files.map((filePath) => {
979
+ const exists = existsSync(join(root, filePath));
980
+ const shortPath = filePath.replace(/^\.planning\//, '');
981
+ return exists ? chalk.green(shortPath) : chalk.red(shortPath);
982
+ });
983
+ console.log(` ${chalk.dim('Files:')} ${fileChecks.join(chalk.dim(', '))}`);
984
+ }
985
+
986
+ const requirements = [];
987
+ if (gateDef?.requires_human_approval) requirements.push('human approval');
988
+ if (gateDef?.requires_verification_pass) requirements.push('verification pass');
989
+ if (requirements.length > 0) {
990
+ console.log(` ${chalk.dim('Needs:')} ${requirements.join(', ')}`);
991
+ }
992
+ }
993
+ }
994
+
995
+ console.log('');
996
+ }
997
+
948
998
  function printDispatchBundleWarnings(bundleResult) {
949
999
  for (const warning of bundleResult.warnings || []) {
950
1000
  console.log(chalk.yellow(`Dispatch bundle warning: ${warning}`));
@@ -1835,7 +1835,7 @@ export function reactivateGovernedRun(root, state, details = {}) {
1835
1835
  export function initializeGovernedRun(root, config, options = {}) {
1836
1836
  let state = readState(root);
1837
1837
  if (!state) {
1838
- return { ok: false, error: 'No governed state.json found' };
1838
+ state = buildFreshIdleStateForNewRun(null, config);
1839
1839
  }
1840
1840
  const allowTerminalRestart = options.allow_terminal_restart === true
1841
1841
  && (state.status === 'completed' || state.status === 'blocked');
package/src/lib/intake.js CHANGED
@@ -120,6 +120,122 @@ function readJsonDir(dirPath) {
120
120
  .filter(Boolean);
121
121
  }
122
122
 
123
+ function buildIntakeNextAction(intent) {
124
+ const intentId = intent?.intent_id || '<intent_id>';
125
+ const status = intent?.status || 'unknown';
126
+ const resolveCommand = `agentxchain intake resolve --intent ${intentId}`;
127
+
128
+ switch (status) {
129
+ case 'detected':
130
+ return {
131
+ label: 'triage',
132
+ summary: 'Triage this detected intent so it can enter governed delivery.',
133
+ command: `agentxchain intake triage --intent ${intentId} --priority <p0-p3> --template <template_id> --charter "<charter>" --acceptance "<criterion>"`,
134
+ alternatives: [
135
+ `agentxchain intake triage --intent ${intentId} --suppress --reason "<reason>"`,
136
+ ],
137
+ recovery: null,
138
+ action_required: true,
139
+ };
140
+ case 'triaged':
141
+ return {
142
+ label: 'approve',
143
+ summary: 'Approve this triaged intent for planning or reject it explicitly.',
144
+ command: `agentxchain intake approve --intent ${intentId}`,
145
+ alternatives: [
146
+ `agentxchain intake triage --intent ${intentId} --reject --reason "<reason>"`,
147
+ ],
148
+ recovery: null,
149
+ action_required: true,
150
+ };
151
+ case 'approved':
152
+ return {
153
+ label: 'plan',
154
+ summary: 'Generate planning artifacts for this approved intent.',
155
+ command: `agentxchain intake plan --intent ${intentId}`,
156
+ alternatives: [],
157
+ recovery: null,
158
+ action_required: true,
159
+ };
160
+ case 'planned':
161
+ return {
162
+ label: 'start',
163
+ summary: 'Start repo-local execution or hand the intent off to a coordinator workstream.',
164
+ command: `agentxchain intake start --intent ${intentId}`,
165
+ alternatives: [
166
+ `agentxchain intake handoff --intent ${intentId} --coordinator-root <path> --workstream <id>`,
167
+ ],
168
+ recovery: null,
169
+ action_required: true,
170
+ };
171
+ case 'executing':
172
+ return {
173
+ label: 'resolve',
174
+ summary: intent?.target_workstream
175
+ ? 'Re-check the coordinator workstream outcome for this intent.'
176
+ : 'Re-check the governed run outcome for this intent.',
177
+ command: resolveCommand,
178
+ alternatives: [],
179
+ recovery: null,
180
+ action_required: true,
181
+ };
182
+ case 'blocked':
183
+ return {
184
+ label: 'recover',
185
+ summary: 'Resolve the linked run blockage, then re-check intake resolution.',
186
+ command: resolveCommand,
187
+ alternatives: [],
188
+ recovery: intent?.run_blocked_recovery || null,
189
+ action_required: true,
190
+ };
191
+ case 'completed':
192
+ return {
193
+ label: 'none',
194
+ summary: 'No action required. This intent completed successfully.',
195
+ command: null,
196
+ alternatives: [],
197
+ recovery: null,
198
+ action_required: false,
199
+ };
200
+ case 'suppressed':
201
+ return {
202
+ label: 'none',
203
+ summary: 'No action required. This intent was suppressed.',
204
+ command: null,
205
+ alternatives: [],
206
+ recovery: null,
207
+ action_required: false,
208
+ };
209
+ case 'rejected':
210
+ return {
211
+ label: 'none',
212
+ summary: 'No action required. This intent was rejected.',
213
+ command: null,
214
+ alternatives: [],
215
+ recovery: null,
216
+ action_required: false,
217
+ };
218
+ case 'failed':
219
+ return {
220
+ label: 'inspect',
221
+ summary: 'Manual inspection required. This intent is in a reserved failed state.',
222
+ command: null,
223
+ alternatives: [],
224
+ recovery: 'Inspect the linked intent and run artifacts manually before continuing.',
225
+ action_required: true,
226
+ };
227
+ default:
228
+ return {
229
+ label: 'inspect',
230
+ summary: 'Manual inspection required. The intent state is not recognized by the current intake surface.',
231
+ command: null,
232
+ alternatives: [],
233
+ recovery: null,
234
+ action_required: true,
235
+ };
236
+ }
237
+ }
238
+
123
239
  // ---------------------------------------------------------------------------
124
240
  // Validation
125
241
  // ---------------------------------------------------------------------------
@@ -329,7 +445,13 @@ export function intakeStatus(root, intentId) {
329
445
  const intent = JSON.parse(readFileSync(intentPath, 'utf8'));
330
446
  const eventPath = join(dirs.events, `${intent.event_id}.json`);
331
447
  const event = existsSync(eventPath) ? JSON.parse(readFileSync(eventPath, 'utf8')) : null;
332
- return { ok: true, intent, event, exitCode: 0 };
448
+ return {
449
+ ok: true,
450
+ intent,
451
+ event,
452
+ next_action: buildIntakeNextAction(intent),
453
+ exitCode: 0,
454
+ };
333
455
  }
334
456
 
335
457
  const events = readJsonDir(dirs.events);
@@ -354,6 +476,7 @@ export function intakeStatus(root, intentId) {
354
476
  template: i.template,
355
477
  status: i.status,
356
478
  updated_at: i.updated_at,
479
+ next_action: buildIntakeNextAction(i),
357
480
  })),
358
481
  };
359
482