roadmapsmith 0.9.28 → 0.9.30

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/src/slash.js CHANGED
@@ -3,65 +3,78 @@
3
3
  const SLASH_ACTIONS = [
4
4
  {
5
5
  id: 'zero',
6
+ tier: 'canonical',
6
7
  description: 'Interview the developer in terminal and generate the first roadmap for an empty or low-context repo.',
7
8
  classicCliExample: 'roadmapsmith zero',
8
9
  taskLabel: 'RoadmapSmith: Zero Mode'
9
10
  },
10
11
  {
11
12
  id: 'maintain',
13
+ tier: 'canonical',
12
14
  description: 'Preserve-first existing-repo flow: update, sync, and audit the roadmap without rebuilding substantive domain content.',
13
15
  classicCliExample: 'roadmapsmith maintain',
14
16
  taskLabel: 'RoadmapSmith: Maintain'
15
17
  },
16
18
  {
17
19
  id: 'status',
20
+ tier: 'canonical',
18
21
  description: 'Inspect CLI, roadmap, VS Code task, Codex, and Claude readiness.',
19
22
  classicCliExample: 'roadmapsmith status --json',
20
23
  taskLabel: 'RoadmapSmith: Status'
21
24
  },
25
+ {
26
+ id: 'validate',
27
+ tier: 'canonical',
28
+ description: 'Inspect per-task evidence status as JSON.',
29
+ classicCliExample: 'roadmapsmith validate --json --project-root .',
30
+ taskLabel: 'RoadmapSmith: Validate'
31
+ },
32
+ {
33
+ id: 'update',
34
+ tier: 'canonical',
35
+ aliases: ['sync'],
36
+ description: 'Apply evidence-backed checklist refresh to ROADMAP.md or complete one task with verified evidence.',
37
+ classicCliExample: 'roadmapsmith update --project-root .',
38
+ taskLabel: 'RoadmapSmith: Update'
39
+ },
40
+ {
41
+ id: 'setup',
42
+ tier: 'canonical',
43
+ description: 'Generate visible VS Code tasks and optional Claude hook wiring.',
44
+ classicCliExample: 'roadmapsmith setup',
45
+ taskLabel: 'RoadmapSmith: Refresh Setup'
46
+ },
22
47
  {
23
48
  id: 'init',
49
+ tier: 'advanced',
24
50
  description: 'Create ROADMAP.md and AGENTS.md when they are missing.',
25
51
  classicCliExample: 'roadmapsmith init',
26
52
  taskLabel: 'RoadmapSmith: Init'
27
53
  },
28
54
  {
29
55
  id: 'generate',
56
+ tier: 'advanced',
30
57
  description: 'Generate or update ROADMAP.md, refusing destructive replacement unless rerun with --full-regen.',
31
58
  classicCliExample: 'roadmapsmith generate --project-root .',
32
59
  taskLabel: 'RoadmapSmith: Generate'
33
60
  },
34
- {
35
- id: 'validate',
36
- description: 'Inspect per-task evidence status as JSON.',
37
- classicCliExample: 'roadmapsmith validate --json --project-root .',
38
- taskLabel: 'RoadmapSmith: Validate'
39
- },
40
- {
41
- id: 'sync',
42
- description: 'Apply evidence-backed checklist sync to ROADMAP.md.',
43
- classicCliExample: 'roadmapsmith sync --project-root .',
44
- taskLabel: 'RoadmapSmith: Sync'
45
- },
46
61
  {
47
62
  id: 'audit',
63
+ tier: 'advanced',
48
64
  description: 'Run sync and print the post-sync mismatch summary.',
49
65
  classicCliExample: 'roadmapsmith sync --audit --project-root .',
50
66
  taskLabel: 'RoadmapSmith: Sync Audit'
51
- },
52
- {
53
- id: 'setup',
54
- description: 'Generate visible VS Code tasks and optional Claude hook wiring.',
55
- classicCliExample: 'roadmapsmith setup',
56
- taskLabel: 'RoadmapSmith: Refresh Setup'
57
67
  }
58
68
  ];
