roadmapsmith 0.9.23 → 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.23",
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.23",
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.23",
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.23"
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.23"
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.23"
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.23"
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.23"
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.23"
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.23"
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.23"
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.23"
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.23"
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.23"
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,12 +3,113 @@
3
3
  const { parseRoadmap } = require('../parser');
4
4
  const { ensureTrailingNewline } = require('../utils');
5
5
 
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
+ ];
13
+
6
14
  function setChecklistState(line, checked) {
7
15
  return line.replace(/- \[( |x|X)\]/, `- [${checked ? 'x' : ' '}]`);
8
16
  }
9
17
 
10
- function formatWarning(indent, reason) {
11
- 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}`;
21
+ }
22
+
23
+ function isWhitespaceCharacter(char) {
24
+ return char === ' ' || char === '\t' || char === '\n' || char === '\r' || char === '\f' || char === '\v';
25
+ }
26
+
27
+ function stripLeadingWarningMarker(value) {
28
+ let index = 0;
29
+ const source = String(value || '');
30
+ while (index < source.length && isWhitespaceCharacter(source[index])) {
31
+ index += 1;
32
+ }
33
+ if (source.slice(index, index + 2) === '⚠️') {
34
+ index += 2;
35
+ }
36
+ while (index < source.length && isWhitespaceCharacter(source[index])) {
37
+ index += 1;
38
+ }
39
+ return source.slice(index);
40
+ }
41
+
42
+ function splitWarningReasonSegments(value) {
43
+ const source = String(value || '');
44
+ const segments = [];
45
+ let current = '';
46
+ let index = 0;
47
+
48
+ while (index < source.length) {
49
+ const char = source[index];
50
+ if (char === ';') {
51
+ const trimmed = current.trim();
52
+ if (trimmed) {
53
+ segments.push(trimmed);
54
+ }
55
+ current = '';
56
+ index += 1;
57
+ while (index < source.length && isWhitespaceCharacter(source[index])) {
58
+ index += 1;
59
+ }
60
+ continue;
61
+ }
62
+
63
+ current += char;
64
+ index += 1;
65
+ }
66
+
67
+ const trimmed = current.trim();
68
+ if (trimmed) {
69
+ segments.push(trimmed);
70
+ }
71
+
72
+ return segments;
73
+ }
74
+
75
+ function normalizeWarningReason(reason) {
76
+ let normalized = String(reason || '').trim();
77
+ if (!normalized) {
78
+ return '';
79
+ }
80
+
81
+ normalized = stripLeadingWarningMarker(normalized).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
+ }
88
+ }
89
+
90
+ return normalized;
91
+ }
92
+
93
+ function normalizeWarningReasons(reasons) {
94
+ const normalized = [];
95
+ const seen = new Set();
96
+ for (const reason of Array.isArray(reasons) ? reasons : [reasons]) {
97
+ for (const chunk of splitWarningReasonSegments(reason)) {
98
+ const clean = normalizeWarningReason(chunk);
99
+ if (!clean || seen.has(clean)) {
100
+ continue;
101
+ }
102
+ seen.add(clean);
103
+ normalized.push(clean);
104
+ }
105
+ }
106
+ return normalized;
107
+ }
108
+
109
+ function shouldPreserveExistingWarning(existingReason, newReason) {
110
+ const cleanExisting = normalizeWarningReason(existingReason);
111
+ const cleanNew = normalizeWarningReason(newReason) || 'validation failed';
112
+ return cleanNew === 'validation failed' && cleanExisting && cleanExisting !== cleanNew;
12
113
  }
13
114
 
14
115
  function applySync(content, parsedTasks, results) {
@@ -30,13 +131,13 @@ function applySync(content, parsedTasks, results) {
30
131
 
31
132
  lines[lineIndex] = setChecklistState(lines[lineIndex], result.passed);
32
133
 
33
- const reason = result.reasons.join('; ');
34
- const warningText = formatWarning(task.indent || '', reason || 'validation failed');
134
+ const reason = normalizeWarningReasons(result.reasons).join('; ');
135
+ const warningText = formatWarning(task.indent || '', reason || 'validation failed', result.attempted);
35
136
  const hasWarning = task.warningLineIndex != null;
36
137
  const warningIndex = hasWarning ? task.warningLineIndex + offset : null;
37
138
  const lastChildLineIndex = (task.lastChildLineIndex != null ? task.lastChildLineIndex : task.lineIndex) + offset;
38
139
 
39
- if (result.passed || !result.attempted) {
140
+ if (result.passed) {
40
141
  if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
41
142
  lines.splice(warningIndex, 1);
42
143
  offset -= 1;
@@ -45,11 +146,9 @@ function applySync(content, parsedTasks, results) {
45
146
  }
46
147
 
47
148
  if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
48
- const existingReason = lines[warningIndex].split('validation failed:')[1];
149
+ const existingReason = normalizeWarningReason(lines[warningIndex]);
49
150
  const newReason = reason || 'validation failed';
50
- // Preserve existing warning when it's more descriptive than the new generic message.
51
- const existingIsMoreSpecific = existingReason && existingReason.trim().length > newReason.length;
52
- if (!existingIsMoreSpecific) {
151
+ if (!shouldPreserveExistingWarning(existingReason, newReason)) {
53
152
  lines[warningIndex] = warningText;
54
153
  }
55
154
  } else {
@@ -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").
@@ -373,6 +430,10 @@ function isDocTask(taskText) {
373
430
  return /\b(add|create|write|update|init|initialize|introduce|setup|document)\b/.test(normalized);
374
431
  }
375
432
 
433
+ function isImplementationTask(taskText) {
434
+ return !isDocTask(taskText) && (isCodeTask(taskText) || taskDescribesChange(taskText));
435
+ }
436
+
376
437
  function findFilesByPathHints(pathHints, fileIndex) {
377
438
  const matches = [];
378
439
  for (const hint of pathHints) {
@@ -1139,7 +1200,12 @@ function validateTask(task, context, config, plugins) {
1139
1200
  }
1140
1201
  }
1141
1202
 
1142
- 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);
1143
1209
  const configuredRules = Array.isArray(config.validators) ? config.validators : [];
1144
1210
  const pluginRules = collectPluginContributions(plugins || [], 'registerValidators', context);
1145
1211
  let overrideResult = null;
@@ -1177,7 +1243,14 @@ function validateTask(task, context, config, plugins) {
1177
1243
  uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
1178
1244
  }
1179
1245
 
1180
- const attempted = hasStrongEvidence || hasWeakEvidence || pathHints.length > 0 || symbolHints.length > 0 || authoritativeEvidence.active;
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;
1181
1254
  const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
1182
1255
  const strongEvidenceCount = strongEvidenceCategories.length;
1183
1256
  // Only pure path hints (not line-reference hints like file.ts:169) count as direct evidence.
@@ -1220,6 +1293,7 @@ function validateTask(task, context, config, plugins) {
1220
1293
  // WHERE to implement, not that implementation is done. Unchecked tasks need authoritative
1221
1294
  // evidence, artifact evidence, or strong code+test threshold to pass.
1222
1295
  // Already-checked tasks with found path hints are preserved via shouldPreserveCheckedTask.
1296
+ const hasHighConfidenceImplementationEvidence = meetsStrongThreshold && evidence.code && evidence.test;
1223
1297
  let passed = authoritativeEvidence.passed || hasArtifactTaskPass || hasTrustedRuleEvidencePass || meetsStrongThreshold;
1224
1298
 
1225
1299
  if (!passed && !task.checked && hasDirectReferencePass) {
@@ -1234,30 +1308,29 @@ function validateTask(task, context, config, plugins) {
1234
1308
  // human/agent judgment that the feature is incomplete.
1235
1309
  if (task.warningText && !task.checked && passed && !authoritativeEvidence.passed) {
1236
1310
  passed = false;
1237
- uniqueReasons.push(task.warningText);
1238
- uniqueReasons = Array.from(new Set(uniqueReasons));
1311
+ if (uniqueReasons.length === 0) {
1312
+ uniqueReasons.push('validation failed');
1313
+ }
1239
1314
  }
1240
1315
  if (negativeSignalMatches.length > 0) {
1241
1316
  passed = false;
1242
1317
  }
1243
1318
 
1244
- // Action-verb gate (Causa 3): unchecked tasks that describe a change to be made
1245
- // (Agregar, Configurar, Add, Fix, Manejo, Recovery path, …) cannot pass on code token overlap alone.
1246
- // Requires either: an Evidence line (authoritativeEvidence.passed), high-confidence evidence
1247
- // (code + test), grant-evidence from config (hasTrustedRuleEvidencePass), or canonical artifact
1248
- // evidence (hasArtifactTaskPass — e.g. "Add SECURITY.md").
1319
+ // Unchecked implementation tasks need explicit evidence or high-confidence implementation
1320
+ // evidence. Weak token overlap, direct file references, or code-only matches are not enough.
1249
1321
  if (
1250
1322
  !task.checked &&
1251
1323
  passed &&
1252
- taskDescribesChange(task.text) &&
1324
+ isImplementationTask(task.text) &&
1253
1325
  !authoritativeEvidence.passed &&
1254
1326
  !hasTrustedRuleEvidencePass &&
1255
- !hasArtifactTaskPass
1327
+ !hasArtifactTaskPass &&
1328
+ !hasHighConfidenceImplementationEvidence
1256
1329
  ) {
1257
1330
  passed = false;
1258
- const actionVerbReason = 'action task requires Evidence line or high-confidence evidence (code + test) to be marked complete';
1259
- if (!uniqueReasons.includes(actionVerbReason)) {
1260
- uniqueReasons.push(actionVerbReason);
1331
+ const implementationReason = 'implementation task requires Evidence line or high-confidence evidence (code + test) to be marked complete';
1332
+ if (!uniqueReasons.includes(implementationReason)) {
1333
+ uniqueReasons.push(implementationReason);
1261
1334
  }
1262
1335
  }
1263
1336