roadmapsmith 0.9.13 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -6,6 +6,9 @@ module.exports = {
6
6
  sync: require('./sync'),
7
7
  validator: require('./validator'),
8
8
  config: require('./config'),
9
+ host: require('./host'),
10
+ slash: require('./slash'),
11
+ zero: require('./zero'),
9
12
  model: require('./model'),
10
13
  renderer: require('./renderer')
11
14
  };
@@ -238,10 +238,25 @@ const MODULE_METADATA = {
238
238
  { text: 'Tune similarity threshold to reduce false-positive merges', priority: 'P0', id: 'prof-mat-match-tune-similarity-threshold' }
239
239
  ]
240
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
+ },
241
253
  config: {
242
254
  state: 'Supports roadmapProfile, product block, milestones, phaseTemplates, plugins.',
243
255
  tasks: [
244
- { text: 'Add JSON schema validation for roadmap-skill.config.json', priority: 'P1', id: 'prof-mat-config-json-schema-validation' }
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' }
245
260
  ]
246
261
  },
247
262
  io: {
@@ -298,7 +313,12 @@ function renderSection7OutputContract(model, lines) {
298
313
  lines.push('');
299
314
  const formatItems = [
300
315
  { text: 'Define stable public output format (stdout, files, exit codes)', priority: 'P0' },
301
- { text: 'Version output format alongside package version', priority: 'P1' }
316
+ { text: 'Version output format alongside package version', priority: 'P1' },
317
+ { text: 'Define explicit contract for sync, sync --audit, and future promote-only flows', priority: 'P0' },
318
+ { text: 'Document current gap: sync --audit is not yet a dedicated read-only audit command', priority: 'P1' },
319
+ { text: 'Add machine-readable audit output (JSON)', priority: 'P1' },
320
+ { text: 'Add audit summary-only output mode', priority: 'P1' },
321
+ { text: 'Define explicit exit-code semantics for sync and audit commands', priority: 'P0' }
302
322
  ];
303
323
  for (const item of formatItems) {
304
324
  const id = `prof-out-${slugify(item.text)}`;
@@ -310,7 +330,9 @@ function renderSection7OutputContract(model, lines) {
310
330
  lines.push('');
311
331
  const breakingItems = [
312
332
  { text: 'Document breaking vs. non-breaking output changes', priority: 'P1' },
313
- { text: 'Add output schema validation to CI', priority: 'P1' }
333
+ { text: 'Add output schema validation to CI', priority: 'P1' },
334
+ { text: 'Separate mutating sync behavior from future read-only audit mode', priority: 'P0' },
335
+ { text: 'Expose weak-evidence, documentation-only, and structural-mismatch findings in audit output', priority: 'P1' }
314
336
  ];
315
337
  for (const item of breakingItems) {
316
338
  const id = `prof-out-${slugify(item.text)}`;
@@ -329,7 +351,13 @@ function renderSection8Testing(model, lines) {
329
351
  { text: 'Unit test coverage for all core modules', priority: 'P0' },
330
352
  { text: 'Integration tests covering the full generate → sync → validate pipeline', priority: 'P0' },
331
353
  { text: 'Regression fixtures for compact and professional profile output', priority: 'P1' },
332
- { text: 'Edge case coverage: empty repo, no config, large monorepo scan', priority: 'P1' }
354
+ { text: 'Edge case coverage: empty repo, no config, large monorepo scan', priority: 'P1' },
355
+ { text: 'Add direct tests for .claude/hooks/roadmap-sync.js payload parsing', priority: 'P1' },
356
+ { text: 'Add direct tests for ROADMAP.md self-edit skip behavior', priority: 'P1' },
357
+ { text: 'Add direct tests for lock-file reentry guard', priority: 'P1' },
358
+ { text: 'Add direct tests for sync failure surfacing when the child process cannot be spawned', priority: 'P0' },
359
+ { text: 'Add regression coverage for environments where node is not available on PATH', priority: 'P0' },
360
+ { text: 'Add integration coverage for pre-commit sync using the absolute Node path', priority: 'P1' }
333
361
  ];
334
362
  for (const item of coverageItems) {
335
363
  const id = `prof-test-${slugify(item.text)}`;
@@ -389,7 +417,18 @@ function renderSection10Documentation(model, lines) {
389
417
  const coreItems = [
390
418
  { text: 'README.md covers install, commands, and profile selection', priority: 'P0' },
391
419
  { text: 'SKILL.md reflects current feature set and guardrails', priority: 'P0' },
392
- { text: 'CHANGELOG.md maintained for each release', priority: 'P1' }
420
+ { text: 'CHANGELOG.md maintained for each release', priority: 'P1' },
421
+ { text: 'README.md documents current sync --audit semantics without claiming read-only behavior', priority: 'P0' },
422
+ { text: 'README.md includes host matrix for Claude Code, Codex/Codex CLI, CI, and manual workflows', priority: 'P1' },
423
+ { text: 'Document distinction between supported Claude hooks and manual workflows on other hosts', priority: 'P1' },
424
+ { text: 'Document Codex/Codex CLI manual fallback workflow', priority: 'P1' },
425
+ { text: 'Document Windows shell caveats: roadmapsmith.cmd, npm.cmd, and PowerShell policy differences', priority: 'P1' },
426
+ { text: 'Skill instructions require extending existing phases before adding new ones', priority: 'P1' },
427
+ { text: 'Document that Claude write-time autoupdate currently depends on Node resolution in the hook environment', priority: 'P1' },
428
+ { text: 'Document the difference between the Claude PostToolUse hook and the git pre-commit hook', priority: 'P1' },
429
+ { text: 'Document current autoupdate reliability boundaries: write-time hook is best-effort, pre-commit is stricter', priority: 'P1' },
430
+ { text: 'Document troubleshooting for hook failure when node is missing from PATH', priority: 'P1' },
431
+ { text: 'Document that Codex/Codex CLI remains manual and does not share the Claude repo-local hook path', priority: 'P1' }
393
432
  ];
394
433
  for (const item of coreItems) {
395
434
  const id = `prof-doc-${slugify(item.text)}`;
package/src/slash.js ADDED
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ const SLASH_ACTIONS = [
4
+ {
5
+ id: 'zero',
6
+ description: 'Interview the developer in terminal and generate the first roadmap for an empty or low-context repo.',
7
+ classicCliExample: 'roadmapsmith zero',
8
+ slashExamples: ['/zero', '/road zero', '/roadmap-sync zero'],
9
+ taskLabel: 'RoadmapSmith: Zero Mode'
10
+ },
11
+ {
12
+ id: 'maintain',
13
+ description: 'Regenerate, sync, and audit the roadmap for an existing repository.',
14
+ classicCliExample: 'roadmapsmith maintain',
15
+ slashExamples: ['/maintain', '/road maintain', '/roadmap-sync maintain'],
16
+ taskLabel: 'RoadmapSmith: Maintain'
17
+ },
18
+ {
19
+ id: 'status',
20
+ description: 'Inspect CLI, roadmap, VS Code task, and Claude hook readiness.',
21
+ classicCliExample: 'roadmapsmith doctor --json',
22
+ slashExamples: ['/status', '/road status', '/roadmap-sync status'],
23
+ taskLabel: 'RoadmapSmith: Status'
24
+ },
25
+ {
26
+ id: 'init',
27
+ description: 'Create ROADMAP.md and AGENTS.md when they are missing.',
28
+ classicCliExample: 'roadmapsmith init',
29
+ slashExamples: ['/init', '/road init', '/roadmap-sync init'],
30
+ taskLabel: 'RoadmapSmith: Init'
31
+ },
32
+ {
33
+ id: 'generate',
34
+ description: 'Rebuild the managed roadmap block from repository context.',
35
+ classicCliExample: 'roadmapsmith generate --project-root .',
36
+ slashExamples: ['/generate', '/road generate', '/roadmap-sync generate'],
37
+ taskLabel: 'RoadmapSmith: Generate'
38
+ },
39
+ {
40
+ id: 'validate',
41
+ description: 'Inspect per-task evidence status as JSON.',
42
+ classicCliExample: 'roadmapsmith validate --json --project-root .',
43
+ slashExamples: ['/validate', '/road validate', '/roadmap-sync validate'],
44
+ taskLabel: 'RoadmapSmith: Validate'
45
+ },
46
+ {
47
+ id: 'sync',
48
+ description: 'Apply evidence-backed checklist sync to ROADMAP.md.',
49
+ classicCliExample: 'roadmapsmith sync --project-root .',
50
+ slashExamples: ['/sync', '/road sync', '/roadmap-sync sync'],
51
+ taskLabel: 'RoadmapSmith: Sync'
52
+ },
53
+ {
54
+ id: 'audit',
55
+ description: 'Run sync and print the post-sync mismatch summary.',
56
+ classicCliExample: 'roadmapsmith sync --audit --project-root .',
57
+ slashExamples: ['/audit', '/road audit', '/roadmap-sync audit'],
58
+ taskLabel: 'RoadmapSmith: Sync Audit'
59
+ },
60
+ {
61
+ id: 'setup',
62
+ description: 'Generate visible VS Code tasks and optional Claude hook wiring.',
63
+ classicCliExample: 'roadmapsmith setup',
64
+ slashExamples: ['/setup', '/road setup', '/roadmap-sync setup'],
65
+ taskLabel: 'RoadmapSmith: Refresh Setup'
66
+ }
67
+ ];
68
+
69
+ const SLASH_ROOT_ALIASES = new Set(['/road', '/roadmap-sync']);
70
+
71
+ const DIRECT_SLASH_ALIAS_TO_ACTION = Object.freeze({
72
+ '/zero': 'zero',
73
+ '/maintain': 'maintain',
74
+ '/status': 'status',
75
+ '/init': 'init',
76
+ '/generate': 'generate',
77
+ '/validate': 'validate',
78
+ '/sync': 'sync',
79
+ '/audit': 'audit',
80
+ '/setup': 'setup'
81
+ });
82
+
83
+ function normalizeActionId(value) {
84
+ return String(value || '').trim().toLowerCase().replace(/^\/+/, '');
85
+ }
86
+
87
+ function isSlashToken(value) {
88
+ return typeof value === 'string' && value.trim().startsWith('/');
89
+ }
90
+
91
+ function getSlashAction(actionId) {
92
+ const normalized = normalizeActionId(actionId);
93
+ return SLASH_ACTIONS.find((action) => action.id === normalized) || null;
94
+ }
95
+
96
+ function getSlashActionSpecs() {
97
+ return SLASH_ACTIONS.map((action) => ({ ...action }));
98
+ }
99
+
100
+ function getSlashSuggestions(query) {
101
+ const normalized = normalizeActionId(query);
102
+ if (!normalized) {
103
+ return getSlashActionSpecs();
104
+ }
105
+
106
+ const startsWithMatches = SLASH_ACTIONS.filter((action) => action.id.startsWith(normalized));
107
+ const containsMatches = SLASH_ACTIONS.filter((action) => {
108
+ return !action.id.startsWith(normalized) && action.id.includes(normalized);
109
+ });
110
+
111
+ return [...startsWithMatches, ...containsMatches].map((action) => ({ ...action }));
112
+ }
113
+
114
+ function resolveSlashInvocation(command, args = []) {
115
+ if (!isSlashToken(command)) {
116
+ return null;
117
+ }
118
+
119
+ const normalizedCommand = String(command).trim().toLowerCase();
120
+
121
+ if (Object.prototype.hasOwnProperty.call(DIRECT_SLASH_ALIAS_TO_ACTION, normalizedCommand)) {
122
+ return {
123
+ kind: 'execute',
124
+ actionId: DIRECT_SLASH_ALIAS_TO_ACTION[normalizedCommand],
125
+ query: normalizeActionId(normalizedCommand),
126
+ source: normalizedCommand,
127
+ suggestions: getSlashSuggestions(normalizedCommand)
128
+ };
129
+ }
130
+
131
+ if (SLASH_ROOT_ALIASES.has(normalizedCommand)) {
132
+ const queryToken = args.length > 0 ? normalizeActionId(args[0]) : '';
133
+ if (!queryToken) {
134
+ return {
135
+ kind: 'palette',
136
+ query: '',
137
+ source: normalizedCommand,
138
+ suggestions: getSlashSuggestions('')
139
+ };
140
+ }
141
+
142
+ const exactAction = getSlashAction(queryToken);
143
+ if (exactAction) {
144
+ return {
145
+ kind: 'execute',
146
+ actionId: exactAction.id,
147
+ query: queryToken,
148
+ source: normalizedCommand,
149
+ suggestions: getSlashSuggestions(queryToken)
150
+ };
151
+ }
152
+
153
+ return {
154
+ kind: 'palette',
155
+ query: queryToken,
156
+ source: normalizedCommand,
157
+ suggestions: getSlashSuggestions(queryToken)
158
+ };
159
+ }
160
+
161
+ return {
162
+ kind: 'palette',
163
+ query: normalizeActionId(normalizedCommand),
164
+ source: normalizedCommand,
165
+ suggestions: getSlashSuggestions(normalizedCommand)
166
+ };
167
+ }
168
+
169
+ function renderSlashPalette(options = {}) {
170
+ const source = options.source || '/road';
171
+ const query = normalizeActionId(options.query);
172
+ const suggestions = Array.isArray(options.suggestions) ? options.suggestions : getSlashSuggestions(query);
173
+ const lines = [];
174
+
175
+ lines.push('RoadmapSmith slash palette');
176
+ lines.push('');
177
+
178
+ if (query) {
179
+ lines.push(`Input: ${source} ${query}`);
180
+ if (suggestions.length > 0) {
181
+ lines.push('No exact slash match was executed. Related actions:');
182
+ } else {
183
+ lines.push('No exact slash match was executed.');
184
+ }
185
+ } else {
186
+ lines.push(`Entry point: ${source}`);
187
+ lines.push('Use an exact slash action to execute work. Incomplete or ambiguous input only shows suggestions.');
188
+ }
189
+
190
+ lines.push('');
191
+
192
+ if (suggestions.length === 0) {
193
+ lines.push('No related slash actions found.');
194
+ } else {
195
+ suggestions.forEach((action) => {
196
+ lines.push(`- /${action.id}: ${action.description}`);
197
+ lines.push(` Classic CLI: ${action.classicCliExample}`);
198
+ lines.push(` Skill form: /roadmap-sync ${action.id}`);
199
+ lines.push(` VS Code task: ${action.taskLabel}`);
200
+ });
201
+ }
202
+
203
+ lines.push('');
204
+ lines.push('Examples:');
205
+ lines.push('- roadmapsmith zero');
206
+ lines.push('- roadmapsmith maintain');
207
+ lines.push('- roadmapsmith /road');
208
+ lines.push('- roadmapsmith /maintain');
209
+ lines.push('- roadmapsmith /roadmap-sync maintain');
210
+ lines.push('');
211
+ lines.push('Installing the skill alone does not expose CLI behavior in VS Code. Use roadmapsmith setup for the visible task/launcher layer.');
212
+
213
+ return lines.join('\n');
214
+ }
215
+
216
+ module.exports = {
217
+ DIRECT_SLASH_ALIAS_TO_ACTION,
218
+ SLASH_ROOT_ALIASES,
219
+ getSlashAction,
220
+ getSlashActionSpecs,
221
+ getSlashSuggestions,
222
+ isSlashToken,
223
+ normalizeActionId,
224
+ renderSlashPalette,
225
+ resolveSlashInvocation
226
+ };
package/src/zero.js ADDED
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const ZERO_MODE_QUESTIONS = [
6
+ { id: 'productName', prompt: '1. What product are we building?' },
7
+ { id: 'primaryUser', prompt: '2. Who is the target user?' },
8
+ { id: 'problemStatement', prompt: '3. What problem does it solve?' },
9
+ { id: 'targetOutcome', prompt: '4. What is the desired v1.0 outcome?' },
10
+ { id: 'antiGoals', prompt: '5. What is explicitly out of scope? Separate multiple items with ;' },
11
+ { id: 'preferredStack', prompt: '6. What stack do you prefer, if any?' },
12
+ { id: 'constraints', prompt: '7. What constraints exist? Separate multiple items with ;' },
13
+ { id: 'doneCriteria', prompt: '8. What does "done" mean for the first usable version? Separate multiple items with ;' }
14
+ ];
15
+
16
+ function splitListAnswer(value) {
17
+ return String(value || '')
18
+ .split(/(?:\r?\n|;)+/)
19
+ .map((item) => item.trim())
20
+ .filter(Boolean);
21
+ }
22
+
23
+ function joinListDefault(values) {
24
+ return Array.isArray(values) && values.length > 0 ? values.join('; ') : '';
25
+ }
26
+
27
+ function deriveDefaultProblemStatement(config) {
28
+ const explicit = config.zeroMode && config.zeroMode.problemStatement;
29
+ if (explicit) {
30
+ return explicit;
31
+ }
32
+ const positioning = String((config.product && config.product.positioning) || '').trim();
33
+ const prefix = 'Core problem: ';
34
+ if (positioning.startsWith(prefix)) {
35
+ return positioning.slice(prefix.length).trim();
36
+ }
37
+ return '';
38
+ }
39
+
40
+ function buildZeroModeDefaults(projectRoot, config) {
41
+ return {
42
+ productName: (config.product && config.product.name) || path.basename(projectRoot),
43
+ primaryUser: (config.product && config.product.primaryUser) || '',
44
+ problemStatement: deriveDefaultProblemStatement(config),
45
+ targetOutcome: (config.product && config.product.targetOutcome) || '',
46
+ antiGoals: joinListDefault(config.product && config.product.antiGoals),
47
+ preferredStack: (config.zeroMode && config.zeroMode.preferredStack) || '',
48
+ constraints: joinListDefault(config.zeroMode && config.zeroMode.constraints),
49
+ doneCriteria: joinListDefault(
50
+ (config.zeroMode && config.zeroMode.doneCriteria && config.zeroMode.doneCriteria.length > 0)
51
+ ? config.zeroMode.doneCriteria
52
+ : (config.product && config.product.successCriteria)
53
+ )
54
+ };
55
+ }
56
+
57
+ async function collectZeroModeAnswers(ask, defaults = {}) {
58
+ const answers = {};
59
+ for (const question of ZERO_MODE_QUESTIONS) {
60
+ const fallback = String(defaults[question.id] || '').trim();
61
+ const suffix = fallback ? ` [${fallback}]` : '';
62
+ const response = await ask(`${question.prompt}${suffix}: `);
63
+ const normalized = String(response || '').trim();
64
+ answers[question.id] = normalized || fallback;
65
+ }
66
+ return answers;
67
+ }
68
+
69
+ function deriveNorthStar(answers) {
70
+ const productName = String(answers.productName || '').trim();
71
+ const primaryUser = String(answers.primaryUser || '').trim();
72
+ const targetOutcome = String(answers.targetOutcome || '').trim();
73
+ if (productName && primaryUser && targetOutcome) {
74
+ return `${productName} helps ${primaryUser} achieve ${targetOutcome}.`;
75
+ }
76
+ if (productName && targetOutcome) {
77
+ return `${productName} exists to deliver ${targetOutcome}.`;
78
+ }
79
+ if (productName) {
80
+ return `Ship the first usable version of ${productName}.`;
81
+ }
82
+ return '';
83
+ }
84
+
85
+ function buildZeroModeConfigPatch(projectRoot, existingUserConfig, answers) {
86
+ const productName = String(answers.productName || '').trim() || path.basename(projectRoot);
87
+ const primaryUser = String(answers.primaryUser || '').trim();
88
+ const problemStatement = String(answers.problemStatement || '').trim();
89
+ const targetOutcome = String(answers.targetOutcome || '').trim();
90
+ const antiGoals = splitListAnswer(answers.antiGoals);
91
+ const constraints = splitListAnswer(answers.constraints);
92
+ const doneCriteria = splitListAnswer(answers.doneCriteria);
93
+ const preferredStack = String(answers.preferredStack || '').trim();
94
+
95
+ return {
96
+ ...existingUserConfig,
97
+ product: {
98
+ ...((existingUserConfig && existingUserConfig.product) || {}),
99
+ name: productName,
100
+ northStar: deriveNorthStar({ productName, primaryUser, targetOutcome }) || (((existingUserConfig || {}).product || {}).northStar || ''),
101
+ positioning: problemStatement ? `Core problem: ${problemStatement}` : ((((existingUserConfig || {}).product || {}).positioning) || ''),
102
+ primaryUser,
103
+ targetOutcome,
104
+ antiGoals,
105
+ risks: constraints.map((constraint) => `Constraint: ${constraint}`),
106
+ successCriteria: doneCriteria
107
+ },
108
+ zeroMode: {
109
+ ...((existingUserConfig && existingUserConfig.zeroMode) || {}),
110
+ problemStatement,
111
+ preferredStack,
112
+ constraints,
113
+ doneCriteria
114
+ }
115
+ };
116
+ }
117
+
118
+ function isInteractiveTerminal(input = process.stdin, output = process.stdout) {
119
+ return Boolean(input && input.isTTY && output && output.isTTY);
120
+ }
121
+
122
+ module.exports = {
123
+ ZERO_MODE_QUESTIONS,
124
+ buildZeroModeConfigPatch,
125
+ buildZeroModeDefaults,
126
+ collectZeroModeAnswers,
127
+ isInteractiveTerminal,
128
+ splitListAnswer
129
+ };