roadmapsmith 0.9.16 → 0.9.22

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.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: roadmap-validate
3
+ description: Inspect evidence-backed roadmap validation through the RoadmapSmith CLI.
4
+ ---
5
+
6
+ # RoadmapSmith Validate
7
+
8
+ Use this command when the user wants per-task evidence status without mutating the roadmap.
9
+
10
+ ## Required behavior
11
+
12
+ 1. Prefer the local engine inside this repository:
13
+ - `node roadmap-skill/bin/cli.js validate --json --project-root .`
14
+ - on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js validate --json --project-root .` if `node` is not in PATH
15
+ 2. Otherwise prefer `roadmapsmith validate --json --project-root .`.
16
+ 3. Treat this command as CLI-backed and non-mutating.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: roadmap-zero
3
+ description: Run the one-command Zero Mode workflow through the RoadmapSmith CLI.
4
+ ---
5
+
6
+ # RoadmapSmith Zero
7
+
8
+ Use this command when the repository is empty or low-context and the user needs the discovery interview plus first roadmap generation.
9
+
10
+ ## Required behavior
11
+
12
+ 1. Prefer the local engine inside this repository:
13
+ - `node roadmap-skill/bin/cli.js zero --project-root .`
14
+ - on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js zero --project-root .` if `node` is not in PATH
15
+ 2. Otherwise prefer `roadmapsmith zero --project-root .`.
16
+ 3. Treat this command as CLI-backed and interactive.
17
+ 4. If the CLI is missing, explain the install path instead of improvising the workflow manually.
package/skills.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "roadmapsmith",
3
+ "description": "One-command roadmap generation and maintenance for coding agents, with a shared native slash bundle for Claude and Codex plus the RoadmapSmith CLI.",
4
+ "triggers": [
5
+ "roadmap",
6
+ "sync roadmap",
7
+ "validate task completion",
8
+ "project milestones",
9
+ "agent roadmap"
10
+ ],
11
+ "usageExamples": [
12
+ "npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
13
+ "npx skills add PapiScholz/roadmapsmith --skill roadmap-sync",
14
+ "roadmapsmith setup",
15
+ "roadmapsmith zero",
16
+ "roadmapsmith maintain",
17
+ "roadmapsmith /roadmap",
18
+ "roadmapsmith /roadmap-update",
19
+ "roadmapsmith validate --json"
20
+ ],
21
+ "install": {
22
+ "command": "npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
23
+ "source": "PapiScholz/roadmapsmith",
24
+ "skill": "*",
25
+ "notes": "Recommended Claude Code install path for native GUI slash commands like /roadmap, /roadmap-zero, /roadmap-maintain, /roadmap-status, /roadmap-init, /roadmap-generate, /roadmap-validate, /roadmap-update, /roadmap-audit, and /roadmap-setup. The legacy /roadmap-sync root remains available for compatibility, especially as /roadmap-sync <action>. Install the roadmapsmith CLI separately for actual command execution, then run /reload-skills and, if applicable, /reload-plugins. Codex native plugin installs use the repo/package .codex-plugin surface instead of this Claude-specific skills CLI path."
26
+ },
27
+ "skills": [
28
+ {
29
+ "name": "roadmap",
30
+ "path": "skills/roadmap",
31
+ "description": "Native slash palette for RoadmapSmith commands and recommended entrypoints across supported hosts.",
32
+ "version": "0.9.22"
33
+ },
34
+ {
35
+ "name": "roadmap-zero",
36
+ "path": "skills/roadmap-zero",
37
+ "description": "Native slash entrypoint for the one-command Zero Mode CLI workflow.",
38
+ "version": "0.9.22"
39
+ },
40
+ {
41
+ "name": "roadmap-maintain",
42
+ "path": "skills/roadmap-maintain",
43
+ "description": "Native slash entrypoint for the preserve-first generate + sync + audit flow.",
44
+ "version": "0.9.22"
45
+ },
46
+ {
47
+ "name": "roadmap-status",
48
+ "path": "skills/roadmap-status",
49
+ "description": "Native slash readiness check grounded in roadmapsmith doctor JSON.",
50
+ "version": "0.9.22"
51
+ },
52
+ {
53
+ "name": "roadmap-init",
54
+ "path": "skills/roadmap-init",
55
+ "description": "Native slash entrypoint for creating ROADMAP.md and AGENTS.md.",
56
+ "version": "0.9.22"
57
+ },
58
+ {
59
+ "name": "roadmap-generate",
60
+ "path": "skills/roadmap-generate",
61
+ "description": "Native slash entrypoint for managed roadmap updates that require --full-regen before destructive replacement.",
62
+ "version": "0.9.22"
63
+ },
64
+ {
65
+ "name": "roadmap-validate",
66
+ "path": "skills/roadmap-validate",
67
+ "description": "Native slash entrypoint for evidence-backed roadmap validation.",
68
+ "version": "0.9.22"
69
+ },
70
+ {
71
+ "name": "roadmap-update",
72
+ "path": "skills/roadmap-update",
73
+ "description": "Native slash entrypoint for applying evidence-backed checklist sync.",
74
+ "version": "0.9.22"
75
+ },
76
+ {
77
+ "name": "roadmap-sync",
78
+ "path": "skills/roadmap-sync",
79
+ "description": "Legacy namespaced root plus policy guidance for RoadmapSmith slash workflows.",
80
+ "version": "0.9.22"
81
+ },
82
+ {
83
+ "name": "roadmap-audit",
84
+ "path": "skills/roadmap-audit",
85
+ "description": "Native slash entrypoint for the current sync-plus-audit workflow.",
86
+ "version": "0.9.22"
87
+ },
88
+ {
89
+ "name": "roadmap-setup",
90
+ "path": "skills/roadmap-setup",
91
+ "description": "Native slash entrypoint for generating RoadmapSmith host integration files.",
92
+ "version": "0.9.22"
93
+ }
94
+ ]
95
+ }
@@ -12,6 +12,14 @@ const WEB_CONFIGS = [
12
12
  ];
