roadmapsmith 0.9.37 → 0.9.39

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.37",
3
+ "version": "0.9.39",
4
4
  "description": "Evidence-backed ROADMAP.md workflows for AI coding agents, with canonical RoadmapSmith status and maintain surfaces plus advanced sync/generate tools and legacy compatibility aliases.",
5
5
  "author": {
6
6
  "name": "PapiScholz"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.37",
3
+ "version": "0.9.39",
4
4
  "description": "Evidence-backed ROADMAP.md workflows for AI coding agents, with canonical RoadmapSmith status and maintain surfaces plus advanced sync/generate tools and legacy compatibility aliases.",
5
5
  "author": {
6
6
  "name": "PapiScholz"
package/bin/cli.js CHANGED
@@ -29,8 +29,8 @@ function printHelp() {
29
29
  ' Canonical commands:',
30
30
  ' roadmapsmith zero [--project-root <path>] [--config <path>] [--product-name <text>] [--primary-user <text>] [--problem-statement <text>] [--target-outcome <text>] [--anti-goal <text> ...] [--preferred-stack <text>] [--constraint <text> ...] [--done-criterion <text> ...]',
31
31
  ' roadmapsmith maintain [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--full-regen] [--refresh-annotations]',
32
- ' roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]',
33
- ' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json] [--strict]',
32
+ ' roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json] [--no-vscode]',
33
+ ' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json] [--strict] [--hide-planned]',
34
34
  ' roadmapsmith update [--task <stable-id> --evidence <text>] [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run]',
35
35
  ' roadmapsmith setup [--project-root <path>] [--config <path>] [--editor vscode] [--hosts <codex,claude>] [--dry-run]',
36
36
  '',
@@ -58,6 +58,9 @@ function isEnabled(value) {
58
58
  }
59
59
 
60
60
  function formatResultLine(task, result) {
61
+ if (result.planned) {
62
+ return `PLAN [${task.id}] ${task.text}`;
63
+ }
61
64
  const diagnostics = Array.isArray(result.diagnostics) ? result.diagnostics : [];
62
65
  const primaryError = diagnostics.find((item) => item.severity === 'error');
63
66
  const warnings = diagnostics.filter((item) => item.severity === 'warning');
@@ -322,6 +325,8 @@ function runSyncCommand(projectRoot, config, flags, options = {}) {
322
325
  console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
323
326
  }
324
327
 
328
+ // options.audit (distinct from flags.audit early return above): used only by
329
+ // runMaintainCommand to print audit summary AFTER mutating ROADMAP.md.
325
330
  if (options.audit) {
326
331
  const audit = auditValidation(syncTasks, results, changes);
327
332
  printAudit(audit);
@@ -412,19 +417,21 @@ function printHumanStatus(payload) {
412
417
  console.log(`CLI resolution: ${payload.cli.kind}${payload.cli.path ? ` (${payload.cli.path})` : ''}${payload.cli.ready ? '' : ' [missing]'}`);
413
418
  console.log(`Roadmap file: ${payload.roadmap.exists ? 'ready' : 'missing'} (${payload.roadmap.path})`);
414
419
  console.log(`Agent rules: ${payload.agents.exists ? 'ready' : 'missing'} (${payload.agents.path})`);
415
- console.log(`VS Code launcher: ${payload.vscode.launcher.exists ? 'ready' : 'missing'} (${payload.vscode.launcher.path})`);
416
- console.log(`VS Code task wrappers: ${payload.vscode.wrappers.ready ? 'ready' : 'incomplete'} (${payload.vscode.wrappers.presentCount}/${payload.vscode.wrappers.expectedCount} files)`);
417
- console.log(`VS Code tasks: ${payload.vscode.tasks.ready ? 'ready' : 'incomplete'} (${payload.vscode.tasks.presentLabels.length}/${payload.vscode.tasks.expectedLabels.length} tasks)`);
418
- console.log(`Node runtime: ${payload.runtime.ready ? `ready (${payload.runtime.kind}${payload.runtime.path ? `: ${payload.runtime.path}` : ''})` : 'missing'}`);
419
- if (!payload.vscode.tasks.ready && payload.vscode.tasks.missingLabels.length > 0) {
420
- console.log(`Missing VS Code tasks: ${payload.vscode.tasks.missingLabels.join(', ')}`);
421
- }
422
- if (Array.isArray(payload.vscode.tasks.missingAdvancedLabels) && payload.vscode.tasks.missingAdvancedLabels.length > 0) {
423
- console.log(`Missing advanced VS Code tasks: ${payload.vscode.tasks.missingAdvancedLabels.join(', ')}`);
424
- }
425
- if (!payload.vscode.wrappers.ready) {
426
- console.log(`Missing task wrapper files: ${payload.vscode.wrappers.missingPaths.join(', ')}`);
420
+ if (!payload.vscode.skipped) {
421
+ console.log(`VS Code launcher: ${payload.vscode.launcher.exists ? 'ready' : 'missing'} (${payload.vscode.launcher.path})`);
422
+ console.log(`VS Code task wrappers: ${payload.vscode.wrappers.ready ? 'ready' : 'incomplete'} (${payload.vscode.wrappers.presentCount}/${payload.vscode.wrappers.expectedCount} files)`);
423
+ console.log(`VS Code tasks: ${payload.vscode.tasks.ready ? 'ready' : 'incomplete'} (${payload.vscode.tasks.presentLabels.length}/${payload.vscode.tasks.expectedLabels.length} tasks)`);
424
+ if (!payload.vscode.tasks.ready && payload.vscode.tasks.missingLabels.length > 0) {
425
+ console.log(`Missing VS Code tasks: ${payload.vscode.tasks.missingLabels.join(', ')}`);
426
+ }
427
+ if (Array.isArray(payload.vscode.tasks.missingAdvancedLabels) && payload.vscode.tasks.missingAdvancedLabels.length > 0) {
428
+ console.log(`Missing advanced VS Code tasks: ${payload.vscode.tasks.missingAdvancedLabels.join(', ')}`);
429
+ }
430
+ if (!payload.vscode.wrappers.ready) {
431
+ console.log(`Missing task wrapper files: ${payload.vscode.wrappers.missingPaths.join(', ')}`);
432
+ }
427
433
  }
434
+ console.log(`Node runtime: ${payload.runtime.ready ? `ready (${payload.runtime.kind}${payload.runtime.path ? `: ${payload.runtime.path}` : ''})` : 'missing'}`);
428
435
  console.log(`Codex readiness: ${payload.hosts.codex.ready ? 'ready' : 'needs setup'} (${payload.hosts.codex.message})`);
429
436
  console.log(`Claude readiness: ${payload.hosts.claude.ready ? 'ready' : 'needs setup'} (${payload.hosts.claude.message})`);
430
437
  printReadinessSummary(payload.summary);
@@ -434,15 +441,20 @@ function printHumanStatus(payload) {
434
441
  if (!payload.cli.ready) {
435
442
  console.log('\nInstalling the skill alone does not expose the CLI in VS Code. Install the CLI and rerun roadmapsmith setup.');
436
443
  }
437
- if (!payload.runtime.ready) {
444
+ if (!payload.vscode.skipped && !payload.runtime.ready) {
438
445
  console.log('\nThe VS Code task runtime is missing. Install Node.js or set ROADMAPSMITH_NODE, then rerun RoadmapSmith: Status.');
439
446
  }
440
447
  }
441
448
 
449
+ function shouldSkipVscode(projectRoot, flags) {
450
+ return isEnabled(flags['no-vscode']) || !fs.existsSync(path.join(projectRoot, '.vscode'));
451
+ }
452
+
442
453
  function runStatusCommand(projectRoot, config, flags, options = {}) {
454
+ const skipVscode = shouldSkipVscode(projectRoot, flags);
443
455
  const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
444
456
  const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
445
- const payload = inspectHostSetup(projectRoot, { roadmapFile, agentsFile, currentCliPath: __filename });
457
+ const payload = inspectHostSetup(projectRoot, { roadmapFile, agentsFile, currentCliPath: __filename, skipVscode });
446
458
 
447
459
  if (options.json) {
448
460
  process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
@@ -450,7 +462,7 @@ function runStatusCommand(projectRoot, config, flags, options = {}) {
450
462
  printHumanStatus(payload);
451
463
  }
452
464
 
453
- const ready = payload.cli.ready && payload.roadmap.exists && payload.agents.exists && payload.vscode.tasks.ready && payload.runtime.ready && payload.claude.ready;
465
+ const ready = payload.cli.ready && payload.roadmap.exists && payload.agents.exists && (payload.vscode.skipped || payload.vscode.tasks.ready) && payload.runtime.ready && payload.claude.ready;
454
466
  if (!ready) {
455
467
  process.exitCode = 1;
456
468
  }
@@ -661,7 +673,12 @@ async function run() {
661
673
  const results = validateTasks(tasks, validationContext, config, validationContext.plugins);
662
674
 
663
675
  const minRank = CONFIDENCE_RANK[config.validation && config.validation.minimumConfidence] ?? 0;
664
- const visibleTasks = tasks.filter((task) => (CONFIDENCE_RANK[results[task.id].confidence] ?? 0) >= minRank);
676
+ const hidePlanned = isEnabled(flags['hide-planned']);
677
+ const visibleTasks = tasks.filter((task) => {
678
+ if ((CONFIDENCE_RANK[results[task.id].confidence] ?? 0) < minRank) return false;
679
+ if (hidePlanned && results[task.id].planned) return false;
680
+ return true;
681
+ });
665
682
 
666
683
  if (isEnabled(flags.json)) {
667
684
  const payload = visibleTasks.map((task) => ({ task, result: results[task.id] }));
@@ -672,7 +689,7 @@ async function run() {
672
689
  });
673
690
  }
674
691
 
675
- const failed = visibleTasks.some((task) => !results[task.id].passed);
692
+ const failed = visibleTasks.some((task) => !results[task.id].passed && !results[task.id].planned);
676
693
  if (failed) {
677
694
  process.exitCode = 1;
678
695
  }
@@ -681,6 +698,7 @@ async function run() {
681
698
 
682
699
  if (effectiveCommand === 'status' || effectiveCommand === 'doctor') {
683
700
  const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
701
+ const skipVscode = shouldSkipVscode(projectRoot, flags);
684
702
  let ok = true;
685
703
  const jsonMode = isEnabled(flags.json);
686
704
  const log = jsonMode ? () => {} : console.log;
@@ -718,7 +736,7 @@ async function run() {
718
736
  let hostStatus = null;
719
737
  if (config) {
720
738
  try {
721
- hostStatus = inspectHostSetup(projectRoot, { roadmapFile, agentsFile });
739
+ hostStatus = inspectHostSetup(projectRoot, { roadmapFile, agentsFile, skipVscode });
722
740
  } catch (error) {
723
741
  logError(`[fail] Host integration error: ${error.message}`);
724
742
  ok = false;
@@ -733,35 +751,37 @@ async function run() {
733
751
  ok = false;
734
752
  }
735
753
 
736
- if (hostStatus.vscode.launcher.exists) {
737
- log(`[ok] VS Code launcher found: ${hostStatus.vscode.launcher.path}`);
738
- } else {
739
- logError(`[fail] VS Code launcher missing: ${hostStatus.vscode.launcher.path}`);
740
- ok = false;
741
- }
742
-
743
- if (hostStatus.vscode.wrappers.ready) {
744
- log(`[ok] VS Code task wrappers ready: ${hostStatus.vscode.wrappers.presentCount}/${hostStatus.vscode.wrappers.expectedCount} files`);
745
- } else {
746
- logError(`[fail] VS Code task wrappers incomplete: missing ${hostStatus.vscode.wrappers.missingPaths.join(', ') || 'wrapper files'}`);
747
- ok = false;
748
- }
749
-
750
- if (hostStatus.vscode.tasks.ready) {
751
- log(`[ok] VS Code tasks ready: ${hostStatus.vscode.tasks.presentLabels.length}/${hostStatus.vscode.tasks.expectedLabels.length} tasks`);
752
- } else {
753
- logError(`[fail] VS Code tasks incomplete: missing ${hostStatus.vscode.tasks.missingLabels.join(', ') || 'managed labels'}`);
754
- ok = false;
755
- }
756
- if (Array.isArray(hostStatus.vscode.tasks.missingAdvancedLabels) && hostStatus.vscode.tasks.missingAdvancedLabels.length > 0) {
757
- log(`[warn] Advanced VS Code tasks missing: ${hostStatus.vscode.tasks.missingAdvancedLabels.join(', ')}`);
758
- }
759
-
760
- if (hostStatus.runtime.ready) {
761
- log(`[ok] Node runtime: ${hostStatus.runtime.kind}${hostStatus.runtime.path ? ` (${hostStatus.runtime.path})` : ''}`);
762
- } else {
763
- logError('[fail] Node runtime missing for VS Code task execution');
764
- ok = false;
754
+ if (!skipVscode) {
755
+ if (hostStatus.vscode.launcher.exists) {
756
+ log(`[ok] VS Code launcher found: ${hostStatus.vscode.launcher.path}`);
757
+ } else {
758
+ logError(`[fail] VS Code launcher missing: ${hostStatus.vscode.launcher.path}`);
759
+ ok = false;
760
+ }
761
+
762
+ if (hostStatus.vscode.wrappers.ready) {
763
+ log(`[ok] VS Code task wrappers ready: ${hostStatus.vscode.wrappers.presentCount}/${hostStatus.vscode.wrappers.expectedCount} files`);
764
+ } else {
765
+ logError(`[fail] VS Code task wrappers incomplete: missing ${hostStatus.vscode.wrappers.missingPaths.join(', ') || 'wrapper files'}`);
766
+ ok = false;
767
+ }
768
+
769
+ if (hostStatus.vscode.tasks.ready) {
770
+ log(`[ok] VS Code tasks ready: ${hostStatus.vscode.tasks.presentLabels.length}/${hostStatus.vscode.tasks.expectedLabels.length} tasks`);
771
+ } else {
772
+ logError(`[fail] VS Code tasks incomplete: missing ${hostStatus.vscode.tasks.missingLabels.join(', ') || 'managed labels'}`);
773
+ ok = false;
774
+ }
775
+ if (Array.isArray(hostStatus.vscode.tasks.missingAdvancedLabels) && hostStatus.vscode.tasks.missingAdvancedLabels.length > 0) {
776
+ log(`[warn] Advanced VS Code tasks missing: ${hostStatus.vscode.tasks.missingAdvancedLabels.join(', ')}`);
777
+ }
778
+
779
+ if (hostStatus.runtime.ready) {
780
+ log(`[ok] Node runtime: ${hostStatus.runtime.kind}${hostStatus.runtime.path ? ` (${hostStatus.runtime.path})` : ''}`);
781
+ } else {
782
+ logError('[fail] Node runtime missing for VS Code task execution');
783
+ ok = false;
784
+ }
765
785
  }
766
786
 
767
787
  if (hostStatus.claude.ready) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.37",
3
+ "version": "0.9.39",
4
4
  "description": "Evidence-backed ROADMAP.md workflows for AI coding agents, with canonical RoadmapSmith status and maintain surfaces plus advanced sync/generate tools and legacy compatibility aliases.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/skills.json CHANGED
@@ -28,67 +28,67 @@
28
28
  "name": "roadmap",
29
29
  "path": "skills/roadmap",
30
30
  "description": "Native slash palette for RoadmapSmith commands and recommended entrypoints across supported hosts.",
31
- "version": "0.9.37"
31
+ "version": "0.9.39"
32
32
  },
33
33
  {
34
34
  "name": "roadmap-zero",
35
35
  "path": "skills/roadmap-zero",
36
36
  "description": "Native slash entrypoint for Zero Mode, including non-interactive config-plus-flag discovery.",
37
- "version": "0.9.37"
37
+ "version": "0.9.39"
38
38
  },
39
39
  {
40
40
  "name": "roadmap-maintain",
41
41
  "path": "skills/roadmap-maintain",
42
42
  "description": "Native slash entrypoint for conservative managed-block maintenance plus sync and audit output.",
43
- "version": "0.9.37"
43
+ "version": "0.9.39"
44
44
  },
45
45
  {
46
46
  "name": "roadmap-status",
47
47
  "path": "skills/roadmap-status",
48
48
  "description": "Native slash readiness check grounded in roadmapsmith status JSON.",
49
- "version": "0.9.37"
49
+ "version": "0.9.39"
50
50
  },
51
51
  {
52
52
  "name": "roadmap-init",
53
53
  "path": "skills/roadmap-init",
54
54
  "description": "Native slash entrypoint for creating ROADMAP.md and AGENTS.md.",
55
- "version": "0.9.37"
55
+ "version": "0.9.39"
56
56
  },
57
57
  {
58
58
  "name": "roadmap-generate",
59
59
  "path": "skills/roadmap-generate",
60
60
  "description": "Native slash entrypoint for managed roadmap updates that require --full-regen before destructive replacement.",
61
- "version": "0.9.37"
61
+ "version": "0.9.39"
62
62
  },
63
63
  {
64
64
  "name": "roadmap-validate",
65
65
  "path": "skills/roadmap-validate",
66
66
  "description": "Native slash entrypoint for evidence-backed roadmap validation.",
67
- "version": "0.9.37"
67
+ "version": "0.9.39"
68
68
  },
69
69
  {
70
70
  "name": "roadmap-update",
71
71
  "path": "skills/roadmap-update",
72
72
  "description": "Native slash entrypoint for evidence-backed inline annotation refresh and verified single-task completion.",
73
- "version": "0.9.37"
73
+ "version": "0.9.39"
74
74
  },
75
75
  {
76
76
  "name": "roadmap-sync",
77
77
  "path": "skills/roadmap-sync",
78
78
  "description": "DEPRECATED legacy compatibility root; use roadmap-maintain or roadmap-update.",
79
- "version": "0.9.37"
79
+ "version": "0.9.39"
80
80
  },
81
81
  {
82
82
  "name": "roadmap-audit",
83
83
  "path": "skills/roadmap-audit",
84
84
  "description": "Native slash entrypoint for the advanced sync-plus-audit mutating summary workflow.",
85
- "version": "0.9.37"
85
+ "version": "0.9.39"
86
86
  },
87
87
  {
88
88
  "name": "roadmap-setup",
89
89
  "path": "skills/roadmap-setup",
90
90
  "description": "Native slash entrypoint for generating RoadmapSmith host integration files.",
91
- "version": "0.9.37"
91
+ "version": "0.9.39"
92
92
  }
93
93
  ]
94
94
  }
@@ -61,10 +61,17 @@ function detectModules(files) {
61
61
  }
62
62
  }
63
63
 
64
+ for (const file of files) {
65
+ const parts = file.split('/');
66
+ if (parts.length === 2 && parts[1] === '__init__.py' && parts[0] && !GENERIC_MODULE_NAMES.has(parts[0])) {
67
+ modules.add(parts[0]);
68
+ }
69
+ }
70
+
64
71
  return Array.from(modules).sort((left, right) => left.localeCompare(right));
65
72
  }
66
73
 
67
- function detectCommands(files) {
74
+ function detectCommands(files, projectRoot) {
68
75
  const commands = new Set();
69
76
  for (const file of files) {
70
77
  if (file.startsWith('bin/')) {
@@ -74,6 +81,44 @@ function detectCommands(files) {
74
81
  commands.add(file.split('/')[1] || file);
75
82
  }
76
83
  }
84
+ if (projectRoot) {
85
+ let pkgContent = null;
86
+ try {
87
+ pkgContent = fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8');
88
+ } catch (err) {
89
+ if (err.code !== 'ENOENT') {
90
+ process.stderr.write(`roadmapsmith: failed to read package.json: ${err.message}\n`);
91
+ }
92
+ }
93
+ if (pkgContent) {
94
+ try {
95
+ const pkg = JSON.parse(pkgContent);
96
+ if (pkg.bin) {
97
+ const binNames = typeof pkg.bin === 'string'
98
+ ? [path.basename(projectRoot)]
99
+ : Object.keys(pkg.bin);
100
+ for (const name of binNames) commands.add(name);
101
+ }
102
+ } catch (err) {
103
+ process.stderr.write(`roadmapsmith: failed to parse package.json: ${err.message}\n`);
104
+ }
105
+ }
106
+ let tomlContent = null;
107
+ try {
108
+ tomlContent = fs.readFileSync(path.join(projectRoot, 'pyproject.toml'), 'utf8');
109
+ } catch (err) {
110
+ if (err.code !== 'ENOENT') {
111
+ process.stderr.write(`roadmapsmith: failed to read pyproject.toml: ${err.message}\n`);
112
+ }
113
+ }
114
+ if (tomlContent) {
115
+ for (const sectionMatch of tomlContent.matchAll(/\[(?:project\.scripts|tool\.poetry\.scripts)\]([\s\S]*?)(?:\n\[|$)/g)) {
116
+ for (const entryMatch of sectionMatch[1].matchAll(/^([\w-]+)\s*=/gm)) {
117
+ commands.add(entryMatch[1]);
118
+ }
119
+ }
120
+ }
121
+ }
77
122
  return Array.from(commands).sort((left, right) => left.localeCompare(right));
78
123
  }
79
124
 
@@ -144,7 +189,7 @@ function scanProject(projectRoot) {
144
189
  const languages = detectLanguages(files);
145
190
  const testFrameworks = detectTestFrameworks(projectRoot, files);
146
191
  const modules = detectModules(files);
147
- const commands = detectCommands(files);
192
+ const commands = detectCommands(files, projectRoot);
148
193
  const todos = collectTodoHints(projectRoot, files);
149
194
  const codeTodos = collectCodeTodoHints(projectRoot, files);
150
195
  const workspaces = detectWorkspaces(projectRoot, files);
@@ -183,6 +228,25 @@ function toCandidate(text, phase, priority, source = 'default') {
183
228
  };
184
229
  }
185
230
 
231
+ function buildDoneCriteriaCandidates(zeroModeConfig) {
232
+ if (!Array.isArray(zeroModeConfig.doneCriteria) || zeroModeConfig.doneCriteria.length === 0) {
233
+ return [];
234
+ }
235
+ return zeroModeConfig.doneCriteria.map((criterion) => {
236
+ const phaseMatch = String(criterion).match(/^\s*\[(P[0-2])\]\s*/i);
237
+ const phase = phaseMatch ? phaseMatch[1].toUpperCase() : 'P0';
238
+ const text = phaseMatch ? criterion.slice(phaseMatch[0].length).trim() : criterion;
239
+ return {
240
+ id: slugify(`zm-${text}`),
241
+ text,
242
+ phase,
243
+ priority: phase,
244
+ checked: false,
245
+ source: 'doneCriteria'
246
+ };
247
+ });
248
+ }
249
+
186
250
  function hasSubstantiveManagedBlock(parsedRoadmap) {
187
251
  if (!parsedRoadmap || !parsedRoadmap.managedRange) {
188
252
  return false;
@@ -203,8 +267,9 @@ function stripTrailingBlankLines(lines) {
203
267
  return next;
204
268
  }
205
269
 
206
- function renderAdditionTask(task) {
207
- return `- [ ] ${task.text} <!-- rs:task=${task.id} -->`;
270
+ function renderAdditionTask(task, planned = false) {
271
+ const flag = planned ? ' planned' : '';
272
+ return `- [ ] ${task.text} <!-- rs:task=${task.id}${flag} -->`;
208
273
  }
209
274
 
210
275
  function isGenericPreserveModeCandidate(candidate) {
@@ -215,6 +280,7 @@ function buildManagedAdditionsLines(tasks, options = {}) {
215
280
  const groups = groupByPhase(tasks);
216
281
  const lines = [];
217
282
  const includeSectionHeading = options.includeSectionHeading !== false;
283
+ const plannedById = options.plannedById || {};
218
284
 
219
285
  if (includeSectionHeading) {
220
286
  lines.push(`## ${ADDITIONS_SECTION_TITLE}`);
@@ -227,7 +293,7 @@ function buildManagedAdditionsLines(tasks, options = {}) {
227
293
  }
228
294
  lines.push(`### Phase ${phase}`);
229
295
  for (const task of groups[phase]) {
230
- lines.push(renderAdditionTask(task));
296
+ lines.push(renderAdditionTask(task, Boolean(plannedById[task.id])));
231
297
  }
232
298
  lines.push('');
233
299
  }
@@ -400,12 +466,13 @@ function findPhaseSectionRange(lines, managedRange, phase) {
400
466
  };
401
467
  }
402
468
 
403
- function buildPreserveModeInsertions(parsedRoadmap, tasks) {
469
+ function buildPreserveModeInsertions(parsedRoadmap, tasks, plannedById = {}) {
404
470
  const managedTasks = sortTasksByPhaseAndText(tasksInManagedBlock(parsedRoadmap));
405
471
  const groups = groupByPhase(tasks);
406
472
  const lines = parsedRoadmap.lines;
407
473
  const insertions = [];
408
474
  const fallbackTasks = [];
475
+ const renderTask = (t) => renderAdditionTask(t, Boolean(plannedById[t.id]));
409
476
 
410
477
  for (const phase of PHASE_ORDER) {
411
478
  const phaseTasks = sortTasksByPhaseAndText(groups[phase] || []);
@@ -423,7 +490,7 @@ function buildPreserveModeInsertions(parsedRoadmap, tasks) {
423
490
  }, null);
424
491
  insertions.push({
425
492
  index: anchor.lastChildLineIndex + 1,
426
- lines: phaseTasks.map(renderAdditionTask)
493
+ lines: phaseTasks.map(renderTask)
427
494
  });
428
495
  continue;
429
496
  }
@@ -436,7 +503,7 @@ function buildPreserveModeInsertions(parsedRoadmap, tasks) {
436
503
  }
437
504
  insertions.push({
438
505
  index: insertionIndex,
439
- lines: phaseTasks.map(renderAdditionTask)
506
+ lines: phaseTasks.map(renderTask)
440
507
  });
441
508
  continue;
442
509
  }
@@ -445,7 +512,7 @@ function buildPreserveModeInsertions(parsedRoadmap, tasks) {
445
512
  }
446
513
 
447
514
  if (fallbackTasks.length > 0) {
448
- const fallbackLines = buildManagedAdditionsLines(fallbackTasks, { includeSectionHeading: true });
515
+ const fallbackLines = buildManagedAdditionsLines(fallbackTasks, { includeSectionHeading: true, plannedById });
449
516
  insertions.push({
450
517
  index: parsedRoadmap.managedRange.end,
451
518
  lines: ['', ...fallbackLines]
@@ -455,13 +522,13 @@ function buildPreserveModeInsertions(parsedRoadmap, tasks) {
455
522
  return insertions.sort((left, right) => right.index - left.index);
456
523
  }
457
524
 
458
- function insertPreserveModeTasks(existingContent, parsedRoadmap, tasks) {
525
+ function insertPreserveModeTasks(existingContent, parsedRoadmap, tasks, plannedById = {}) {
459
526
  if (!parsedRoadmap || !parsedRoadmap.managedRange || tasks.length === 0) {
460
527
  return existingContent;
461
528
  }
462
529
 
463
530
  const nextLines = parsedRoadmap.lines.slice();
464
- const insertions = buildPreserveModeInsertions(parsedRoadmap, tasks);
531
+ const insertions = buildPreserveModeInsertions(parsedRoadmap, tasks, plannedById);
465
532
  for (const insertion of insertions) {
466
533
  nextLines.splice(insertion.index, 0, ...insertion.lines);
467
534
  }
@@ -637,7 +704,7 @@ function buildSteps(phases, config) {
637
704
  }));
638
705
  }
639
706
 
640
- function createModel(scan, tasks, config, customSections, checkedById) {
707
+ function createModel(scan, tasks, config, customSections, checkedById, plannedById = {}) {
641
708
  const phases = groupByPhase(tasks);
642
709
 
643
710
  const implemented = [
@@ -728,7 +795,9 @@ function createModel(scan, tasks, config, customSections, checkedById) {
728
795
  successCriteria,
729
796
  customSections,
730
797
  customPhases: config.customPhases || [],
731
- checkedById
798
+ moduleMetadata: config.moduleMetadata || {},
799
+ checkedById,
800
+ plannedById
732
801
  });
733
802
  }
734
803
 
@@ -757,8 +826,10 @@ function generateRoadmapDocument(options) {
757
826
  const scan = scanProject(projectRoot);
758
827
  const existing = parseRoadmap(existingContent);
759
828
  const existingCheckedById = {};
829
+ const existingPlannedById = {};
760
830
  for (const task of existing.tasks) {
761
831
  existingCheckedById[task.id] = task.checked;
832
+ if (task.planned) existingPlannedById[task.id] = true;
762
833
  }
763
834
  const existingManagedTasks = tasksInManagedBlock(existing);
764
835
 
@@ -814,7 +885,14 @@ function generateRoadmapDocument(options) {
814
885
 
815
886
  const baseCandidates = buildDefaultCandidates(scan, config);
816
887
  const matcherCandidates = applyTaskMatchers(scan, config);
817
- const allCandidates = dedupeTasks([...baseCandidates, ...matcherCandidates, ...pluginTaskCandidates]);
888
+ const doneCriteriaCandidates = buildDoneCriteriaCandidates(zeroModeConfig);
889
+ const allCandidates = dedupeTasks([...doneCriteriaCandidates, ...baseCandidates, ...matcherCandidates, ...pluginTaskCandidates]);
890
+
891
+ const plannedById = { ...existingPlannedById };
892
+ for (const candidate of doneCriteriaCandidates) {
893
+ const isNew = !findBestTaskMatch(candidate, existingManagedTasks, { allowFuzzy: true });
894
+ if (isNew) plannedById[candidate.id] = true;
895
+ }
818
896
 
819
897
  if (hasSubstantiveManagedBlock(existing) && preserveManagedBlock && !forceFullRegenerate) {
820
898
  const unmatchedCandidates = allCandidates.filter((candidate) => {
@@ -829,7 +907,7 @@ function generateRoadmapDocument(options) {
829
907
  return existingContent;
830
908
  }
831
909
 
832
- return insertPreserveModeTasks(existingContent, existing, preserveModeCandidates);
910
+ return insertPreserveModeTasks(existingContent, existing, preserveModeCandidates, plannedById);
833
911
  }
834
912
 
835
913
  if (hasSubstantiveManagedBlock(existing) && !forceFullRegenerate) {
@@ -840,7 +918,7 @@ function generateRoadmapDocument(options) {
840
918
  allowFuzzy: true,
841
919
  includeUnmatchedExisting: false
842
920
  });
843
- const model = createModel(scan, merged, config, [profileSection, ...generatedZeroModeSection, ...configSections, ...pluginSections], existingCheckedById);
921
+ const model = createModel(scan, merged, config, [profileSection, ...generatedZeroModeSection, ...configSections, ...pluginSections], existingCheckedById, plannedById);
844
922
  const profile = config.roadmapProfile || 'compact';
845
923
  const managedBody = renderBody(model, profile);
846
924
 
package/src/host.js CHANGED
@@ -1318,7 +1318,10 @@ function inspectHostSetup(projectRoot, options = {}) {
1318
1318
  const agentsFile = options.agentsFile;
1319
1319
  const runtime = detectNodeRuntime(options.env || process.env);
1320
1320
  const cli = detectCliResolution(projectRoot, { currentCliPath: options.currentCliPath });
1321
- const vscode = inspectVsCodeTasks(projectRoot);
1321
+ const skipVscode = Boolean(options.skipVscode);
1322
+ const vscode = skipVscode
1323
+ ? { skipped: true, launcher: { path: null, exists: false }, wrappers: { expectedCount: 2, presentCount: 0, ready: false, windows: { path: null, exists: false }, posix: { path: null, exists: false }, missingPaths: [] }, tasks: { path: null, exists: false, ready: false, expectedLabels: [], advancedLabels: [], presentLabels: [], missingLabels: [], missingAdvancedLabels: [] } }
1324
+ : inspectVsCodeTasks(projectRoot);
1322
1325
  const claude = inspectClaudeSetup(projectRoot);
1323
1326
  const bundle = inspectSharedBundleSurface();
1324
1327
  const codexNative = inspectCodexPluginState(projectRoot, options);
package/src/model.js CHANGED
@@ -23,7 +23,8 @@ function createRoadmapModel(input) {
23
23
  successCriteria: input.successCriteria || [],
24
24
  customSections: input.customSections || [],
25
25
  customPhases: input.customPhases || [],
26
- checkedById: input.checkedById || {}
26
+ checkedById: input.checkedById || {},
27
+ plannedById: input.plannedById || {}
27
28
  };
28
29
  }
29
30
 
@@ -147,6 +147,7 @@ function parseRoadmap(content) {
147
147
 
148
148
  const { indent, checked, text, markerId, markerFlags } = taskLine;
149
149
  const noTest = /\brs:no-test\b/i.test(markerFlags);
150
+ const isPlanned = /\bplanned\b/i.test(markerFlags);
150
151
  const kindMatch = markerFlags.match(/\brs:kind=(\S+)/i);
151
152
  const taskKind = kindMatch ? kindMatch[1].toLowerCase() : null;
152
153
  const verifiedByMatch = markerFlags.match(/\brs:verified-by=(\S+)/i);
@@ -247,6 +248,7 @@ function parseRoadmap(content) {
247
248
  blockedByIds,
248
249
  markerId,
249
250
  noTest,
251
+ planned: isPlanned,
250
252
  kind: taskKind,
251
253
  verifiedBy: taskVerifiedBy,
252
254
  indent,
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { slugify, ensureTrailingNewline } = require('../utils');
4
- const { taskLine, checkedState } = require('./helpers');
4
+ const { taskLine, checkedState, plannedState } = require('./helpers');
5
5
 
6
6
  function renderCompact(model) {
7
7
  const lines = [];
@@ -22,17 +22,17 @@ function renderCompact(model) {
22
22
  lines.push('');
23
23
  lines.push('### Phase P0 (Critical)');
24
24
  for (const task of model.phases.P0) {
25
- lines.push(taskLine(task));
25
+ lines.push(taskLine(task, plannedState(model, task.id)));
26
26
  }
27
27
  lines.push('');
28
28
  lines.push('### Phase P1 (Important)');
29
29
  for (const task of model.phases.P1) {
30
- lines.push(taskLine(task));
30
+ lines.push(taskLine(task, plannedState(model, task.id)));
31
31
  }
32
32
  lines.push('');
33
33
  lines.push('### Phase P2 (Optimization)');
34
34
  for (const task of model.phases.P2) {
35
- lines.push(taskLine(task));
35
+ lines.push(taskLine(task, plannedState(model, task.id)));
36
36
  }
37
37
  lines.push('');
38
38
 
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
- function taskLine(task) {
4
- return `- [${task.checked ? 'x' : ' '}] ${task.text} <!-- rs:task=${task.id} -->`;
3
+ function taskLine(task, planned = false) {
4
+ const flag = planned ? ' planned' : '';
5
+ return `- [${task.checked ? 'x' : ' '}] ${task.text} <!-- rs:task=${task.id}${flag} -->`;
5
6
  }
6
7
 
7
8
  function sectionHeader(n, title) {
@@ -12,8 +13,12 @@ function checkedState(model, id) {
12
13
  return Boolean(model.checkedById && model.checkedById[id]);
13
14
  }
14
15
 
16
+ function plannedState(model, id) {
17
+ return Boolean(model.plannedById && model.plannedById[id]);
18
+ }
19
+
15
20
  function priorityLabel(priority) {
16
21
  return priority ? `\`[${priority}]\`` : '';
17
22
  }
18
23
 
19
- module.exports = { taskLine, sectionHeader, checkedState, priorityLabel };
24
+ module.exports = { taskLine, sectionHeader, checkedState, plannedState, priorityLabel };
@@ -1,14 +1,15 @@
1
1
  'use strict';
2
2
 
3
3
  const { slugify, ensureTrailingNewline } = require('../utils');
4
- const { sectionHeader, checkedState, priorityLabel } = require('./helpers');
4
+ const { sectionHeader, checkedState, plannedState, priorityLabel } = require('./helpers');
5
5
 
6
6
  function taskLineWithPriority(task, model) {
7
7
  const pri = task.priority ? `${priorityLabel(task.priority)} ` : '';
8
8
  const id = task.id || `prof-task-${slugify(task.text || String(task))}`;
9
9
  const text = task.text || String(task);
10
10
  const checked = task.checked || checkedState(model, id);
11
- return `- [${checked ? 'x' : ' '}] ${pri}${text} <!-- rs:task=${id} -->`;
11
+ const plannedFlag = plannedState(model, id) ? ' planned' : '';
12
+ return `- [${checked ? 'x' : ' '}] ${pri}${text} <!-- rs:task=${id}${plannedFlag} -->`;
12
13
  }
13
14
 
14
15
  function exitLine(item, phN, stN, model) {
@@ -203,70 +204,6 @@ function renderSection5Milestones(model, lines) {
203
204
  }
204
205
  }
205
206
 
206
- const MODULE_METADATA = {
207
- generator: {
208
- state: 'Compact and professional profiles supported; Phase→Step→Task model implemented.',
209
- tasks: [
210
- { text: 'Improve Phase→Step→Task model inference quality', priority: 'P0', id: 'prof-mat-generator-improve-phase-step-task-inference' },
211
- { text: 'Add scan-driven task suggestions per detected module', priority: 'P1', id: 'prof-mat-generator-scan-driven-task-suggestions' }
212
- ]
213
- },
214
- parser: {
215
- state: 'Parses managed blocks, rs:task IDs, and checked state.',
216
- tasks: [
217
- { text: 'Add parser validation for Phase→Step hierarchy markers', priority: 'P1', id: 'prof-mat-parser-phase-hierarchy-validation' },
218
- { text: 'Improve section boundary detection for professional format', priority: 'P1', id: 'prof-mat-parser-professional-section-detection' }
219
- ]
220
- },
221
- renderer: {
222
- state: 'Dispatcher supports compact, professional, and enterprise (error) profiles.',
223
- tasks: [
224
- { text: 'Add snapshot regression fixtures for compact and professional', priority: 'P0', id: 'prof-mat-renderer-snapshot-regression-fixtures' },
225
- { text: 'Harden priority label rendering for edge cases', priority: 'P1', id: 'prof-mat-renderer-priority-label-edge-cases' }
226
- ]
227
- },
228
- validator: {
229
- state: 'Evidence-based validation against file, symbol, and test presence.',
230
- tasks: [
231
- { text: 'Extend validator to verify Phase→Step→Task IDs survive sync', priority: 'P1', id: 'prof-mat-validator-phase-step-task-id-sync' },
232
- { text: 'Add validation coverage for professional profile task IDs', priority: 'P1', id: 'prof-mat-validator-professional-task-id-coverage' }
233
- ]
234
- },
235
- match: {
236
- state: 'Task similarity matching with edit-distance threshold.',
237
- tasks: [
238
- { text: 'Tune similarity threshold to reduce false-positive merges', priority: 'P0', id: 'prof-mat-match-tune-similarity-threshold' }
239
- ]
240
- },
241
- sync: {
242
- state: 'Applies validation outcomes to ROADMAP.md and can append warning lines for failed attempts.',
243
- tasks: [
244
- { text: 'Define explicit contract for sync, sync --audit, and future promote-only flows', priority: 'P0', id: 'prof-mat-sync-define-command-contract' },
245
- { text: 'Separate mutating sync behavior from future read-only audit mode', priority: 'P0', id: 'prof-mat-sync-separate-mutation-from-read-only-audit' },
246
- { text: 'Expose weak-evidence, documentation-only, and structural-mismatch findings in audit output', priority: 'P1', id: 'prof-mat-sync-expose-rich-audit-findings' },
247
- { text: 'Claude PostToolUse hook must invoke the CLI without relying on bare "node" in PATH', priority: 'P0', id: 'prof-mat-sync-claude-hook-avoid-bare-node-path' },
248
- { text: 'Claude PostToolUse hook must fail visibly when sync execution fails', priority: 'P0', id: 'prof-mat-sync-claude-hook-fail-visibly-on-sync-error' },
249
- { text: 'Claude PostToolUse hook must keep lock-file cleanup on both success and failure', priority: 'P1', id: 'prof-mat-sync-claude-hook-cleanup-lockfile-on-both-paths' },
250
- { text: 'Differentiate write-time hook sync from commit-time pre-commit sync in the command contract', priority: 'P1', id: 'prof-mat-sync-differentiate-write-time-and-pre-commit-sync' }
251
- ]
252
- },
253
- config: {
254
- state: 'Supports roadmapProfile, product block, milestones, phaseTemplates, plugins.',
255
- tasks: [
256
- { text: 'Add JSON schema validation for roadmap-skill.config.json', priority: 'P1', id: 'prof-mat-config-json-schema-validation' },
257
- { text: 'Add init --professional or init --with-config bootstrap flow', priority: 'P0', id: 'prof-mat-config-add-init-with-config-bootstrap-flow' },
258
- { text: 'Honor versioned roadmap config instead of regenerating from defaults', priority: 'P1', id: 'prof-mat-config-honor-versioned-config-before-defaults' },
259
- { text: 'Define manual-to-managed migration flow and drift warnings between skill and CLI guidance', priority: 'P1', id: 'prof-mat-config-define-manual-to-managed-migration-and-drift-warnings' }
260
- ]
261
- },
262
- io: {
263
- state: 'Scans files, detects languages, test frameworks, commands, modules.',
264
- tasks: [
265
- { text: 'Improve module detection for monorepo workspace layouts', priority: 'P2', id: 'prof-mat-io-monorepo-workspace-detection' }
266
- ]
267
- }
268
- };
269
-
270
207
  function renderSection6MaturityPath(model, lines) {
271
208
  lines.push(sectionHeader(6, 'Command-by-Command / Module-by-Module Maturity Path'));
272
209
  lines.push('');
@@ -275,31 +212,40 @@ function renderSection6MaturityPath(model, lines) {
275
212
 
276
213
  if (allAreas.length === 0) {
277
214
  const id = 'prof-mat-identify-boundaries';
278
- lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] \`[P1]\` Identify command/module boundaries for the next increment <!-- rs:task=${id} -->`);
215
+ const implSummary = (model.currentState && model.currentState.implementedSummary) || '';
216
+ const hasDetectedFiles = /^[1-9]/.test(implSummary);
217
+ const taskText = hasDetectedFiles
218
+ ? `Define module boundaries (scanner detected files but no top-level structure)`
219
+ : `Identify command/module boundaries for the next increment`;
220
+ lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] \`[P1]\` ${taskText} <!-- rs:task=${id} -->`);
279
221
  lines.push('');
280
222
  return;
281
223
  }
282
224
 
225
+ const moduleMetadata = (model.moduleMetadata && typeof model.moduleMetadata === 'object') ? model.moduleMetadata : {};
226
+
283
227
  for (const area of allAreas) {
284
228
  const rawName = area.replace(/^(Module:|Command:)\s*/i, '').trim();
285
- const meta = MODULE_METADATA[rawName.toLowerCase()];
229
+ const meta = moduleMetadata[rawName.toLowerCase()];
286
230
  const displayName = rawName;
287
231
 
288
232
  lines.push(`### ${displayName}`);
289
233
  lines.push('');
290
- if (meta) {
234
+ if (meta && typeof meta === 'object') {
291
235
  lines.push(`**Current state:** ${meta.state}`);
292
236
  lines.push('');
293
- for (const task of meta.tasks) {
237
+ for (const task of (Array.isArray(meta.tasks) ? meta.tasks : [])) {
294
238
  lines.push(`- [${checkedState(model, task.id) ? 'x' : ' '}] ${priorityLabel(task.priority)} ${task.text} <!-- rs:task=${task.id} -->`);
295
239
  }
296
240
  } else {
297
241
  const isCommand = /^Command:/i.test(area);
298
242
  const kind = isCommand ? 'command' : 'module';
299
- const nextId = `prof-mat-${slugify(rawName)}-define-maturity-criteria`;
243
+ const docId = `prof-mat-${slugify(rawName)}-document-api`;
244
+ const testId = `prof-mat-${slugify(rawName)}-add-test-coverage`;
300
245
  lines.push(`**Current state:** ${kind} detected in scan.`);
301
246
  lines.push('');
302
- lines.push(`- [${checkedState(model, nextId) ? 'x' : ' '}] \`[P1]\` Define maturity criteria and testability gates for ${displayName} <!-- rs:task=${nextId} -->`);
247
+ lines.push(`- [${checkedState(model, docId) ? 'x' : ' '}] \`[P1]\` Document ${displayName} public API <!-- rs:task=${docId} -->`);
248
+ lines.push(`- [${checkedState(model, testId) ? 'x' : ' '}] \`[P1]\` Add test coverage for ${displayName} <!-- rs:task=${testId} -->`);
303
249
  }
304
250
  lines.push('');
305
251
  }
package/src/utils.js CHANGED
@@ -15,8 +15,7 @@ function slugify(text) {
15
15
  return String(text || '')
16
16
  .toLowerCase()
17
17
  .replace(/[^a-z0-9]+/g, '-')
18
- .replace(/^-+|-+$/g, '')
19
- .replace(/-{2,}/g, '-') || 'task';
18
+ .replace(/^-|-$/g, '') || 'task';
20
19
  }
21
20
 
22
21
  function normalizeText(text) {
@@ -450,11 +450,7 @@ function normalizePathCandidateToken(rawToken) {
450
450
  if (!stripped) {
451
451
  return '';
452
452
  }
453
- const normalized = stripped.replace(/\\/g, '/');
454
- if (/^~\//.test(normalized)) {
455
- return normalized;
456
- }
457
- return normalized.replace(/^~(?=\/)/, '~');
453
+ return stripped.replace(/\\/g, '/');
458
454
  }
459
455
 
460
456
  function isExternalPathToken(token) {
@@ -1860,6 +1856,22 @@ function buildDiscoveredEvidenceLine(evidence) {
1860
1856
  }
1861
1857
 
1862
1858
  function validateTask(task, context, config, plugins) {
1859
+ if (task.planned) {
1860
+ return {
1861
+ passed: true,
1862
+ planned: true,
1863
+ confidence: 'low',
1864
+ attempted: false,
1865
+ reasons: [],
1866
+ evidence: { code: false, test: false, artifact: false, files: [], codeFiles: [], testFiles: [], weakPathFiles: [], weakPathContentTokens: [], artifactFiles: [], heuristicArtifacts: [], symbols: [], structuralEvidence: null, authoritative: false, authoritativeFiles: [], authoritativeSummaries: [] },
1867
+ diagnostics: [],
1868
+ verificationRecipe: null,
1869
+ staleEvidenceDetected: false,
1870
+ staleEvidenceResolved: false,
1871
+ generatedTestEvidence: null
1872
+ };
1873
+ }
1874
+
1863
1875
  if (task.verifiedBy === 'human') {
1864
1876
  const hasEvidenceLine = Array.isArray(task.evidenceLines) &&
1865
1877
  task.evidenceLines.some((e) => e.text && e.text.trim().length > 0);