roadmapsmith 0.9.14 → 0.9.15

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
@@ -12,15 +12,22 @@ Production-grade roadmap generator and sync tool for agent-driven projects.
12
12
 
13
13
  ```bash
14
14
  npm install -g roadmapsmith
15
+ roadmapsmith setup
16
+ roadmapsmith zero
17
+ roadmapsmith maintain
15
18
  ```
16
19
 
20
+ Slash entrypoints are also supported from the CLI and launcher, for example: `roadmapsmith /road`, `roadmapsmith /zero`, `roadmapsmith /maintain`, and `roadmapsmith /roadmap-sync maintain`.
21
+ The generated VS Code task layer now resolves Node automatically where possible; if it cannot, RoadmapSmith prints a readable runtime diagnostic instead of a dead task.
22
+ `RoadmapSmith: Status` now treats "ready" as runnable task UX, not merely generated files.
23
+
17
24
  ### Agent Skill
18
25
 
19
26
  ```bash
20
27
  npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
21
28
  ```
22
29
 
23
- This adds the `roadmap-sync` agent skill. It does not install the CLI package.
30
+ This adds the `roadmap-sync` agent skill only. It does not install the CLI and it does not create visible VS Code actions by itself.
24
31
 
25
32
  ## Updating
26
33
 
@@ -37,12 +44,14 @@ npm install roadmapsmith@latest
37
44
  npx roadmapsmith@latest validate --json
38
45
  ```
39
46
 
40
- The `roadmap-sync` agent skill is separate from the CLI. Re-running the skills install updates the agent instructions, but it does not update the `roadmapsmith` npm binary:
47
+ The `roadmap-sync` agent skill is separate from the CLI. Re-running the skills install updates the agent instructions, but it does not update the `roadmapsmith` npm binary or the generated VS Code host files:
41
48
 
42
49
  ```bash
43
50
  npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
44
51
  ```
45
52
 
53
+ After updating the CLI, rerun `roadmapsmith setup` in repositories where you want the latest VS Code tasks, task wrappers, launcher behavior, or Claude hook template.
54
+
46
55
  Fixes are available through `@latest` only after a new npm package version has been published. Before publication, install from a local checkout or a packed tarball for testing.
47
56
 
48
57
  ## Operating Modes
@@ -51,11 +60,11 @@ Fixes are available through `@latest` only after a new npm package version has b
51
60
 
52
61
  Agent-guided discovery for empty or low-context repositories. The developer has a product idea but no implementation files, no stack decision, and no ROADMAP.md yet.
53
62
 
54
- The CLI creates governance files. The AI agent (using the `roadmap-sync` skill) performs the discovery interview before generating the roadmap.
63
+ Run `roadmapsmith setup` first if you want visible VS Code tasks. `roadmapsmith zero` is the one-command entrypoint: it runs the terminal interview, creates governance files when needed, and generates the first roadmap.
55
64
 
56
65
  ```bash
57
- roadmapsmith init
58
- roadmapsmith generate --project-root .
66
+ roadmapsmith setup
67
+ roadmapsmith zero
59
68
  ```
60
69
 
61
70
  ### Sync/Audit Mode
@@ -63,30 +72,50 @@ roadmapsmith generate --project-root .
63
72
  Repository-backed roadmap generation, validation, and synchronization. Use when the repository already has code, tests, docs, TODOs, or an existing ROADMAP.md.
64
73
 
65
74
  ```bash
