agentxchain 2.103.0 → 2.104.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
@@ -224,7 +224,7 @@ agentxchain step
224
224
  | `supervise` | Run `watch` plus optional macOS auto-nudge |
225
225
  | `claim` / `release` | Human override of legacy lock ownership |
226
226
  | `rebind` | Rebuild Cursor bindings |
227
- | `generate` | Regenerate VS Code agent files |
227
+ | `generate` | Regenerate VS Code agent files; use `generate planning` to restore scaffold-owned governed planning docs |
228
228
  | `branch` | Manage Cursor branch override for launches |
229
229
  | `doctor` | Check local environment and setup |
230
230
  | `stop` | Stop watch daemon and local sessions |
@@ -55,7 +55,7 @@ import { configCommand } from '../src/commands/config.js';
55
55
  import { updateCommand } from '../src/commands/update.js';
56
56
  import { watchCommand } from '../src/commands/watch.js';
57
57
  import { claimCommand, releaseCommand } from '../src/commands/claim.js';
58
- import { generateCommand } from '../src/commands/generate.js';
58
+ import { generateCommand, generatePlanningCommand } from '../src/commands/generate.js';
59
59
  import { doctorCommand } from '../src/commands/doctor.js';
60
60
  import { superviseCommand } from '../src/commands/supervise.js';
61
61
  import { validateCommand } from '../src/commands/validate.js';
@@ -223,11 +223,19 @@ program
223
223
  .option('--unset', 'Remove override and follow the active git branch automatically')
224
224
  .action(branchCommand);
225
225
 
226
- program
226
+ const generateCmd = program
227
227
  .command('generate')
228
- .description('Regenerate VS Code agent files (.agent.md, hooks) from agentxchain.json')
228
+ .description('Regenerate VS Code agent files, or governed planning artifacts via subcommands')
229
229
  .action(generateCommand);
230
230
 
231
+ generateCmd
232
+ .command('planning')
233
+ .description('Generate or restore scaffold-owned governed planning artifacts')
234
+ .option('--dry-run', 'Show which planning artifacts would be written without changing files')
235
+ .option('--force', 'Overwrite existing scaffold-owned planning artifacts')
236
+ .option('-j, --json', 'Output as JSON')
237
+ .action(generatePlanningCommand);
238
+
231
239
  program
232
240
  .command('watch')