59
69
 
60
70
  const SLASH_ROOT_ALIASES = new Set(['/roadmap', '/road']);
61
71
  const LEGACY_ROUTER_ALIAS = '/roadmap-sync';
72
+ const CANONICAL_ACTION_IDS = Object.freeze(SLASH_ACTIONS.filter((action) => action.tier === 'canonical').map((action) => action.id));
73
+ const ADVANCED_ACTION_IDS = Object.freeze(SLASH_ACTIONS.filter((action) => action.tier === 'advanced').map((action) => action.id));
74
+ const COMPATIBILITY_ACTION_IDS = Object.freeze([]);
62
75
 
63
76
  function getNamespacedDirectSlash(actionId) {
64
- return actionId === 'sync' ? '/roadmap-update' : `/roadmap-${actionId}`;
77
+ return `/roadmap-${actionId}`;
65
78
  }
66
79
 
67
80
  const DIRECT_HOST_NATIVE_ALIAS_TO_ACTION = Object.freeze(
@@ -69,25 +82,64 @@ const DIRECT_HOST_NATIVE_ALIAS_TO_ACTION = Object.freeze(
69
82
  );
70
83
 
71
84
  const DIRECT_DEPRECATED_CLI_ALIAS_TO_ACTION = Object.freeze(
72
- Object.fromEntries(SLASH_ACTIONS.map((action) => [`/${action.id}`, action.id]))
85
+ Object.fromEntries(
86
+ SLASH_ACTIONS.flatMap((action) => {
87
+ const aliases = Array.isArray(action.aliases) ? action.aliases : [];
88
+ return [action.id, ...aliases].map((alias) => [`/${alias}`, action.id]);
89
+ })
90
+ )
73
91
  );
74
92
 
75
- function getHostNativeSkillNames() {
93
+ const ACTION_ALIAS_TO_ID = Object.freeze(
94
+ Object.fromEntries(
95
+ SLASH_ACTIONS.flatMap((action) => {
96
+ const aliases = Array.isArray(action.aliases) ? action.aliases : [];
97
+ return [action.id, ...aliases].map((alias) => [alias, action.id]);
98
+ })
99
+ )
100
+ );
101
+
102
+ function mapActionIdsToSkillNames(actionIds) {
76
103
  return [
77
104
  'roadmap',
78
- 'roadmap-zero',
79
- 'roadmap-maintain',
80
- 'roadmap-status',
81
- 'roadmap-init',
82
- 'roadmap-generate',
83
- 'roadmap-validate',
84
- 'roadmap-update',
85
- 'roadmap-sync',
86
- 'roadmap-audit',
87
- 'roadmap-setup'
105
+ ...actionIds.map((actionId) => getNamespacedDirectSlash(actionId).slice(1))
88
106
  ];
89
107
  }
90
108
 
109
+ function getCanonicalHostNativeSkillNames() {
110
+ return mapActionIdsToSkillNames(CANONICAL_ACTION_IDS);
111
+ }
112
+
113
+ function getAdvancedHostNativeSkillNames() {
114
+ return ADVANCED_ACTION_IDS.map((actionId) => getNamespacedDirectSlash(actionId).slice(1));
115
+ }
116
+
117
+ function getCompatibilityHostNativeSkillNames() {
118
+ return [
119
+ 'roadmap-sync'
120
+ ];
121
+ }
122
+
123
+ function getHostNativeSkillNames() {
124
+ return [
125
+ ...getCanonicalHostNativeSkillNames(),
126
+ ...getAdvancedHostNativeSkillNames(),
127
+ ...getCompatibilityHostNativeSkillNames()
128
+ ];
129
+ }
130
+
131
+ function getCanonicalHostNativeSlashCommands() {
132
+ return getCanonicalHostNativeSkillNames().map((name) => `/${name}`);
133
+ }
134
+
135
+ function getAdvancedHostNativeSlashCommands() {
136
+ return getAdvancedHostNativeSkillNames().map((name) => `/${name}`);
137
+ }
138
+
139
+ function getCompatibilityHostNativeSlashCommands() {
140
+ return getCompatibilityHostNativeSkillNames().map((name) => `/${name}`);
141
+ }
142
+
91
143
  function getHostNativeSlashCommands() {
92
144
  return getHostNativeSkillNames().map((name) => `/${name}`);
93
145
  }
@@ -97,23 +149,29 @@ function normalizeActionId(value) {
97
149
  if (normalized.startsWith('roadmap-')) {
98
150
  normalized = normalized.slice('roadmap-'.length);
99
151
  }
100
- if (normalized === 'update') {
101
- normalized = 'sync';
102
- }
103
152
  return normalized;
104
153
  }
105
154
 
155
+ function canonicalizeActionId(value) {
156
+ const normalized = normalizeActionId(value);
157
+ return ACTION_ALIAS_TO_ID[normalized] || normalized;
158
+ }
159
+
106
160
  function isSlashToken(value) {
107
161
  return typeof value === 'string' && value.trim().startsWith('/');
108
162
  }
109
163
 
110
164
  function getSlashAction(actionId) {
111
- const normalized = normalizeActionId(actionId);
165
+ const normalized = canonicalizeActionId(actionId);
112
166
  return SLASH_ACTIONS.find((action) => action.id === normalized) || null;
113
167
  }
114
168
 
115
169
  function getLegacyRouterSlash(action) {
116
- return `/roadmap-sync ${action.id === 'sync' ? 'update' : action.id}`;
170
+ return `/roadmap-sync ${action.id}`;
171
+ }
172
+
173
+ function actionSearchTerms(action) {
174
+ return [action.id, ...(Array.isArray(action.aliases) ? action.aliases : [])];
117
175
  }
118
176
 
119
177
  function getSlashActionSpecs() {
@@ -127,13 +185,22 @@ function getSlashActionSpecs() {
127
185
 
128
186
  function getSlashSuggestions(query) {
129
187
  const normalized = normalizeActionId(query);
188
+ const visibleActions = SLASH_ACTIONS.filter((action) => action.tier !== 'compatibility');
130
189
  if (!normalized) {
131
- return getSlashActionSpecs();
190
+ return visibleActions.map((action) => ({
191
+ ...action,
192
+ directSlash: getNamespacedDirectSlash(action.id),
193
+ routerSlash: `/roadmap ${action.id}`,
194
+ legacyRouterSlash: getLegacyRouterSlash(action)
195
+ }));
132
196
  }
133
197
 
134
- const startsWithMatches = SLASH_ACTIONS.filter((action) => action.id.startsWith(normalized));
135
- const containsMatches = SLASH_ACTIONS.filter((action) => {
136
- return !action.id.startsWith(normalized) && action.id.includes(normalized);
198
+ const startsWithMatches = visibleActions.filter((action) => {
199
+ return actionSearchTerms(action).some((term) => term.startsWith(normalized));
200
+ });
201
+ const containsMatches = visibleActions.filter((action) => {
202
+ return !actionSearchTerms(action).some((term) => term.startsWith(normalized))
203
+ && actionSearchTerms(action).some((term) => term.includes(normalized));
137
204
  });
138
205
 
139
206
  return [...startsWithMatches, ...containsMatches].map((action) => ({
@@ -175,12 +242,12 @@ function resolveSlashInvocation(command, args = []) {
175
242
  const normalizedCommand = String(command).trim().toLowerCase();
176
243
 
177
244
  if (normalizedCommand === LEGACY_ROUTER_ALIAS) {
245
+ const deprecationMessage = 'Legacy CLI compatibility root /roadmap-sync <action> is deprecated. Use /roadmap <action> or the direct /roadmap-* commands.';
178
246
  if (args.length === 0) {
179
- return paletteResponse(normalizedCommand, '');
247
+ return paletteResponse(normalizedCommand, '', true, deprecationMessage);
180
248
  }
181
249
 
182
250
  const queryToken = normalizeActionId(args[0]);
183
- const deprecationMessage = 'Legacy CLI compatibility root /roadmap-sync <action> is deprecated. Use /roadmap <action> or the direct /roadmap-* commands.';
184
251
  const exactAction = getSlashAction(queryToken);
185
252
  if (exactAction) {
186
253
  return executeResponse(normalizedCommand, exactAction.id, queryToken, true, deprecationMessage);
@@ -268,7 +335,9 @@ function renderSlashPalette(options = {}) {
268
335
  suggestions.forEach((action) => {
269
336
  lines.push(`- ${action.directSlash}: ${action.description}`);
270
337
  lines.push(` Router form: ${action.routerSlash}`);
271
- lines.push(` Legacy router: ${action.legacyRouterSlash}`);
338
+ if (options.deprecated || source === LEGACY_ROUTER_ALIAS) {
339
+ lines.push(` Legacy router: ${action.legacyRouterSlash}`);
340
+ }
272
341
  lines.push(` Classic CLI: ${action.classicCliExample}`);
273
342
  lines.push(` VS Code task: ${action.taskLabel}`);
274
343
  });
@@ -280,7 +349,9 @@ function renderSlashPalette(options = {}) {
280
349
  lines.push('- roadmapsmith /roadmap maintain');
281
350
  lines.push('- roadmapsmith /roadmap-maintain');
282
351
  lines.push('- roadmapsmith /roadmap-update');
283
- lines.push('- roadmapsmith /roadmap-sync validate');
352
+ if (options.deprecated || source === LEGACY_ROUTER_ALIAS) {
353
+ lines.push('- roadmapsmith /roadmap-sync validate');
354
+ }
284
355
  lines.push('');
285
356
  lines.push('Installing the skill alone does not expose CLI behavior in VS Code. Use roadmapsmith setup for the visible task/launcher layer.');
286
357
 
@@ -288,10 +359,20 @@ function renderSlashPalette(options = {}) {
288
359
  }
289
360
 
290
361
  module.exports = {
362
+ ACTION_ALIAS_TO_ID,
291
363
  DIRECT_DEPRECATED_CLI_ALIAS_TO_ACTION,
292
364
  DIRECT_HOST_NATIVE_ALIAS_TO_ACTION,
365
+ ADVANCED_ACTION_IDS,
366
+ CANONICAL_ACTION_IDS,
367
+ COMPATIBILITY_ACTION_IDS,
293
368
  LEGACY_ROUTER_ALIAS,
294
369
  SLASH_ROOT_ALIASES,
370
+ getAdvancedHostNativeSkillNames,
371
+ getAdvancedHostNativeSlashCommands,
372
+ getCanonicalHostNativeSkillNames,
373
+ getCanonicalHostNativeSlashCommands,
374
+ getCompatibilityHostNativeSkillNames,
375
+ getCompatibilityHostNativeSlashCommands,
295
376
  getHostNativeSkillNames,
296
377
  getHostNativeSlashCommands,
297
378
  getNamespacedDirectSlash,
@@ -299,6 +380,7 @@ module.exports = {
299
380
  getSlashActionSpecs,
300
381
  getSlashSuggestions,
301
382
  isSlashToken,
383
+ canonicalizeActionId,
302
384
  normalizeActionId,
303
385
  renderSlashPalette,
304
386
  resolveSlashInvocation
package/src/sync/index.js CHANGED
@@ -112,6 +112,23 @@ function shouldPreserveExistingWarning(existingReason, newReason) {
112
112
  return cleanNew === 'validation failed' && cleanExisting && cleanExisting !== cleanNew;
113
113
  }
114
114
 
115
+ function formatVerificationRecipe(indent, recipe) {
116
+ return `${indent} - Verification recipe: ${recipe}`;
117
+ }
118
+
119
+ function formatTestEvidence(indent, evidence) {
120
+ return `${indent} - Test evidence: ${evidence}`;
121
+ }
122
+
123
+ function findVerificationRecipeIndex(lines, taskLineIndex) {
124
+ for (let index = taskLineIndex + 1; index < lines.length; index += 1) {
125
+ const line = lines[index];
126
+ if (!line.trim() || /^\s*#{2,}\s/.test(line) || /^\s*- \[[ xX]\]\s/.test(line)) break;
127
+ if (/^\s*- Verification recipe:/i.test(line)) return index;
128
+ }
129
+ return null;
130
+ }
131
+
115
132
  function applySync(content, parsedTasks, results) {
116
133
  const parsed = parseRoadmap(content);
117
134
  const lines = [...parsed.lines];
@@ -142,6 +159,19 @@ function applySync(content, parsedTasks, results) {
142
159
  lines.splice(warningIndex, 1);
143
160
  offset -= 1;
144
161
  }
162
+ const recipeIndex = findVerificationRecipeIndex(lines, lineIndex);
163
+ if (recipeIndex != null && recipeIndex >= 0 && recipeIndex < lines.length) {
164
+ lines.splice(recipeIndex, 1);
165
+ offset -= 1;
166
+ }
167
+ if (
168
+ result.generatedTestEvidence &&
169
+ (!Array.isArray(task.testEvidenceLines) || task.testEvidenceLines.length === 0)
170
+ ) {
171
+ const insertionIndex = Math.max(lineIndex + 1, (task.lastChildLineIndex + offset) + 1);
172
+ lines.splice(insertionIndex, 0, formatTestEvidence(task.indent || '', result.generatedTestEvidence));
173
+ offset += 1;
174
+ }
145
175
  if (
146
176
  result.staleEvidenceResolved &&
147
177
  (!Array.isArray(task.evidenceLines) || task.evidenceLines.length === 0) &&
@@ -154,10 +184,7 @@ function applySync(content, parsedTasks, results) {
154
184
  lines.splice(insertionIndex, 0, `${task.indent || ''} - Evidence: ${result.discoveredEvidence}`);
155
185
  offset += 1;
156
186
  }
157
- continue;
158
- }
159
-
160
- if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
187
+ } else if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
161
188
  const existingReason = normalizeWarningReason(lines[warningIndex]);
162
189
  const newReason = reason || 'validation failed';
163
190
  if (!shouldPreserveExistingWarning(existingReason, newReason)) {
@@ -167,6 +194,23 @@ function applySync(content, parsedTasks, results) {
167
194
  lines.splice(lastChildLineIndex + 1, 0, warningText);
168
195
  offset += 1;
169
196
  }
197
+
198
+ const recipeIndex = findVerificationRecipeIndex(lines, lineIndex);
199
+ if (result.passed) {
200
+ continue;
201
+ }
202
+ if (result.verificationRecipe) {
203
+ const recipeLine = formatVerificationRecipe(task.indent || '', result.verificationRecipe);
204
+ if (recipeIndex != null && recipeIndex >= 0 && recipeIndex < lines.length) {
205
+ lines[recipeIndex] = recipeLine;
206
+ } else {
207
+ lines.splice(lastChildLineIndex + 1, 0, recipeLine);
208
+ offset += 1;
209
+ }
210
+ } else if (recipeIndex != null && recipeIndex >= 0 && recipeIndex < lines.length) {
211
+ lines.splice(recipeIndex, 1);
212
+ offset -= 1;
213
+ }
170
214
  }
171
215
 
172
216
  return ensureTrailingNewline(lines.join('\n'));