agentxchain 2.81.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/README.md CHANGED
@@ -13,6 +13,7 @@ Legacy IDE-window coordination is still shipped as a compatibility mode for team
13
13
  - [Quickstart](https://agentxchain.dev/docs/quickstart/)
14
14
  - [Getting Started](https://agentxchain.dev/docs/getting-started/)
15
15
  - [CLI reference](https://agentxchain.dev/docs/cli/)
16
+ - [Lights-Out Scheduling](https://agentxchain.dev/docs/lights-out-scheduling/)
16
17
  - [Templates](https://agentxchain.dev/docs/templates/)
17
18
  - [Export schema reference](https://agentxchain.dev/docs/export-schema/)
18
19
  - [Adapter reference](https://agentxchain.dev/docs/adapters/)
@@ -71,6 +72,8 @@ Duplicate execution remains intentional for the current 36-file slice until a la
71
72
 
72
73
  ### Governed workflow
73
74
 
75
+ Run `agentxchain init --governed` for the guided scaffold. Use the explicit non-interactive form below for scripts, CI, or copy-paste onboarding:
76
+
74
77
  ```bash
75
78
  agentxchain init --governed --goal "Build an API change planner for release teams" --dir my-agentxchain-project -y
76
79
  cd my-agentxchain-project
@@ -151,7 +154,7 @@ agentxchain step
151
154
 
152
155
  ## Command Sets
153
156
 
154
- ### Governed
157
+ ### Governed lifecycle and execution
155
158
 
156
159
  | Command | What it does |
157
160
  |---|---|
@@ -171,10 +174,37 @@ agentxchain step
171
174
  | `replay turn` | Replay an accepted turn's machine-evidence commands from history for audit and drift detection |
172
175
  | `verify protocol` | Run the shipped protocol conformance suite against a target implementation |
173
176
  | `dashboard` | Open the local governance dashboard in your browser for repo-local runs or multi-repo coordinator initiatives, including pending gate approvals |
177
+ | `run [--auto-approve] [--max-turns N] [--dry-run]` | Drive a governed run from start to completion — dispatches turns, handles gates, manages rejection/retry |
178
+
179
+ ### Governed proof and inspection
180
+
181
+ | Command | What it does |
182
+ |---|---|
183
+ | `audit [--format json]` | Live governance audit report with cost summary, decision history, and artifact inventory |
184
+ | `diff <left> <right>` | Compare two governed runs side by side (phase, decisions, artifacts, timing) |
185
+ | `report` | Generate a governance report for the current run |
186
+ | `events [--type <type>] [--limit N]` | Inspect the lifecycle event stream (turns, phases, gates, governance events) |
187
+ | `history [--limit N] [--role <role>]` | Query accepted-turn history from append-only JSONL |
188
+ | `role list\|show` | List all configured roles or inspect a single role's charter, runtime, and phase assignment |
189
+ | `turn show` | Inspect the active turn in detail (assignment, artifacts, timing, verification) |
190
+ | `phase list\|show` | List configured phases or inspect a single phase's gate requirements and state |
191
+ | `gate list\|show [--evaluate]` | List configured gates or evaluate a gate's current pass/fail state |
192
+ | `doctor [--json]` | Governed project health check: config, roles, runtimes, state, schedules, plugins, workflow-kit, connector handoff |
193
+ | `connector check [--json]` | Live health probes for all configured connectors (api_proxy, remote_agent, MCP stdio/streamable_http) |
194
+
195
+ ### Governed automation, plugins, and continuity
196
+
197
+ | Command | What it does |
198
+ |---|---|
174
199
  | `multi init\|status\|step\|resume\|approve-gate\|resync` | Run the multi-repo coordinator lifecycle, including blocked-state recovery via `multi resume` |
175
200
  | `intake record\|triage\|approve\|plan\|start\|scan\|resolve` | Continuous-delivery intake: turn delivery signals into governed work items |
176
201
  | `intake handoff` | Bridge a planned intake intent to a coordinator workstream for multi-repo execution |
177
- | `plugin install\|list\|remove` | Install, inspect, or remove governed hook plugins backed by `agentxchain-plugin.json` manifests |
202
+ | `schedule list\|run-due\|daemon\|status` | Run repo-local lights-out scheduling: inspect schedules, execute due runs, poll in a local daemon loop, or check daemon heartbeat |
203
+ | `plugin install\|list\|remove` | Install, inspect, or remove governed hook plugins under `.agentxchain/plugins/` |
204
+ | `plugin list-available` | List bundled built-in plugins installable by short name |
205
+ | `export [--output <path>]` | Export run state for cross-machine continuity |
206
+ | `restore --input <path>` | Restore run state from a prior export on a same-repo, same-commit checkout |
207
+ | `restart` | Rebuild lost session context from `.agentxchain/session.json` |
178
208
 
179
209
  ### Shared utilities
180
210
 
@@ -131,7 +131,7 @@ program
131
131
  program
132
132
  .command('init')
133
133
  .description('Create a new AgentXchain project folder')
134
- .option('-y, --yes', 'Skip prompts, use defaults')
134
+ .option('-y, --yes', 'Skip guided prompts, use defaults')
135
135
  .option('--governed', 'Create a governed project (orchestrator-owned state)')
136
136
  .option('--dir <path>', 'Scaffold target directory. Use "." for in-place bootstrap.')
137
137
  .option('--template <id>', 'Governed scaffold template: generic, api-service, cli-tool, library, web-app, enterprise-app')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.81.0",
3
+ "version": "2.83.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,7 +5,7 @@ import chalk from 'chalk';
5
5
  import inquirer from 'inquirer';
6
6
  import { CONFIG_FILE, LOCK_FILE, STATE_FILE } from '../lib/config.js';
7
7
  import { generateVSCodeFiles } from '../lib/generate-vscode.js';
8
- import { loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS, buildSystemSpecContent } from '../lib/governed-templates.js';
8
+ import { loadAllGovernedTemplates, loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS, buildSystemSpecContent } from '../lib/governed-templates.js';
9
9
  import { normalizeWorkflowKit, VALID_PROMPT_TRANSPORTS } from '../lib/normalized-config.js';
10
10
 
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -527,6 +527,76 @@ function formatInitTarget(dir) {
527
527
  return dir;
528
528
  }
529
529
 
530
+ function normalizeOptionalGoal(value) {
531
+ if (typeof value !== 'string') return undefined;
532
+ const trimmed = value.trim();
533
+ return trimmed ? trimmed : undefined;
534
+ }
535
+
536
+ export function buildGovernedTemplateChoices(templates = loadAllGovernedTemplates()) {
537
+ return templates.map((template) => ({
538
+ name: `${chalk.cyan(template.display_name)} (${template.id}) — ${template.description}`,
539
+ value: template.id,
540
+ short: template.id,
541
+ }));
542
+ }
543
+
544
+ export async function resolveGovernedInitAnswers(opts, prompt = (questions) => inquirer.prompt(questions)) {
545
+ const explicitDir = resolveInitDirOption(opts.dir);
546
+ let templateId = opts.template || null;
547
+
548
+ if (!templateId) {
549
+ const { template } = await prompt([{
550
+ type: 'list',
551
+ name: 'template',
552
+ message: 'Governed template:',
553
+ choices: buildGovernedTemplateChoices(),
554
+ default: 'generic',
555
+ }]);
556
+ templateId = template;
557
+ }
558
+
559
+ const { name } = await prompt([{
560
+ type: 'input',
561
+ name: 'name',
562
+ message: 'Project name:',
563
+ default: explicitDir
564
+ ? inferProjectNameFromTarget(explicitDir, 'My AgentXchain Project')
565
+ : 'My AgentXchain Project',
566
+ }]);
567
+ const projectName = name;
568
+ let folderName = explicitDir || slugify(projectName);
569
+
570
+ let projectGoal = normalizeOptionalGoal(opts.goal);
571
+ if (!projectGoal) {
572
+ const { goal } = await prompt([{
573
+ type: 'input',
574
+ name: 'goal',
575
+ message: 'Project goal (recommended; shown to every agent turn):',
576
+ default: '',
577
+ }]);
578
+ projectGoal = normalizeOptionalGoal(goal);
579
+ }
580
+
581
+ if (!explicitDir) {
582
+ const { folder } = await prompt([{
583
+ type: 'input',
584
+ name: 'folder',
585
+ message: 'Folder name:',
586
+ default: folderName,
587
+ }]);
588
+ folderName = folder;
589
+ }
590
+
591
+ return {
592
+ explicitDir,
593
+ templateId,
594
+ projectName,
595
+ folderName,
596
+ goal: projectGoal,
597
+ };
598
+ }
599
+
530
600
  function generateWorkflowKitPlaceholder(artifact, projectName) {
531
601
  const filename = basename(artifact.path);
532
602
  const title = filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
@@ -813,12 +883,24 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
813
883
 
814
884
  async function initGoverned(opts) {
815
885
  let projectName, folderName;
816
- const templateId = opts.template || 'generic';
886
+ let templateId;
817
887
  let selectedTemplate;
818
888
  let explicitDir;
889
+ let projectGoal;
819
890
 
820
891
  try {
821
- explicitDir = resolveInitDirOption(opts.dir);
892
+ if (opts.yes) {
893
+ explicitDir = resolveInitDirOption(opts.dir);
894
+ templateId = opts.template || 'generic';
895
+ projectGoal = normalizeOptionalGoal(opts.goal);
896
+ } else {
897
+ const answers = await resolveGovernedInitAnswers(opts);
898
+ explicitDir = answers.explicitDir;
899
+ templateId = answers.templateId;
900
+ projectName = answers.projectName;
901
+ folderName = answers.folderName;
902
+ projectGoal = answers.goal;
903
+ }
822
904
  } catch (err) {
823
905
  console.error(chalk.red(` Error: ${err.message}`));
824
906
  process.exit(1);
@@ -842,27 +924,6 @@ async function initGoverned(opts) {
842
924
  ? inferProjectNameFromTarget(explicitDir, 'My AgentXchain Project')
843
925
  : 'My AgentXchain Project';
844
926
  folderName = explicitDir || slugify(projectName);
845
- } else {
846
- const { name } = await inquirer.prompt([{
847
- type: 'input',
848
- name: 'name',
849
- message: 'Project name:',
850
- default: explicitDir
851
- ? inferProjectNameFromTarget(explicitDir, 'My AgentXchain Project')
852
- : 'My AgentXchain Project'
853
- }]);
854
- projectName = name;
855
- folderName = explicitDir || slugify(projectName);
856
-
857
- if (!explicitDir) {
858
- const { folder } = await inquirer.prompt([{
859
- type: 'input',
860
- name: 'folder',
861
- message: 'Folder name:',
862
- default: folderName
863
- }]);
864
- folderName = folder;
865
- }
866
927
  }
867
928
 
868
929
  const dir = resolve(process.cwd(), folderName);
@@ -908,7 +969,10 @@ async function initGoverned(opts) {
908
969
  }
909
970
  }
910
971
 
911
- const { config, scaffoldWorkflowKitConfig } = scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
972
+ const scaffoldOptions = projectGoal
973
+ ? { ...opts, goal: projectGoal }
974
+ : { ...opts };
975
+ const { config, scaffoldWorkflowKitConfig } = scaffoldGoverned(dir, projectName, projectId, templateId, scaffoldOptions, workflowKitConfig);
912
976
 
913
977
  console.log('');
914
978
  console.log(chalk.green(` ✓ Created governed project ${chalk.bold(targetLabel)}/`));
@@ -933,6 +997,9 @@ async function initGoverned(opts) {
933
997
  console.log(` ${chalk.dim('Roles:')} ${promptRoleIds.join(', ')}`);
934
998
  console.log(` ${chalk.dim('Phases:')} ${phaseNames.join(' → ')} ${chalk.dim(selectedTemplate.scaffold_blueprint ? '(template-defined; edit routing in agentxchain.json to customize)' : '(default; extend via routing in agentxchain.json)')}`);
935
999
  console.log(` ${chalk.dim('Template:')} ${templateId}`);
1000
+ if (config.project?.goal) {
1001
+ console.log(` ${chalk.dim('Goal:')} ${config.project.goal}`);
1002
+ }
936
1003
  console.log(` ${chalk.dim('Dev runtime:')} ${formatGovernedRuntimeCommand(localDevRuntime)} ${chalk.dim(`(${localDevRuntime.prompt_transport})`)}`);
937
1004
  console.log(` ${chalk.dim('Protocol:')} governed convergence`);
938
1005
  console.log('');
@@ -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
 
@@ -235,6 +235,14 @@ export async function multiStepCommand(options) {
235
235
  // Fire on_escalation for the blocked resync
236
236
  fireEscalationHook(workspacePath, configResult.config, state, resync.blocked_reason || 'resync failure');
237
237
  console.error(`Coordinator resync entered blocked state: ${resync.blocked_reason || 'unknown reason'}`);
238
+ for (const mismatch of resync.mismatch_details || []) {
239
+ const codeTag = mismatch.code ? `[${mismatch.code}] ` : '';
240
+ console.error(` - ${codeTag}${mismatch.message}`);
241
+ if (mismatch.code === 'repo_run_id_mismatch') {
242
+ console.error(` expected: ${mismatch.expected_run_id}`);
243
+ console.error(` actual: ${mismatch.actual_run_id}`);
244
+ }
245
+ }
238
246
  process.exitCode = 1;
239
247
  return;
240
248
  }
@@ -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
 
@@ -36,9 +36,9 @@ import {
36
36
  getDispatchEffectiveContextPath,
37
37
  getDispatchPromptPath,
38
38
  } from '../lib/turn-paths.js';
39
- import { safeWriteJson } from '../lib/safe-write.js';
40
39
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
41
40
  import { runHooks } from '../lib/hook-runner.js';
41
+ import { summarizeRunProvenance } from '../lib/run-provenance.js';
42
42
 
43
43
  export async function resumeCommand(opts) {
44
44
  const context = loadProjectContext();
@@ -100,6 +100,11 @@ export async function resumeCommand(opts) {
100
100
  process.exit(1);
101
101
  }
102
102
 
103
+ if (state.pending_phase_transition || state.pending_run_completion) {
104
+ printRecoverySummary(state, 'This run is awaiting approval.');
105
+ process.exit(1);
106
+ }
107
+
103
108
  // §47: paused + retained turn with failed/retrying status → re-dispatch same turn
104
109
  if (state.status === 'paused' && activeCount > 0) {
105
110
  // Resolve which turn to re-dispatch
@@ -124,15 +129,18 @@ export async function resumeCommand(opts) {
124
129
 
125
130
  const turnStatus = retainedTurn.status;
126
131
  if (turnStatus === 'failed' || turnStatus === 'retrying') {
132
+ printResumeRunContext({ root, state, config });
127
133
  console.log(chalk.yellow(`Re-dispatching failed turn: ${retainedTurn.turn_id}`));
128
134
  console.log(` Role: ${retainedTurn.assigned_role}`);
129
135
  console.log(` Attempt: ${retainedTurn.attempt}`);
130
136
  console.log('');
131
137
 
132
- // Reactivate the run
133
- state.status = 'active';
134
- state.blocked_on = null;
135
- safeWriteJson(statePath, state);
138
+ const reactivated = reactivateGovernedRun(root, state, { via: 'resume --turn', notificationConfig: config });
139
+ if (!reactivated.ok) {
140
+ console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
141
+ process.exit(1);
142
+ }
143
+ state = reactivated.state;
136
144
 
137
145
  // Write dispatch bundle for the existing turn
138
146
  const bundleResult = writeDispatchBundle(root, state, config);
@@ -181,6 +189,7 @@ export async function resumeCommand(opts) {
181
189
  process.exit(1);
182
190
  }
183
191
 
192
+ printResumeRunContext({ root, state, config });
184
193
  console.log(chalk.yellow(`Re-dispatching blocked turn: ${retainedTurn.turn_id}`));
185
194
  console.log(` Role: ${retainedTurn.assigned_role}`);
186
195
  console.log(` Attempt: ${retainedTurn.attempt}`);
@@ -236,14 +245,18 @@ export async function resumeCommand(opts) {
236
245
 
237
246
  // §47: paused + run_id exists → resume same run
238
247
  if (state.status === 'paused' && state.run_id) {
239
- state.status = 'active';
240
- state.blocked_on = null;
241
- state.blocked_reason = null;
242
- state.escalation = null;
243
- safeWriteJson(statePath, state);
248
+ const reactivated = reactivateGovernedRun(root, state, { via: 'resume', notificationConfig: config });
249
+ if (!reactivated.ok) {
250
+ console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
251
+ process.exit(1);
252
+ }
253
+ state = reactivated.state;
244
254
  console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
245
255
  }
246
256
 
257
+ // Print run-context header before dispatch
258
+ printResumeRunContext({ root, state, config });
259
+
247
260
  // Resolve target role
248
261
  const roleId = resolveTargetRole(opts, state, config);
249
262
  if (!roleId) {
@@ -295,6 +308,53 @@ export async function resumeCommand(opts) {
295
308
 
296
309
  // ── Helpers ─────────────────────────────────────────────────────────────────
297
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
+
298
358
  function resolveTargetRole(opts, state, config) {
299
359
  const phase = state.phase;
300
360
  const routing = config.routing?.[phase];
@@ -422,6 +482,19 @@ function printAssignmentWarnings(assignResult) {
422
482
  }
423
483
  }
424
484
 
485
+ function printRecoverySummary(state, heading) {
486
+ const recovery = deriveRecoveryDescriptor(state);
487
+ console.log(chalk.yellow(heading));
488
+ if (!recovery) {
489
+ return;
490
+ }
491
+ console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
492
+ console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
493
+ if (recovery.detail) {
494
+ console.log(` ${chalk.dim('Detail:')} ${recovery.detail}`);
495
+ }
496
+ }
497
+
425
498
  function printAssignmentHookFailure(result, roleId) {
426
499
  const recovery = deriveRecoveryDescriptor(result.state);
427
500
  const hookName = result.hookResults?.blocker?.hook_name
@@ -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,
@@ -60,7 +61,6 @@ import {
60
61
  getTurnStagingResultPath,
61
62
  } from '../lib/turn-paths.js';
62
63
  import { dispatchApiProxy } from '../lib/adapters/api-proxy-adapter.js';
63
- import { safeWriteJson } from '../lib/safe-write.js';
64
64
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
65
65
  import { runHooks } from '../lib/hook-runner.js';
66
66
  import { finalizeDispatchManifest, verifyDispatchManifest } from '../lib/dispatch-manifest.js';
@@ -165,8 +165,8 @@ export async function stepCommand(opts) {
165
165
  }
166
166
 
167
167
  if (!skipAssignment) {
168
- if (state.status === 'paused' && (state.pending_phase_transition || state.pending_run_completion)) {
169
- printRecoverySummary(state, 'This run is paused for approval.');
168
+ if (state.pending_phase_transition || state.pending_run_completion) {
169
+ printRecoverySummary(state, 'This run is awaiting approval.');
170
170
  process.exit(1);
171
171
  }
172
172
 
@@ -231,10 +231,12 @@ export async function stepCommand(opts) {
231
231
  const turnStatus = pausedTurn?.status;
232
232
  if (turnStatus === 'failed' || turnStatus === 'retrying') {
233
233
  console.log(chalk.yellow(`Re-dispatching failed turn: ${pausedTurn.turn_id}`));
234
- state.status = 'active';
235
- state.blocked_on = null;
236
- state.blocked_reason = null;
237
- safeWriteJson(join(root, STATE_PATH), state);
234
+ const reactivated = reactivateGovernedRun(root, state, { via: 'step --resume', notificationConfig: config });
235
+ if (!reactivated.ok) {
236
+ console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
237
+ process.exit(1);
238
+ }
239
+ state = reactivated.state;
238
240
  skipAssignment = true;
239
241
 
240
242
  const bundleResult = writeDispatchBundle(root, state, config);
@@ -270,11 +272,12 @@ export async function stepCommand(opts) {
270
272
  }
271
273
 
272
274
  if (!skipAssignment && state.status === 'paused' && state.run_id) {
273
- state.status = 'active';
274
- state.blocked_on = null;
275
- state.blocked_reason = null;
276
- state.escalation = null;
277
- safeWriteJson(join(root, STATE_PATH), state);
275
+ const reactivated = reactivateGovernedRun(root, state, { via: 'step', notificationConfig: config });
276
+ if (!reactivated.ok) {
277
+ console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
278
+ process.exit(1);
279
+ }
280
+ state = reactivated.state;
278
281
  console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
279
282
  }
280
283
 
@@ -324,6 +327,8 @@ export async function stepCommand(opts) {
324
327
  const runtimeType = runtime?.type || role?.runtime_class || 'manual';
325
328
  const hooksConfig = config.hooks || {};
326
329
 
330
+ printStepRunContext({ root, state, config });
331
+
327
332
  if (bundleWritten && hooksConfig.after_dispatch?.length > 0) {
328
333
  const afterDispatchHooks = runHooks(root, hooksConfig, 'after_dispatch', {
329
334
  turn_id: turn.turn_id,
@@ -943,6 +948,53 @@ function printRecoverySummary(state, heading) {
943
948
  }
944
949
  }
945
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
+
946
998
  function printDispatchBundleWarnings(bundleResult) {
947
999
  for (const warning of bundleResult.warnings || []) {
948
1000
  console.log(chalk.yellow(`Dispatch bundle warning: ${warning}`));
@@ -239,6 +239,7 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
239
239
  const resyncedRepos = [];
240
240
  const projectedAcceptances = [];
241
241
  const barrierChanges = [];
242
+ const mismatchDetails = [];
242
243
 
243
244
  // Step 1: Refresh repo_runs from repo-local authority
244
245
  const updatedRepoRuns = { ...state.repo_runs };
@@ -262,6 +263,13 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
262
263
  if (repoRun.run_id && repoRun.run_id !== (repoState.run_id ?? null)) {
263
264
  const reason = buildRunIdMismatchReason(repoId, repoRun.run_id, repoState.run_id ?? null);
264
265
  runIdMismatches.push(reason);
266
+ mismatchDetails.push({
267
+ code: 'repo_run_id_mismatch',
268
+ repo_id: repoId,
269
+ expected_run_id: repoRun.run_id,
270
+ actual_run_id: repoState.run_id ?? null,
271
+ message: reason,
272
+ });
265
273
  errors.push(reason);
266
274
  continue;
267
275
  }
@@ -447,6 +455,7 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
447
455
  resynced_repos: [...new Set(resyncedRepos)],
448
456
  projected_acceptances: projectedAcceptances,
449
457
  barrier_changes: barrierChanges,
458
+ mismatch_details: mismatchDetails,
450
459
  errors,
451
460
  blocked_reason: blockedReason || undefined,
452
461
  };
@@ -118,6 +118,9 @@ export function writeDispatchBundle(root, state, config, opts = {}) {
118
118
  budget_reservation_usd: state.budget_reservations?.[turn.turn_id]?.reserved_usd ?? null,
119
119
  active_siblings: activeSiblings,
120
120
  };
121
+ if (turn.intake_context) {
122
+ assignment.intake_context = turn.intake_context;
123
+ }
121
124
  if (turn.conflict_context) {
122
125
  assignment.conflict_context = turn.conflict_context;
123
126
  }
@@ -523,6 +526,27 @@ function renderContext(state, config, root, turn, role) {
523
526
  lines.push('');
524
527
  }
525
528
 
529
+ if (turn.intake_context) {
530
+ lines.push('## Intake Intent');
531
+ lines.push('');
532
+ lines.push(`- **Intent:** ${turn.intake_context.intent_id || 'unknown'}`);
533
+ lines.push(`- **Event:** ${turn.intake_context.event_id || 'unknown'}`);
534
+ lines.push(`- **Source:** ${turn.intake_context.source || 'unknown'}`);
535
+ if (turn.intake_context.category) {
536
+ lines.push(`- **Category:** ${turn.intake_context.category}`);
537
+ }
538
+ if (turn.intake_context.charter) {
539
+ lines.push(`- **Charter:** ${turn.intake_context.charter}`);
540
+ }
541
+ if (Array.isArray(turn.intake_context.acceptance_contract) && turn.intake_context.acceptance_contract.length > 0) {
542
+ lines.push('- **Acceptance Contract:**');
543
+ for (const requirement of turn.intake_context.acceptance_contract) {
544
+ lines.push(` - ${requirement}`);
545
+ }
546
+ }
547
+ lines.push('');
548
+ }
549
+
526
550
  // Inherited context from parent run (when --inherit-context was used)
527
551
  if (state.inherited_context) {
528
552
  // First turn gets the full rendering; subsequent turns get compact
@@ -1197,13 +1221,13 @@ function buildTurnResultTemplate(state, turn, roleId, role) {
1197
1221
  role: roleId,
1198
1222
  runtime_id: turn.runtime_id,
1199
1223
  status: 'completed',
1200
- summary: 'TODO: describe what you accomplished this turn',
1224
+ summary: '<one-line summary of what you accomplished>',
1201
1225
  decisions: [
1202
1226
  {
1203
1227
  id: 'DEC-001',
1204
1228
  category: 'implementation',
1205
- statement: 'TODO: describe the decision',
1206
- rationale: 'TODO: explain why',
1229
+ statement: '<what was decided and why it matters>',
1230
+ rationale: '<reasoning behind this decision>',
1207
1231
  },
1208
1232
  ],
1209
1233
  objections: isReviewOnly
@@ -1211,29 +1235,29 @@ function buildTurnResultTemplate(state, turn, roleId, role) {
1211
1235
  {
1212
1236
  id: 'OBJ-001',
1213
1237
  severity: 'medium',
1214
- against_turn_id: state.last_completed_turn_id || 'TODO',
1215
- statement: 'TODO: challenge the previous turn (required for review_only roles)',
1238
+ against_turn_id: state.last_completed_turn_id || '<turn_id of the turn you are reviewing>',
1239
+ statement: '<specific objection to the previous turn required for review_only roles>',
1216
1240
  status: 'raised',
1217
1241
  },
1218
1242
  ]
1219
1243
  : [],
1220
- files_changed: isReviewOnly ? [] : ['TODO: list every file you modified'],
1244
+ files_changed: isReviewOnly ? [] : ['<path/to/modified/file>'],
1221
1245
  artifacts_created: [],
1222
1246
  verification: {
1223
1247
  status: isReviewOnly ? 'skipped' : 'pass',
1224
- commands: isReviewOnly ? [] : ['TODO: list commands you ran'],
1248
+ commands: isReviewOnly ? [] : ['<command you ran to verify>'],
1225
1249
  evidence_summary: isReviewOnly
1226
1250
  ? 'Review turn — no verification commands required.'
1227
- : 'TODO: describe what you verified',
1251
+ : '<what you verified and how>',
1228
1252
  machine_evidence: isReviewOnly
1229
1253
  ? []
1230
- : [{ command: 'TODO', exit_code: 0 }],
1254
+ : [{ command: '<exact command that was run>', exit_code: 0 }],
1231
1255
  },
1232
1256
  artifact: {
1233
1257
  type: isReviewOnly ? 'review' : 'workspace',
1234
1258
  ref: isReviewOnly ? null : 'git:dirty',
1235
1259
  },
1236
- proposed_next_role: 'TODO',
1260
+ proposed_next_role: '<role_id that should act next>',
1237
1261
  phase_transition_request: null,
1238
1262
  run_completion_request: null,
1239
1263
  needs_human_reason: null,
@@ -1774,6 +1774,12 @@ export function reactivateGovernedRun(root, state, details = {}) {
1774
1774
  if (!state || typeof state !== 'object') {
1775
1775
  return { ok: false, error: 'State is required.' };
1776
1776
  }
1777
+ if (state.status !== 'blocked' && state.status !== 'paused') {
1778
+ return { ok: false, error: `Cannot reactivate run: status is "${state.status}", expected "blocked" or "paused".` };
1779
+ }
1780
+ if (state.pending_phase_transition || state.pending_run_completion) {
1781
+ return { ok: false, error: 'Cannot reactivate run: this run is awaiting approval. Use approve-transition or approve-completion.' };
1782
+ }
1777
1783
 
1778
1784
  const now = new Date().toISOString();
1779
1785
  const wasEscalation = state.status === 'blocked' && typeof state.blocked_on === 'string' && state.blocked_on.startsWith('escalation:');
@@ -1819,7 +1825,7 @@ export function reactivateGovernedRun(root, state, details = {}) {
1819
1825
  // ── Core Operations ──────────────────────────────────────────────────────────
1820
1826
 
1821
1827
  /**
1822
- * Initialize a governed run from idle state.
1828
+ * Initialize a governed run from bootstrap state.
1823
1829
  * Creates a run_id and sets status to 'active'.
1824
1830
  *
1825
1831
  * @param {string} root - project root directory
@@ -1829,7 +1835,7 @@ export function reactivateGovernedRun(root, state, details = {}) {
1829
1835
  export function initializeGovernedRun(root, config, options = {}) {
1830
1836
  let state = readState(root);
1831
1837
  if (!state) {
1832
- return { ok: false, error: 'No governed state.json found' };
1838
+ state = buildFreshIdleStateForNewRun(null, config);
1833
1839
  }
1834
1840
  const allowTerminalRestart = options.allow_terminal_restart === true
1835
1841
  && (state.status === 'completed' || state.status === 'blocked');
@@ -1837,8 +1843,8 @@ export function initializeGovernedRun(root, config, options = {}) {
1837
1843
  return { ok: false, error: 'Cannot initialize run: this run is already completed. Start a new project or reset state.' };
1838
1844
  }
1839
1845
  const allowBlockedBootstrap = state.status === 'blocked' && state.run_id === null && getActiveTurnCount(state) === 0;
1840
- if (state.status !== 'idle' && state.status !== 'paused' && !allowBlockedBootstrap && !allowTerminalRestart) {
1841
- return { ok: false, error: `Cannot initialize run: status is "${state.status}", expected "idle", "paused", or pre-run "blocked"` };
1846
+ if (state.status !== 'idle' && !allowBlockedBootstrap && !allowTerminalRestart) {
1847
+ return { ok: false, error: `Cannot initialize run: status is "${state.status}", expected "idle" or pre-run "blocked"` };
1842
1848
  }
1843
1849
  if (allowTerminalRestart) {
1844
1850
  state = buildFreshIdleStateForNewRun(state, config);
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
 
@@ -521,6 +644,20 @@ export function startIntent(root, intentId, options = {}) {
521
644
  };
522
645
  }
523
646
 
647
+ const loadedEvent = readEvent(root, intent.event_id);
648
+ if (!loadedEvent.ok) {
649
+ return loadedEvent;
650
+ }
651
+ const { event } = loadedEvent;
652
+ const intakeContext = {
653
+ intent_id: intent.intent_id,
654
+ event_id: intent.event_id,
655
+ source: event.source || null,
656
+ category: event.category || null,
657
+ charter: intent.charter || null,
658
+ acceptance_contract: Array.isArray(intent.acceptance_contract) ? intent.acceptance_contract : [],
659
+ };
660
+
524
661
  // Load governed project context
525
662
  const context = loadProjectContext(root);
526
663
  if (!context) {
@@ -569,6 +706,10 @@ export function startIntent(root, intentId, options = {}) {
569
706
  };
570
707
  }
571
708
 
709
+ if (state.status === 'paused') {
710
+ return { ok: false, error: 'cannot start: run is paused (awaiting approval). Resolve the blocking gate before starting a new intake turn.', exitCode: 1 };
711
+ }
712
+
572
713
  if (state.pending_phase_transition) {
573
714
  return { ok: false, error: `cannot start: pending phase transition to "${state.pending_phase_transition}"`, exitCode: 1 };
574
715
  }
@@ -579,21 +720,20 @@ export function startIntent(root, intentId, options = {}) {
579
720
 
580
721
  // Bootstrap: idle with no run → initialize
581
722
  if (state.status === 'idle' && !state.run_id) {
582
- const initResult = initializeGovernedRun(root, config);
723
+ const initResult = initializeGovernedRun(root, config, {
724
+ provenance: {
725
+ trigger: 'intake',
726
+ intake_intent_id: intent.intent_id,
727
+ trigger_reason: intent.charter || null,
728
+ created_by: 'operator',
729
+ },
730
+ });
583
731
  if (!initResult.ok) {
584
732
  return { ok: false, error: `run initialization failed: ${initResult.error}`, exitCode: 1 };
585
733
  }
586
734
  state = initResult.state;
587
735
  }
588
736
 
589
- // Resume: paused with no active turns → reactivate
590
- if (state.status === 'paused' && state.run_id) {
591
- state.status = 'active';
592
- state.blocked_on = null;
593
- state.escalation = null;
594
- safeWriteJson(statePath, state);
595
- }
596
-
597
737
  // Resolve role
598
738
  const roleId = resolveIntakeRole(options.role, state, config);
599
739
  if (!roleId.ok) {
@@ -614,6 +754,12 @@ export function startIntent(root, intentId, options = {}) {
614
754
  return { ok: false, error: 'turn assignment succeeded but turn not found in state', exitCode: 1 };
615
755
  }
616
756
 
757
+ assignedTurn.intake_context = intakeContext;
758
+ if (state.active_turns?.[assignedTurn.turn_id]) {
759
+ state.active_turns[assignedTurn.turn_id].intake_context = intakeContext;
760
+ safeWriteJson(statePath, state);
761
+ }
762
+
617
763
  // Write dispatch bundle
618
764
  const bundleResult = writeDispatchBundle(root, state, config);
619
765
  if (!bundleResult.ok) {
@@ -263,9 +263,68 @@ function validateSchema(tr) {
263
263
  }
264
264
  }
265
265
 
266
+ errors.push(...collectUnfilledTemplatePlaceholderErrors(tr));
267
+
266
268
  return errors;
267
269
  }
268
270
 
271
+ function collectUnfilledTemplatePlaceholderErrors(tr) {
272
+ const errors = [];
273
+
274
+ checkPlaceholder(errors, 'summary', tr.summary);
275
+ checkPlaceholder(errors, 'proposed_next_role', tr.proposed_next_role);
276
+
277
+ if (Array.isArray(tr.decisions)) {
278
+ for (let i = 0; i < tr.decisions.length; i++) {
279
+ const decision = tr.decisions[i];
280
+ checkPlaceholder(errors, `decisions[${i}].statement`, decision?.statement);
281
+ checkPlaceholder(errors, `decisions[${i}].rationale`, decision?.rationale);
282
+ }
283
+ }
284
+
285
+ if (Array.isArray(tr.objections)) {
286
+ for (let i = 0; i < tr.objections.length; i++) {
287
+ const objection = tr.objections[i];
288
+ checkPlaceholder(errors, `objections[${i}].against_turn_id`, objection?.against_turn_id);
289
+ checkPlaceholder(errors, `objections[${i}].statement`, objection?.statement);
290
+ }
291
+ }
292
+
293
+ if (Array.isArray(tr.files_changed)) {
294
+ for (let i = 0; i < tr.files_changed.length; i++) {
295
+ checkPlaceholder(errors, `files_changed[${i}]`, tr.files_changed[i]);
296
+ }
297
+ }
298
+
299
+ const verification = tr.verification;
300
+ if (verification && typeof verification === 'object' && !Array.isArray(verification)) {
301
+ if (Array.isArray(verification.commands)) {
302
+ for (let i = 0; i < verification.commands.length; i++) {
303
+ checkPlaceholder(errors, `verification.commands[${i}]`, verification.commands[i]);
304
+ }
305
+ }
306
+ checkPlaceholder(errors, 'verification.evidence_summary', verification.evidence_summary);
307
+
308
+ if (Array.isArray(verification.machine_evidence)) {
309
+ for (let i = 0; i < verification.machine_evidence.length; i++) {
310
+ checkPlaceholder(
311
+ errors,
312
+ `verification.machine_evidence[${i}].command`,
313
+ verification.machine_evidence[i]?.command
314
+ );
315
+ }
316
+ }
317
+ }
318
+
319
+ return errors;
320
+ }
321
+
322
+ function checkPlaceholder(errors, fieldPath, value) {
323
+ if (typeof value === 'string' && /^<[^>]+>$/.test(value)) {
324
+ errors.push(`${fieldPath} contains an unfilled template placeholder: "${value}".`);
325
+ }
326
+ }
327
+
269
328
  function validateDecision(dec, index) {
270
329
  const errors = [];
271
330
  const prefix = `decisions[${index}]`;
@@ -56,6 +56,31 @@ function evaluatePmSignoff(content) {
56
56
  return { ok: true };
57
57
  }
58
58
 
59
+ const SYSTEM_SPEC_SCAFFOLD_PLACEHOLDER = /^\(.*\)$/;
60
+ const SYSTEM_SPEC_ACCEPTANCE_SCAFFOLD = /^- \[ \] Name the executable checks/;
61
+
62
+ function isSystemSpecPlaceholderLine(line) {
63
+ return SYSTEM_SPEC_SCAFFOLD_PLACEHOLDER.test(line) || SYSTEM_SPEC_ACCEPTANCE_SCAFFOLD.test(line);
64
+ }
65
+
66
+ function hasSectionRealContent(content, sectionHeader, isPlaceholderFn) {
67
+ const lines = content.split(/\r?\n/);
68
+ const headerIndex = lines.findIndex((line) => line.trim().startsWith(sectionHeader));
69
+ if (headerIndex === -1) {
70
+ return { found: false, hasContent: false };
71
+ }
72
+
73
+ for (let i = headerIndex + 1; i < lines.length; i++) {
74
+ const line = lines[i].trim();
75
+ if (line.startsWith('## ')) break;
76
+ if (!line) continue;
77
+ if (isPlaceholderFn(line)) continue;
78
+ return { found: true, hasContent: true };
79
+ }
80
+
81
+ return { found: true, hasContent: false };
82
+ }
83
+
59
84
  function evaluateSystemSpec(content) {
60
85
  const requiredSections = ['## Purpose', '## Interface', '## Acceptance Tests'];
61
86
  const missingSections = requiredSections.filter((section) => !content.includes(section));
@@ -67,6 +92,21 @@ function evaluateSystemSpec(content) {
67
92
  };
68
93
  }
69
94
 
95
+ const placeholderSections = [];
96
+ for (const section of requiredSections) {
97
+ const result = hasSectionRealContent(content, section, isSystemSpecPlaceholderLine);
98
+ if (result.found && !result.hasContent) {
99
+ placeholderSections.push(section);
100
+ }
101
+ }
102
+
103
+ if (placeholderSections.length > 0) {
104
+ return {
105
+ ok: false,
106
+ reason: `${placeholderSections.join(' and ')} in .planning/SYSTEM_SPEC.md still contains only scaffold placeholder text. Replace placeholder content with real spec content before planning can exit.`,
107
+ };
108
+ }
109
+
70
110
  return { ok: true };
71
111
  }
72
112
 
@@ -362,6 +402,15 @@ function evaluateShipVerdict(content) {
362
402
  return { ok: true };
363
403
  }
364
404
 
405
+ const SECTION_CHECK_SCAFFOLD_PLACEHOLDERS = [
406
+ /^\(Content here\.\)$/i,
407
+ /^\(Operator fills this in\.\)$/i,
408
+ ];
409
+
410
+ function isSectionCheckPlaceholderLine(line) {
411
+ return SECTION_CHECK_SCAFFOLD_PLACEHOLDERS.some((re) => re.test(line));
412
+ }
413
+
365
414
  function evaluateSectionCheck(content, config) {
366
415
  if (!config?.required_sections?.length) {
367
416
  return { ok: true };
@@ -375,6 +424,22 @@ function evaluateSectionCheck(content, config) {
375
424
  reason: `Document must contain sections: ${missing.join(', ')}`,
376
425
  };
377
426
  }
427
+
428
+ const placeholderSections = [];
429
+ for (const section of config.required_sections) {
430
+ const result = hasSectionRealContent(content, section, isSectionCheckPlaceholderLine);
431
+ if (result.found && !result.hasContent) {
432
+ placeholderSections.push(section);
433
+ }
434
+ }
435
+
436
+ if (placeholderSections.length > 0) {
437
+ return {
438
+ ok: false,
439
+ reason: `Sections still contain only scaffold placeholder text: ${placeholderSections.join(', ')}. Replace placeholder content before this gate can pass.`,
440
+ };
441
+ }
442
+
378
443
  return { ok: true };
379
444
  }
380
445