233
241
  .description('Watch lock.json and coordinate agent turns (the referee)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.103.0",
3
+ "version": "2.104.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,10 +9,12 @@ CLI_DIR="${SCRIPT_DIR}/.."
9
9
  cd "$CLI_DIR"
10
10
 
11
11
  STRICT_MODE=0
12
+ PUBLISH_GATE=0
12
13
  TARGET_VERSION="2.0.0"
13
14
 
14
15
  usage() {
15
- echo "Usage: bash scripts/release-preflight.sh [--strict] [--target-version <semver>]" >&2
16
+ echo "Usage: bash scripts/release-preflight.sh [--strict] [--publish-gate] [--target-version <semver>]" >&2
17
+ echo " --publish-gate Run only release-critical checks (no full test suite). Use in CI publish workflows." >&2
16
18
  }
17
19
 
18
20
  while [[ $# -gt 0 ]]; do
@@ -21,6 +23,11 @@ while [[ $# -gt 0 ]]; do
21
23
  STRICT_MODE=1
22
24
  shift
23
25
  ;;
26
+ --publish-gate)
27
+ PUBLISH_GATE=1
28
+ STRICT_MODE=1
29
+ shift
30
+ ;;
24
31
  --target-version)
25
32
  if [[ -z "${2:-}" ]]; then
26
33
  echo "Error: --target-version requires a semver argument" >&2
@@ -99,49 +106,86 @@ else
99
106
  fi
100
107
 
101
108
  # 3. Tests
102
- echo "[3/6] Test suite"
103
- # Install MCP example deps — tests start example servers as subprocesses
104
- for example_dir in "${CLI_DIR}/../examples/mcp-echo-agent" "${CLI_DIR}/../examples/mcp-http-echo-agent"; do
105
- if [[ -f "${example_dir}/package.json" && ! -d "${example_dir}/node_modules" ]]; then
106
- echo " Installing deps for $(basename "$example_dir")..."
107
- (cd "$example_dir" && env -u NODE_AUTH_TOKEN -u NPM_CONFIG_USERCONFIG npm install --ignore-scripts --userconfig /dev/null 2>&1) || true
109
+ if [[ "$PUBLISH_GATE" -eq 1 ]]; then
110
+ echo "[3/6] Release-gate tests (targeted subset)"
111
+ # In publish-gate mode, run only release-critical tests to avoid CI hangs.
112
+ # The full test suite is a pre-tag responsibility, not a publish-time gate.
113
+ GATE_TESTS=(
114
+ test/release-preflight.test.js
115
+ test/release-docs-content.test.js
116
+ test/release-notes-gate.test.js
117
+ test/release-identity-hardening.test.js
118
+ test/normalized-config.test.js
119
+ test/conformance.test.js
120
+ )
121
+ GATE_TEST_ARGS=()
122
+ for t in "${GATE_TESTS[@]}"; do
123
+ if [[ -f "$t" ]]; then
124
+ GATE_TEST_ARGS+=("$t")
125
+ fi
126
+ done
127
+ if [[ ${#GATE_TEST_ARGS[@]} -eq 0 ]]; then
128
+ fail "No release-gate test files found"
129
+ else
130
+ if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 node --test "${GATE_TEST_ARGS[@]}"; then
131
+ TEST_STATUS=0
132
+ else
133
+ TEST_STATUS=$?
134
+ fi
135
+ NODE_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ tests / { print $3; exit }')"
136
+ NODE_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ fail / { print $3; exit }')"
137
+ if [ "$TEST_STATUS" -eq 0 ] && [ "${NODE_FAIL:-0}" = "0" ]; then
138
+ pass "${NODE_PASS:-?} release-gate tests passed, 0 failures"
139
+ else
140
+ fail "Release-gate tests failed"
141
+ printf '%s\n' "$TEST_OUTPUT" | tail -20
142
+ fi
108
143
  fi
109
- done
110
- if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test; then
111
- TEST_STATUS=0
112
144
  else
113
- TEST_STATUS=$?
114
- fi
115
- TEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# pass / { print $3 }')"
116
- TEST_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# fail / { print $3 }')"
117
- if [ -z "${TEST_PASS:-}" ]; then
118
- VITEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+passed/ { for (i = 1; i <= NF; i++) if ($i ~ /^[0-9]+$/) { print $i; exit } }')"
119
- NODE_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ tests / { print $3; exit }')"
120
- if [ -n "${VITEST_PASS:-}" ] && [ -n "${NODE_PASS:-}" ]; then
121
- TEST_PASS="$((VITEST_PASS + NODE_PASS))"
122
- elif [ -n "${NODE_PASS:-}" ]; then
123
- TEST_PASS="${NODE_PASS}"
124
- elif [ -n "${VITEST_PASS:-}" ]; then
125
- TEST_PASS="${VITEST_PASS}"
145
+ echo "[3/6] Test suite"
146
+ # Install MCP example deps — tests start example servers as subprocesses
147
+ for example_dir in "${CLI_DIR}/../examples/mcp-echo-agent" "${CLI_DIR}/../examples/mcp-http-echo-agent"; do
148
+ if [[ -f "${example_dir}/package.json" && ! -d "${example_dir}/node_modules" ]]; then
149
+ echo " Installing deps for $(basename "$example_dir")..."
150
+ (cd "$example_dir" && env -u NODE_AUTH_TOKEN -u NPM_CONFIG_USERCONFIG npm install --ignore-scripts --userconfig /dev/null 2>&1) || true
151
+ fi
152
+ done
153
+ if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test; then
154
+ TEST_STATUS=0
155
+ else
156
+ TEST_STATUS=$?
126
157
  fi
127
- fi
128
- if [ -z "${TEST_FAIL:-}" ]; then
129
- NODE_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ fail / { print $3; exit }')"
130
- if [ -n "${NODE_FAIL:-}" ]; then
131
- TEST_FAIL="${NODE_FAIL}"
132
- elif printf '%s\n' "$TEST_OUTPUT" | grep -Eq '^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+passed'; then
133
- TEST_FAIL=0
158
+ TEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# pass / { print $3 }')"
159
+ TEST_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# fail / { print $3 }')"
160
+ if [ -z "${TEST_PASS:-}" ]; then
161
+ VITEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+passed/ { for (i = 1; i <= NF; i++) if ($i ~ /^[0-9]+$/) { print $i; exit } }')"
162
+ NODE_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ tests / { print $3; exit }')"
163
+ if [ -n "${VITEST_PASS:-}" ] && [ -n "${NODE_PASS:-}" ]; then
164
+ TEST_PASS="$((VITEST_PASS + NODE_PASS))"
165
+ elif [ -n "${NODE_PASS:-}" ]; then
166
+ TEST_PASS="${NODE_PASS}"
167
+ elif [ -n "${VITEST_PASS:-}" ]; then
168
+ TEST_PASS="${VITEST_PASS}"
169
+ fi
134
170
  fi
135
- fi
136
- if [ "$TEST_STATUS" -eq 0 ] && [ "${TEST_FAIL:-0}" = "0" ]; then
137
- if [ -n "${TEST_PASS:-}" ]; then
138
- pass "${TEST_PASS} tests passed, 0 failures"
171
+ if [ -z "${TEST_FAIL:-}" ]; then
172
+ NODE_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ fail / { print $3; exit }')"
173
+ if [ -n "${NODE_FAIL:-}" ]; then
174
+ TEST_FAIL="${NODE_FAIL}"
175
+ elif printf '%s\n' "$TEST_OUTPUT" | grep -Eq '^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+passed'; then
176
+ TEST_FAIL=0
177
+ fi
178
+ fi
179
+ if [ "$TEST_STATUS" -eq 0 ] && [ "${TEST_FAIL:-0}" = "0" ]; then
180
+ if [ -n "${TEST_PASS:-}" ]; then
181
+ pass "${TEST_PASS} tests passed, 0 failures"
182
+ else
183
+ pass "npm test passed, 0 failures"
184
+ fi
139
185
  else
140
- pass "npm test passed, 0 failures"
186
+ fail "npm test failed"
187
+ printf '%s\n' "$TEST_OUTPUT" | tail -20
141
188
  fi
142
- else
143
- fail "npm test failed"
144
- printf '%s\n' "$TEST_OUTPUT" | tail -20
145
189
  fi
146
190
 
147
191
  # 4. CHANGELOG has target version
@@ -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,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
@@ -0,0 +1,131 @@
1
+ import { basename } from 'node:path';
2
+ import { buildSystemSpecContent } from './governed-templates.js';
3
+
4
+ export const GOVERNED_BASELINE_PLANNING_PATHS = Object.freeze([
5
+ '.planning/PM_SIGNOFF.md',
6
+ '.planning/ROADMAP.md',
7
+ '.planning/SYSTEM_SPEC.md',
8
+ '.planning/IMPLEMENTATION_NOTES.md',
9
+ '.planning/acceptance-matrix.md',
10
+ '.planning/ship-verdict.md',
11
+ '.planning/RELEASE_NOTES.md',
12
+ ]);
13
+
14
+ const PHASE_DISPLAY_NAMES = Object.freeze({
15
+ qa: 'QA',
16
+ });
17
+
18
+ function formatPhaseDisplayName(phaseKey) {
19
+ if (PHASE_DISPLAY_NAMES[phaseKey]) {
20
+ return PHASE_DISPLAY_NAMES[phaseKey];
21
+ }
22
+ return phaseKey.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
23
+ }
24
+
25
+ function buildRoadmapPhaseTable(routing, roles) {
26
+ const rows = Object.entries(routing).map(([phaseKey, phaseConfig]) => {
27
+ const phaseName = formatPhaseDisplayName(phaseKey);
28
+ const entryRole = phaseConfig.entry_role;
29
+ const role = roles[entryRole];
30
+ const goal = role?.mandate || phaseName;
31
+ const status = phaseKey === Object.keys(routing)[0] ? 'In progress' : 'Pending';
32
+ return `| ${phaseName} | ${goal} | ${status} |`;
33
+ });
34
+ return `| Phase | Goal | Status |\n|-------|------|--------|\n${rows.join('\n')}\n`;
35
+ }
36
+
37
+ export function interpolateTemplateContent(contentTemplate, projectName) {
38
+ return contentTemplate.replaceAll('{{project_name}}', projectName);
39
+ }
40
+
41
+ export function appendAcceptanceHints(baseMatrix, acceptanceHints) {
42
+ if (!Array.isArray(acceptanceHints) || acceptanceHints.length === 0) {
43
+ return baseMatrix;
44
+ }
45
+
46
+ const hintLines = acceptanceHints.map((hint) => `- [ ] ${hint}`).join('\n');
47
+ return `${baseMatrix}\n\n## Template Guidance\n${hintLines}\n`;
48
+ }
49
+
50
+ export function generateWorkflowKitPlaceholder(artifact, projectName) {
51
+ const filename = basename(artifact.path);
52
+ const title = filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
53
+
54
+ if (artifact.semantics === 'section_check' && artifact.semantics_config?.required_sections?.length) {
55
+ const sections = artifact.semantics_config.required_sections
56
+ .map((section) => `${section}\n\n(Content here.)\n`)
57
+ .join('\n');
58
+ return `# ${title} — ${projectName}\n\n${sections}`;
59
+ }
60
+
61
+ return `# ${title} — ${projectName}\n\n(Operator fills this in.)\n`;
62
+ }
63
+
64
+ export function buildGovernedPlanningArtifacts({ projectName, routing, roles, template, workflowKitConfig }) {
65
+ const artifacts = [
66
+ {
67
+ path: '.planning/PM_SIGNOFF.md',
68
+ source: 'core',
69
+ content: `# 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`,
70
+ },
71
+ {
72
+ path: '.planning/ROADMAP.md',
73
+ source: 'core',
74
+ content: `# Roadmap — ${projectName}\n\n## Phases\n\n${buildRoadmapPhaseTable(routing, roles)}`,
75
+ },
76
+ {
77
+ path: '.planning/SYSTEM_SPEC.md',
78
+ source: 'core',
79
+ content: buildSystemSpecContent(projectName, template?.system_spec_overlay),
80
+ },
81
+ {
82
+ path: '.planning/IMPLEMENTATION_NOTES.md',
83
+ source: 'core',
84
+ content: `# 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`,
85
+ },
86
+ {
87
+ path: '.planning/acceptance-matrix.md',
88
+ source: 'core',
89
+ content: appendAcceptanceHints(
90
+ `# Acceptance Matrix — ${projectName}\n\n| Req # | Requirement | Acceptance criteria | Test status | Last tested | Status |\n|-------|-------------|-------------------|-------------|-------------|--------|\n| (QA fills this from ROADMAP.md) | | | | | |\n`,
91
+ template?.acceptance_hints,
92
+ ),
93
+ },
94
+ {
95
+ path: '.planning/ship-verdict.md',
96
+ source: 'core',
97
+ content: `# 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`,
98
+ },
99
+ {
100
+ path: '.planning/RELEASE_NOTES.md',
101
+ source: 'core',
102
+ content: `# 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`,
103
+ },
104
+ ];
105
+
106
+ for (const artifact of template?.planning_artifacts || []) {
107
+ artifacts.push({
108
+ path: `.planning/${artifact.filename}`,
109
+ source: 'template',
110
+ content: interpolateTemplateContent(artifact.content_template, projectName),
111
+ });
112
+ }
113
+
114
+ const seenPaths = new Set(GOVERNED_BASELINE_PLANNING_PATHS);
115
+ if (workflowKitConfig?.phases && typeof workflowKitConfig.phases === 'object') {
116
+ for (const phaseConfig of Object.values(workflowKitConfig.phases)) {
117
+ if (!Array.isArray(phaseConfig.artifacts)) continue;
118
+ for (const artifact of phaseConfig.artifacts) {
119
+ if (!artifact.path || seenPaths.has(artifact.path)) continue;
120
+ seenPaths.add(artifact.path);
121
+ artifacts.push({
122
+ path: artifact.path,
123
+ source: 'workflow_kit',
124
+ content: generateWorkflowKitPlaceholder(artifact, projectName),
125
+ });
126
+ }
127
+ }
128
+ }
129
+
130
+ return artifacts;
131
+ }