roadmapsmith 0.9.33 → 0.9.36

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.33",
3
+ "version": "0.9.36",
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.33",
3
+ "version": "0.9.36",
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/README.md CHANGED
@@ -8,7 +8,7 @@ This package owns the RoadmapSmith CLI, validator, sync engine, host setup files
8
8
 
9
9
  - `roadmapsmith setup`
10
10
  - `roadmapsmith zero`
11
- - `roadmapsmith maintain`
11
+ - `roadmapsmith maintain [--dry-run]`
12
12
  - `roadmapsmith status [--json]`
13
13
  - `roadmapsmith validate [--json] [--strict]`
14
14
  - `roadmapsmith update [--task <stable-id> --evidence "<single-line evidence>"]`
@@ -40,11 +40,39 @@ This package owns the RoadmapSmith CLI, validator, sync engine, host setup files
40
40
 
41
41
  ### Zero Mode
42
42
 
43
- Use for empty or low-context repositories. `roadmapsmith zero` runs the terminal interview, updates config, and generates the first roadmap.
43
+ Use for empty or low-context repositories. `roadmapsmith zero` runs the terminal interview when TTY is available, or consumes a complete brief from config plus flags in non-interactive environments.
44
+
45
+ Non-interactive inputs:
46
+
47
+ - `--product-name`
48
+ - `--primary-user`
49
+ - `--problem-statement`
50
+ - `--target-outcome`
51
+ - `--anti-goal` (repeatable)
52
+ - `--preferred-stack`
53
+ - `--constraint` (repeatable)
54
+ - `--done-criterion` (repeatable)
55
+
56
+ Without TTY, Zero Mode requires a complete brief from config/flags and fails clearly when required fields are missing.
44
57
 
45
58
  ### Sync/Audit Mode
46
59
 
47
- Use for repositories that already contain code, tests, docs, TODOs, or an existing roadmap. `roadmapsmith maintain` is the preserve-first one-command flow.
60
+ Use for repositories that already contain code, tests, docs, TODOs, or an existing roadmap. `roadmapsmith maintain` is the preserve-first one-command flow for an existing managed roadmap block.
61
+
62
+ If `ROADMAP.md` is authored and non-empty but has no `<!-- rs:managed:* -->` block:
63
+
64
+ - `maintain` refuses to seed managed content implicitly
65
+ - `update` is the conservative inline-annotation path
66
+ - `generate` is the explicit managed-section creation path
67
+
68
+ For write-capable surfaces (`maintain`, `generate`, `sync`, `update`), prefer `--dry-run` first.
69
+
70
+ ## Managed Block Ownership
71
+
72
+ - `maintain` owns the managed roadmap block only.
73
+ - `update` can annotate existing task lines even when no managed block exists.
74
+ - `generate` is the explicit path that creates or replaces managed roadmap content.
75
+ - Manually inserting empty `<!-- rs:managed:start -->` markers is no longer required as a workaround.
48
76
 
49
77
  ## Verification Model
50
78
 
package/bin/cli.js CHANGED
@@ -14,14 +14,21 @@ const { generateRoadmapDocument } = require('../src/generator');
14
14
  const { parseRoadmap, tasksInManagedBlock } = require('../src/parser');
15
15
  const { buildValidationContext, validateTasks, auditValidation, CONFIDENCE_RANK, applyMinimumConfidence } = require('../src/validator');
16
16
  const { applySync } = require('../src/sync');
17
- const { buildZeroModeConfigPatch, buildZeroModeDefaults, collectZeroModeAnswers, isInteractiveTerminal } = require('../src/zero');
17
+ const {
18
+ buildZeroModeConfigPatch,
19
+ collectZeroModeAnswers,
20
+ formatMissingZeroModeFields,
21
+ getMissingZeroModeFields,
22
+ isInteractiveTerminal,
23
+ resolveZeroModeAnswers
24
+ } = require('../src/zero');
18
25
 
19
26
  function printHelp() {
20
27
  console.log([
21
28
  'Usage:',
22
29
  ' Canonical commands:',
23
- ' roadmapsmith zero [--project-root <path>] [--config <path>]',
24
- ' roadmapsmith maintain [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--full-regen] [--refresh-annotations]',
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
+ ' roadmapsmith maintain [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--full-regen] [--refresh-annotations]',
25
32
  ' roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]',
26
33
  ' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json] [--strict]',
27
34
  ' roadmapsmith update [--task <stable-id> --evidence <text>] [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run]',
@@ -85,6 +92,15 @@ function printAudit(audit) {
85
92
  console.log(`- [${item.task.id}] ${item.task.text}`);
86
93
  });
87
94
  }
95
+ if (Array.isArray(audit.newlyUnchecked) && audit.newlyUnchecked.length > 0) {
96
+ console.log(`Unchecked by this run (${audit.newlyUnchecked.length}): ${audit.newlyUnchecked.join(', ')}`);
97
+ }
98
+ if (Array.isArray(audit.humanVerifiedTasks) && audit.humanVerifiedTasks.length > 0) {
99
+ console.log(`Human-verified tasks (${audit.humanVerifiedTasks.length}):`);
100
+ audit.humanVerifiedTasks.forEach((item) => {
101
+ console.log(`- [${item.task.id}] ${item.task.text}`);
102
+ });
103
+ }
88
104
  }
