roadmapsmith 0.9.24 → 0.9.25

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.25",
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.25",
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.25",
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.25"
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.25"
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.25"
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.25"
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.25"
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.25"
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.25"
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.25"
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.25"
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.25"
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.25"
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,6 +221,54 @@ 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);
@@ -278,39 +317,49 @@ function collectPathishTokens(text) {
278
317
  // to lineReferenceHints and excluded from hasDirectReferencePass scoring.
279
318
  const LINE_REF_RE = /^(.+?):(\d+)(?:-\d+)?$/;
280
319
 
320
+ function normalizeExplicitPathCandidate(rawToken) {
321
+ const clean = stripTrailingPathPunctuation(String(rawToken || '').trim());
322
+ if (!clean || clean.includes('*') || clean.includes('?')) {
323
+ return null;
324
+ }
325
+
326
+ const lineMatch = LINE_REF_RE.exec(clean);
327
+ if (lineMatch) {
328
+ const linePath = stripTrailingPathPunctuation(lineMatch[1]);
329
+ if (isRealFilePath(linePath)) {
330
+ return { path: linePath, isLineReference: true };
331
+ }
332
+ return null;
333
+ }
334
+
335
+ if (!isRealFilePath(clean)) {
336
+ return null;
337
+ }
338
+
339
+ return { path: clean, isLineReference: false };
340
+ }
341
+
281
342
  function extractExplicitPaths(text) {
282
343
  const results = new Set();
283
344
  const lineReferenceHints = new Set();
284
345
 
285
346
  const quoted = String(text).match(/`([^`]+)`/g) || [];
286
347
  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);
348
+ const normalized = normalizeExplicitPathCandidate(token.slice(1, -1));
349
+ if (!normalized) continue;
350
+ results.add(normalized.path);
351
+ if (normalized.isLineReference) {
352
+ lineReferenceHints.add(normalized.path);
299
353
  }
300
354
  }
301
355
 
302
356
  for (const word of String(text).split(/\s+/)) {
303
357
  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);
358
+ const normalized = normalizeExplicitPathCandidate(word);
359
+ if (!normalized) continue;
360
+ results.add(normalized.path);
361
+ if (normalized.isLineReference) {
362
+ lineReferenceHints.add(normalized.path);
314
363
  }
315
364
  }
316
365
 
@@ -363,6 +412,14 @@ function isCodeTask(taskText) {
363
412
  return CODE_HINTS.some((hint) => normalized.includes(hint));
364
413
  }
365
414
 
415
+ function isHttpExpectationTask(taskText) {
416
+ const text = String(taskText || '');
417
+ if (!/(?:->|→)/.test(text) || !/\bHTTP\s+\d{3}\b/i.test(text)) {
418
+ return false;
419
+ }
420
+ return /\b(?:GET|POST|PUT|PATCH|DELETE)\b/i.test(text) || /\/api\//i.test(text);
421
+ }
422
+
366
423
  function isDocTask(taskText) {
367
424
  const normalized = String(taskText).toLowerCase();
368
425
  // Use word-boundary matching to avoid substring false positives (e.g. "specific" ≠ "spec").
@@ -1143,7 +1200,12 @@ function validateTask(task, context, config, plugins) {
1143
1200
  }
1144
1201
  }
1145
1202
 
1146
- const requiresTest = !task.noTest && context.testFrameworks.length > 0 && isCodeTask(task.text) && !isDocTask(task.text);
1203
+ const requiresTest =
1204
+ !task.noTest &&
1205
+ context.testFrameworks.length > 0 &&
1206
+ isCodeTask(task.text) &&
1207
+ !isDocTask(task.text) &&
1208
+ !isHttpExpectationTask(task.text);
1147
1209
  const configuredRules = Array.isArray(config.validators) ? config.validators : [];
1148
1210
  const pluginRules = collectPluginContributions(plugins || [], 'registerValidators', context);
1149
1211
  let overrideResult = null;
@@ -1181,13 +1243,14 @@ function validateTask(task, context, config, plugins) {
1181
1243
  uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
1182
1244
  }
1183
1245
 
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;
1246
+ const hasConcreteReferenceEvidence = filesFromPaths.length > 0 || filesFromSymbols.length > 0;
1247
+ const hasConcreteAttemptEvidence =
1248
+ authoritativeEvidence.active ||
1249
+ hasRuleGrantedEvidence ||
1250
+ filesFromTests.length > 0 ||
1251
+ hasConcreteReferenceEvidence ||
1252
+ (isDocTask(task.text) && filesFromArtifacts.length > 0);
1253
+ const attempted = hasConcreteAttemptEvidence;
1191
1254
  const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
1192
1255
  const strongEvidenceCount = strongEvidenceCategories.length;
1193
1256
  // Only pure path hints (not line-reference hints like file.ts:169) count as direct evidence.