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.
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/README.md +5 -3
- package/bin/cli.js +4 -3
- package/package.json +2 -2
- package/skills/roadmap-sync/agents/openai.yaml +1 -1
- package/skills.json +13 -13
- package/src/host.js +23 -4
- package/src/parser/index.js +21 -3
- package/src/slash.js +1 -1
- package/src/sync/index.js +108 -9
- package/src/validator/index.js +121 -48
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.9.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
50
|
-
"version": "0.9.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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([\'
|
|
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
|
|
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();
|
package/src/parser/index.js
CHANGED
|
@@ -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 = '⚠️
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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]
|
|
149
|
+
const existingReason = normalizeWarningReason(lines[warningIndex]);
|
|
49
150
|
const newReason = reason || 'validation failed';
|
|
50
|
-
|
|
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 {
|
package/src/validator/index.js
CHANGED
|
@@ -197,18 +197,9 @@ function hasFileExtension(token) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
function isLikelyPath(token) {
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
1238
|
-
|
|
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
|
-
//
|
|
1245
|
-
//
|
|
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
|
-
|
|
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
|
|
1259
|
-
if (!uniqueReasons.includes(
|
|
1260
|
-
uniqueReasons.push(
|
|
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
|
|