13
13
  const STYLE_CONFIGS = ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs'];
14
14
  const WEB_DEPS = new Set(['next', 'react', 'vue', 'svelte', 'astro', 'vite', 'nuxt', 'gatsby', 'remix', '@remix-run/react']);
15
+ const ELECTRON_DEPS = new Set(['electron', 'electron-builder', 'electron-forge', '@electron-forge/cli', 'electron-updater']);
16
+ const ELECTRON_CONFIGS = [
17
+ 'electron-builder.json',
18
+ 'electron-builder.yml',
19
+ 'electron-builder.yaml',
20
+ 'forge.config.js',
21
+ 'forge.config.ts'
22
+ ];
15
23
  const LANDING_ROUTE_RE = /(?:^|\/)(?:contact|services|about|pricing|hero|cta|landing)(?:\/|\.)/i;
16
24
 
17
25
  function readPackageDeps(projectRoot) {
@@ -59,9 +67,14 @@ function classifyProject({ projectRoot, files }) {
59
67
 
60
68
  let webScore = 0;
61
69
  let landingScore = 0;
70
+ let electronScore = 0;
62
71
  const deps = readPackageDeps(projectRoot);
63
72
 
64
73
  for (const dep of deps) {
74
+ if (ELECTRON_DEPS.has(dep)) {
75
+ electronScore += 3;
76
+ signals.push(`dependency: ${dep}`);
77
+ }
65
78
  if (WEB_DEPS.has(dep)) {
66
79
  webScore += 2;
67
80
  signals.push(`dependency: ${dep}`);
@@ -82,6 +95,23 @@ function classifyProject({ projectRoot, files }) {
82
95
  }
83
96
  }
84
97
 
98
+ if (hasDir(files, 'electron/')) {
99
+ electronScore += 3;
100
+ signals.push('directory: electron');
101
+ }
102
+
103
+ if (files.some((file) => /^electron\/.+\.(js|ts|cjs|mjs)$/.test(file))) {
104
+ electronScore += 2;
105
+ signals.push('electron main/preload sources');
106
+ }
107
+
108
+ for (const cfg of ELECTRON_CONFIGS) {
109
+ if (hasFilename(files, cfg)) {
110
+ electronScore += 2;
111
+ signals.push(`config: ${cfg}`);
112
+ }
113
+ }
114
+
85
115
  for (const cfg of WEB_CONFIGS) {
86
116
  if (hasFilename(files, cfg)) {
87
117
  webScore += 3;
@@ -122,6 +152,11 @@ function classifyProject({ projectRoot, files }) {
122
152
  return { type: 'npm-package', confidence: 'low', signals };
123
153
  }
124
154
 
155
+ if (electronScore >= 3) {
156
+ const confidence = electronScore >= 6 ? 'high' : 'medium';
157
+ return { type: 'electron-app', confidence, signals };
158
+ }
159
+
125
160
  if (webScore >= 3) {
126
161
  const type = landingScore >= 3 ? 'landing-site' : 'frontend-web';
127
162
  const confidence = webScore >= 7 ? 'high' : 'medium';
@@ -4,8 +4,8 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { walkFiles, detectLanguages, detectTestFrameworks, detectWorkspaces } = require('../io');
6
6
  const { createRoadmapModel, PHASE_ORDER } = require('../model');
7
- const { slugify, ensureTrailingNewline } = require('../utils');
8
- const { parseRoadmap, upsertManagedBlock } = require('../parser');
7
+ const { slugify } = require('../utils');
8
+ const { parseRoadmap, tasksInManagedBlock, upsertManagedBlock } = require('../parser');
9
9
  const { findBestTaskMatch, dedupeTasks } = require('../match');
10
10
  const { collectPluginContributions } = require('../config');
11
11
  const { renderBody } = require('../renderer');
@@ -13,6 +13,8 @@ const { classifyProject } = require('../classifier');
13
13
 
14
14
  const IMPL_PATTERN_RE = /[/|]TODO|TODO[|/]|[/|]FIXME|FIXME[|/]/;
15
15
  const COMMENT_TODO_RE = /(?:\/\/|#|\*\s*).*\b(?:TODO|FIXME)\b/;
16
+ const ADDITIONS_SECTION_TITLE = 'RoadmapSmith Additions';
17
+ const PHASE_LABEL_RE = /`?\[(P[0-2])\]`?/i;
16
18
 
17
19
  function isTodoMarker(line) {
18
20
  return COMMENT_TODO_RE.test(line) && !IMPL_PATTERN_RE.test(line);
@@ -181,6 +183,54 @@ function toCandidate(text, phase, priority, source = 'default') {
181
183
  };
182
184
  }
183
185
 
186
+ function hasSubstantiveManagedBlock(parsedRoadmap) {
187
+ if (!parsedRoadmap || !parsedRoadmap.managedRange) {
188
+ return false;
189
+ }
190
+
191
+ const managedLines = parsedRoadmap.lines.slice(
192
+ parsedRoadmap.managedRange.start + 1,
193
+ parsedRoadmap.managedRange.end
194
+ );
195
+ return managedLines.some((line) => line.trim().length > 0);
196
+ }
197
+
198
+ function stripTrailingBlankLines(lines) {
199
+ const next = Array.isArray(lines) ? lines.slice() : [];
200
+ while (next.length > 0 && !next[next.length - 1].trim()) {
201
+ next.pop();
202
+ }
203
+ return next;
204
+ }
205
+
206
+ function renderAdditionTask(task) {
207
+ return `- [ ] ${task.text} <!-- rs:task=${task.id} -->`;
208
+ }
209
+
210
+ function buildManagedAdditionsLines(tasks, options = {}) {
211
+ const groups = groupByPhase(tasks);
212
+ const lines = [];
213
+ const includeSectionHeading = options.includeSectionHeading !== false;
214
+
215
+ if (includeSectionHeading) {
216
+ lines.push(`## ${ADDITIONS_SECTION_TITLE}`);
217
+ lines.push('');
218
+ }
219
+
220
+ for (const phase of PHASE_ORDER) {
221
+ if (!groups[phase] || groups[phase].length === 0) {
222
+ continue;
223
+ }
224
+ lines.push(`### Phase ${phase}`);
225
+ for (const task of groups[phase]) {
226
+ lines.push(renderAdditionTask(task));
227
+ }
228
+ lines.push('');
229
+ }
230
+
231
+ return stripTrailingBlankLines(lines);
232
+ }
233
+
184
234
  const WEB_CANDIDATES_COMMON = [
185
235
  { text: 'Add SEO metadata: title, description, and canonical URL for all pages', phase: 'P0' },
186
236
  { text: 'Implement responsive and mobile-first layout across all breakpoints', phase: 'P0' },
@@ -285,6 +335,9 @@ function applyTaskMatchers(scan, config) {
285
335
  }
286
336
 
287
337
  function inferPhase(existingTask) {
338
+ const textPhaseMatch = String(existingTask.text || '').match(PHASE_LABEL_RE);
339
+ if (textPhaseMatch) return textPhaseMatch[1].toUpperCase();
340
+
288
341
  const section = String(existingTask.section || '').toUpperCase();
289
342
  if (section.includes('P0')) return 'P0';
290
343
  if (section.includes('P1')) return 'P1';
@@ -292,12 +345,134 @@ function inferPhase(existingTask) {
292
345
  return 'P1';
293
346
  }
294
347
 
295
- function mergeWithExisting(candidates, existingTasks) {
348
+ function sortTasksByPhaseAndText(tasks) {
349
+ return tasks.slice().sort((left, right) => {
350
+ const leftPhaseIndex = PHASE_ORDER.indexOf(left.phase);
351
+ const rightPhaseIndex = PHASE_ORDER.indexOf(right.phase);
352
+ if (leftPhaseIndex !== rightPhaseIndex) {
353
+ return leftPhaseIndex - rightPhaseIndex;
354
+ }
355
+ return left.text.localeCompare(right.text);
356
+ });
357
+ }
358
+
359
+ function findPhaseSectionRange(lines, managedRange, phase) {
360
+ const headingPattern = /^(#{2,4})\s+(.*)$/;
361
+ const phasePattern = new RegExp(`\\b${phase}\\b`, 'i');
362
+ let sectionStart = -1;
363
+ let sectionLevel = 0;
364
+
365
+ for (let index = managedRange.start + 1; index < managedRange.end; index += 1) {
366
+ const match = lines[index].trim().match(headingPattern);
367
+ if (!match) {
368
+ continue;
369
+ }
370
+ if (!phasePattern.test(match[2])) {
371
+ continue;
372
+ }
373
+ sectionStart = index;
374
+ sectionLevel = match[1].length;
375
+ }
376
+
377
+ if (sectionStart < 0) {
378
+ return null;
379
+ }
380
+
381
+ let sectionEnd = managedRange.end;
382
+ for (let index = sectionStart + 1; index < managedRange.end; index += 1) {
383
+ const match = lines[index].trim().match(headingPattern);
384
+ if (!match) {
385
+ continue;
386
+ }
387
+ if (match[1].length <= sectionLevel) {
388
+ sectionEnd = index;
389
+ break;
390
+ }
391
+ }
392
+
393
+ return {
394
+ start: sectionStart,
395
+ end: sectionEnd
396
+ };
397
+ }
398
+
399
+ function buildPreserveModeInsertions(parsedRoadmap, tasks) {
400
+ const managedTasks = sortTasksByPhaseAndText(tasksInManagedBlock(parsedRoadmap));
401
+ const groups = groupByPhase(tasks);
402
+ const lines = parsedRoadmap.lines;
403
+ const insertions = [];
404
+ const fallbackTasks = [];
405
+
406
+ for (const phase of PHASE_ORDER) {
407
+ const phaseTasks = sortTasksByPhaseAndText(groups[phase] || []);
408
+ if (phaseTasks.length === 0) {
409
+ continue;
410
+ }
411
+
412
+ const samePhaseTasks = managedTasks.filter((task) => inferPhase(task) === phase);
413
+ if (samePhaseTasks.length > 0) {
414
+ const anchor = samePhaseTasks.reduce((latest, task) => {
415
+ if (!latest || task.lastChildLineIndex > latest.lastChildLineIndex) {
416
+ return task;
417
+ }
418
+ return latest;
419
+ }, null);
420
+ insertions.push({
421
+ index: anchor.lastChildLineIndex + 1,
422
+ lines: phaseTasks.map(renderAdditionTask)
423
+ });
424
+ continue;
425
+ }
426
+
427
+ const phaseSection = findPhaseSectionRange(lines, parsedRoadmap.managedRange, phase);
428
+ if (phaseSection) {
429
+ let insertionIndex = phaseSection.end;
430
+ while (insertionIndex > phaseSection.start + 1 && !lines[insertionIndex - 1].trim()) {
431
+ insertionIndex -= 1;
432
+ }
433
+ insertions.push({
434
+ index: insertionIndex,
435
+ lines: phaseTasks.map(renderAdditionTask)
436
+ });
437
+ continue;
438
+ }
439
+
440
+ fallbackTasks.push(...phaseTasks);
441
+ }
442
+
443
+ if (fallbackTasks.length > 0) {
444
+ const fallbackLines = buildManagedAdditionsLines(fallbackTasks, { includeSectionHeading: true });
445
+ insertions.push({
446
+ index: parsedRoadmap.managedRange.end,
447
+ lines: ['', ...fallbackLines]
448
+ });
449
+ }
450
+
451
+ return insertions.sort((left, right) => right.index - left.index);
452
+ }
453
+
454
+ function insertPreserveModeTasks(existingContent, parsedRoadmap, tasks) {
455
+ if (!parsedRoadmap || !parsedRoadmap.managedRange || tasks.length === 0) {
456
+ return existingContent;
457
+ }
458
+
459
+ const nextLines = parsedRoadmap.lines.slice();
460
+ const insertions = buildPreserveModeInsertions(parsedRoadmap, tasks);
461
+ for (const insertion of insertions) {
462
+ nextLines.splice(insertion.index, 0, ...insertion.lines);
463
+ }
464
+
465
+ return nextLines.join('\n');
466
+ }
467
+
468
+ function mergeWithExisting(candidates, existingTasks, options = {}) {
296
469
  const matchedExistingIds = new Set();
297
470
  const merged = [];
298
471
 
299
472
  for (const candidate of candidates) {
300
- const match = findBestTaskMatch(candidate, existingTasks);
473
+ const match = findBestTaskMatch(candidate, existingTasks, {
474
+ allowFuzzy: options.allowFuzzy !== false
475
+ });
301
476
  if (match) {
302
477
  matchedExistingIds.add(match.task.id);
303
478
  merged.push({
@@ -311,20 +486,22 @@ function mergeWithExisting(candidates, existingTasks) {
311
486
  merged.push(candidate);
312
487
  }
313
488
 
314
- for (const existing of existingTasks) {
315
- if (matchedExistingIds.has(existing.id)) {
316
- continue;
317
- }
489
+ if (options.includeUnmatchedExisting) {
490
+ for (const existing of existingTasks) {
491
+ if (matchedExistingIds.has(existing.id)) {
492
+ continue;
493
+ }
318
494
 
319
- const phase = inferPhase(existing);
320
- merged.push({
321
- id: existing.id,
322
- text: existing.text,
323
- phase,
324
- priority: phase,
325
- checked: existing.checked,
326
- source: 'existing'
327
- });
495
+ const phase = inferPhase(existing);
496
+ merged.push({
497
+ id: existing.id,
498
+ text: existing.text,
499
+ phase,
500
+ priority: phase,
501
+ checked: existing.checked,
502
+ source: 'existing'
503
+ });
504
+ }
328
505
  }
329
506
 
330
507
  return dedupeTasks(merged);
@@ -566,6 +743,8 @@ function generateRoadmapDocument(options) {
566
743
  const zeroModeConfig = config.zeroMode || {};
567
744
  const plugins = options.plugins || [];
568
745
  const existingContent = options.existingContent || '';
746
+ const preserveManagedBlock = options.preserveManagedBlock === true;
747
+ const forceFullRegenerate = options.forceFullRegenerate === true;
569
748
 
570
749
  const scan = scanProject(projectRoot);
571
750
  const existing = parseRoadmap(existingContent);
@@ -573,7 +752,7 @@ function generateRoadmapDocument(options) {
573
752
  for (const task of existing.tasks) {
574
753
  existingCheckedById[task.id] = task.checked;
575
754
  }
576
- const existingPhaseTasks = existing.tasks.filter((task) => /^Phase P[0-2]/i.test(String(task.section || '')));
755
+ const existingManagedTasks = tasksInManagedBlock(existing);
577
756
 
578
757
  const pluginTaskCandidates = collectPluginContributions(plugins, 'registerTaskDetectors', {
579
758
  projectRoot,
@@ -627,7 +806,28 @@ function generateRoadmapDocument(options) {
627
806
 
628
807
  const baseCandidates = buildDefaultCandidates(scan, config);
629
808
  const matcherCandidates = applyTaskMatchers(scan, config);
630
- const merged = mergeWithExisting([...baseCandidates, ...matcherCandidates, ...pluginTaskCandidates], existingPhaseTasks);
809
+ const allCandidates = dedupeTasks([...baseCandidates, ...matcherCandidates, ...pluginTaskCandidates]);
810
+
811
+ if (hasSubstantiveManagedBlock(existing) && preserveManagedBlock && !forceFullRegenerate) {
812
+ const unmatchedCandidates = allCandidates.filter((candidate) => {
813
+ return !findBestTaskMatch(candidate, existingManagedTasks, { allowFuzzy: false });
814
+ });
815
+
816
+ if (unmatchedCandidates.length === 0) {
817
+ return existingContent;
818
+ }
819
+
820
+ return insertPreserveModeTasks(existingContent, existing, unmatchedCandidates);
821
+ }
822
+
823
+ if (hasSubstantiveManagedBlock(existing) && !forceFullRegenerate) {
824
+ throw new Error('Refusing to regenerate a substantive managed roadmap block. Rerun with --full-regen to replace it explicitly.');
825
+ }
826
+
827
+ const merged = mergeWithExisting(allCandidates, existingManagedTasks, {
828
+ allowFuzzy: true,
829
+ includeUnmatchedExisting: false
830
+ });
631
831
  const model = createModel(scan, merged, config, [profileSection, ...generatedZeroModeSection, ...configSections, ...pluginSections], existingCheckedById);
632
832
  const profile = config.roadmapProfile || 'compact';
633
833
  const managedBody = renderBody(model, profile);