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 +1 -1
- package/bin/agentxchain.js +11 -3
- package/package.json +1 -1
- package/scripts/release-preflight.sh +82 -38
- package/src/commands/generate.js +126 -1
- package/src/commands/init.js +15 -97
- package/src/lib/planning-artifacts.js +131 -0
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 |
|
package/bin/agentxchain.js
CHANGED
|
@@ -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
|
|
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
|
@@ -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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
package/src/commands/generate.js
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
+
}
|