66
- roadmapsmith generate --project-root .
67
- roadmapsmith validate --json
68
- roadmapsmith sync
69
- roadmapsmith sync --dry-run
75
+ roadmapsmith setup
76
+ roadmapsmith maintain
77
+ ```
78
+
79
+ ## Recommended Daily Flow
80
+
81
+ Use the public entrypoints first:
82
+
83
+ ```bash
84
+ roadmapsmith setup
85
+ roadmapsmith zero # empty repo
86
+ roadmapsmith maintain # existing repo
70
87
  ```
71
88
 
89
+ Use the lower-level commands only when you want manual control over generation, validation, or sync.
90
+
72
91
  ## Host Support Today
73
92
 
74
93
  | Host | Current support |
75
94
  |---|---|
76
- | Claude Code | Manual hook setup is documented and supported. |
77
- | Codex / Codex CLI | Manual CLI workflow only; no documented auto-hook equivalent yet. |
95
+ | Claude Code | Supported through `roadmapsmith setup`: visible VS Code tasks, slash-capable launcher UX, and optional repo-local Claude hook wiring. |
96
+ | Codex / Codex CLI | Supported through a visible VS Code task workflow and slash-capable launcher UX after `roadmapsmith setup`. Codex chat itself remains unchanged unless the host exposes native slash registration. |
78
97
  | CI | Use disposable checkouts if you run `sync --audit`, because it still mutates the roadmap today. |
79
98
  | Other hosts | Use the skill plus manual CLI commands. |
80
99
 
100
+ If Node is installed outside PATH, set `ROADMAPSMITH_NODE` to a working `node` executable before using the generated VS Code tasks.
101
+
81
102
  ---
82
103
 
83
104
  ## Commands
84
105
 
85
106
  ```bash
107
+ roadmapsmith /road
108
+ roadmapsmith /zero
109
+ roadmapsmith /maintain
110
+ roadmapsmith /roadmap-sync maintain
111
+ roadmapsmith setup [--project-root <path>] [--config <path>] [--editor vscode] [--hosts <codex,claude>] [--dry-run]
112
+ roadmapsmith zero [--project-root <path>] [--config <path>]
113
+ roadmapsmith maintain [--project-root <path>] [--config <path>] [--roadmap-file <path>]
86
114
  roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]
87
115
  roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit]
88
116
  roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]
89
117
  roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]
118
+ roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]
90
119
  ```
91
120
 
92
121
  ## Behavior
@@ -241,10 +270,9 @@ module.exports = {
241
270
  ## Example Usage
242
271
 
243
272
  ```bash
244
- roadmapsmith init
245
- roadmapsmith generate --project-root .
273
+ roadmapsmith zero
274
+ roadmapsmith maintain
246
275
  roadmapsmith validate --json
247
- roadmapsmith sync
248
276
  roadmapsmith sync --dry-run
249
277
  ```
250
278
 
@@ -261,6 +289,8 @@ roadmapsmith sync --dry-run
261
289
  npm test
262
290
  ```
263
291
 
292
+ If `npm test` fails in your shell with "`node` is not recognized", treat that as a local PATH/runtime issue first and rerun the suite with an explicit Node executable.
293
+
264
294
  ## Publishing
265
295
 
266
296
  ```bash
@@ -270,6 +300,12 @@ npm publish --access public
270
300
  git push origin main --follow-tags
271
301
  ```
272
302
 
303
+ Repository-specific release note:
304
+
305
+ - The canonical release automation lives in `.github/workflows/ci.yml`.
306
+ - This repository publishes from GitHub Actions on `main`; local `npm publish` is a maintainer workflow, not the default repo release path.
307
+ - Before publishing, verify the UX/release gate in `docs/release-ux-gate.md` and update `CHANGELOG.md` with the user-visible behavior changes.
308
+
273
309
  ## Versioning Strategy
274
310
 
275
311
  - `patch`: bug fixes and non-breaking validation/generation improvements.
package/bin/cli.js CHANGED
@@ -3,23 +3,34 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const readline = require('node:readline/promises');
6
7
  const { parseArgv } = require('../src/utils');
7
- const { loadConfig, resolveRoadmapFile, resolveAgentsFile, loadPlugins } = require('../src/config');
8
+ const { loadConfig, resolveRoadmapFile, resolveAgentsFile, loadPlugins, readUserConfig, resolveConfigPath } = require('../src/config');
8
9
  const { readTextIfExists, writeText, printDryRunDiff } = require('../src/io');
