agentxchain 2.103.0 → 2.105.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.
Files changed (66) hide show
  1. package/README.md +13 -7
  2. package/bin/agentxchain.js +16 -8
  3. package/dashboard/app.js +111 -7
  4. package/dashboard/components/blocked.js +95 -11
  5. package/dashboard/components/blockers.js +85 -86
  6. package/dashboard/components/coordinator-timeouts.js +13 -0
  7. package/dashboard/components/cross-repo.js +17 -12
  8. package/dashboard/components/gate.js +31 -11
  9. package/dashboard/components/initiative.js +173 -78
  10. package/dashboard/components/ledger.js +28 -0
  11. package/dashboard/components/live-status.js +39 -0
  12. package/dashboard/components/run-history.js +76 -1
  13. package/dashboard/components/timeline.js +5 -1
  14. package/dashboard/index.html +21 -0
  15. package/dashboard/live-observer.js +91 -0
  16. package/package.json +1 -1
  17. package/scripts/release-bump.sh +26 -3
  18. package/scripts/release-preflight.sh +82 -38
  19. package/src/commands/accept-turn.js +3 -3
  20. package/src/commands/decisions.js +98 -29
  21. package/src/commands/diff.js +27 -4
  22. package/src/commands/doctor.js +48 -16
  23. package/src/commands/generate.js +126 -1
  24. package/src/commands/history.js +21 -3
  25. package/src/commands/init.js +15 -97
  26. package/src/commands/multi.js +223 -54
  27. package/src/commands/phase.js +11 -13
  28. package/src/commands/reject-turn.js +1 -1
  29. package/src/commands/restart.js +28 -11
  30. package/src/commands/resume.js +6 -6
  31. package/src/commands/role.js +51 -14
  32. package/src/commands/run.js +5 -11
  33. package/src/commands/status.js +145 -13
  34. package/src/commands/step.js +36 -29
  35. package/src/lib/admission-control.js +14 -12
  36. package/src/lib/blocked-state.js +150 -0
  37. package/src/lib/conflict-actions.js +17 -0
  38. package/src/lib/context-section-parser.js +2 -0
  39. package/src/lib/continuity-status.js +1 -1
  40. package/src/lib/coordinator-blocker-presentation.js +127 -0
  41. package/src/lib/coordinator-event-narrative.js +43 -0
  42. package/src/lib/coordinator-gate-approval.js +98 -0
  43. package/src/lib/coordinator-gate-evaluation-presentation.js +57 -0
  44. package/src/lib/coordinator-next-actions.js +128 -0
  45. package/src/lib/coordinator-pending-gate-presentation.js +79 -0
  46. package/src/lib/coordinator-presentation-detail.js +11 -0
  47. package/src/lib/coordinator-repo-snapshots.js +53 -0
  48. package/src/lib/coordinator-repo-status-presentation.js +134 -0
  49. package/src/lib/dashboard/actions.js +105 -29
  50. package/src/lib/dashboard/bridge-server.js +7 -0
  51. package/src/lib/dashboard/coordinator-blockers.js +17 -0
  52. package/src/lib/dashboard/coordinator-repo-status.js +50 -0
  53. package/src/lib/dashboard/coordinator-timeout-status.js +34 -11
  54. package/src/lib/dashboard/state-reader.js +36 -1
  55. package/src/lib/dispatch-bundle.js +23 -0
  56. package/src/lib/export-diff.js +70 -38
  57. package/src/lib/export-verifier.js +3 -0
  58. package/src/lib/history-diff-summary.js +249 -0
  59. package/src/lib/manual-qa-fallback.js +18 -0
  60. package/src/lib/normalized-config.js +27 -22
  61. package/src/lib/planning-artifacts.js +131 -0
  62. package/src/lib/recent-event-summary.js +132 -0
  63. package/src/lib/repo-decisions.js +69 -28
  64. package/src/lib/report.js +353 -145
  65. package/src/lib/run-diff.js +4 -0
  66. package/src/lib/runtime-capabilities.js +222 -0
