roadmapsmith 0.9.14 → 0.9.16

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/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) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.14",
4
- "description": "Evidence-backed ROADMAP.md generator and sync tool for AI coding agents.",
3
+ "version": "0.9.16",
4
+ "description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "roadmapsmith": "bin/cli.js"
package/src/config.js CHANGED
@@ -24,6 +24,12 @@ const DEFAULT_CONFIG = {
24
24
  steps: [],
25
25
  phases: []
26
26
  },
27
+ zeroMode: {
28
+ problemStatement: '',
29
+ preferredStack: '',
30
+ constraints: [],
31
+ doneCriteria: []
32
+ },
27
33
  validation: {
28
34
  minimumConfidence: 'low'
29
35
  },
@@ -77,6 +83,16 @@ function mergeConfig(userConfig) {
77
83
  ? userConfig.product.phases
78
84
  : DEFAULT_CONFIG.product.phases
79
85
  },
86
+ zeroMode: {
87
+ ...DEFAULT_CONFIG.zeroMode,
88
+ ...((userConfig && userConfig.zeroMode) || {}),
89
+ constraints: (userConfig && userConfig.zeroMode && Array.isArray(userConfig.zeroMode.constraints))
90
+ ? userConfig.zeroMode.constraints
91
+ : DEFAULT_CONFIG.zeroMode.constraints,
92
+ doneCriteria: (userConfig && userConfig.zeroMode && Array.isArray(userConfig.zeroMode.doneCriteria))
93
+ ? userConfig.zeroMode.doneCriteria
94
+ : DEFAULT_CONFIG.zeroMode.doneCriteria
95
+ },
80
96
  validation: {
81
97
  ...DEFAULT_CONFIG.validation,
82
98
  ...((userConfig && userConfig.validation) || {})
@@ -84,11 +100,15 @@ function mergeConfig(userConfig) {
84
100
  };
85
101
  }
86
102
 
87
- function loadConfig(options = {}) {
103
+ function resolveConfigPath(options = {}) {
88
104
  const projectRoot = path.resolve(options.projectRoot || process.cwd());
89
- const resolvedConfigPath = options.configPath
105
+ return options.configPath
90
106
  ? path.resolve(projectRoot, String(options.configPath))
91
107
  : path.resolve(projectRoot, 'roadmap-skill.config.json');
108
+ }
109
+
110
+ function loadConfig(options = {}) {
111
+ const resolvedConfigPath = resolveConfigPath(options);
92
112
 
93
113
  const content = readTextIfExists(resolvedConfigPath);
94
114
  let userConfig = {};
@@ -114,6 +134,15 @@ function loadConfig(options = {}) {
114
134
  return merged;
115
135
  }
116
136
 
137
+ function readUserConfig(options = {}) {
138
+ const resolvedConfigPath = resolveConfigPath(options);
139
+ const content = readTextIfExists(resolvedConfigPath);
140
+ if (!content) {
141
+ return {};
142
+ }
143
+ return safeParseJson(content, resolvedConfigPath);
144
+ }
145
+
117
146
  function resolveRoadmapFile(projectRoot, config, overridePath) {
118
147
  if (overridePath) {
119
148
  return path.resolve(projectRoot, overridePath);
@@ -214,6 +243,8 @@ module.exports = {
214
243
  collectPluginContributions,
215
244
  loadConfig,
216
245
  loadPlugins,
246
+ readUserConfig,
217
247
  resolveAgentsFile,
248
+ resolveConfigPath,
218
249
  resolveRoadmapFile
219
250
  };