10
+ const { buildSetupFiles, applySetupFiles, inspectHostSetup, parseHosts, assertSupportedEditor } = require('../src/host');
11
+ const { getSlashAction, renderSlashPalette, resolveSlashInvocation } = require('../src/slash');
9
12
  const { renderRoadmapTemplate, renderAgentsTemplate } = require('../src/templates');
10
13
  const { generateRoadmapDocument } = require('../src/generator');
11
14
  const { parseRoadmap } = require('../src/parser');
12
15
  const { buildValidationContext, validateTasks, auditValidation, CONFIDENCE_RANK, applyMinimumConfidence } = require('../src/validator');
13
16
  const { applySync } = require('../src/sync');
17
+ const { buildZeroModeConfigPatch, buildZeroModeDefaults, collectZeroModeAnswers, isInteractiveTerminal } = require('../src/zero');
14
18
 
15
19
  function printHelp() {
16
20
  console.log([
17
21
  'Usage:',
22
+ ' roadmapsmith zero [--project-root <path>] [--config <path>]',
23
+ ' roadmapsmith maintain [--project-root <path>] [--config <path>] [--roadmap-file <path>]',
24
+ ' roadmapsmith /road',
25
+ ' roadmapsmith /road <action>',
26
+ ' roadmapsmith /roadmap-sync <action>',
27
+ ' roadmapsmith /zero | /maintain | /status | /init | /generate | /validate | /sync | /audit | /setup',
18
28
  ' roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]',
29
+ ' roadmapsmith setup [--project-root <path>] [--config <path>] [--editor vscode] [--hosts <codex,claude>] [--dry-run]',
19
30
  ' roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit]',
20
31
  ' roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]',
21
32
  ' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]',
22
- ' roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>]'
33
+ ' roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]'
23
34
  ].join('\n'));
24
35
  }
25
36
 
@@ -68,10 +79,194 @@ function printAudit(audit) {
68
79
  }
69
80
  }
70
81
 
