roadmapsmith 0.9.24 → 0.9.26

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,7 +1,7 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.24",
4
- "description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude.",
3
+ "version": "0.9.26",
4
+ "description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude plus the roadmapsmith status readiness surface.",
5
5
  "author": {
6
6
  "name": "PapiScholz"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.24",
4
- "description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude.",
3
+ "version": "0.9.26",
4
+ "description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude plus the roadmapsmith status readiness surface.",
5
5
  "author": {
6
6
  "name": "PapiScholz"
7
7
  },
package/README.md CHANGED
@@ -120,7 +120,8 @@ roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]
120
120
  roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit] [--full-regen]
121
121
  roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]
122
122
  roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]
123
- roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]
123
+ roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]
124
+ roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json] # compatibility alias
124
125
  ```
125
126
 
126
127
  ## Claude Code native slash commands
@@ -153,7 +154,7 @@ Then restart Codex, open the plugin directory, install `roadmapsmith` from the `
153
154
 
154
155
  Codex native plugin support means install/enable discovery inside Codex. It is separate from Claude-specific `/reload-skills` behavior, and the VS Code task layer remains the fallback/manual workflow when you are not using the plugin directory.
155
156
 
156
- `roadmapsmith doctor --json` now reports native slash surfaces separately from the VS Code task layer:
157
+ `roadmapsmith status --json` now reports native slash surfaces separately from the VS Code task layer (`doctor --json` remains a compatibility alias):
157
158
 
158
159
  - `claudeGui`
159
160
  - `claudeCli`
@@ -179,7 +180,8 @@ The repo does not remove user-global skills automatically. Use the `doctor` outp
179
180
  - code OR test OR artifact evidence required.
180
181
  - test evidence required for code tasks when test frameworks are detected.
181
182
  - Validation failures in sync write warning lines:
182
- - `- ⚠️ attempted but validation failed: <reason>`
183
+ - `- ⚠️ attempted but validation failed: <reason>` when there is concrete attempt evidence
184
+ - `- ⚠️ no implementation evidence found yet: <reason>` when there is not
183
185
  - Preserves unmanaged markdown content by updating only the managed roadmap block.
184
186
 
185
187
  ## Defaults
package/bin/cli.js CHANGED
@@ -31,7 +31,8 @@ function printHelp() {
31
31
  ' roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit] [--full-regen]',
32
32
  ' roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]',
33
33
  ' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]',
34
- ' roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]'
34
+ ' roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]',
35
+ ' roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json] # compatibility alias'
35
36
  ].join('\n'));
36
37
  }
37
38
 