89
105
 
90
106
  function printReadinessSummary(summary) {
@@ -140,6 +156,42 @@ function formatSetupVerb(result, dryRun) {
140
156
  return result.before == null ? 'Created' : 'Updated';
141
157
  }
142
158
 
159
+ function emitPreWriteWarning(roadmapFile, options = {}) {
160
+ if (options.dryRun || options.suppressWarning) {
161
+ return;
162
+ }
163
+ if (options.warningState && options.warningState.emitted) {
164
+ return;
165
+ }
166
+
167
+ const commandName = options.commandName || 'roadmapsmith';
168
+ console.log(`Warning: ${commandName} will modify ${roadmapFile}. Review the preview first with --dry-run.`);
169
+ if (options.managedSectionNote) {
170
+ console.log(options.managedSectionNote);
171
+ }
172
+
173
+ if (options.warningState) {
174
+ options.warningState.emitted = true;
175
+ }
176
+ }
177
+
178
+ function assertMaintainCanWrite(roadmapFile, content) {
179
+ if (content == null || String(content).trim().length === 0) {
180
+ return;
181
+ }
182
+
183
+ const parsed = parseRoadmap(content);
184
+ if (parsed.hasManagedBlock) {
185
+ return;
186
+ }
187
+
188
+ throw new Error(
189
+ `Refusing to let roadmapsmith maintain seed a managed section into an authored roadmap without <!-- rs:managed:start --> markers: ${roadmapFile}\n` +
190
+ 'For conservative inline annotations, run `roadmapsmith update --dry-run` then `roadmapsmith update`.\n' +
191
+ 'To intentionally create a managed section, run `roadmapsmith generate --dry-run` then `roadmapsmith generate`.'
192
+ );
193
+ }
194
+
143
195
  function runInitCommand(projectRoot, config, flags) {
144
196
  const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
145
197
  const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
@@ -187,6 +239,13 @@ function runGenerateCommand(projectRoot, config, flags, options = {}) {
187
239
  forceFullRegenerate: options.forceFullRegenerate === true || isEnabled(flags['full-regen'])
188
240
  });
189
241
 
242
+ emitPreWriteWarning(roadmapFile, {
243
+ commandName: options.commandName || 'roadmapsmith generate',
244
+ dryRun,
245
+ suppressWarning: options.suppressPreWriteWarning,
246
+ warningState: options.warningState,
247
+ managedSectionNote: 'This command may create or replace a managed roadmap section depending on the current file state and flags.'
248
+ });
190
249
  const writeResult = writeText(roadmapFile, document, { dryRun });
191
250
  if (dryRun) {
192
251
  if (writeResult.changed) {
@@ -226,9 +285,25 @@ function runSyncCommand(projectRoot, config, flags, options = {}) {
226
285
  const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
227
286
  const results = validateTasks(syncTasks, validationContext, config, validationContext.plugins);
228
287
  applyMinimumConfidence(results, config.validation?.minimumConfidence);
288
+ if (isEnabled(flags.audit)) {
289
+ const audit = auditValidation(syncTasks, results);
290
+ printAudit(audit);
291
+ const hasMismatch = audit.checkedWithoutEvidence.length > 0 || audit.readyButUnchecked.length > 0;
292
+ if (hasMismatch) {
293
+ process.exitCode = 2;
294
+ }
295
+ return;
296
+ }
297
+
229
298
  const forceRefresh = isEnabled(flags['refresh-annotations']);
230
- const next = applySync(content, syncTasks, results, { forceRefresh });
299
+ const { content: next, changes } = applySync(content, syncTasks, results, { forceRefresh });
231
300
  const dryRun = isEnabled(flags['dry-run']);
301
+ emitPreWriteWarning(roadmapFile, {
302
+ commandName: options.commandName || 'roadmapsmith sync',
303
+ dryRun,
304
+ suppressWarning: options.suppressPreWriteWarning,
305
+ warningState: options.warningState
306
+ });
232
307
  const writeResult = writeText(roadmapFile, next, { dryRun });
233
308
 
234
309
  if (dryRun) {
@@ -238,11 +313,17 @@ function runSyncCommand(projectRoot, config, flags, options = {}) {
238
313
  console.log(`No changes for ${roadmapFile}`);
239
314
  }
240
315
  } else {
316
+ if (changes.newlyUnchecked.length > 0) {
317
+ console.log(`Unchecked ${changes.newlyUnchecked.length} task(s): ${changes.newlyUnchecked.join(', ')}`);
318
+ }
319
+ if (changes.newlyChecked.length > 0) {
320
+ console.log(`Checked ${changes.newlyChecked.length} task(s): ${changes.newlyChecked.join(', ')}`);
321
+ }
241
322
  console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
242
323
  }
243
324
 
244
- if (options.audit || isEnabled(flags.audit)) {
245
- const audit = auditValidation(syncTasks, results);
325
+ if (options.audit) {
326
+ const audit = auditValidation(syncTasks, results, changes);
246
327
  printAudit(audit);
247
328
  }
248
329
  }
@@ -266,7 +347,7 @@ function runUpdateCommand(projectRoot, config, flags) {
266
347
  const hasTask = flags.task != null;
267
348
  const hasEvidence = flags.evidence != null;
268
349
  if (!hasTask && !hasEvidence) {
269
- runSyncCommand(projectRoot, config, flags);
350
+ runSyncCommand(projectRoot, config, flags, { commandName: 'roadmapsmith update' });
270
351
  return;
271
352
  }
272
353
  if (!hasTask || !hasEvidence || Array.isArray(flags.task) || Array.isArray(flags.evidence)) {
@@ -298,14 +379,21 @@ function runUpdateCommand(projectRoot, config, flags) {
298
379
  const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
299
380
  const result = validateTasks([draftTask], validationContext, config, validationContext.plugins)[taskId];
300
381
  const errors = (result.diagnostics || []).filter((item) => item.severity === 'error');
382
+ const isEscapeHatch = draftTask.verifiedBy === 'human' || draftTask.kind === 'docs';
301
383
  const suppliedEvidenceResolved = result.evidence.authoritative && result.evidence.authoritativeFiles.length > 0;
302
- if (!suppliedEvidenceResolved || !result.passed || result.confidence !== 'high' || errors.length > 0) {
384
+ const evidenceAccepted = isEscapeHatch ? result.passed : (suppliedEvidenceResolved && result.passed && result.confidence === 'high');
385
+ if (!evidenceAccepted || errors.length > 0) {
303
386
  const reasons = result.reasons.length > 0 ? `: ${result.reasons.join('; ')}` : '';
304
- throw new Error(`Task ${taskId} was not updated; supplied evidence must resolve in the repository and validate at high confidence${reasons}`);
387
+ const hint = isEscapeHatch ? '; escape-hatch task requires an Evidence: child line' : '; supplied evidence must resolve in the repository and validate at high confidence';
388
+ throw new Error(`Task ${taskId} was not updated${hint}${reasons}`);
305
389
  }
306
390
 
307
- const next = applySync(draft, [draftTask], { [taskId]: result });
391
+ const { content: next } = applySync(draft, [draftTask], { [taskId]: result });
308
392
  const dryRun = isEnabled(flags['dry-run']);
393
+ emitPreWriteWarning(roadmapFile, {
394
+ commandName: 'roadmapsmith update',
395
+ dryRun
396
+ });
309
397
  const writeResult = writeText(roadmapFile, next, { dryRun });
310
398
  if (dryRun) {
311
399
  if (writeResult.changed) {
@@ -371,40 +459,69 @@ function runStatusCommand(projectRoot, config, flags, options = {}) {
371
459
  async function runZeroCommand(projectRoot, flags) {
372
460
  const configPath = resolveConfigPath({ projectRoot, configPath: flags.config });
373
461
  const config = loadConfig({ projectRoot, configPath: flags.config });
374
- if (!isInteractiveTerminal(process.stdin, process.stdout)) {
375
- throw new Error('Zero Mode requires an interactive terminal. Run roadmapsmith zero from a terminal session, or add a config/brief workflow before retrying in non-interactive mode.');
462
+ const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
463
+ const roadmapExistedBeforeInit = fs.existsSync(roadmapFile);
464
+ const defaults = resolveZeroModeAnswers(projectRoot, config, flags);
465
+ const interactive = isInteractiveTerminal(process.stdin, process.stdout);
466
+ let answers = defaults;
467
+
468
+ if (!interactive) {
469
+ const missingFields = getMissingZeroModeFields(defaults);
470
+ if (missingFields.length > 0) {
471
+ throw new Error(
472
+ 'Zero Mode requires a complete brief in non-interactive environments. Missing: ' +
473
+ `${formatMissingZeroModeFields(missingFields).join(', ')}. ` +
474
+ 'Provide the missing values with CLI flags or in roadmap-skill.config.json before retrying.'
475
+ );
476
+ }
477
+ console.log('RoadmapSmith Zero Mode');
478
+ console.log('Using config/flag discovery inputs in non-interactive mode.\n');
479
+ } else {
480
+ const rl = readline.createInterface({
481
+ input: process.stdin,
482
+ output: process.stdout
483
+ });
484
+
485
+ try {
486
+ console.log('RoadmapSmith Zero Mode');
487
+ console.log('Answer the discovery interview to generate the first roadmap.\n');
488
+ answers = await collectZeroModeAnswers((prompt) => rl.question(prompt), defaults);
489
+ } finally {
490
+ rl.close();
491
+ }
376
492
  }
377
493
 
378
- const defaults = buildZeroModeDefaults(projectRoot, config);
379
- const rl = readline.createInterface({
380
- input: process.stdin,
381
- output: process.stdout
494
+ const existingUserConfig = readUserConfig({ projectRoot, configPath: flags.config });
495
+ const nextUserConfig = buildZeroModeConfigPatch(projectRoot, existingUserConfig, answers);
496
+ writeText(configPath, JSON.stringify(nextUserConfig, null, 2));
497
+ console.log(`Updated ${configPath}`);
498
+ const nextConfig = loadConfig({ projectRoot, configPath: flags.config });
499
+ runInitCommand(projectRoot, nextConfig, flags);
500
+ runGenerateCommand(projectRoot, nextConfig, flags, {
501
+ commandName: 'roadmapsmith zero',
502
+ suppressPreWriteWarning: true,
503
+ forceFullRegenerate: !roadmapExistedBeforeInit
382
504
  });
383
-
384
- try {
385
- console.log('RoadmapSmith Zero Mode');
386
- console.log('Answer the discovery interview to generate the first roadmap.\n');
387
- const answers = await collectZeroModeAnswers((prompt) => rl.question(prompt), defaults);
388
- const existingUserConfig = readUserConfig({ projectRoot, configPath: flags.config });
389
- const nextUserConfig = buildZeroModeConfigPatch(projectRoot, existingUserConfig, answers);
390
- writeText(configPath, JSON.stringify(nextUserConfig, null, 2));
391
- console.log(`Updated ${configPath}`);
392
- const nextConfig = loadConfig({ projectRoot, configPath: flags.config });
393
- runInitCommand(projectRoot, nextConfig, flags);
394
- runGenerateCommand(projectRoot, nextConfig, flags);
395
- } finally {
396
- rl.close();
397
- }
398
505
  }
399
506
 
400
507
  function runMaintainCommand(projectRoot, flags) {
401
508
  const config = loadConfig({ projectRoot, configPath: flags.config });
509
+ const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
510
+ const existingContent = readTextIfExists(roadmapFile);
511
+ assertMaintainCanWrite(roadmapFile, existingContent);
402
512
  const fullRegen = isEnabled(flags['full-regen']);
513
+ const warningState = { emitted: false };
403
514
  runGenerateCommand(projectRoot, config, flags, {
515
+ commandName: 'roadmapsmith maintain',
404
516
  preserveManagedBlock: !fullRegen,
405
- forceFullRegenerate: fullRegen
517
+ forceFullRegenerate: fullRegen,
518
+ warningState
519
+ });
520
+ runSyncCommand(projectRoot, config, { ...flags, audit: undefined }, {
521
+ audit: true,
522
+ commandName: 'roadmapsmith maintain',
523
+ warningState
406
524
  });
407
- runSyncCommand(projectRoot, config, { ...flags, audit: true }, { audit: true });
408
525
  }
409
526
 
410
527
  async function run() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.33",
3
+ "version": "0.9.36",
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": {
@@ -9,7 +9,8 @@ Use this command when the repository already has code, tests, docs, or an existi
9
9
 
10
10
  ## Required behavior
11
11
 
12
- 1. Run `roadmapsmith maintain --project-root .`.
12
+ 1. Run `roadmapsmith maintain --project-root .`. Prefer `--dry-run` first when previewing impact.
13
13
  2. Treat this command as CLI-backed. Do not silently replace it with manual reasoning when the CLI is unavailable.
14
- 3. Mention that maintain runs preserve-first generate, sync, and audit in one invocation.
15
- 4. After a successful maintain cycle, do not propose generate, sync, or audit separately unless the user needs manual control or inspection.
14
+ 3. Mention that maintain runs preserve-first generate, sync, and audit in one invocation, but only for an existing managed roadmap block.
15
+ 4. If the roadmap is non-empty and lacks `<!-- rs:managed:* -->`, direct the user to `roadmapsmith update` for conservative inline annotations or `roadmapsmith generate` for explicit managed-section creation.
16
+ 5. After a successful maintain cycle, do not propose generate, sync, or audit separately unless the user needs manual control or inspection.
@@ -2,6 +2,6 @@
2
2
  "interface": {
3
3
  "display_name": "Roadmap Sync (Deprecated)",
4
4
  "short_description": "DEPRECATED legacy root; use /roadmap-maintain or /roadmap-update.",
5
- "default_prompt": "Prefer /roadmap, /roadmap-status, /roadmap-maintain, and /roadmap-update."
5
+ "default_prompt": "Prefer /roadmap, /roadmap-status, /roadmap-maintain, and /roadmap-update; use /roadmap-update when markers are missing."
6
6
  }
7
7
  }
@@ -10,4 +10,5 @@ Use this command when the repository is empty or low-context and the user needs
10
10
  ## Required behavior
11
11
 
12
12
  1. Run `roadmapsmith zero --project-root .`.
13
- 2. If the CLI is missing, explain the install path instead of improvising the workflow manually.
13
+ 2. In non-interactive environments, provide or reuse a complete brief from config plus flags such as `--primary-user`, `--problem-statement`, `--target-outcome`, and repeatable `--done-criterion`.
14
+ 3. If the CLI is missing, explain the install path instead of improvising the workflow manually.
package/skills.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
13
13
  "roadmapsmith setup",
14
14
  "roadmapsmith zero",
15
- "roadmapsmith maintain",
15
+ "roadmapsmith maintain --dry-run",
16
16
  "roadmapsmith /roadmap",
17
17
  "roadmapsmith update --task p2-customer-history --evidence \"src/app/api/customers/route.ts, test/customers.test.js\"",
18
18
  "roadmapsmith validate --json"
@@ -21,74 +21,74 @@
21
21
  "command": "npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
22
22
  "source": "PapiScholz/roadmapsmith",
23
23
  "skill": "*",
24
- "notes": "Recommended Claude Code install path for native GUI slash commands. Canonical surfaces: /roadmap, /roadmap-zero, /roadmap-maintain, /roadmap-status, /roadmap-validate, /roadmap-update, /roadmap-setup. The public CLI update family covers both checklist refresh and verified single-task completion; sync remains the advanced alias for the mutating refresh path. Advanced surfaces: /roadmap-init, /roadmap-generate, /roadmap-audit. DEPRECATED: /roadmap-sync remains only as a compatibility root for existing automation; use /roadmap-maintain or /roadmap-update for new workflows. Install the roadmapsmith CLI separately for actual command execution; roadmapsmith status is the public readiness command and roadmapsmith doctor remains a compatibility alias. 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."
24
+ "notes": "Recommended Claude Code install path for native GUI slash commands. Canonical surfaces: /roadmap, /roadmap-zero, /roadmap-maintain, /roadmap-status, /roadmap-validate, /roadmap-update, /roadmap-setup. The public CLI update family covers both checklist refresh and verified single-task completion; sync remains the advanced alias for the mutating refresh path. /roadmap-maintain now owns only existing managed roadmap blocks, while /roadmap-update is the conservative annotation path for authored roadmaps without markers. /roadmap-zero can consume a complete discovery brief from config plus CLI flags in non-interactive environments. Advanced surfaces: /roadmap-init, /roadmap-generate, /roadmap-audit. DEPRECATED: /roadmap-sync remains only as a compatibility root for existing automation; use /roadmap-maintain or /roadmap-update for new workflows. Install the roadmapsmith CLI separately for actual command execution; roadmapsmith status is the public readiness command and roadmapsmith doctor remains a compatibility alias. 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."
25
25
  },
26
26
  "skills": [
27
27
  {
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.33"
31
+ "version": "0.9.36"
32
32
  },
33
33
  {
34
34
  "name": "roadmap-zero",
35
35
  "path": "skills/roadmap-zero",
36
- "description": "Native slash entrypoint for the one-command Zero Mode CLI workflow.",
37
- "version": "0.9.33"
36
+ "description": "Native slash entrypoint for Zero Mode, including non-interactive config-plus-flag discovery.",
37
+ "version": "0.9.36"
38
38
  },
39
39
  {
40
40
  "name": "roadmap-maintain",
41
41
  "path": "skills/roadmap-maintain",
42
- "description": "Native slash entrypoint for the preserve-first generate + sync + audit flow.",
43
- "version": "0.9.33"
42
+ "description": "Native slash entrypoint for conservative managed-block maintenance plus sync and audit output.",
43
+ "version": "0.9.36"
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.33"
49
+ "version": "0.9.36"
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.33"
55
+ "version": "0.9.36"
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.33"
61
+ "version": "0.9.36"
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.33"
67
+ "version": "0.9.36"
68
68
  },
69
69
  {
70
70
  "name": "roadmap-update",
71
71
  "path": "skills/roadmap-update",
72
- "description": "Native slash entrypoint for evidence-backed sync and verified single-task completion.",
73
- "version": "0.9.33"
72
+ "description": "Native slash entrypoint for evidence-backed inline annotation refresh and verified single-task completion.",
73
+ "version": "0.9.36"
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.33"
79
+ "version": "0.9.36"
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.33"
85
+ "version": "0.9.36"
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.33"
91
+ "version": "0.9.36"
92
92
  }
93
93
  ]
94
94
  }
package/src/config.js CHANGED
@@ -30,6 +30,9 @@ const DEFAULT_CONFIG = {
30
30
  constraints: [],
31
31
  doneCriteria: []
32
32
  },
33
+ scan: {
34
+ excludeDirs: []
35
+ },
33
36
  validation: {
34
37
  minimumConfidence: 'low',
35
38
  testReports: [],
@@ -95,6 +98,11 @@ function mergeConfig(userConfig) {
95
98
  ? userConfig.zeroMode.doneCriteria
96
99
  : DEFAULT_CONFIG.zeroMode.doneCriteria
97
100
  },
101
+ scan: {
102
+ excludeDirs: Array.isArray(userConfig && userConfig.scan && userConfig.scan.excludeDirs)
103
+ ? userConfig.scan.excludeDirs
104
+ : DEFAULT_CONFIG.scan.excludeDirs
105
+ },
98
106
  validation: {
99
107
  ...DEFAULT_CONFIG.validation,
100
108
  ...((userConfig && userConfig.validation) || {}),
package/src/io.js CHANGED
@@ -12,6 +12,16 @@ const DEFAULT_IGNORED_DIRS = new Set([
12
12
  '.nuxt',
13
13
  '.turbo',
14
14
  '.cache',
15
+ '.open-next',
16
+ '.vercel',
17
+ '.svelte-kit',
18
+ '.parcel-cache',
19
+ '.angular',
20
+ '.expo',
21
+ '.serverless',
22
+ '.wrangler',
23
+ '.tmp',
24
+ 'tmp',
15
25
  'dist',
16
26
  'dist-electron',
17
27
  'build',
@@ -68,7 +78,10 @@ function writeText(filePath, content, options = {}) {
68
78
  }
69
79
 
70
80
  function walkFiles(rootPath, options = {}) {
71
- const ignoredDirs = options.ignoredDirs || DEFAULT_IGNORED_DIRS;
81
+ let ignoredDirs = options.ignoredDirs || DEFAULT_IGNORED_DIRS;
82
+ if (Array.isArray(options.extraIgnoredDirs) && options.extraIgnoredDirs.length > 0) {
83
+ ignoredDirs = new Set([...ignoredDirs, ...options.extraIgnoredDirs]);
84
+ }
72
85
  const result = [];
73
86
 
74
87
  function walk(current) {
@@ -147,6 +147,10 @@ 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 kindMatch = markerFlags.match(/\brs:kind=(\S+)/i);
151
+ const taskKind = kindMatch ? kindMatch[1].toLowerCase() : null;
152
+ const verifiedByMatch = markerFlags.match(/\brs:verified-by=(\S+)/i);
153
+ const taskVerifiedBy = verifiedByMatch ? verifiedByMatch[1].toLowerCase() : null;
150
154
  const taskIndentWidth = getIndentWidth(indent);
151
155
 
152
156
  let warningLineIndex = null;
@@ -243,6 +247,8 @@ function parseRoadmap(content) {
243
247
  blockedByIds,
244
248
  markerId,
245
249
  noTest,
250
+ kind: taskKind,
251
+ verifiedBy: taskVerifiedBy,
246
252
  indent,
247
253
  section
248
254
  });
package/src/sync/index.js CHANGED
@@ -139,6 +139,13 @@ function applySync(content, parsedTasks, results, options) {
139
139
  const lines = [...parsed.lines];
140
140
  const tasks = parsedTasks || parsed.tasks;
141
141
 
142
+ const changes = {
143
+ newlyUnchecked: [],
144
+ newlyChecked: [],
145
+ warningsAdded: [],
146
+ warningsRemoved: []
147
+ };
148
+
142
149
  let offset = 0;
143
150
  for (const task of tasks) {
144
151
  const result = results[task.id];
@@ -151,7 +158,13 @@ function applySync(content, parsedTasks, results, options) {
151
158
  continue;
152
159
  }
153
160
 
161
+ const wasChecked = task.checked;
154
162
  lines[lineIndex] = setChecklistState(lines[lineIndex], result.passed);
163
+ if (wasChecked && !result.passed) {
164
+ changes.newlyUnchecked.push(task.id);
165
+ } else if (!wasChecked && result.passed) {
166
+ changes.newlyChecked.push(task.id);
167
+ }
155
168
 
156
169
  const reason = normalizeWarningReasons(result.reasons).join('; ');
157
170
  const warningText = formatWarning(task.indent || '', reason || 'validation failed', result.attempted);
@@ -198,6 +211,7 @@ function applySync(content, parsedTasks, results, options) {
198
211
  } else {
199
212
  lines.splice(lastChildLineIndex + 1, 0, warningText);
200
213
  offset += 1;
214
+ changes.warningsAdded.push(task.id);
201
215
  }
202
216
 
203
217
  const recipeIndex = findVerificationRecipeIndex(lines, lineIndex);
@@ -218,7 +232,7 @@ function applySync(content, parsedTasks, results, options) {
218
232
  }
219
233
  }
220
234
 
221
- return ensureTrailingNewline(lines.join('\n'));
235
+ return { content: ensureTrailingNewline(lines.join('\n')), changes };
222
236
  }
223
237
 
224
238
  module.exports = {
@@ -13,7 +13,11 @@ const CODE_EXTENSIONS = new Set([
13
13
  ]);
14
14
  const TRANSLATION_DIR_SEGMENTS = ['locale', 'locales', 'i18n', 'translations'];
15
15
  const DEFAULT_EXCLUDED_PATH_PREFIXES = ['.claude/', '.agent/', 'roadmap-skill/'];
16
- const GENERATED_OUTPUT_PREFIXES = ['dist-electron/', 'dist/', 'build/', 'out/', '.next/', 'coverage/'];
16
+ const GENERATED_OUTPUT_PREFIXES = [
17
+ 'dist-electron/', 'dist/', 'build/', 'out/', '.next/', 'coverage/',
18
+ '.open-next/', '.vercel/', '.svelte-kit/', '.parcel-cache/', '.angular/',
19
+ '.expo/', '.serverless/', '.wrangler/', '.tmp/', 'tmp/'
20
+ ];
17
21
  const AUXILIARY_HEURISTIC_PATH_SEGMENTS = new Set(['scripts', 'tools', 'tooling', 'demo', 'demos']);
18
22
 
19
23
  // "docs" omitted from DOC_HINTS — it is a path prefix in scan tasks, not a doc-authoring keyword.
@@ -246,6 +250,7 @@ function readFileIndex(projectRoot, files, config) {
246
250
  if (SELF_REFERENTIAL_FILES.has(relativePath)) continue;
247
251
  if (isFixturePath(relativePath)) continue;
248
252
  if (shouldExcludeByDefaultPath(relativePath, config)) continue;
253
+ if (isGeneratedOutputPath(relativePath)) continue;
249
254
 
250
255
  const absolutePath = path.resolve(projectRoot, relativePath);
251
256
  const ext = path.extname(relativePath).toLowerCase();
@@ -1774,7 +1779,10 @@ function evaluateDeterministicVerification(task, context) {
1774
1779
  }
1775
1780
 
1776
1781
  function buildValidationContext(projectRoot, config, plugins, options = {}) {
1777
- const files = walkFiles(projectRoot);
1782
+ const userExcludeDirs = Array.isArray(config && config.scan && config.scan.excludeDirs)
1783
+ ? config.scan.excludeDirs
1784
+ : [];
1785
+ const files = walkFiles(projectRoot, { extraIgnoredDirs: userExcludeDirs });
1778
1786
  const fileIndex = readFileIndex(projectRoot, files, config);
1779
1787
  const testFrameworks = detectTestFrameworks(projectRoot, files);
1780
1788
  const pathHintResolver = buildPathHintResolver(fileIndex);
@@ -1852,6 +1860,39 @@ function buildDiscoveredEvidenceLine(evidence) {
1852
1860
  }
1853
1861
 
1854
1862
  function validateTask(task, context, config, plugins) {
1863
+ if (task.verifiedBy === 'human') {
1864
+ const hasEvidenceLine = Array.isArray(task.evidenceLines) &&
1865
+ task.evidenceLines.some((e) => e.text && e.text.trim().length > 0);
1866
+ if (!hasEvidenceLine) {
1867
+ return {
1868
+ passed: false,
1869
+ confidence: 'low',
1870
+ attempted: false,
1871
+ reasons: ['human-verified tasks require an Evidence: child line'],
1872
+ evidence: { code: false, test: false, artifact: false, files: [], codeFiles: [], testFiles: [], weakPathFiles: [], weakPathContentTokens: [], artifactFiles: [], heuristicArtifacts: [], symbols: [], structuralEvidence: null, authoritative: false, authoritativeFiles: [], authoritativeSummaries: [] },
1873
+ diagnostics: [],
1874
+ verificationRecipe: null,
1875
+ staleEvidenceDetected: false,
1876
+ staleEvidenceResolved: false,
1877
+ generatedTestEvidence: null
1878
+ };
1879
+ }
1880
+ const evidenceText = task.evidenceLines[0].text;
1881
+ return {
1882
+ passed: true,
1883
+ confidence: 'medium',
1884
+ attempted: true,
1885
+ reasons: [],
1886
+ evidence: { code: false, test: false, artifact: false, files: [], codeFiles: [], testFiles: [], weakPathFiles: [], weakPathContentTokens: [], artifactFiles: [], heuristicArtifacts: [], symbols: [], structuralEvidence: null, authoritative: true, authoritativeFiles: [], authoritativeSummaries: [evidenceText] },
1887
+ diagnostics: [],
1888
+ verificationRecipe: null,
1889
+ staleEvidenceDetected: false,
1890
+ staleEvidenceResolved: false,
1891
+ generatedTestEvidence: null,
1892
+ humanVerified: true
1893
+ };
1894
+ }
1895
+
1855
1896
  const {
1856
1897
  paths: pathHints,
1857
1898
  externalPaths,
@@ -1943,6 +1984,7 @@ function validateTask(task, context, config, plugins) {
1943
1984
 
1944
1985
  const requiresTest =
1945
1986
  !task.noTest &&
1987
+ task.kind !== 'docs' &&
1946
1988
  context.testFrameworks.length > 0 &&
1947
1989
  isCodeTask(task.text) &&
1948
1990
  !isDocTask(task.text) &&
@@ -2010,6 +2052,7 @@ function validateTask(task, context, config, plugins) {
2010
2052
  // Only pure path hints (not line-reference hints like file.ts:169) count as direct evidence.
2011
2053
  const hasDirectReferencePass = filesFromPurePathHints.length > 0 || filesFromSymbols.length > 0;
2012
2054
  const hasArtifactTaskPass = evidence.artifact && (
2055
+ task.kind === 'docs' ||
2013
2056
  isDocTask(task.text) ||
2014
2057
  evidence.heuristicArtifacts.length > 0 ||
2015
2058
  filesFromPaths.some((relativePath) => !CODE_EXTENSIONS.has(path.extname(relativePath).toLowerCase()))
@@ -2226,17 +2269,23 @@ function validateTasks(tasks, context, config, plugins) {
2226
2269
  return result;
2227
2270
  }
2228
2271
 
2229
- function auditValidation(tasks, results) {
2272
+ function auditValidation(tasks, results, changes) {
2230
2273
  const checkedWithoutEvidence = [];
2231
2274
  const readyButUnchecked = [];
2232
2275
  const checkedWithWeakEvidence = [];
2233
2276
  const documentationOnlyEvidenceForImplementation = [];
2234
2277
  const checkedWithNoStructuralEvidence = [];
2278
+ const humanVerifiedTasks = [];
2279
+ const newlyUnchecked = Array.isArray(changes && changes.newlyUnchecked) ? changes.newlyUnchecked : [];
2235
2280
 
2236
2281
  for (const task of tasks) {
2237
2282
  const result = results[task.id];
2238
2283
  if (!result) continue;
2239
2284
 
2285
+ if (result.humanVerified) {
2286
+ humanVerifiedTasks.push({ task, result });
2287
+ }
2288
+
2240
2289
  if (task.checked && !result.passed) {
2241
2290
  checkedWithoutEvidence.push({ task, result });
2242
2291
  }
@@ -2265,6 +2314,8 @@ function auditValidation(tasks, results) {
2265
2314
  checkedWithWeakEvidence,
2266
2315
  documentationOnlyEvidenceForImplementation,
2267
2316
  checkedWithNoStructuralEvidence,
2317
+ humanVerifiedTasks,
2318
+ newlyUnchecked
2268
2319
  };
2269
2320
  }
2270
2321
 
package/src/zero.js CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  const path = require('path');
4
4
 
5
+ const ZERO_MODE_FLAG_SPECS = {
6
+ productName: { flag: '--product-name', configPath: 'product.name', type: 'scalar' },
7
+ primaryUser: { flag: '--primary-user', configPath: 'product.primaryUser', type: 'scalar', required: true },
8
+ problemStatement: { flag: '--problem-statement', configPath: 'zeroMode.problemStatement', type: 'scalar', required: true },
9
+ targetOutcome: { flag: '--target-outcome', configPath: 'product.targetOutcome', type: 'scalar', required: true },
10
+ antiGoals: { flag: '--anti-goal', configPath: 'product.antiGoals', type: 'list' },
11
+ preferredStack: { flag: '--preferred-stack', configPath: 'zeroMode.preferredStack', type: 'scalar' },
12
+ constraints: { flag: '--constraint', configPath: 'zeroMode.constraints', type: 'list' },
13
+ doneCriteria: { flag: '--done-criterion', configPath: 'zeroMode.doneCriteria', type: 'list', required: true }
14
+ };
15
+
5
16
  const ZERO_MODE_QUESTIONS = [
6
17
  { id: 'productName', prompt: '1. What product are we building?' },
7
18
  { id: 'primaryUser', prompt: '2. Who is the target user?' },
@@ -54,6 +65,80 @@ function buildZeroModeDefaults(projectRoot, config) {
54
65
  };
55
66
  }
56
67
 
68
+ function getSingleFlagValue(value) {
69
+ if (Array.isArray(value)) {
70
+ return value[value.length - 1];
71
+ }
72
+ return value;
73
+ }
74
+
75
+ function getListFlagValues(value) {
76
+ const rawValues = Array.isArray(value) ? value : [value];
77
+ return rawValues.flatMap((item) => splitListAnswer(item));
78
+ }
79
+
80
+ function resolveZeroModeAnswers(projectRoot, config, flags = {}) {
81
+ const defaults = buildZeroModeDefaults(projectRoot, config);
82
+ const answers = { ...defaults };
83
+
84
+ for (const [field, spec] of Object.entries(ZERO_MODE_FLAG_SPECS)) {
85
+ const flagKey = spec.flag.slice(2);
86
+ if (!Object.prototype.hasOwnProperty.call(flags, flagKey)) {
87
+ continue;
88
+ }
89
+
90
+ if (spec.type === 'list') {
91
+ answers[field] = getListFlagValues(flags[flagKey]).join('; ');
92
+ continue;
93
+ }
94
+
95
+ const raw = getSingleFlagValue(flags[flagKey]);
96
+ answers[field] = String(raw == null ? '' : raw).trim();
97
+ }
98
+
99
+ if (!String(answers.productName || '').trim()) {
100
+ answers.productName = path.basename(projectRoot);
101
+ }
102
+
103
+ return answers;
104
+ }
105
+
106
+ function getMissingZeroModeFields(answers = {}) {
107
+ const missing = [];
108
+ for (const [field, spec] of Object.entries(ZERO_MODE_FLAG_SPECS)) {
109
+ if (!spec.required) {
110
+ continue;
111
+ }
112
+
113
+ if (spec.type === 'list') {
114
+ if (splitListAnswer(answers[field]).length === 0) {
115
+ missing.push({
116
+ field,
117
+ flag: spec.flag,
118
+ configPath: spec.configPath
119
+ });
120
+ }
121
+ continue;
122
+ }
123
+
124
+ if (!String(answers[field] || '').trim()) {
125
+ missing.push({
126
+ field,
127
+ flag: spec.flag,
128
+ configPath: spec.configPath
129
+ });
130
+ }
131
+ }
132
+ return missing;
133
+ }
134
+
135
+ function formatMissingZeroModeFields(missingFields) {
136
+ return missingFields.map((item) => {
137
+ const label = item.field.replace(/([A-Z])/g, ' $1').toLowerCase();
138
+ return `${label} (${item.flag} or config ${item.configPath})`;
139
+ });
140
+ }
141
+
57
142
  async function collectZeroModeAnswers(ask, defaults = {}) {
58
143
  const answers = {};
59
144
  for (const question of ZERO_MODE_QUESTIONS) {
@@ -124,6 +209,9 @@ module.exports = {
124
209
  buildZeroModeConfigPatch,
125
210
  buildZeroModeDefaults,
126
211
  collectZeroModeAnswers,
212
+ formatMissingZeroModeFields,
213
+ getMissingZeroModeFields,
127
214
  isInteractiveTerminal,
215
+ resolveZeroModeAnswers,
128
216
  splitListAnswer
129
217
  };