82
+ function formatSetupVerb(result, dryRun) {
83
+ if (dryRun) {
84
+ return result.before == null ? 'Would create' : 'Would update';
85
+ }
86
+ return result.before == null ? 'Created' : 'Updated';
87
+ }
88
+
89
+ function runInitCommand(projectRoot, config, flags) {
90
+ const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
91
+ const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
92
+ const dryRun = isEnabled(flags['dry-run']);
93
+
94
+ const roadmapExists = fs.existsSync(roadmapFile);
95
+ const agentsExists = fs.existsSync(agentsFile);
96
+
97
+ if (!roadmapExists) {
98
+ const roadmap = renderRoadmapTemplate();
99
+ const result = writeText(roadmapFile, roadmap, { dryRun });
100
+ if (dryRun && result.changed) {
101
+ printDryRunDiff(roadmapFile, result.before, result.after);
102
+ }
103
+ console.log(`${dryRun ? 'Would create' : 'Created'} ${roadmapFile}`);
104
+ } else {
105
+ console.log(`Skipped existing ${roadmapFile}`);
106
+ }
107
+
108
+ if (!agentsExists) {
109
+ const agents = renderAgentsTemplate({ roadmapPath: path.basename(roadmapFile) });
110
+ const result = writeText(agentsFile, agents, { dryRun });
111
+ if (dryRun && result.changed) {
112
+ printDryRunDiff(agentsFile, result.before, result.after);
113
+ }
114
+ console.log(`${dryRun ? 'Would create' : 'Created'} ${agentsFile}`);
115
+ } else {
116
+ console.log(`Skipped existing ${agentsFile}`);
117
+ }
118
+ }
119
+
120
+ function runGenerateCommand(projectRoot, config, flags, options = {}) {
121
+ const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
122
+ const plugins = loadPlugins(projectRoot, config.plugins);
123
+ const existingContent = readTextIfExists(roadmapFile) || '';
124
+ const dryRun = isEnabled(flags['dry-run']);
125
+
126
+ const document = generateRoadmapDocument({
127
+ projectRoot,
128
+ roadmapPath: roadmapFile,
129
+ existingContent,
130
+ config,
131
+ plugins
132
+ });
133
+
134
+ const writeResult = writeText(roadmapFile, document, { dryRun });
135
+ if (dryRun) {
136
+ if (writeResult.changed) {
137
+ printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
138
+ } else {
139
+ console.log(`No changes for ${roadmapFile}`);
140
+ }
141
+ } else {
142
+ console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
143
+ }
144
+
145
+ if (options.audit || isEnabled(flags.audit)) {
146
+ const parsedRoadmap = parseRoadmap(document);
147
+ const validationContext = buildValidationContext(projectRoot, config, plugins);
148
+ const results = validateTasks(parsedRoadmap.tasks, validationContext, config, plugins);
149
+ const audit = auditValidation(parsedRoadmap.tasks, results);
150
+ printAudit(audit);
151
+ }
152
+ }
153
+
154
+ function runSyncCommand(projectRoot, config, flags, options = {}) {
155
+ const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
156
+ const content = readTextIfExists(roadmapFile);
157
+ if (content == null) {
158
+ throw new Error(`Roadmap not found: ${roadmapFile}`);
159
+ }
160
+
161
+ const parsedRoadmap = parseRoadmap(content);
162
+ const syncTasks = tasksInManagedBlock(parsedRoadmap);
163
+ const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
164
+ const results = validateTasks(syncTasks, validationContext, config, validationContext.plugins);
165
+ applyMinimumConfidence(results, config.validation?.minimumConfidence);
166
+ const next = applySync(content, syncTasks, results);
167
+ const dryRun = isEnabled(flags['dry-run']);
168
+ const writeResult = writeText(roadmapFile, next, { dryRun });
169
+
170
+ if (dryRun) {
171
+ if (writeResult.changed) {
172
+ printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
173
+ } else {
174
+ console.log(`No changes for ${roadmapFile}`);
175
+ }
176
+ } else {
177
+ console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
178
+ }
179
+
180
+ if (options.audit || isEnabled(flags.audit)) {
181
+ const audit = auditValidation(syncTasks, results);
182
+ printAudit(audit);
183
+ }
184
+ }
185
+
186
+ function printHumanStatus(payload) {
187
+ console.log('RoadmapSmith status\n');
188
+ console.log(`Project root: ${payload.projectRoot}`);
189
+ console.log(`CLI resolution: ${payload.cli.kind}${payload.cli.path ? ` (${payload.cli.path})` : ''}${payload.cli.ready ? '' : ' [missing]'}`);
190
+ console.log(`Roadmap file: ${payload.roadmap.exists ? 'ready' : 'missing'} (${payload.roadmap.path})`);
191
+ console.log(`Agent rules: ${payload.agents.exists ? 'ready' : 'missing'} (${payload.agents.path})`);
192
+ console.log(`VS Code launcher: ${payload.vscode.launcher.exists ? 'ready' : 'missing'} (${payload.vscode.launcher.path})`);
193
+ console.log(`VS Code task wrappers: ${payload.vscode.wrappers.ready ? 'ready' : 'incomplete'} (${payload.vscode.wrappers.presentCount}/${payload.vscode.wrappers.expectedCount} files)`);
194
+ console.log(`VS Code tasks: ${payload.vscode.tasks.ready ? 'ready' : 'incomplete'} (${payload.vscode.tasks.presentLabels.length}/${payload.vscode.tasks.expectedLabels.length} tasks)`);
195
+ console.log(`Node runtime: ${payload.runtime.ready ? `ready (${payload.runtime.kind}${payload.runtime.path ? `: ${payload.runtime.path}` : ''})` : 'missing'}`);
196
+ if (!payload.vscode.tasks.ready && payload.vscode.tasks.missingLabels.length > 0) {
197
+ console.log(`Missing VS Code tasks: ${payload.vscode.tasks.missingLabels.join(', ')}`);
198
+ }
199
+ if (!payload.vscode.wrappers.ready) {
200
+ console.log(`Missing task wrapper files: ${payload.vscode.wrappers.missingPaths.join(', ')}`);
201
+ }
202
+ console.log(`Codex readiness: ${payload.hosts.codex.ready ? 'ready' : 'needs setup'} (${payload.hosts.codex.message})`);
203
+ console.log(`Claude readiness: ${payload.hosts.claude.ready ? 'ready' : 'needs setup'} (${payload.hosts.claude.message})`);
204
+ console.log('\nRecommended entrypoints: roadmapsmith zero (empty repo), roadmapsmith maintain (existing repo).');
205
+ if (!payload.cli.ready) {
206
+ console.log('\nInstalling the skill alone does not expose the CLI in VS Code. Install the CLI and rerun roadmapsmith setup.');
207
+ }
208
+ if (!payload.runtime.ready) {
209
+ console.log('\nThe VS Code task runtime is missing. Install Node.js or set ROADMAPSMITH_NODE, then rerun RoadmapSmith: Status.');
210
+ }
211
+ }
212
+
213
+ function runStatusCommand(projectRoot, config, flags, options = {}) {
214
+ const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
215
+ const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
216
+ const payload = inspectHostSetup(projectRoot, { roadmapFile, agentsFile });
217
+
218
+ if (options.json) {
219
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
220
+ } else {
221
+ printHumanStatus(payload);
222
+ }
223
+
224
+ const ready = payload.cli.ready && payload.roadmap.exists && payload.agents.exists && payload.vscode.tasks.ready && payload.runtime.ready && payload.claude.ready;
225
+ if (!ready) {
226
+ process.exitCode = 1;
227
+ }
228
+ }
229
+
230
+ async function runZeroCommand(projectRoot, flags) {
231
+ const configPath = resolveConfigPath({ projectRoot, configPath: flags.config });
232
+ const config = loadConfig({ projectRoot, configPath: flags.config });
233
+ if (!isInteractiveTerminal(process.stdin, process.stdout)) {
234
+ throw new Error('Zero Mode requires an interactive terminal. Run roadmapsmith zero from a terminal session, or add a config/brief workflow before retrying in non-interactive mode.');
235
+ }
236
+
237
+ const defaults = buildZeroModeDefaults(projectRoot, config);
238
+ const rl = readline.createInterface({
239
+ input: process.stdin,
240
+ output: process.stdout
241
+ });
242
+
243
+ try {
244
+ console.log('RoadmapSmith Zero Mode');
245
+ console.log('Answer the discovery interview to generate the first roadmap.\n');
246
+ const answers = await collectZeroModeAnswers((prompt) => rl.question(prompt), defaults);
247
+ const existingUserConfig = readUserConfig({ projectRoot, configPath: flags.config });
248
+ const nextUserConfig = buildZeroModeConfigPatch(projectRoot, existingUserConfig, answers);
249
+ writeText(configPath, JSON.stringify(nextUserConfig, null, 2));
250
+ console.log(`Updated ${configPath}`);
251
+ const nextConfig = loadConfig({ projectRoot, configPath: flags.config });
252
+ runInitCommand(projectRoot, nextConfig, flags);
253
+ runGenerateCommand(projectRoot, nextConfig, flags);
254
+ } finally {
255
+ rl.close();
256
+ }
257
+ }
258
+
259
+ function runMaintainCommand(projectRoot, flags) {
260
+ const config = loadConfig({ projectRoot, configPath: flags.config });
261
+ runGenerateCommand(projectRoot, config, flags);
262
+ runSyncCommand(projectRoot, config, { ...flags, audit: true }, { audit: true });
263
+ }
264
+
71
265
  async function run() {
72
266
  const parsed = parseArgv(process.argv.slice(2));
73
267
  const command = parsed.command;
74
268
  const flags = parsed.flags;
269
+ let effectiveCommand = command;
75
270
 
76
271
  if (isEnabled(flags.version) || isEnabled(flags.v)) {
77
272
  const pkg = require(path.join(__dirname, '..', 'package.json'));
@@ -84,113 +279,91 @@ async function run() {
84
279
  return;
85
280
  }
86
281
 
87
- if (command === 'init') {
88
- const projectRoot = process.cwd();
89
- const config = loadConfig({ projectRoot });
90
- const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
91
- const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
92
- const dryRun = isEnabled(flags['dry-run']);
282
+ const slashInvocation = resolveSlashInvocation(command, parsed.args);
283
+ if (slashInvocation) {
284
+ if (slashInvocation.kind === 'palette') {
285
+ process.stdout.write(renderSlashPalette(slashInvocation) + '\n');
286
+ return;
287
+ }
93
288
 
94
- const roadmapExists = fs.existsSync(roadmapFile);
95
- const agentsExists = fs.existsSync(agentsFile);
289
+ const slashAction = getSlashAction(slashInvocation.actionId);
290
+ if (!slashAction) {
291
+ process.stdout.write(renderSlashPalette(slashInvocation) + '\n');
292
+ return;
293
+ }
96
294
 
97
- if (!roadmapExists) {
98
- const roadmap = renderRoadmapTemplate();
99
- const result = writeText(roadmapFile, roadmap, { dryRun });
100
- if (dryRun && result.changed) {
101
- printDryRunDiff(roadmapFile, result.before, result.after);
102
- }
103
- console.log(`${dryRun ? 'Would create' : 'Created'} ${roadmapFile}`);
104
- } else {
105
- console.log(`Skipped existing ${roadmapFile}`);
295
+ if (slashAction.id === 'status') {
296
+ const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
297
+ const config = loadConfig({ projectRoot, configPath: flags.config });
298
+ runStatusCommand(projectRoot, config, flags, { json: isEnabled(flags.json) });
299
+ return;
106
300
  }
107
301
 
108
- if (!agentsExists) {
109
- const agents = renderAgentsTemplate({ roadmapPath: path.basename(roadmapFile) });
110
- const result = writeText(agentsFile, agents, { dryRun });
111
- if (dryRun && result.changed) {
112
- printDryRunDiff(agentsFile, result.before, result.after);
113
- }
114
- console.log(`${dryRun ? 'Would create' : 'Created'} ${agentsFile}`);
302
+ if (slashAction.id === 'audit') {
303
+ flags.audit = true;
304
+ effectiveCommand = 'sync';
115
305
  } else {
116
- console.log(`Skipped existing ${agentsFile}`);
306
+ effectiveCommand = slashAction.id;
117
307
  }
118
- return;
119
308
  }
120
309
 
121
- if (command === 'generate') {
310
+ if (effectiveCommand === 'zero') {
122
311
  const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
123
- const config = loadConfig({ projectRoot, configPath: flags.config });
124
- const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
125
- const plugins = loadPlugins(projectRoot, config.plugins);
126
- const existingContent = readTextIfExists(roadmapFile) || '';
127
- const dryRun = isEnabled(flags['dry-run']);
128
-
129
- const document = generateRoadmapDocument({
130
- projectRoot,
131
- roadmapPath: roadmapFile,
132
- existingContent,
133
- config,
134
- plugins
135
- });
312
+ await runZeroCommand(projectRoot, flags);
313
+ return;
314
+ }
136
315
 
137
- const writeResult = writeText(roadmapFile, document, { dryRun });
138
- if (dryRun) {
139
- if (writeResult.changed) {
140
- printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
141
- } else {
142
- console.log(`No changes for ${roadmapFile}`);
143
- }
144
- } else {
145
- console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
146
- }
316
+ if (effectiveCommand === 'maintain') {
317
+ const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
318
+ runMaintainCommand(projectRoot, flags);
319
+ return;
320
+ }
147
321
 
148
- if (isEnabled(flags.audit)) {
149
- const parsedRoadmap = parseRoadmap(document);
150
- const validationContext = buildValidationContext(projectRoot, config, plugins);
151
- const results = validateTasks(parsedRoadmap.tasks, validationContext, config, plugins);
152
- const audit = auditValidation(parsedRoadmap.tasks, results);
153
- printAudit(audit);
154
- }
322
+ if (effectiveCommand === 'init') {
323
+ const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
324
+ const config = loadConfig({ projectRoot, configPath: flags.config });
325
+ runInitCommand(projectRoot, config, flags);
155
326
  return;
156
327
  }
157
328
 
158
- if (command === 'sync') {
329
+ if (effectiveCommand === 'generate') {
159
330
  const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
160
331
  const config = loadConfig({ projectRoot, configPath: flags.config });
161
- const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
162
- const content = readTextIfExists(roadmapFile);
163
- if (content == null) {
164
- throw new Error(`Roadmap not found: ${roadmapFile}`);
165
- }
332
+ runGenerateCommand(projectRoot, config, flags);
333
+ return;
334
+ }
166
335
 
167
- const parsedRoadmap = parseRoadmap(content);
168
- const syncTasks = tasksInManagedBlock(parsedRoadmap);
169
- const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
170
- const results = validateTasks(syncTasks, validationContext, config, validationContext.plugins);
171
- applyMinimumConfidence(results, config.validation?.minimumConfidence);
172
- const next = applySync(content, syncTasks, results);
336
+ if (effectiveCommand === 'setup') {
337
+ const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
338
+ loadConfig({ projectRoot, configPath: flags.config });
339
+ const editor = assertSupportedEditor(flags.editor || 'vscode');
340
+ const hosts = parseHosts(flags.hosts || 'codex,claude');
173
341
  const dryRun = isEnabled(flags['dry-run']);
174
- const writeResult = writeText(roadmapFile, next, { dryRun });
342
+ const setupPlan = buildSetupFiles(projectRoot, { editor, hosts });
343
+ const results = applySetupFiles(setupPlan, { dryRun });
175
344
 
176
- if (dryRun) {
177
- if (writeResult.changed) {
178
- printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
179
- } else {
180
- console.log(`No changes for ${roadmapFile}`);
345
+ results.forEach((result) => {
346
+ if (dryRun && result.changed) {
347
+ printDryRunDiff(result.path, result.before, result.after);
181
348
  }
182
- } else {
183
- console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
184
- }
349
+ if (!result.changed) {
350
+ console.log(`No changes for ${result.path}`);
351
+ return;
352
+ }
353
+ console.log(`${formatSetupVerb(result, dryRun)} ${result.path}`);
354
+ });
185
355
 
186
- if (isEnabled(flags.audit)) {
187
- const audit = auditValidation(syncTasks, results);
188
- printAudit(audit);
189
- }
190
356
  return;
191
357
  }
192
358
 
193
- if (command === 'validate') {
359
+ if (effectiveCommand === 'sync') {
360
+ const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
361
+ const config = loadConfig({ projectRoot, configPath: flags.config });
362
+ runSyncCommand(projectRoot, config, flags);
363
+ return;
364
+ }
365
+
366
+ if (effectiveCommand === 'validate') {
194
367
  const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
195
368
  const config = loadConfig({ projectRoot, configPath: flags.config });
196
369
  const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
@@ -223,38 +396,112 @@ async function run() {
223
396
  return;
224
397
  }
225
398
 
226
- if (command === 'doctor') {
399
+ if (effectiveCommand === 'doctor') {
227
400
  const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
228
401
  let ok = true;
402
+ const jsonMode = isEnabled(flags.json);
403
+ const log = jsonMode ? () => {} : console.log;
404
+ const logError = jsonMode ? () => {} : console.error;
229
405
 
230
406
  let config;
407
+ let roadmapFile = null;
408
+ let agentsFile = null;
231
409
  try {
232
410
  config = loadConfig({ projectRoot, configPath: flags.config });
233
- console.log('[ok] Config loaded without errors');
411
+ log('[ok] Config loaded without errors');
234
412
  } catch (error) {
235
- console.error(`[fail] Config error: ${error.message}`);
413
+ logError(`[fail] Config error: ${error.message}`);
236
414
  ok = false;
237
415
  }
238
416
 
239
417
  if (config) {
240
- const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
418
+ roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
241
419
  if (fs.existsSync(roadmapFile)) {
242
- console.log(`[ok] ROADMAP file found: ${roadmapFile}`);
420
+ log(`[ok] ROADMAP file found: ${roadmapFile}`);
243
421
  } else {
244
- console.error(`[fail] ROADMAP file not found: ${roadmapFile}`);
422
+ logError(`[fail] ROADMAP file not found: ${roadmapFile}`);
245
423
  ok = false;
246
424
  }
425
+
426
+ agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
427
+ if (fs.existsSync(agentsFile)) {
428
+ log(`[ok] Agent rules file found: ${agentsFile}`);
429
+ } else {
430
+ logError(`[fail] Agent rules file not found: ${agentsFile}`);
431
+ ok = false;
432
+ }
433
+ }
434
+
435
+ let hostStatus = null;
436
+ if (config) {
437
+ try {
438
+ hostStatus = inspectHostSetup(projectRoot, { roadmapFile, agentsFile });
439
+ } catch (error) {
440
+ logError(`[fail] Host integration error: ${error.message}`);
441
+ ok = false;
442
+ }
443
+ }
444
+
445
+ if (hostStatus) {
446
+ if (hostStatus.cli.ready) {
447
+ log(`[ok] CLI resolution: ${hostStatus.cli.kind}${hostStatus.cli.path ? ` (${hostStatus.cli.path})` : ''}`);
448
+ } else {
449
+ logError('[fail] CLI resolution: missing local package and global command');
450
+ ok = false;
451
+ }
452
+
453
+ if (hostStatus.vscode.launcher.exists) {
454
+ log(`[ok] VS Code launcher found: ${hostStatus.vscode.launcher.path}`);
455
+ } else {
456
+ logError(`[fail] VS Code launcher missing: ${hostStatus.vscode.launcher.path}`);
457
+ ok = false;
458
+ }
459
+
460
+ if (hostStatus.vscode.wrappers.ready) {
461
+ log(`[ok] VS Code task wrappers ready: ${hostStatus.vscode.wrappers.presentCount}/${hostStatus.vscode.wrappers.expectedCount} files`);
462
+ } else {
463
+ logError(`[fail] VS Code task wrappers incomplete: missing ${hostStatus.vscode.wrappers.missingPaths.join(', ') || 'wrapper files'}`);
464
+ ok = false;
465
+ }
466
+
467
+ if (hostStatus.vscode.tasks.ready) {
468
+ log(`[ok] VS Code tasks ready: ${hostStatus.vscode.tasks.presentLabels.length}/${hostStatus.vscode.tasks.expectedLabels.length} tasks`);
469
+ } else {
470
+ logError(`[fail] VS Code tasks incomplete: missing ${hostStatus.vscode.tasks.missingLabels.join(', ') || 'managed labels'}`);
471
+ ok = false;
472
+ }
473
+
474
+ if (hostStatus.runtime.ready) {
475
+ log(`[ok] Node runtime: ${hostStatus.runtime.kind}${hostStatus.runtime.path ? ` (${hostStatus.runtime.path})` : ''}`);
476
+ } else {
477
+ logError('[fail] Node runtime missing for VS Code task execution');
478
+ ok = false;
479
+ }
480
+
481
+ if (hostStatus.claude.ready) {
482
+ log(`[ok] Claude hook ready: ${hostStatus.claude.hookFile.path}`);
483
+ } else {
484
+ logError(`[fail] Claude hook incomplete: ${hostStatus.hosts.claude.message}`);
485
+ ok = false;
486
+ }
487
+ }
488
+
489
+ if (jsonMode) {
490
+ process.stdout.write(JSON.stringify(hostStatus || {
491
+ projectRoot,
492
+ error: 'doctor failed before host inspection'
493
+ }, null, 2) + '\n');
247
494
  }
248
495
 
249
496
  if (!ok) {
250
497
  process.exitCode = 1;
251
498
  return;
252
499
  }
253
- console.log('doctor: all checks passed');
500
+ log('doctor: all checks passed');
254
501
  return;
255
502
  }
256
503
 
257
- throw new Error(`Unknown command: ${command}`);
504
+ throw new Error(`Unknown command: ${effectiveCommand}`);
258
505
  }
259
506
 
260
507
  run().catch((error) => {