@@ -245,7 +246,7 @@ function printHumanStatus(payload) {
245
246
  function runStatusCommand(projectRoot, config, flags, options = {}) {
246
247
  const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
247
248
  const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
248
- const payload = inspectHostSetup(projectRoot, { roadmapFile, agentsFile });
249
+ const payload = inspectHostSetup(projectRoot, { roadmapFile, agentsFile, currentCliPath: __filename });
249
250
 
250
251
  if (options.json) {
251
252
  process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
@@ -443,7 +444,7 @@ async function run() {
443
444
  return;
444
445
  }
445
446
 
446
- if (effectiveCommand === 'doctor') {
447
+ if (effectiveCommand === 'status' || effectiveCommand === 'doctor') {
447
448
  const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
448
449
  let ok = true;
449
450
  const jsonMode = isEnabled(flags.json);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.24",
4
- "description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude.",
3
+ "version": "0.9.26",
4
+ "description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude plus the roadmapsmith status readiness surface.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "roadmapsmith": "bin/cli.js"
@@ -2,6 +2,6 @@
2
2
  "interface": {
3
3
  "display_name": "Roadmap Sync",
4
4
  "short_description": "Legacy root plus policy for evidence-backed ROADMAP.md slash workflows.",
5
- "default_prompt": "Prefer /roadmap for discovery, /roadmap-maintain for existing repos, and /roadmap-update for direct sync."
5
+ "default_prompt": "Prefer /roadmap, /roadmap-status, /roadmap-maintain, and /roadmap-update."
6
6
  }
7
7
  }
package/skills.json CHANGED
@@ -22,74 +22,74 @@
22
22
  "command": "npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
23
23
  "source": "PapiScholz/roadmapsmith",
24
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."
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; roadmapsmith status is the visible 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."
26
26
  },
27
27
  "skills": [
28
28
  {
29
29
  "name": "roadmap",
30
30
  "path": "skills/roadmap",
31
31
  "description": "Native slash palette for RoadmapSmith commands and recommended entrypoints across supported hosts.",
32
- "version": "0.9.24"
32
+ "version": "0.9.26"
33
33
  },
34
34
  {
35
35
  "name": "roadmap-zero",
36
36
  "path": "skills/roadmap-zero",
37
37
  "description": "Native slash entrypoint for the one-command Zero Mode CLI workflow.",
38
- "version": "0.9.24"
38
+ "version": "0.9.26"
39
39
  },
40
40
  {
41
41
  "name": "roadmap-maintain",
42
42
  "path": "skills/roadmap-maintain",
43
43
  "description": "Native slash entrypoint for the preserve-first generate + sync + audit flow.",
44
- "version": "0.9.24"
44
+ "version": "0.9.26"
45
45
  },
46
46
  {
47
47
  "name": "roadmap-status",
48
48
  "path": "skills/roadmap-status",
49
- "description": "Native slash readiness check grounded in roadmapsmith doctor JSON.",
50
- "version": "0.9.24"
49
+ "description": "Native slash readiness check grounded in roadmapsmith status JSON.",
50
+ "version": "0.9.26"
51
51
  },
52
52
  {
53
53
  "name": "roadmap-init",
54
54
  "path": "skills/roadmap-init",
55
55
  "description": "Native slash entrypoint for creating ROADMAP.md and AGENTS.md.",
56
- "version": "0.9.24"
56
+ "version": "0.9.26"
57
57
  },
58
58
  {
59
59
  "name": "roadmap-generate",
60
60
  "path": "skills/roadmap-generate",
61
61
  "description": "Native slash entrypoint for managed roadmap updates that require --full-regen before destructive replacement.",
62
- "version": "0.9.24"
62
+ "version": "0.9.26"
63
63
  },
64
64
  {
65
65
  "name": "roadmap-validate",
66
66
  "path": "skills/roadmap-validate",
67
67
  "description": "Native slash entrypoint for evidence-backed roadmap validation.",
68
- "version": "0.9.24"
68
+ "version": "0.9.26"
69
69
  },
70
70
  {
71
71
  "name": "roadmap-update",
72
72
  "path": "skills/roadmap-update",
73
73
  "description": "Native slash entrypoint for applying evidence-backed checklist sync.",
74
- "version": "0.9.24"
74
+ "version": "0.9.26"
75
75
  },
76
76
  {
77
77
  "name": "roadmap-sync",
78
78
  "path": "skills/roadmap-sync",
79
79
  "description": "Legacy namespaced root plus policy guidance for RoadmapSmith slash workflows.",
80
- "version": "0.9.24"
80
+ "version": "0.9.26"
81
81
  },
82
82
  {
83
83
  "name": "roadmap-audit",
84
84
  "path": "skills/roadmap-audit",
85
85
  "description": "Native slash entrypoint for the current sync-plus-audit workflow.",
86
- "version": "0.9.24"
86
+ "version": "0.9.26"
87
87
  },
88
88
  {
89
89
  "name": "roadmap-setup",
90
90
  "path": "skills/roadmap-setup",
91
91
  "description": "Native slash entrypoint for generating RoadmapSmith host integration files.",
92
- "version": "0.9.24"
92
+ "version": "0.9.26"
93
93
  }
94
94
  ]
95
95
  }
package/src/host.js CHANGED
@@ -921,7 +921,7 @@ function renderVsCodeLauncher() {
921
921
  'function explain() {',
922
922
  ' console.log(\'RoadmapSmith layers:\\n\');',
923
923
  ' console.log(\'1. The roadmap-sync skill guides the agent. It does not add VS Code buttons or install the CLI.\');',
924
- ' console.log(\'2. The roadmapsmith CLI executes zero/maintain plus init/generate/validate/sync/setup/doctor, with --full-regen reserved for destructive replacement.\');',
924
+ ' console.log(\'2. The roadmapsmith CLI executes zero/maintain plus init/generate/validate/sync/setup/status, with doctor kept as a compatibility alias and --full-regen reserved for destructive replacement.\');',
925
925
  ' console.log(\'3. roadmapsmith setup makes the CLI visible in VS Code through tasks and optional Claude hook wiring.\\n\');',
926
926
  ' console.log(\'Typical VS Code workflow:\');',
927
927
  ' console.log(\'- Run "RoadmapSmith: Status" to inspect readiness.\');',
@@ -1020,7 +1020,7 @@ function renderVsCodeLauncher() {
1020
1020
  '}',
1021
1021
  '',
1022
1022
  'function status() {',
1023
- ' const result = runCli([\'doctor\', \'--project-root\', PROJECT_ROOT, \'--json\'], { capture: true, allowMissingCli: true });',
1023
+ ' const result = runCli([\'status\', \'--project-root\', PROJECT_ROOT, \'--json\'], { capture: true, allowMissingCli: true });',
1024
1024
  ' if (!result || result.missingCli) {',
1025
1025
  ' printMissingCliStatus();',
1026
1026
  ' return;',
@@ -1143,7 +1143,26 @@ function findGlobalRoadmapsmith() {
1143
1143
  return findCommandPath('roadmapsmith');
1144
1144
  }
1145
1145
 
1146
- function detectCliResolution(projectRoot) {
1146
+ function isRoadmapsmithCliEntrypoint(filePath) {
1147
+ const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
1148
+ if (!normalized) {
1149
+ return false;
1150
+ }
1151
+ return normalized.endsWith('/roadmap-skill/bin/cli.js')
1152
+ || normalized.endsWith('/roadmapsmith/bin/cli.js')
1153
+ || /(?:^|[\\/])roadmapsmith(?:\.(?:cmd|exe|bat|ps1))?$/.test(normalized);
1154
+ }
1155
+
1156
+ function detectCliResolution(projectRoot, options = {}) {
1157
+ const currentCliPath = options.currentCliPath || process.argv[1] || null;
1158
+ if (currentCliPath && isRoadmapsmithCliEntrypoint(currentCliPath)) {
1159
+ return {
1160
+ ready: true,
1161
+ kind: 'current-process',
1162
+ path: currentCliPath
1163
+ };
1164
+ }
1165
+
1147
1166
  const workspaceDevCli = path.join(projectRoot, 'roadmap-skill', 'bin', 'cli.js');
1148
1167
  if (fs.existsSync(workspaceDevCli)) {
1149
1168
  return {
@@ -1244,7 +1263,7 @@ function inspectHostSetup(projectRoot, options = {}) {
1244
1263
  const roadmapFile = options.roadmapFile;
1245
1264
  const agentsFile = options.agentsFile;
1246
1265
  const runtime = detectNodeRuntime(options.env || process.env);
1247
- const cli = detectCliResolution(projectRoot);
1266
+ const cli = detectCliResolution(projectRoot, { currentCliPath: options.currentCliPath });
1248
1267
  const vscode = inspectVsCodeTasks(projectRoot);
1249
1268
  const claude = inspectClaudeSetup(projectRoot);
1250
1269
  const bundle = inspectSharedBundleSurface();
@@ -4,7 +4,12 @@ const { slugify } = require('../utils');
4
4
 
5
5
  const MANAGED_START = '<!-- rs:managed:start -->';
6
6
  const MANAGED_END = '<!-- rs:managed:end -->';
7
- const WARNING_PREFIX = '⚠️ attempted but validation failed:';
7
+ const WARNING_PREFIX = '⚠️';
8
+ const WARNING_REASON_PREFIXES = [
9
+ 'attempted but validation failed:',
10
+ 'no implementation evidence found yet:',
11
+ 'validation failed:'
12
+ ];
8
13
 
9
14
  function getIndentWidth(text) {
10
15
  return String(text || '').replace(/\t/g, ' ').length;
@@ -86,7 +91,14 @@ function parseEvidenceLine(content) {
86
91
 
87
92
  function parseWarningLine(content) {
88
93
  if (!content.startsWith(WARNING_PREFIX)) return null;
89
- return content.slice(WARNING_PREFIX.length).trim();
94
+ let normalized = content.slice(WARNING_PREFIX.length).trim();
95
+ for (const prefix of WARNING_REASON_PREFIXES) {
96
+ if (normalized.startsWith(prefix)) {
97
+ normalized = normalized.slice(prefix.length).trim();
98
+ break;
99
+ }
100
+ }
101
+ return normalized;
90
102
  }
91
103
 
92
104
  function parseBlockedByLine(content) {
@@ -98,6 +110,7 @@ function parseRoadmap(content) {
98
110
  const lines = String(content || '').split(/\r?\n/);
99
111
  const managedRange = findManagedRange(lines);
100
112
  const tasks = [];
113
+ const implicitIdCounts = new Map();
101
114
  let section = '';
102
115
 
103
116
  for (let index = 0; index < lines.length; index += 1) {
@@ -162,7 +175,12 @@ function parseRoadmap(content) {
162
175
  }
163
176
  }
164
177
 
165
- const id = markerId || slugify(text);
178
+ const baseId = markerId || slugify(text);
179
+ const nextImplicitCount = markerId ? 1 : (implicitIdCounts.get(baseId) || 0) + 1;
180
+ if (!markerId) {
181
+ implicitIdCounts.set(baseId, nextImplicitCount);
182
+ }
183
+ const id = markerId || (nextImplicitCount === 1 ? baseId : `${baseId}-${nextImplicitCount}`);
166
184
  tasks.push({
167
185
  id,
168
186
  text,
package/src/slash.js CHANGED
@@ -16,7 +16,7 @@ const SLASH_ACTIONS = [
16
16
  {
17
17
  id: 'status',
18
18
  description: 'Inspect CLI, roadmap, VS Code task, Codex, and Claude readiness.',
19
- classicCliExample: 'roadmapsmith doctor --json',
19
+ classicCliExample: 'roadmapsmith status --json',
20
20
  taskLabel: 'RoadmapSmith: Status'
21
21
  },
22
22
  {
package/src/sync/index.js CHANGED
@@ -3,14 +3,21 @@
3
3
  const { parseRoadmap } = require('../parser');
4
4
  const { ensureTrailingNewline } = require('../utils');
5
5
 
6
- const WARNING_REASON_PREFIX = 'attempted but validation failed:';
6
+ const ATTEMPTED_WARNING_REASON_PREFIX = 'attempted but validation failed:';
7
+ const NO_EVIDENCE_WARNING_REASON_PREFIX = 'no implementation evidence found yet:';
8
+ const WARNING_REASON_PREFIXES = [
9
+ ATTEMPTED_WARNING_REASON_PREFIX,
10
+ NO_EVIDENCE_WARNING_REASON_PREFIX,
11
+ 'validation failed:'
12
+ ];
7
13
 
8
14
  function setChecklistState(line, checked) {
9
15
  return line.replace(/- \[( |x|X)\]/, `- [${checked ? 'x' : ' '}]`);
10
16
  }
11
17
 
12
- function formatWarning(indent, reason) {
13
- return `${indent} - ⚠️ attempted but validation failed: ${reason}`;
18
+ function formatWarning(indent, reason, attempted) {
19
+ const prefix = attempted ? ATTEMPTED_WARNING_REASON_PREFIX : NO_EVIDENCE_WARNING_REASON_PREFIX;
20
+ return `${indent} - ⚠️ ${prefix} ${reason}`;
14
21
  }
15
22
 
16
23
  function isWhitespaceCharacter(char) {
@@ -72,9 +79,12 @@ function normalizeWarningReason(reason) {
72
79
  }
73
80
 
74
81
  normalized = stripLeadingWarningMarker(normalized).trim();
75
- const prefixIndex = normalized.indexOf(WARNING_REASON_PREFIX);
76
- if (prefixIndex >= 0) {
77
- normalized = normalized.slice(prefixIndex + WARNING_REASON_PREFIX.length).trim();
82
+ for (const prefix of WARNING_REASON_PREFIXES) {
83
+ const prefixIndex = normalized.indexOf(prefix);
84
+ if (prefixIndex >= 0) {
85
+ normalized = normalized.slice(prefixIndex + prefix.length).trim();
86
+ break;
87
+ }
78
88
  }
79
89
 
80
90
  return normalized;
@@ -122,12 +132,12 @@ function applySync(content, parsedTasks, results) {
122
132
  lines[lineIndex] = setChecklistState(lines[lineIndex], result.passed);
123
133
 
124
134
  const reason = normalizeWarningReasons(result.reasons).join('; ');
125
- const warningText = formatWarning(task.indent || '', reason || 'validation failed');
135
+ const warningText = formatWarning(task.indent || '', reason || 'validation failed', result.attempted);
126
136
  const hasWarning = task.warningLineIndex != null;
127
137
  const warningIndex = hasWarning ? task.warningLineIndex + offset : null;
128
138
  const lastChildLineIndex = (task.lastChildLineIndex != null ? task.lastChildLineIndex : task.lineIndex) + offset;
129
139
 
130
- if (result.passed || !result.attempted) {
140
+ if (result.passed) {
131
141
  if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
132
142
  lines.splice(warningIndex, 1);
133
143
  offset -= 1;
@@ -136,7 +146,7 @@ function applySync(content, parsedTasks, results) {
136
146
  }
137
147
 
138
148
  if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
139
- const existingReason = lines[warningIndex].split('validation failed:')[1];
149
+ const existingReason = normalizeWarningReason(lines[warningIndex]);
140
150
  const newReason = reason || 'validation failed';
141
151
  if (!shouldPreserveExistingWarning(existingReason, newReason)) {
142
152
  lines[warningIndex] = warningText;
@@ -197,18 +197,9 @@ function hasFileExtension(token) {
197
197
  }
198
198
 
199
199
  function isLikelyPath(token) {
200
- if (token.includes('*') || token.includes('?')) return false; // glob/wildcard
201
- if (/^\/api\//i.test(token)) return false; // HTTP API route paths are not file paths
202
- if (/^\.{1,2}\/|^\//.test(token)) {
203
- // Bare "/" or "./" with nothing after is not a real path (e.g. "API / ESC-POS" → "/")
204
- return /[A-Za-z0-9_]/.test(token);
205
- }
206
- if (hasFileExtension(token)) return true;
207
- if (KNOWN_PATH_ROOTS.some((root) => token.startsWith(root))) return true;
208
- // The ">= 2 slashes" rule was intentionally removed: it caused conceptual slash phrases
209
- // like "code/test/artifact" or "build/test/deploy" to be treated as file paths.
210
- // Real multi-segment paths are caught by the extension or known-root rules above.
211
- return false;
200
+ if (isRealFilePath(token)) return true;
201
+ // Preserve the legacy extension-only fallback for standalone path-looking tokens.
202
+ return hasFileExtension(token);
212
203
  }
213
204
 
214
205
  // Matches standalone filenames without a slash — e.g. "roadmap-skill.config.json",
@@ -230,21 +221,83 @@ function hasKnownFileExtension(token) {
230
221
  return KNOWN_FILE_EXTENSIONS.has(token.slice(lastDot).toLowerCase());
231
222
  }
232
223
 
224
+ function startsWithKnownPathRoot(token) {
225
+ const normalized = String(token || '').replace(/\\/g, '/').toLowerCase();
226
+ return KNOWN_PATH_ROOTS.some((root) => normalized.startsWith(root.toLowerCase()));
227
+ }
228
+
229
+ function isHttpRouteToken(token) {
230
+ const normalized = String(token || '').trim();
231
+ if (!normalized) return false;
232
+ if (/^(GET|POST|PUT|PATCH|DELETE)\s+\/\S+$/i.test(normalized)) {
233
+ return true;
234
+ }
235
+ return /^\/api\//i.test(normalized);
236
+ }
237
+
238
+ function isMimeTypeToken(token) {
239
+ return /^[A-Za-z0-9.+-]+\/[A-Za-z0-9.+-]+$/.test(String(token || '').trim());
240
+ }
241
+
242
+ function looksLikeFormulaToken(token) {
243
+ return /[=×÷]/.test(String(token || '').trim());
244
+ }
245
+
246
+ function isRealFilePath(token) {
247
+ const normalized = String(token || '').trim().replace(/\\/g, '/');
248
+ if (!normalized) return false;
249
+ if (normalized.includes('*') || normalized.includes('?')) return false;
250
+ if (/\s/.test(normalized)) return false;
251
+ if (looksLikeFormulaToken(normalized)) return false;
252
+ if (isHttpRouteToken(normalized)) return false;
253
+
254
+ const looksLikePath =
255
+ hasKnownFileExtension(normalized) ||
256
+ startsWithKnownPathRoot(normalized) ||
257
+ /^\.{1,2}\//.test(normalized) ||
258
+ /^\//.test(normalized);
259
+ if (!looksLikePath) return false;
260
+
261
+ if (!hasKnownFileExtension(normalized) && !startsWithKnownPathRoot(normalized) && isMimeTypeToken(normalized)) {
262
+ return false;
263
+ }
264
+
265
+ if (/^\.{1,2}\/|^\//.test(normalized)) {
266
+ return /[A-Za-z0-9_]/.test(normalized);
267
+ }
268
+
269
+ return true;
270
+ }
271
+
233
272
  function isAsciiAlphaNumeric(char) {
234
273
  if (!char || char.length === 0) return false;
235
274
  const code = char.charCodeAt(0);
236
275
  return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
237
276
  }
238
277
 
239
- function isPathTokenCharacter(char) {
240
- return isAsciiAlphaNumeric(char) || char === '.' || char === '_' || char === '-' || char === '/' || char === '\\';
278
+ function isPathTokenCharacter(char, current) {
279
+ if (char === '~') {
280
+ return !current;
281
+ }
282
+ return isAsciiAlphaNumeric(char) || char === '.' || char === '_' || char === '-' || char === '/' || char === '\\' || char === ':';
241
283
  }
242
284
 
243
285
  function stripTrailingPathPunctuation(token) {
244
286
  let result = String(token || '');
245
287
  while (result.length > 0) {
246
288
  const lastChar = result[result.length - 1];
247
- if (lastChar !== '.' && lastChar !== ',' && lastChar !== ';' && lastChar !== ':' && lastChar !== '!' && lastChar !== '?' && lastChar !== ')') {
289
+ if (
290
+ lastChar !== '.' &&
291
+ lastChar !== ',' &&
292
+ lastChar !== ';' &&
293
+ lastChar !== ':' &&
294
+ lastChar !== '!' &&
295
+ lastChar !== '?' &&
296
+ lastChar !== ')' &&
297
+ lastChar !== ']' &&
298
+ lastChar !== '>' &&
299
+ lastChar !== '`'
300
+ ) {
248
301
  break;
249
302
  }
250
303
  result = result.slice(0, -1);
@@ -255,22 +308,41 @@ function stripTrailingPathPunctuation(token) {
255
308
  function collectPathishTokens(text) {
256
309
  const tokens = [];
257
310
  let current = '';
311
+ let tokenStart = -1;
258
312
  const source = String(text || '');
259
313
  for (let index = 0; index < source.length; index += 1) {
260
314
  const char = source[index];
261
- if (isPathTokenCharacter(char)) {
315
+ if (isPathTokenCharacter(char, current)) {
316
+ if (!current) {
317
+ tokenStart = index;
318
+ }
262
319
  current += char;
263
320
  continue;
264
321
  }
265
322
  if (current) {
266
- tokens.push(stripTrailingPathPunctuation(current));
323
+ const value = stripTrailingPathPunctuation(current);
324
+ if (value) {
325
+ tokens.push({
326
+ value,
327
+ start: tokenStart,
328
+ end: tokenStart + value.length
329
+ });
330
+ }
267
331
  current = '';
332
+ tokenStart = -1;
268
333
  }
269
334
  }
270
335
  if (current) {
271
- tokens.push(stripTrailingPathPunctuation(current));
336
+ const value = stripTrailingPathPunctuation(current);
337
+ if (value) {
338
+ tokens.push({
339
+ value,
340
+ start: tokenStart,
341
+ end: tokenStart + value.length
342
+ });
343
+ }
272
344
  }
273
- return tokens.filter(Boolean);
345
+ return tokens;
274
346
  }
275
347
 
276
348
  // LINE_REF_RE matches "path/file.ext:NN" or "path/file.ext:NN-MM" — indicates WHERE
@@ -278,46 +350,142 @@ function collectPathishTokens(text) {
278
350
  // to lineReferenceHints and excluded from hasDirectReferencePass scoring.
279
351
  const LINE_REF_RE = /^(.+?):(\d+)(?:-\d+)?$/;
280
352
 
353
+ function normalizePathCandidateToken(rawToken) {
354
+ const stripped = stripTrailingPathPunctuation(String(rawToken || '').trim());
355
+ if (!stripped) {
356
+ return '';
357
+ }
358
+ const normalized = stripped.replace(/\\/g, '/');
359
+ if (/^~\//.test(normalized)) {
360
+ return normalized;
361
+ }
362
+ return normalized.replace(/^~(?=\/)/, '~');
363
+ }
364
+
365
+ function isExternalPathToken(token) {
366
+ return /^~\//.test(String(token || '').trim().replace(/\\/g, '/'));
367
+ }
368
+
369
+ function classifyExplicitPathCandidate(rawToken) {
370
+ const clean = normalizePathCandidateToken(rawToken);
371
+ if (!clean || clean.includes('*') || clean.includes('?')) {
372
+ return null;
373
+ }
374
+
375
+ const lineMatch = LINE_REF_RE.exec(clean);
376
+ if (lineMatch) {
377
+ const linePath = normalizePathCandidateToken(lineMatch[1]);
378
+ if (isExternalPathToken(linePath)) {
379
+ return { path: linePath, kind: 'external', isLineReference: true };
380
+ }
381
+ if (isRealFilePath(linePath)) {
382
+ return { path: linePath, kind: 'repo', isLineReference: true };
383
+ }
384
+ return null;
385
+ }
386
+
387
+ if (isExternalPathToken(clean)) {
388
+ return { path: clean, kind: 'external', isLineReference: false };
389
+ }
390
+
391
+ if (!isRealFilePath(clean)) {
392
+ return null;
393
+ }
394
+
395
+ return { path: clean, kind: 'repo', isLineReference: false };
396
+ }
397
+
398
+ function addClassifiedPath(classified, results, externalPaths, lineReferenceHints) {
399
+ if (!classified) return;
400
+ if (classified.kind === 'external') {
401
+ externalPaths.add(classified.path);
402
+ } else {
403
+ results.add(classified.path);
404
+ if (classified.isLineReference) {
405
+ lineReferenceHints.add(classified.path);
406
+ }
407
+ }
408
+ }
409
+
410
+ function findHttpRequestRouteRanges(text) {
411
+ const ranges = [];
412
+ const pattern = /\b(?:GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s`]+)/gi;
413
+ let match = pattern.exec(String(text || ''));
414
+ while (match) {
415
+ const routeToken = match[1];
416
+ const routeStart = match.index + match[0].length - routeToken.length;
417
+ ranges.push({ start: routeStart, end: routeStart + routeToken.length });
418
+ match = pattern.exec(String(text || ''));
419
+ }
420
+ return ranges;
421
+ }
422
+
423
+ function isTokenInsideRanges(token, ranges) {
424
+ return ranges.some((range) => token.start >= range.start && token.end <= range.end);
425
+ }
426
+
427
+ function addPathTokensFromPlainText(text, results, externalPaths, lineReferenceHints) {
428
+ const ignoredRanges = findHttpRequestRouteRanges(text);
429
+ for (const token of collectPathishTokens(text)) {
430
+ if (!token.value.includes('/') && !isExternalPathToken(token.value)) {
431
+ continue;
432
+ }
433
+ if (isTokenInsideRanges(token, ignoredRanges)) {
434
+ continue;
435
+ }
436
+ addClassifiedPath(classifyExplicitPathCandidate(token.value), results, externalPaths, lineReferenceHints);
437
+ }
438
+ }
439
+
440
+ function addPathTokensFromBacktickSpan(text, results, externalPaths, lineReferenceHints) {
441
+ const wholeSpan = classifyExplicitPathCandidate(text);
442
+ if (wholeSpan) {
443
+ addClassifiedPath(wholeSpan, results, externalPaths, lineReferenceHints);
444
+ return;
445
+ }
446
+
447
+ if (!/[;,]/.test(text)) {
448
+ return;
449
+ }
450
+
451
+ for (const part of text.split(/[;,]/)) {
452
+ addClassifiedPath(classifyExplicitPathCandidate(part), results, externalPaths, lineReferenceHints);
453
+ }
454
+ }
455
+
281
456
  function extractExplicitPaths(text) {
282
457
  const results = new Set();
458
+ const externalPaths = new Set();
283
459
  const lineReferenceHints = new Set();
460
+ const source = String(text || '');
461
+ let cursor = 0;
284
462
 
285
- const quoted = String(text).match(/`([^`]+)`/g) || [];
286
- for (const token of quoted) {
287
- const clean = token.slice(1, -1);
288
- if (clean.includes('*') || clean.includes('?')) continue; // glob
289
- const hasSlash = clean.includes('/') || clean.includes('\\');
290
- // Require a slash or a known file extension — rejects property access like err.message,
291
- // fs.readFileSync, error.stack (whose extensions are not in KNOWN_FILE_EXTENSIONS).
292
- if (!hasSlash && !hasKnownFileExtension(clean)) continue;
293
- const lineMatch = LINE_REF_RE.exec(clean);
294
- if (lineMatch && hasKnownFileExtension(lineMatch[1])) {
295
- lineReferenceHints.add(lineMatch[1]);
296
- results.add(lineMatch[1]);
297
- } else {
298
- results.add(clean);
463
+ while (cursor < source.length) {
464
+ const openTick = source.indexOf('`', cursor);
465
+ if (openTick < 0) {
466
+ addPathTokensFromPlainText(source.slice(cursor), results, externalPaths, lineReferenceHints);
467
+ break;
299
468
  }
300
- }
301
469
 
302
- for (const word of String(text).split(/\s+/)) {
303
- if (!word.includes('/')) continue;
304
- const token = stripTrailingPathPunctuation(word);
305
- if (token.includes('*') || token.includes('?')) continue; // glob
306
- const lineMatch = LINE_REF_RE.exec(token);
307
- if (lineMatch && hasKnownFileExtension(lineMatch[1])) {
308
- if (isLikelyPath(lineMatch[1])) {
309
- lineReferenceHints.add(lineMatch[1]);
310
- results.add(lineMatch[1]);
311
- }
312
- } else if (isLikelyPath(token)) {
313
- results.add(token);
470
+ addPathTokensFromPlainText(source.slice(cursor, openTick), results, externalPaths, lineReferenceHints);
471
+ const closeTick = source.indexOf('`', openTick + 1);
472
+ if (closeTick < 0) {
473
+ addPathTokensFromPlainText(source.slice(openTick), results, externalPaths, lineReferenceHints);
474
+ break;
314
475
  }
476
+
477
+ addPathTokensFromBacktickSpan(source.slice(openTick + 1, closeTick), results, externalPaths, lineReferenceHints);
478
+ cursor = closeTick + 1;
315
479
  }
316
480
 
317
481
  const paths = Array.from(results)
318
482
  .filter((p) => !p.includes('*') && !p.includes('?'))
319
483
  .sort((left, right) => left.localeCompare(right));
320
- return { paths, lineReferenceHints };
484
+ return {
485
+ paths,
486
+ externalPaths: Array.from(externalPaths).sort((left, right) => left.localeCompare(right)),
487
+ lineReferenceHints
488
+ };
321
489
  }
322
490
 
323
491
  // Standalone filenames (no slash) mentioned in task prose — e.g. "roadmap-skill.config.json",
@@ -363,6 +531,14 @@ function isCodeTask(taskText) {
363
531
  return CODE_HINTS.some((hint) => normalized.includes(hint));
364
532
  }
365
533
 
534
+ function isHttpExpectationTask(taskText) {
535
+ const text = String(taskText || '');
536
+ if (!/(?:->|→)/.test(text) || !/\bHTTP\s+\d{3}\b/i.test(text)) {
537
+ return false;
538
+ }
539
+ return /\b(?:GET|POST|PUT|PATCH|DELETE)\b/i.test(text) || /\/api\//i.test(text);
540
+ }
541
+
366
542
  function isDocTask(taskText) {
367
543
  const normalized = String(taskText).toLowerCase();
368
544
  // Use word-boundary matching to avoid substring false positives (e.g. "specific" ≠ "spec").
@@ -377,10 +553,59 @@ function isImplementationTask(taskText) {
377
553
  return !isDocTask(taskText) && (isCodeTask(taskText) || taskDescribesChange(taskText));
378
554
  }
379
555
 
380
- function findFilesByPathHints(pathHints, fileIndex) {
556
+ function deriveNextAppRouteAlias(relativePath) {
557
+ const normalized = normalizePathForMatch(relativePath);
558
+ const match = normalized.match(/^(?:src\/)?app(?:\/(.*))?\/(page|route)\.(?:js|jsx|ts|tsx)$/);
559
+ if (!match) {
560
+ return null;
561
+ }
562
+
563
+ const routePath = match[1] || '';
564
+ const segments = routePath ? routePath.split('/').filter(Boolean) : [];
565
+ const visibleSegments = [];
566
+ for (const segment of segments) {
567
+ if (/^\([^)]*\)$/.test(segment)) {
568
+ continue;
569
+ }
570
+ if (segment.includes('(') || segment.includes(')') || segment.includes('[') || segment.includes(']') || segment.startsWith('@')) {
571
+ return null;
572
+ }
573
+ visibleSegments.push(segment);
574
+ }
575
+
576
+ return visibleSegments.length > 0 ? `/${visibleSegments.join('/')}` : '/';
577
+ }
578
+
579
+ function buildPathHintResolver(fileIndex) {
580
+ const routeAliasIndex = new Map();
581
+ for (const file of fileIndex) {
582
+ const alias = deriveNextAppRouteAlias(file.relativePath);
583
+ if (!alias) {
584
+ continue;
585
+ }
586
+ const existing = routeAliasIndex.get(alias) || [];
587
+ existing.push(file.relativePath);
588
+ routeAliasIndex.set(alias, existing);
589
+ }
590
+
591
+ for (const [alias, matches] of routeAliasIndex.entries()) {
592
+ routeAliasIndex.set(alias, Array.from(new Set(matches)).sort((left, right) => left.localeCompare(right)));
593
+ }
594
+
595
+ return {
596
+ fileIndex,
597
+ routeAliasIndex
598
+ };
599
+ }
600
+
601
+ function findFilesByPathHints(pathHints, pathHintResolver) {
602
+ const resolver = Array.isArray(pathHintResolver)
603
+ ? buildPathHintResolver(pathHintResolver)
604
+ : pathHintResolver;
605
+ const fileIndex = resolver.fileIndex;
381
606
  const matches = [];
382
607
  for (const hint of pathHints) {
383
- const normalizedHint = hint.replace(/\\/g, '/');
608
+ const normalizedHint = normalizePathCandidateToken(hint);
384
609
  const direct = fileIndex.find((file) => file.relativePath === normalizedHint);
385
610
  if (direct) {
386
611
  matches.push(direct.relativePath);
@@ -392,6 +617,11 @@ function findFilesByPathHints(pathHints, fileIndex) {
392
617
  matches.push(file.relativePath);
393
618
  }
394
619
  }
620
+
621
+ const routeMatches = resolver.routeAliasIndex.get(normalizedHint);
622
+ if (routeMatches && routeMatches.length > 0) {
623
+ matches.push(...routeMatches);
624
+ }
395
625
  }
396
626
  return Array.from(new Set(matches)).sort((left, right) => left.localeCompare(right));
397
627
  }
@@ -683,19 +913,12 @@ function isTestPath(relativePath) {
683
913
  return /(^|\/)(__tests__|tests)\//.test(relativePath) || /\.test\.|\.spec\.|_test\.go$/.test(relativePath);
684
914
  }
685
915
 
686
- function extractEvidencePaths(evidenceText) {
687
- const paths = new Set();
688
- for (const rawCandidate of collectPathishTokens(evidenceText)) {
689
- const candidate = rawCandidate.split('\\').join('/');
690
- if (!candidate.includes('/') || candidate.includes('*') || candidate.includes('?')) {
691
- continue;
692
- }
693
- if (!hasKnownFileExtension(candidate)) {
694
- continue;
695
- }
696
- paths.add(candidate.replace(/^\.\//, ''));
697
- }
698
- return Array.from(paths).sort((left, right) => left.localeCompare(right));
916
+ function extractReferencedPaths(text) {
917
+ const extracted = extractExplicitPaths(text);
918
+ return {
919
+ repoPaths: extracted.paths,
920
+ externalPaths: extracted.externalPaths
921
+ };
699
922
  }
700
923
 
701
924
  function evidenceLineHasPassingSummary(evidenceText) {
@@ -714,7 +937,7 @@ function evidenceSummaryImpliesTests(evidenceText) {
714
937
  /\b(?:vitest|jest|npm test|pnpm test|yarn test|bun test)\b/i.test(String(evidenceText || ''));
715
938
  }
716
939
 
717
- function evaluateAuthoritativeEvidence(task, fileIndex) {
940
+ function evaluateAuthoritativeEvidence(task, pathHintResolver) {
718
941
  const evidenceLines = Array.isArray(task.evidenceLines) ? task.evidenceLines : [];
719
942
  if (evidenceLines.length === 0) {
720
943
  return {
@@ -729,8 +952,9 @@ function evaluateAuthoritativeEvidence(task, fileIndex) {
729
952
  };
730
953
  }
731
954
 
732
- const referencedPaths = unionArrays(...evidenceLines.map((line) => extractEvidencePaths(line.text)));
733
- const matchedPaths = referencedPaths.length > 0 ? findFilesByPathHints(referencedPaths, fileIndex) : [];
955
+ const extractedReferences = evidenceLines.map((line) => extractReferencedPaths(line.text));
956
+ const referencedPaths = unionArrays(...extractedReferences.map((entry) => entry.repoPaths));
957
+ const matchedPaths = referencedPaths.length > 0 ? findFilesByPathHints(referencedPaths, pathHintResolver) : [];
734
958
  const summaryMatches = evidenceLines
735
959
  .filter((line) => evidenceLineHasPassingSummary(line.text))
736
960
  .map((line) => line.text);
@@ -1053,6 +1277,7 @@ function buildValidationContext(projectRoot, config, plugins) {
1053
1277
  const files = walkFiles(projectRoot);
1054
1278
  const fileIndex = readFileIndex(projectRoot, files, config);
1055
1279
  const testFrameworks = detectTestFrameworks(projectRoot, files);
1280
+ const pathHintResolver = buildPathHintResolver(fileIndex);
1056
1281
 
1057
1282
  return {
1058
1283
  projectRoot,
@@ -1060,26 +1285,31 @@ function buildValidationContext(projectRoot, config, plugins) {
1060
1285
  plugins,
1061
1286
  files,
1062
1287
  fileIndex,
1288
+ pathHintResolver,
1063
1289
  testFrameworks
1064
1290
  };
1065
1291
  }
1066
1292
 
1067
1293
  function validateTask(task, context, config, plugins) {
1068
- const { paths: pathHints, lineReferenceHints } = extractExplicitPaths(task.text);
1294
+ const {
1295
+ paths: pathHints,
1296
+ externalPaths,
1297
+ lineReferenceHints
1298
+ } = extractExplicitPaths(task.text);
1069
1299
  // Paths that are line-reference hints (file.ts:NN) indicate WHERE to implement,
1070
1300
  // not that implementation exists. They are excluded from hasDirectReferencePass.
1071
1301
  const purePathHints = pathHints.filter((p) => !lineReferenceHints.has(p));
1072
1302
  const standaloneFilenames = extractStandaloneFilenames(task.text);
1073
1303
  const symbolHints = extractSymbolHints(task.text);
1074
- const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.fileIndex);
1304
+ const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.pathHintResolver);
1075
1305
 
1076
- const filesFromPaths = findFilesByPathHints(pathHints, context.fileIndex);
1077
- const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.fileIndex);
1306
+ const filesFromPaths = findFilesByPathHints(pathHints, context.pathHintResolver);
1307
+ const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.pathHintResolver);
1078
1308
  const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
1079
1309
  // Combine path hints AND standalone filenames for token exclusion so that tokens
1080
1310
  // derived from any referenced filename (e.g. "roadmap-skill" from
1081
1311
  // "roadmap-skill.config.json") are excluded from code evidence scoring.
1082
- const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...standaloneFilenames]);
1312
+ const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...externalPaths, ...standaloneFilenames]);
1083
1313
  const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens);
1084
1314
  const filesFromWeakPathTokens = findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens);
1085
1315
  const weakPathContentTokens = findWeakPathContentSpecificTokens(task.text, context.fileIndex, filesFromWeakPathTokens, pathDerivedTokens);
@@ -1143,7 +1373,12 @@ function validateTask(task, context, config, plugins) {
1143
1373
  }
1144
1374
  }
1145
1375
 
1146
- const requiresTest = !task.noTest && context.testFrameworks.length > 0 && isCodeTask(task.text) && !isDocTask(task.text);
1376
+ const requiresTest =
1377
+ !task.noTest &&
1378
+ context.testFrameworks.length > 0 &&
1379
+ isCodeTask(task.text) &&
1380
+ !isDocTask(task.text) &&
1381
+ !isHttpExpectationTask(task.text);
1147
1382
  const configuredRules = Array.isArray(config.validators) ? config.validators : [];
1148
1383
  const pluginRules = collectPluginContributions(plugins || [], 'registerValidators', context);
1149
1384
  let overrideResult = null;
@@ -1181,13 +1416,14 @@ function validateTask(task, context, config, plugins) {
1181
1416
  uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
1182
1417
  }
1183
1418
 
1184
- const hasConcreteReferenceEvidence = filesFromPurePathHints.length > 0 || filesFromSymbols.length > 0;
1185
- const attempted = authoritativeEvidence.active
1186
- || hasRuleGrantedEvidence
1187
- || evidence.code
1188
- || evidence.test
1189
- || evidence.artifact
1190
- || hasConcreteReferenceEvidence;
1419
+ const hasConcreteReferenceEvidence = filesFromPaths.length > 0 || filesFromSymbols.length > 0;
1420
+ const hasConcreteAttemptEvidence =
1421
+ authoritativeEvidence.active ||
1422
+ hasRuleGrantedEvidence ||
1423
+ filesFromTests.length > 0 ||
1424
+ hasConcreteReferenceEvidence ||
1425
+ (isDocTask(task.text) && filesFromArtifacts.length > 0);
1426
+ const attempted = hasConcreteAttemptEvidence;
1191
1427
  const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
1192
1428
  const strongEvidenceCount = strongEvidenceCategories.length;
1193
1429
  // Only pure path hints (not line-reference hints like file.ts:169) count as direct evidence.