@@ -11,6 +11,11 @@ import { loadNormalizedConfig, detectConfigVersion } from '../lib/normalized-con
11
11
  import { readDaemonState, evaluateDaemonStatus } from '../lib/run-schedule.js';
12
12
  import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
13
13
  import { PLUGIN_MANIFEST_FILE } from '../lib/plugins.js';
14
+ import {
15
+ getRoleRuntimeCapabilityContract,
16
+ summarizeRoleRuntimeCapability,
17
+ summarizeRuntimeCapabilityContract,
18
+ } from '../lib/runtime-capabilities.js';
14
19
 
15
20
  export async function doctorCommand(opts = {}) {
16
21
  const root = findProjectRoot(process.cwd());
@@ -76,8 +81,9 @@ function governedDoctor(root, rawConfig, opts) {
76
81
  // 3. Runtime reachable — one sub-check per runtime
77
82
  // Use normalized runtimes if available, otherwise fall back to raw config
78
83
  const runtimes = (normalized && normalized.runtimes) || rawConfig.runtimes || {};
84
+ const rolesByRuntime = buildRolesByRuntime(normalized?.roles || {});
79
85
  for (const [rtId, rt] of Object.entries(runtimes)) {
80
- const check = checkRuntimeReachable(rtId, rt);
86
+ const check = checkRuntimeReachable(rtId, rt, rolesByRuntime[rtId] || []);
81
87
  checks.push(check);
82
88
  }
83
89
  const connectorProbe = getConnectorProbeRecommendation(runtimes);
@@ -290,6 +296,12 @@ function governedDoctor(root, rawConfig, opts) {
290
296
  ? chalk.dim('INFO')
291
297
  : chalk.red('FAIL');
292
298
  console.log(` ${badge} ${c.name.padEnd(24)} ${chalk.dim(c.detail)}`);
299
+ if (c.runtime_contract) {
300
+ console.log(` ${chalk.dim(summarizeRuntimeCapabilityContract(c.runtime_contract))}`);
301
+ if (Array.isArray(c.bound_roles) && c.bound_roles.length > 0) {
302
+ console.log(` ${chalk.dim(`roles: ${c.bound_roles.map(summarizeRoleRuntimeCapability).join(' | ')}`)}`);
303
+ }
304
+ }
293
305
  }
294
306
 
295
307
  console.log('');
@@ -309,25 +321,45 @@ function governedDoctor(root, rawConfig, opts) {
309
321
  process.exit(failCount > 0 ? 1 : 0);
310
322
  }
311
323
 
312
- function checkRuntimeReachable(rtId, rt) {
324
+ function buildRolesByRuntime(roles) {
325
+ const grouped = {};
326
+ for (const [roleId, role] of Object.entries(roles || {})) {
327
+ if (!role?.runtime_id) continue;
328
+ if (!grouped[role.runtime_id]) grouped[role.runtime_id] = [];
329
+ grouped[role.runtime_id].push([roleId, role]);
330
+ }
331
+ return grouped;
332
+ }
333
+
334
+ function attachRuntimeContract(baseCheck, rtId, rt, boundRoleEntries) {
335
+ const bound_roles = boundRoleEntries.map(([roleId, role]) => getRoleRuntimeCapabilityContract(roleId, role, rt));
336
+ return {
337
+ ...baseCheck,
338
+ runtime_type: rt?.type || 'unknown',
339
+ runtime_contract: bound_roles[0]?.runtime_contract || getRoleRuntimeCapabilityContract('__unbound__', { write_authority: 'unknown' }, rt).runtime_contract,
340
+ bound_roles,
341
+ };
342
+ }
343
+
344
+ function checkRuntimeReachable(rtId, rt, boundRoleEntries = []) {
313
345
  const base = { id: `runtime_${rtId}`, name: `Runtime: ${rtId}` };
314
346
 
315
347
  if (!rt || !rt.type) {
316
- return { ...base, level: 'warn', detail: 'No runtime type specified' };
348
+ return attachRuntimeContract({ ...base, level: 'warn', detail: 'No runtime type specified' }, rtId, rt, boundRoleEntries);
317
349
  }
318
350
 
319
351
  switch (rt.type) {
320
352
  case 'manual':
321
- return { ...base, level: 'pass', detail: 'Manual runtime (no binary needed)' };
353
+ return attachRuntimeContract({ ...base, level: 'pass', detail: 'Manual runtime (no binary needed)' }, rtId, rt, boundRoleEntries);
322
354
 
323
355
  case 'local_cli': {
324
356
  const cmd = Array.isArray(rt.command) ? rt.command[0] : (typeof rt.command === 'string' ? rt.command.split(/\s+/)[0] : null);
325
- if (!cmd) return { ...base, level: 'warn', detail: 'No command configured' };
357
+ if (!cmd) return attachRuntimeContract({ ...base, level: 'warn', detail: 'No command configured' }, rtId, rt, boundRoleEntries);
326
358
  try {
327
359
  execSync(`command -v ${cmd}`, { stdio: 'ignore' });
328
- return { ...base, level: 'pass', detail: `${cmd} binary found` };
360
+ return attachRuntimeContract({ ...base, level: 'pass', detail: `${cmd} binary found` }, rtId, rt, boundRoleEntries);
329
361
  } catch {
330
- return { ...base, level: 'fail', detail: `${cmd} not found in PATH` };
362
+ return attachRuntimeContract({ ...base, level: 'fail', detail: `${cmd} not found in PATH` }, rtId, rt, boundRoleEntries);
331
363
  }
332
364
  }
333
365
 
@@ -335,34 +367,34 @@ function checkRuntimeReachable(rtId, rt) {
335
367
  const envVar = rt.auth_env;
336
368
  if (!envVar) {
337
369
  // ollama and similar providers may not require auth
338
- return { ...base, level: 'pass', detail: `${rt.provider || 'unknown'} provider (no auth required)` };
370
+ return attachRuntimeContract({ ...base, level: 'pass', detail: `${rt.provider || 'unknown'} provider (no auth required)` }, rtId, rt, boundRoleEntries);
339
371
  }
340
372
  if (process.env[envVar]) {
341
- return { ...base, level: 'pass', detail: `${envVar} is set` };
373
+ return attachRuntimeContract({ ...base, level: 'pass', detail: `${envVar} is set` }, rtId, rt, boundRoleEntries);
342
374
  }
343
- return { ...base, level: 'fail', detail: `${envVar} not set` };
375
+ return attachRuntimeContract({ ...base, level: 'fail', detail: `${envVar} not set` }, rtId, rt, boundRoleEntries);
344
376
  }
345
377
 
346
378
  case 'mcp': {
347
379
  const transport = rt.transport || 'stdio';
348
380
  if (transport === 'streamable_http') {
349
- return { ...base, level: 'warn', detail: 'Remote MCP endpoint (cannot verify at doctor time)' };
381
+ return attachRuntimeContract({ ...base, level: 'warn', detail: 'Remote MCP endpoint (cannot verify at doctor time)' }, rtId, rt, boundRoleEntries);
350
382
  }
351
383
  const cmd = Array.isArray(rt.command) ? rt.command[0] : (typeof rt.command === 'string' ? rt.command.split(/\s+/)[0] : null);
352
- if (!cmd) return { ...base, level: 'warn', detail: 'No MCP command configured' };
384
+ if (!cmd) return attachRuntimeContract({ ...base, level: 'warn', detail: 'No MCP command configured' }, rtId, rt, boundRoleEntries);
353
385
  try {
354
386
  execSync(`command -v ${cmd}`, { stdio: 'ignore' });
355
- return { ...base, level: 'pass', detail: `${cmd} binary found` };
387
+ return attachRuntimeContract({ ...base, level: 'pass', detail: `${cmd} binary found` }, rtId, rt, boundRoleEntries);
356
388
  } catch {
357
- return { ...base, level: 'fail', detail: `${cmd} not found in PATH` };
389
+ return attachRuntimeContract({ ...base, level: 'fail', detail: `${cmd} not found in PATH` }, rtId, rt, boundRoleEntries);
358
390
  }
359
391
  }
360
392
 
361
393
  case 'remote_agent':
362
- return { ...base, level: 'warn', detail: 'Remote agent endpoint (cannot verify at doctor time)' };
394
+ return attachRuntimeContract({ ...base, level: 'warn', detail: 'Remote agent endpoint (cannot verify at doctor time)' }, rtId, rt, boundRoleEntries);
363
395
 
364
396
  default:
365
- return { ...base, level: 'warn', detail: `Unknown runtime type: ${rt.type}` };
397
+ return attachRuntimeContract({ ...base, level: 'warn', detail: `Unknown runtime type: ${rt.type}` }, rtId, rt, boundRoleEntries);
366
398
  }
367
399
  }
368
400
 
@@ -1,6 +1,10 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
1
3
  import chalk from 'chalk';
2
- import { loadConfig } from '../lib/config.js';
4
+ import { loadConfig, loadProjectContext } from '../lib/config.js';
3
5
  import { generateVSCodeFiles } from '../lib/generate-vscode.js';
6
+ import { loadGovernedTemplate } from '../lib/governed-templates.js';
7
+ import { buildGovernedPlanningArtifacts } from '../lib/planning-artifacts.js';
4
8
 
5
9
  export async function generateCommand() {
6
10
  const result = loadConfig();
@@ -42,3 +46,124 @@ export async function generateCommand() {
42
46
  console.log(chalk.dim(' Select an agent from the Chat dropdown to start a turn.'));
43
47
  console.log('');
44
48
  }
49
+
50
+ function failPlanningGenerate(message, opts = {}) {
51
+ if (opts.json) {
52
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
53
+ } else {
54
+ console.log(chalk.red(` ${message}`));
55
+ }
56
+ process.exit(1);
57
+ }
58
+
59
+ export async function generatePlanningCommand(opts = {}) {
60
+ const context = loadProjectContext();
61
+ if (!context) {
62
+ failPlanningGenerate('No valid agentxchain.json found. Run `agentxchain init --governed` first.', opts);
63
+ }
64
+
65
+ if (context.version !== 4) {
66
+ failPlanningGenerate('`generate planning` only works in governed repos.', opts);
67
+ }
68
+
69
+ const templateId = context.rawConfig.template || 'generic';
70
+ let template;
71
+ try {
72
+ template = loadGovernedTemplate(templateId);
73
+ } catch (err) {
74
+ failPlanningGenerate(err.message, opts);
75
+ }
76
+
77
+ const projectName = context.config?.project?.name || context.rawConfig?.project?.name || 'AgentXchain Project';
78
+ const artifacts = buildGovernedPlanningArtifacts({
79
+ projectName,
80
+ routing: context.config.routing || {},
81
+ roles: context.config.roles || {},
82
+ template,
83
+ workflowKitConfig: context.config.workflow_kit || null,
84
+ });
85
+
86
+ const created = [];
87
+ const overwritten = [];
88
+ const skippedExisting = [];
89
+
90
+ for (const artifact of artifacts) {
91
+ const absPath = join(context.root, artifact.path);
92
+ if (existsSync(absPath)) {
93
+ if (opts.force) {
94
+ overwritten.push(artifact.path);
95
+ } else {
96
+ skippedExisting.push(artifact.path);
97
+ continue;
98
+ }
99
+ } else {
100
+ created.push(artifact.path);
101
+ }
102
+
103
+ if (opts.dryRun) {
104
+ continue;
105
+ }
106
+
107
+ const parentDir = dirname(absPath);
108
+ if (!existsSync(parentDir)) {
109
+ mkdirSync(parentDir, { recursive: true });
110
+ }
111
+ writeFileSync(absPath, artifact.content);
112
+ }
113
+
114
+ const payload = {
115
+ ok: true,
116
+ mode: 'planning',
117
+ dry_run: Boolean(opts.dryRun),
118
+ force: Boolean(opts.force),
119
+ template: template.id,
120
+ project: projectName,
121
+ total_artifacts: artifacts.length,
122
+ created,
123
+ overwritten,
124
+ skipped_existing: skippedExisting,
125
+ };
126
+
127
+ if (opts.json) {
128
+ console.log(JSON.stringify(payload, null, 2));
129
+ return;
130
+ }
131
+
132
+ console.log('');
133
+ console.log(chalk.bold(' Generating governed planning artifacts...'));
134
+ console.log(chalk.dim(` Project: ${projectName}`));
135
+ console.log(chalk.dim(` Template: ${template.id}`));
136
+ console.log('');
137
+
138
+ if (created.length > 0) {
139
+ console.log(chalk.green(` ${opts.dryRun ? 'Would create' : 'Created'} ${created.length} artifact${created.length === 1 ? '' : 's'}:`));
140
+ for (const path of created) {
141
+ console.log(chalk.green(` ${path}`));
142
+ }
143
+ }
144
+
145
+ if (overwritten.length > 0) {
146
+ console.log(chalk.yellow(` ${opts.dryRun ? 'Would overwrite' : 'Overwrote'} ${overwritten.length} artifact${overwritten.length === 1 ? '' : 's'}:`));
147
+ for (const path of overwritten) {
148
+ console.log(chalk.yellow(` ${path}`));
149
+ }
150
+ }
151
+
152
+ if (skippedExisting.length > 0) {
153
+ console.log(chalk.dim(` Preserved ${skippedExisting.length} existing artifact${skippedExisting.length === 1 ? '' : 's'}:`));
154
+ for (const path of skippedExisting) {
155
+ console.log(chalk.dim(` ${path}`));
156
+ }
157
+ }
158
+
159
+ if (created.length === 0 && overwritten.length === 0) {
160
+ console.log(chalk.dim(` ${opts.force ? 'Nothing to overwrite.' : 'All scaffold-owned planning artifacts already exist.'}`));
161
+ }
162
+
163
+ if (opts.dryRun) {
164
+ console.log('');
165
+ console.log(chalk.dim(' No files were written. Re-run without `--dry-run` to apply.'));
166
+ }
167
+
168
+ console.log('');
169
+ }
@@ -5,10 +5,11 @@
5
5
  */
6
6
 
7
7
  import { resolve } from 'path';
8
- import { existsSync, readFileSync } from 'fs';
8
+ import { existsSync } from 'fs';
9
9
  import chalk from 'chalk';
10
10
  import { queryRunHistory, queryRunLineage, isInheritable } from '../lib/run-history.js';
11
- import { getRunTriggerLabel, summarizeRunProvenance } from '../lib/run-provenance.js';
11
+ import { buildRunOutcomeSummary } from '../lib/history-diff-summary.js';
12
+ import { getRunTriggerLabel } from '../lib/run-provenance.js';
12
13
 
13
14
  /**
14
15
  * @param {object} opts - { json?: boolean, limit?: number, status?: string, dir?: string }
@@ -67,7 +68,11 @@ export async function historyCommand(opts) {
67
68
  });
68
69
 
69
70
  if (opts.json) {
70
- const enriched = entries.map(e => ({ ...e, inheritable: isInheritable(e) }));
71
+ const enriched = entries.map((e) => ({
72
+ ...e,
73
+ inheritable: isInheritable(e),
74
+ outcome_summary: buildRunOutcomeSummary(e),
75
+ }));
71
76
  console.log(JSON.stringify(enriched, null, 2));
72
77
  return;
73
78
  }
@@ -85,6 +90,7 @@ export async function historyCommand(opts) {
85
90
  pad('#', 4),
86
91
  pad('Run ID', 14),
87
92
  pad('Status', 11),
93
+ pad('Outcome', 11),
88
94
  pad('Trigger', 14),
89
95
  pad('Ctx', 4),
90
96
  pad('Phases', 8),
@@ -102,6 +108,7 @@ export async function historyCommand(opts) {
102
108
  const idx = String(i + 1);
103
109
  const runId = (entry.run_id || '—').slice(0, 12);
104
110
  const status = formatStatus(entry.status);
111
+ const outcome = buildRunOutcomeSummary(entry);
105
112
  const trigger = getRunTriggerLabel(entry.provenance);
106
113
  const ctx = isInheritable(entry) ? '✓' : '—';
107
114
  const phases = String(entry.phases_completed?.length || 0);
@@ -121,6 +128,7 @@ export async function historyCommand(opts) {
121
128
  pad(idx, 4),
122
129
  pad(runId, 14),
123
130
  pad(status, 11),
131
+ pad(outcome.label, 11),
124
132
  pad(trigger, 14),
125
133
  pad(ctx, 4),
126
134
  pad(phases, 8),
@@ -130,6 +138,10 @@ export async function historyCommand(opts) {
130
138
  pad(date, 20),
131
139
  pad(headline, 42),
132
140
  ].join(' '));
141
+
142
+ if (outcome.next_action) {
143
+ console.log(chalk.dim(` next: ${truncateLine(outcome.next_action, 148)}`));
144
+ }
133
145
  });
134
146
 
135
147
  console.log(chalk.dim(`\n${entries.length} run(s) shown`));
@@ -176,3 +188,9 @@ function formatHeadline(headline) {
176
188
  if (normalized.length <= 40) return normalized;
177
189
  return `${normalized.slice(0, 39)}…`;
178
190
  }
191
+
192
+ function truncateLine(value, max) {
193
+ if (typeof value !== 'string') return '';
194
+ if (value.length <= max) return value;
195
+ return `${value.slice(0, max - 1)}…`;
196
+ }
@@ -5,8 +5,9 @@ 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 { loadAllGovernedTemplates, loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS, buildSystemSpecContent } from '../lib/governed-templates.js';
8
+ import { loadAllGovernedTemplates, loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS } from '../lib/governed-templates.js';
9
9
  import { normalizeWorkflowKit, VALID_PROMPT_TRANSPORTS } from '../lib/normalized-config.js';
10
+ import { buildGovernedPlanningArtifacts, interpolateTemplateContent } from '../lib/planning-artifacts.js';
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
13
  const TEMPLATES_DIR = join(__dirname, '../templates');
@@ -47,24 +48,11 @@ function loadTemplates() {
47
48
  return templates;
48
49
  }
49
50
 
50
- function interpolateTemplateContent(contentTemplate, projectName) {
51
- return contentTemplate.replaceAll('{{project_name}}', projectName);
52
- }
53
-
54
51
  function appendPromptOverride(basePrompt, override) {
55
52
  if (!override || !override.trim()) return basePrompt;
56
53
  return `${basePrompt}\n\n---\n\n## Project-Type-Specific Guidance\n\n${override.trim()}\n`;
57
54
  }
58
55
 
59
- function appendAcceptanceHints(baseMatrix, acceptanceHints) {
60
- if (!Array.isArray(acceptanceHints) || acceptanceHints.length === 0) {
61
- return baseMatrix;
62
- }
63
-
64
- const hintLines = acceptanceHints.map((hint) => `- [ ] ${hint}`).join('\n');
65
- return `${baseMatrix}\n\n## Template Guidance\n${hintLines}\n`;
66
- }
67
-
68
56
  function findGitRoot(startDir) {
69
57
  let current = resolve(startDir);
70
58
  while (true) {
@@ -598,20 +586,6 @@ export async function resolveGovernedInitAnswers(opts, prompt = (questions) => i
598
586
  };
599
587
  }
600
588
 
601
- function generateWorkflowKitPlaceholder(artifact, projectName) {
602
- const filename = basename(artifact.path);
603
- const title = filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
604
-
605
- if (artifact.semantics === 'section_check' && artifact.semantics_config?.required_sections?.length) {
606
- const sections = artifact.semantics_config.required_sections
607
- .map(s => `${s}\n\n(Content here.)\n`)
608
- .join('\n');
609
- return `# ${title} — ${projectName}\n\n${sections}`;
610
- }
611
-
612
- return `# ${title} — ${projectName}\n\n(Operator fills this in.)\n`;
613
- }
614
-
615
589
  function cloneJsonCompatible(value) {
616
590
  return value == null ? value : JSON.parse(JSON.stringify(value));
617
591
  }
@@ -653,29 +627,6 @@ function buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitC
653
627
  };
654
628
  }
655
629
 
656
- const PHASE_DISPLAY_NAMES = Object.freeze({
657
- qa: 'QA',
658
- });
659
-
660
- function formatPhaseDisplayName(phaseKey) {
661
- if (PHASE_DISPLAY_NAMES[phaseKey]) {
662
- return PHASE_DISPLAY_NAMES[phaseKey];
663
- }
664
- return phaseKey.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
665
- }
666
-
667
- function buildRoadmapPhaseTable(routing, roles) {
668
- const rows = Object.entries(routing).map(([phaseKey, phaseConfig]) => {
669
- const phaseName = formatPhaseDisplayName(phaseKey);
670
- const entryRole = phaseConfig.entry_role;
671
- const role = roles[entryRole];
672
- const goal = role?.mandate || phaseName;
673
- const status = phaseKey === Object.keys(routing)[0] ? 'In progress' : 'Pending';
674
- return `| ${phaseName} | ${goal} | ${status} |`;
675
- });
676
- return `| Phase | Goal | Status |\n|-------|------|--------|\n${rows.join('\n')}\n`;
677
- }
678
-
679
630
  function buildPlanningSummaryLines(template, workflowKitConfig) {
680
631
  const lines = [
681
632
  'PM_SIGNOFF.md / ROADMAP.md / SYSTEM_SPEC.md',
@@ -821,53 +772,20 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
821
772
  }
822
773
 
823
774
  // Planning artifacts
824
- writeFileSync(join(dir, '.planning', 'PM_SIGNOFF.md'), `# PM Signoff — ${projectName}\n\nApproved: NO\n\n> This scaffold starts blocked on purpose. Change this to \`Approved: YES\` only after a human reviews the planning artifacts and is ready to open the planning gate.\n\n## Discovery Checklist\n- [ ] Target user defined\n- [ ] Core pain point defined\n- [ ] Core workflow defined\n- [ ] MVP scope defined\n- [ ] Out-of-scope list defined\n- [ ] Success metric defined\n\n## Notes for team\n(PM and human add final kickoff notes here.)\n`);
825
- writeFileSync(join(dir, '.planning', 'ROADMAP.md'), `# Roadmap — ${projectName}\n\n## Phases\n\n${buildRoadmapPhaseTable(routing, roles)}`);
826
- writeFileSync(join(dir, '.planning', 'SYSTEM_SPEC.md'), buildSystemSpecContent(projectName, template.system_spec_overlay));
827
- writeFileSync(join(dir, '.planning', 'IMPLEMENTATION_NOTES.md'), `# Implementation Notes — ${projectName}\n\n## Changes\n\n(Dev fills this during implementation)\n\n## Verification\n\n(Dev fills this during implementation)\n\n## Unresolved Follow-ups\n\n(Dev lists any known gaps, tech debt, or follow-up items here.)\n`);
828
- const baseAcceptanceMatrix = `# Acceptance Matrix — ${projectName}\n\n| Req # | Requirement | Acceptance criteria | Test status | Last tested | Status |\n|-------|-------------|-------------------|-------------|-------------|--------|\n| (QA fills this from ROADMAP.md) | | | | | |\n`;
829
- writeFileSync(
830
- join(dir, '.planning', 'acceptance-matrix.md'),
831
- appendAcceptanceHints(baseAcceptanceMatrix, template.acceptance_hints)
832
- );
833
- writeFileSync(join(dir, '.planning', 'ship-verdict.md'), `# Ship Verdict — ${projectName}\n\n## Verdict: PENDING\n\n## QA Summary\n\n(QA writes the final ship/no-ship assessment here.)\n\n## Open Blockers\n\n(List any blocking issues.)\n\n## Conditions\n\n(List any conditions for shipping.)\n`);
834
- writeFileSync(join(dir, '.planning', 'RELEASE_NOTES.md'), `# Release Notes — ${projectName}\n\n## User Impact\n\n(QA fills this during the QA phase)\n\n## Verification Summary\n\n(QA fills this during the QA phase)\n\n## Upgrade Notes\n\n(QA fills this during the QA phase)\n\n## Known Issues\n\n(QA fills this during the QA phase)\n`);
835
- for (const artifact of template.planning_artifacts) {
836
- writeFileSync(
837
- join(dir, '.planning', artifact.filename),
838
- interpolateTemplateContent(artifact.content_template, projectName)
839
- );
840
- }
841
-
842
- // Workflow-kit custom artifacts — only scaffold files from explicit workflow_kit config
843
- // that are not already handled by the default scaffold above
844
- if (scaffoldWorkflowKitConfig && scaffoldWorkflowKitConfig.phases && typeof scaffoldWorkflowKitConfig.phases === 'object') {
845
- const defaultScaffoldPaths = new Set([
846
- '.planning/PM_SIGNOFF.md',
847
- '.planning/ROADMAP.md',
848
- '.planning/SYSTEM_SPEC.md',
849
- '.planning/IMPLEMENTATION_NOTES.md',
850
- '.planning/acceptance-matrix.md',
851
- '.planning/ship-verdict.md',
852
- '.planning/RELEASE_NOTES.md',
853
- ]);
854
-
855
- for (const phaseConfig of Object.values(scaffoldWorkflowKitConfig.phases)) {
856
- if (!Array.isArray(phaseConfig.artifacts)) continue;
857
- for (const artifact of phaseConfig.artifacts) {
858
- if (!artifact.path || defaultScaffoldPaths.has(artifact.path)) continue;
859
- const absPath = join(dir, artifact.path);
860
- if (existsSync(absPath)) continue;
861
-
862
- // Ensure parent directory exists
863
- const parentDir = dirname(absPath);
864
- if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
865
-
866
- // Generate placeholder content based on semantics type
867
- const content = generateWorkflowKitPlaceholder(artifact, projectName);
868
- writeFileSync(absPath, content);
869
- }
775
+ for (const artifact of buildGovernedPlanningArtifacts({
776
+ projectName,
777
+ routing,
778
+ roles,
779
+ template,
780
+ workflowKitConfig: scaffoldWorkflowKitConfig,
781
+ })) {
782
+ const absPath = join(dir, artifact.path);
783
+ if (artifact.source === 'workflow_kit' && existsSync(absPath)) {
784
+ continue;
870
785
  }
786
+ const parentDir = dirname(absPath);
787
+ if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
788
+ writeFileSync(absPath, artifact.content);
871
789
  }
872
790
 
873
791
  // TALK.md