roadmapsmith 0.9.27 → 0.9.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.27",
3
+ "version": "0.9.29",
4
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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.27",
3
+ "version": "0.9.29",
4
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"
package/README.md CHANGED
@@ -28,7 +28,7 @@ npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code
28
28
  ```
29
29
 
30
30
  This is the recommended Claude Code install path for native GUI slash commands such as `/roadmap`, `/roadmap-zero`, `/roadmap-maintain`, `/roadmap-status`, `/roadmap-init`, `/roadmap-generate`, `/roadmap-validate`, `/roadmap-update`, `/roadmap-audit`, and `/roadmap-setup`.
31
- If you install only `--skill roadmap-sync`, Claude GUI will expose only `/roadmap-sync`.
31
+ `roadmap-sync` is deprecated compatibility only; install the full bundle for new workflows and use `/roadmap-maintain` or `/roadmap-update`.
32
32
  The skill bundle does not install the CLI and it does not create visible VS Code actions by itself.
33
33
  The published `roadmapsmith` package/plugin surface now also ships the shared bundle files for both hosts (`skills.json`, `skills/*`, `.codex-plugin/plugin.json`, `.claude-plugin/plugin.json`) for downstream host installers, but consuming the CLI alone still does not auto-register native Codex or Claude GUI commands.
34
34
 
@@ -92,6 +92,19 @@ roadmapsmith maintain # existing repo
92
92
 
93
93
  Use the lower-level commands only when you want manual control over generation, validation, or sync.
94
94
 
95
+ ## Deterministic completion
96
+
97
+ For unchecked implementation tasks, related files and matching test names are candidate signals only. `maintain` completes a task only from explicit `Evidence:` or typed child verification metadata:
98
+
99
+ ```markdown
100
+ - [ ] Enable ESLint builds
101
+ - Verify: kind=property; file=next.config.js; key=ignoreDuringBuilds; equals=false
102
+ - [ ] Prevent double submit
103
+ - Verify: kind=behavior; source=src/login.tsx; test=src/__tests__/login.test.tsx; case=disables submit; trigger=fireEvent.click; assertion=toBeDisabled
104
+ ```
105
+
106
+ `contains`, `property`, and `endpoints` are static checks. A behavioral check also requires a fresh `validation.testReports` entry using `format: "vitest-json"`; RoadmapSmith reads reports but never executes tests. Missing proof produces a `Verification recipe:` and a warning rather than a checked box. A fresh passing report is persisted as `Test evidence: file=<test>; case=<case>; status=PASS; verifiedAt=<ISO timestamp>` and becomes stale when it predates the referenced source or test file.
107
+
95
108
  ## Host Support Today
96
109
 
97
110
  | Host | Current support |
@@ -101,7 +114,11 @@ Use the lower-level commands only when you want manual control over generation,
101
114
  | CI | Use disposable checkouts if you run `sync --audit`, because it still mutates the roadmap today. |
102
115
  | Other hosts | Use the skill plus manual CLI commands. |
103
116
 
104
- If Node is installed outside PATH, set `ROADMAPSMITH_NODE` to a working `node` executable before using the generated VS Code tasks.
117
+ If Node is installed outside PATH, set `ROADMAPSMITH_NODE` to a working `node` executable before using the generated VS Code tasks. If the globally installed `roadmapsmith` shim itself cannot resolve Node, bypass it in PowerShell with:
118
+
119
+ ```powershell
120
+ & "C:\Program Files\nodejs\node.exe" "$env:APPDATA\npm\node_modules\roadmapsmith\bin\cli.js" <command>
121
+ ```
105
122
 
106
123
  ---
107
124
 
@@ -119,6 +136,7 @@ roadmapsmith maintain [--project-root <path>] [--config <path>] [--roadmap-file
119
136
  roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]
120
137
  roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit] [--full-regen]
121
138
  roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]
139
+ roadmapsmith update --task <stable-id> --evidence <text> [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run]
122
140
  roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]
123
141
  roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]
124
142
  roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json] # compatibility alias
@@ -176,13 +194,14 @@ The repo does not remove user-global skills automatically. Use the `doctor` outp
176
194
  - Uses stable task IDs: `<!-- rs:task=<slug> -->`.
177
195
  - Sync marks `[x]` only when validation passes.
178
196
  - `sync --audit` currently runs sync and then prints a mismatch summary; it is not yet a dedicated read-only audit mode.
179
- - Validation evidence gate:
180
- - code OR test OR artifact evidence required.
181
- - test evidence required for code tasks when test frameworks are detected.
197
+ - For unchecked implementation tasks, heuristic code/test/path matches are candidates only. Completion requires explicit `Evidence:`, a trusted validator rule, or typed deterministic `Verify:` metadata.
198
+ - `Verify:` supports `contains`, `property`, `endpoints`, and `behavior`; behavior additionally requires a fresh configured Vitest JSON report or a fresh `Test evidence:` annotation.
182
199
  - Validation failures in sync write warning lines:
183
200
  - `- ⚠️ attempted but validation failed: <reason>` when there is concrete attempt evidence
184
201
  - `- ⚠️ no implementation evidence found yet: <reason>` when there is not
185
202
  - Preserves unmanaged markdown content by updating only the managed roadmap block.
203
+ - `validate` emits structured diagnostics in human and JSON output, including `FAIL:NOT_IMPLEMENTED`, `FAIL:NO_TEST`, `FAIL:MISSING_REFERENCE`, `FAIL:WRONG_VALUE`, `FAIL:PARTIAL`, `WARN:STALE_EVIDENCE`, `WARN:REQUIRES_HUMAN_EVIDENCE`, `WARN:NO_STATIC_SIGNAL`, `WARN:HAS_EXPLICIT_PENDING`, and `WARN:STALE_TEST_REPORT`.
204
+ - `update --task <id> --evidence <text>` writes only when the evidence validates at high confidence; otherwise the roadmap is unchanged.
186
205
 
187
206
  ## Defaults
188
207
 
package/bin/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const readline = require('node:readline/promises');
7
- const { parseArgv } = require('../src/utils');
7
+ const { ensureTrailingNewline, parseArgv } = require('../src/utils');
8
8
  const { loadConfig, resolveRoadmapFile, resolveAgentsFile, loadPlugins, readUserConfig, resolveConfigPath } = require('../src/config');
9
9
  const { readTextIfExists, writeText, printDryRunDiff } = require('../src/io');
10
10
  const { buildSetupFiles, applySetupFiles, inspectHostSetup, parseHosts, assertSupportedEditor } = require('../src/host');
@@ -30,6 +30,7 @@ function printHelp() {
30
30
  ' roadmapsmith setup [--project-root <path>] [--config <path>] [--editor vscode] [--hosts <codex,claude>] [--dry-run]',
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
+ ' roadmapsmith update --task <stable-id> --evidence <text> [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run]',
33
34
  ' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]',
34
35
  ' roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]',
35
36
  ' roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json] # compatibility alias'
@@ -44,8 +45,15 @@ function isEnabled(value) {
44
45
  }
45
46
 
46
47
  function formatResultLine(task, result) {
47
- const status = result.passed ? 'PASS' : 'FAIL';
48
- const reason = result.reasons.length > 0 ? ` :: ${result.reasons.join('; ')}` : '';
48
+ const diagnostics = Array.isArray(result.diagnostics) ? result.diagnostics : [];
49
+ const primaryError = diagnostics.find((item) => item.severity === 'error');
50
+ const warnings = diagnostics.filter((item) => item.severity === 'warning');
51
+ const status = primaryError
52
+ ? `FAIL:${primaryError.code}`
53
+ : (result.passed ? 'PASS' : (warnings[0] ? `WARN:${warnings[0].code}` : 'FAIL'));
54
+ const parts = [...result.reasons];
55
+ warnings.forEach((item) => parts.push(`WARN:${item.code} ${item.message}`));
56
+ const reason = parts.length > 0 ? ` :: ${parts.join('; ')}` : '';
49
57
  return `${status} [${task.id}] ${task.text}${reason}`;
50
58
  }
51
59
 
@@ -215,6 +223,77 @@ function runSyncCommand(projectRoot, config, flags, options = {}) {
215
223
  }
216
224
  }
217
225
 
226
+ function addEvidenceToTask(content, task, evidenceText) {
227
+ const lines = String(content || '').split(/\r?\n/);
228
+ const evidenceLine = `${task.indent || ''} - Evidence: ${evidenceText}`;
229
+ if (Array.isArray(task.evidenceLines) && task.evidenceLines.length > 0) {
230
+ lines[task.evidenceLines[0].lineIndex] = evidenceLine;
231
+ return ensureTrailingNewline(lines.join('\n'));
232
+ }
233
+
234
+ const insertionIndex = task.warningLineIndex != null
235
+ ? task.warningLineIndex
236
+ : task.lastChildLineIndex + 1;
237
+ lines.splice(insertionIndex, 0, evidenceLine);
238
+ return ensureTrailingNewline(lines.join('\n'));
239
+ }
240
+
241
+ function runUpdateCommand(projectRoot, config, flags) {
242
+ const hasTask = flags.task != null;
243
+ const hasEvidence = flags.evidence != null;
244
+ if (!hasTask && !hasEvidence) {
245
+ runSyncCommand(projectRoot, config, flags);
246
+ return;
247
+ }
248
+ if (!hasTask || !hasEvidence || Array.isArray(flags.task) || Array.isArray(flags.evidence)) {
249
+ throw new Error('update requires exactly one --task <stable-id> and one --evidence <text> value');
250
+ }
251
+
252
+ const taskId = String(flags.task).trim();
253
+ const evidenceText = String(flags.evidence).trim();
254
+ if (!taskId || !evidenceText || /[\r\n]/.test(evidenceText)) {
255
+ throw new Error('update requires a non-empty single-line --task and --evidence value');
256
+ }
257
+
258
+ const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
259
+ const content = readTextIfExists(roadmapFile);
260
+ if (content == null) {
261
+ throw new Error(`Roadmap not found: ${roadmapFile}`);
262
+ }
263
+
264
+ const parsedRoadmap = parseRoadmap(content);
265
+ const matches = tasksInManagedBlock(parsedRoadmap).filter((task) => task.id === taskId);
266
+ if (matches.length !== 1) {
267
+ throw new Error(matches.length === 0
268
+ ? `Roadmap task not found: ${taskId}`
269
+ : `Roadmap task ID is ambiguous: ${taskId}`);
270
+ }
271
+
272
+ const draft = addEvidenceToTask(content, matches[0], evidenceText);
273
+ const draftTask = tasksInManagedBlock(parseRoadmap(draft)).find((task) => task.id === taskId);
274
+ const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
275
+ const result = validateTasks([draftTask], validationContext, config, validationContext.plugins)[taskId];
276
+ const errors = (result.diagnostics || []).filter((item) => item.severity === 'error');
277
+ const suppliedEvidenceResolved = result.evidence.authoritative && result.evidence.authoritativeFiles.length > 0;
278
+ if (!suppliedEvidenceResolved || !result.passed || result.confidence !== 'high' || errors.length > 0) {
279
+ const reasons = result.reasons.length > 0 ? `: ${result.reasons.join('; ')}` : '';
280
+ throw new Error(`Task ${taskId} was not updated; supplied evidence must resolve in the repository and validate at high confidence${reasons}`);
281
+ }
282
+
283
+ const next = applySync(draft, [draftTask], { [taskId]: result });
284
+ const dryRun = isEnabled(flags['dry-run']);
285
+ const writeResult = writeText(roadmapFile, next, { dryRun });
286
+ if (dryRun) {
287
+ if (writeResult.changed) {
288
+ printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
289
+ } else {
290
+ console.log(`No changes for ${roadmapFile}`);
291
+ }
292
+ } else {
293
+ console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
294
+ }
295
+ }
296
+
218
297
  function printHumanStatus(payload) {
219
298
  console.log('RoadmapSmith status\n');
220
299
  console.log(`Project root: ${payload.projectRoot}`);
@@ -343,6 +422,8 @@ async function run() {
343
422
  if (slashAction.id === 'audit') {
344
423
  flags.audit = true;
345
424
  effectiveCommand = 'sync';
425
+ } else if (slashAction.id === 'sync' && String(command).trim().toLowerCase() === '/roadmap-update') {
426
+ effectiveCommand = 'update';
346
427
  } else {
347
428
  effectiveCommand = slashAction.id;
348
429
  }
@@ -411,6 +492,13 @@ async function run() {
411
492
  return;
412
493
  }
413
494
 
495
+ if (effectiveCommand === 'update') {
496
+ const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
497
+ const config = loadConfig({ projectRoot, configPath: flags.config });
498
+ runUpdateCommand(projectRoot, config, flags);
499
+ return;
500
+ }
501
+
414
502
  if (effectiveCommand === 'validate') {
415
503
  const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
416
504
  const config = loadConfig({ projectRoot, configPath: flags.config });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.27",
3
+ "version": "0.9.29",
4
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": {
@@ -13,7 +13,7 @@ Use this command as the native discovery entrypoint for the shared RoadmapSmith
13
13
  2. When working inside the RoadmapSmith repository itself and `roadmap-skill/bin/cli.js` exists, prefer the local engine:
14
14
  - `node roadmap-skill/bin/cli.js /roadmap`
15
15
  - on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js /roadmap` if `node` is not in PATH
16
- 3. Otherwise, if the `roadmapsmith` CLI is available, you may run `roadmapsmith /roadmap` from the project root and use that output directly.
16
+ 3. Otherwise, if the `roadmapsmith` CLI is available, you may run `roadmapsmith /roadmap` from the project root and use that output directly. If its global shim cannot resolve `node`, use `& "C:\Program Files\nodejs\node.exe" "$env:APPDATA\npm\node_modules\roadmapsmith\bin\cli.js" /roadmap`.
17
17
  4. If the CLI is missing, provide the palette manually and explain the install path:
18
18
  - `npm install -g roadmapsmith`
19
19
  - `npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code`
@@ -12,7 +12,8 @@ Use this command when the repository already has code, tests, docs, or an existi
12
12
  1. Prefer the local engine inside this repository:
13
13
  - `node roadmap-skill/bin/cli.js maintain --project-root .`
14
14
  - on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js maintain --project-root .` if `node` is not in PATH
15
- 2. Otherwise prefer `roadmapsmith maintain --project-root .`.
15
+ 2. Otherwise prefer `roadmapsmith maintain --project-root .`. If that global shim fails because `node` is not in PATH, run `& "C:\Program Files\nodejs\node.exe" "$env:APPDATA\npm\node_modules\roadmapsmith\bin\cli.js" maintain --project-root .`.
16
16
  3. Treat this command as CLI-backed. Do not silently replace it with manual reasoning when the CLI is unavailable.
17
17
  4. Mention that maintain runs preserve-first generate, sync, and audit in one invocation.
18
- 5. Mention that `roadmapsmith maintain --full-regen` or `roadmapsmith generate --full-regen` is the explicit destructive rebuild path when the user truly wants a full managed-block replacement.
18
+ 5. After a successful maintain cycle, do not propose generate, sync, or audit separately unless the user needs manual control or inspection.
19
+ 6. Mention that `roadmapsmith maintain --full-regen` or `roadmapsmith generate --full-regen` is the explicit destructive rebuild path when the user truly wants a full managed-block replacement.
@@ -12,6 +12,6 @@ Use this command to inspect whether the shared bundle, native host surfaces, CLI
12
12
  1. Prefer the local engine inside this repository:
13
13
  - `node roadmap-skill/bin/cli.js doctor --json --project-root .`
14
14
  - on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js doctor --json --project-root .` if `node` is not in PATH
15
- 2. Otherwise prefer `roadmapsmith doctor --json --project-root .`.
15
+ 2. Otherwise prefer `roadmapsmith doctor --json --project-root .`. If its global shim cannot resolve `node`, use `& "C:\Program Files\nodejs\node.exe" "$env:APPDATA\npm\node_modules\roadmapsmith\bin\cli.js" doctor --json --project-root .`.
16
16
  3. Parse and summarize the JSON output in plain language.
17
17
  4. Explicitly call out missing commands or duplicate `/roadmap-sync` registration when doctor reports them.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: roadmap-sync
3
- description: Legacy namespaced root and policy guidance for RoadmapSmith slash workflows.
3
+ description: DEPRECATED legacy root for RoadmapSmith slash workflows; use roadmap-maintain or roadmap-update.
4
4
  ---
5
5
 
6
- # RoadmapSmith Legacy Root
6
+ # RoadmapSmith Legacy Root (Deprecated)
7
7
 
8
- Use this skill when the host exposes or the user invokes `/roadmap-sync`, or when the agent needs the RoadmapSmith operating rules for roadmap maintenance.
8
+ Use this skill only when the host exposes or the user explicitly invokes `/roadmap-sync`. For new work, use `/roadmap-maintain` for the daily flow or `/roadmap-update` for evidence-backed completion.
9
9
 
10
10
  ## Required behavior
11
11
 
@@ -17,4 +17,4 @@ Use this skill when the host exposes or the user invokes `/roadmap-sync`, or whe
17
17
  - `/roadmap-status`
18
18
  - `/roadmap-init`, `/roadmap-generate`, `/roadmap-validate`, `/roadmap-update`, `/roadmap-audit`, and `/roadmap-setup`
19
19
  3. When the user explicitly invokes `/roadmap-sync <action>`, route to the matching CLI-backed action without changing semantics and mention the migration path to `/roadmap <action>` or the direct `/roadmap-*` command.
20
- 4. Preserve the operating rules for evidence-backed roadmap maintenance and checklist synchronization.
20
+ 4. Preserve the operating rules for evidence-backed roadmap maintenance and checklist synchronization: heuristic file/token matches may diagnose candidates, but only explicit `Evidence:` or typed `Verify:` checks may complete an unchecked implementation task. Never claim a behavioral task is complete without fresh test evidence or human verification.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "interface": {
3
- "display_name": "Roadmap Sync",
4
- "short_description": "Legacy root plus policy for evidence-backed ROADMAP.md slash workflows.",
3
+ "display_name": "Roadmap Sync (Deprecated)",
4
+ "short_description": "DEPRECATED legacy root; use /roadmap-maintain or /roadmap-update.",
5
5
  "default_prompt": "Prefer /roadmap, /roadmap-status, /roadmap-maintain, and /roadmap-update."
6
6
  }
7
7
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: roadmap-update
3
- description: Apply evidence-backed checklist sync through the RoadmapSmith CLI.
3
+ description: Apply evidence-backed checklist sync or complete one task with verified evidence through the RoadmapSmith CLI.
4
4
  ---
5
5
 
6
6
  # RoadmapSmith Update
@@ -10,8 +10,9 @@ Use this command when the user wants the direct sync surface without routing thr
10
10
  ## Required behavior
11
11
 
12
12
  1. Prefer the local engine inside this repository:
13
- - `node roadmap-skill/bin/cli.js sync --project-root .`
14
- - on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js sync --project-root .` if `node` is not in PATH
15
- 2. Otherwise prefer `roadmapsmith sync --project-root .`.
13
+ - `node roadmap-skill/bin/cli.js update --project-root .`
14
+ - on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js update --project-root .` if `node` is not in PATH
15
+ 2. Otherwise prefer `roadmapsmith update --project-root .`. If that global shim fails because `node` is not in PATH, run `& "C:\Program Files\nodejs\node.exe" "$env:APPDATA\npm\node_modules\roadmapsmith\bin\cli.js" update --project-root .`.
16
16
  3. Explain that `/roadmap-update` is the visible namespaced sync command, while `/roadmap-sync <action>` remains legacy compatibility.
17
- 4. Keep the evidence-backed sync semantics unchanged: sync updates checklist state from repository evidence; it is not a full regeneration path.
17
+ 4. Keep the evidence-backed sync semantics unchanged: no-argument update syncs the roadmap from repository evidence; it is not a full regeneration path.
18
+ 5. To complete one task, run `roadmapsmith update --task <stable-id> --evidence "<single-line evidence>"`. It writes only after the supplied evidence validates at high confidence; use `--dry-run` to preview it.
package/skills.json CHANGED
@@ -10,86 +10,85 @@
10
10
  ],
11
11
  "usageExamples": [
12
12
  "npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
13
- "npx skills add PapiScholz/roadmapsmith --skill roadmap-sync",
14
13
  "roadmapsmith setup",
15
14
  "roadmapsmith zero",
16
15
  "roadmapsmith maintain",
17
16
  "roadmapsmith /roadmap",
18
- "roadmapsmith /roadmap-update",
17
+ "roadmapsmith update --task p2-customer-history --evidence \"src/app/api/customers/route.ts, test/customers.test.js\"",
19
18
  "roadmapsmith validate --json"
20
19
  ],
21
20
  "install": {
22
21
  "command": "npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
23
22
  "source": "PapiScholz/roadmapsmith",
24
23
  "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; 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."
24
+ "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. DEPRECATED: /roadmap-sync remains only as a compatibility root for existing automation; use /roadmap-maintain or /roadmap-update for new workflows. Install the roadmapsmith CLI separately for actual command execution; roadmapsmith status is the 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
25
  },
27
26
  "skills": [
28
27
  {
29
28
  "name": "roadmap",
30
29
  "path": "skills/roadmap",
31
30
  "description": "Native slash palette for RoadmapSmith commands and recommended entrypoints across supported hosts.",
32
- "version": "0.9.27"
31
+ "version": "0.9.29"
33
32
  },
34
33
  {
35
34
  "name": "roadmap-zero",
36
35
  "path": "skills/roadmap-zero",
37
36
  "description": "Native slash entrypoint for the one-command Zero Mode CLI workflow.",
38
- "version": "0.9.27"
37
+ "version": "0.9.29"
39
38
  },
40
39
  {
41
40
  "name": "roadmap-maintain",
42
41
  "path": "skills/roadmap-maintain",
43
42
  "description": "Native slash entrypoint for the preserve-first generate + sync + audit flow.",
44
- "version": "0.9.27"
43
+ "version": "0.9.29"
45
44
  },
46
45
  {
47
46
  "name": "roadmap-status",
48
47
  "path": "skills/roadmap-status",
49
48
  "description": "Native slash readiness check grounded in roadmapsmith status JSON.",
50
- "version": "0.9.27"
49
+ "version": "0.9.29"
51
50
  },
52
51
  {
53
52
  "name": "roadmap-init",
54
53
  "path": "skills/roadmap-init",
55
54
  "description": "Native slash entrypoint for creating ROADMAP.md and AGENTS.md.",
56
- "version": "0.9.27"
55
+ "version": "0.9.29"
57
56
  },
58
57
  {
59
58
  "name": "roadmap-generate",
60
59
  "path": "skills/roadmap-generate",
61
60
  "description": "Native slash entrypoint for managed roadmap updates that require --full-regen before destructive replacement.",
62
- "version": "0.9.27"
61
+ "version": "0.9.29"
63
62
  },
64
63
  {
65
64
  "name": "roadmap-validate",
66
65
  "path": "skills/roadmap-validate",
67
66
  "description": "Native slash entrypoint for evidence-backed roadmap validation.",
68
- "version": "0.9.27"
67
+ "version": "0.9.29"
69
68
  },
70
69
  {
71
70
  "name": "roadmap-update",
72
71
  "path": "skills/roadmap-update",
73
- "description": "Native slash entrypoint for applying evidence-backed checklist sync.",
74
- "version": "0.9.27"
72
+ "description": "Native slash entrypoint for evidence-backed sync and verified single-task completion.",
73
+ "version": "0.9.29"
75
74
  },
76
75
  {
77
76
  "name": "roadmap-sync",
78
77
  "path": "skills/roadmap-sync",
79
- "description": "Legacy namespaced root plus policy guidance for RoadmapSmith slash workflows.",
80
- "version": "0.9.27"
78
+ "description": "DEPRECATED legacy compatibility root; use roadmap-maintain or roadmap-update.",
79
+ "version": "0.9.29"
81
80
  },
82
81
  {
83
82
  "name": "roadmap-audit",
84
83
  "path": "skills/roadmap-audit",
85
84
  "description": "Native slash entrypoint for the current sync-plus-audit workflow.",
86
- "version": "0.9.27"
85
+ "version": "0.9.29"
87
86
  },
88
87
  {
89
88
  "name": "roadmap-setup",
90
89
  "path": "skills/roadmap-setup",
91
90
  "description": "Native slash entrypoint for generating RoadmapSmith host integration files.",
92
- "version": "0.9.27"
91
+ "version": "0.9.29"
93
92
  }
94
93
  ]
95
94
  }
package/src/config.js CHANGED
@@ -31,7 +31,9 @@ const DEFAULT_CONFIG = {
31
31
  doneCriteria: []
32
32
  },
33
33
  validation: {
34
- minimumConfidence: 'low'
34
+ minimumConfidence: 'low',
35
+ testReports: [],
36
+ recipeCommand: ''
35
37
  },
36
38
  milestones: [
37
39
  { version: 'v0.1', goal: 'Foundation baseline complete' },
@@ -95,7 +97,10 @@ function mergeConfig(userConfig) {
95
97
  },
96
98
  validation: {
97
99
  ...DEFAULT_CONFIG.validation,
98
- ...((userConfig && userConfig.validation) || {})
100
+ ...((userConfig && userConfig.validation) || {}),
101
+ testReports: userConfig && userConfig.validation && Array.isArray(userConfig.validation.testReports)
102
+ ? userConfig.validation.testReports
103
+ : DEFAULT_CONFIG.validation.testReports
99
104
  }
100
105
  };
101
106
  }
package/src/io.js CHANGED
@@ -13,7 +13,9 @@ const DEFAULT_IGNORED_DIRS = new Set([
13
13
  '.turbo',
14
14
  '.cache',
15
15
  'dist',
16
+ 'dist-electron',
16
17
  'build',
18
+ 'out',
17
19
  'coverage',
18
20
  'target',
19
21
  'node_modules',
@@ -89,6 +89,26 @@ function parseEvidenceLine(content) {
89
89
  return content.slice(9).trim();
90
90
  }
91
91
 
92
+ function parsePrefixedChildLine(content, prefix) {
93
+ const normalizedPrefix = `${prefix}:`;
94
+ if (content.slice(0, normalizedPrefix.length).toLowerCase() !== normalizedPrefix.toLowerCase()) {
95
+ return null;
96
+ }
97
+ return content.slice(normalizedPrefix.length).trim();
98
+ }
99
+
100
+ function parseVerifyLine(content) {
101
+ return parsePrefixedChildLine(content, 'Verify');
102
+ }
103
+
104
+ function parseTestEvidenceLine(content) {
105
+ return parsePrefixedChildLine(content, 'Test evidence');
106
+ }
107
+
108
+ function parseVerificationRecipeLine(content) {
109
+ return parsePrefixedChildLine(content, 'Verification recipe');
110
+ }
111
+
92
112
  function parseWarningLine(content) {
93
113
  if (!content.startsWith(WARNING_PREFIX)) return null;
94
114
  let normalized = content.slice(WARNING_PREFIX.length).trim();
@@ -132,6 +152,10 @@ function parseRoadmap(content) {
132
152
  let warningLineIndex = null;
133
153
  let warningText = null;
134
154
  const evidenceLines = [];
155
+ const verifyLines = [];
156
+ const testEvidenceLines = [];
157
+ const explicitPendingItems = [];
158
+ let verificationRecipeLineIndex = null;
135
159
  const blockedByIds = [];
136
160
  let lastChildLineIndex = index;
137
161
  for (let childIndex = index + 1; childIndex < lines.length; childIndex += 1) {
@@ -162,6 +186,28 @@ function parseRoadmap(content) {
162
186
  });
163
187
  }
164
188
 
189
+ const verifyText = parseVerifyLine(childBullet.content);
190
+ if (verifyText != null) {
191
+ verifyLines.push({ lineIndex: childIndex, text: verifyText, raw: childLine });
192
+ }
193
+
194
+ const testEvidenceText = parseTestEvidenceLine(childBullet.content);
195
+ if (testEvidenceText != null) {
196
+ testEvidenceLines.push({ lineIndex: childIndex, text: testEvidenceText, raw: childLine });
197
+ }
198
+
199
+ const verificationRecipeText = parseVerificationRecipeLine(childBullet.content);
200
+ if (verificationRecipeText != null) {
201
+ verificationRecipeLineIndex = childIndex;
202
+ }
203
+
204
+ if (childBullet.content.startsWith('❌')) {
205
+ explicitPendingItems.push({
206
+ lineIndex: childIndex,
207
+ text: childBullet.content.slice('❌'.length).trim()
208
+ });
209
+ }
210
+
165
211
  const warningTextValue = parseWarningLine(childBullet.content);
166
212
  if (warningTextValue != null) {
167
213
  warningLineIndex = childIndex;
@@ -190,6 +236,10 @@ function parseRoadmap(content) {
190
236
  warningLineIndex,
191
237
  warningText,
192
238
  evidenceLines,
239
+ verifyLines,
240
+ testEvidenceLines,
241
+ verificationRecipeLineIndex,
242
+ explicitPendingItems,
193
243
  blockedByIds,
194
244
  markerId,
195
245
  noTest,
package/src/sync/index.js CHANGED
@@ -112,6 +112,23 @@ function shouldPreserveExistingWarning(existingReason, newReason) {
112
112
  return cleanNew === 'validation failed' && cleanExisting && cleanExisting !== cleanNew;
113
113
  }
114
114
 
115
+ function formatVerificationRecipe(indent, recipe) {
116
+ return `${indent} - Verification recipe: ${recipe}`;
117
+ }
118
+
119
+ function formatTestEvidence(indent, evidence) {
120
+ return `${indent} - Test evidence: ${evidence}`;
121
+ }
122
+
123
+ function findVerificationRecipeIndex(lines, taskLineIndex) {
124
+ for (let index = taskLineIndex + 1; index < lines.length; index += 1) {
125
+ const line = lines[index];
126
+ if (!line.trim() || /^\s*#{2,}\s/.test(line) || /^\s*- \[[ xX]\]\s/.test(line)) break;
127
+ if (/^\s*- Verification recipe:/i.test(line)) return index;
128
+ }
129
+ return null;
130
+ }
131
+
115
132
  function applySync(content, parsedTasks, results) {
116
133
  const parsed = parseRoadmap(content);
117
134
  const lines = [...parsed.lines];
@@ -142,10 +159,32 @@ function applySync(content, parsedTasks, results) {
142
159
  lines.splice(warningIndex, 1);
143
160
  offset -= 1;
144
161
  }
145
- continue;
146
- }
147
-
148
- if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
162
+ const recipeIndex = findVerificationRecipeIndex(lines, lineIndex);
163
+ if (recipeIndex != null && recipeIndex >= 0 && recipeIndex < lines.length) {
164
+ lines.splice(recipeIndex, 1);
165
+ offset -= 1;
166
+ }
167
+ if (
168
+ result.generatedTestEvidence &&
169
+ (!Array.isArray(task.testEvidenceLines) || task.testEvidenceLines.length === 0)
170
+ ) {
171
+ const insertionIndex = Math.max(lineIndex + 1, (task.lastChildLineIndex + offset) + 1);
172
+ lines.splice(insertionIndex, 0, formatTestEvidence(task.indent || '', result.generatedTestEvidence));
173
+ offset += 1;
174
+ }
175
+ if (
176
+ result.staleEvidenceResolved &&
177
+ (!Array.isArray(task.evidenceLines) || task.evidenceLines.length === 0) &&
178
+ result.discoveredEvidence
179
+ ) {
180
+ const insertionIndex = Math.max(
181
+ lineIndex + 1,
182
+ (task.lastChildLineIndex + offset) + 1
183
+ );
184
+ lines.splice(insertionIndex, 0, `${task.indent || ''} - Evidence: ${result.discoveredEvidence}`);
185
+ offset += 1;
186
+ }
187
+ } else if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
149
188
  const existingReason = normalizeWarningReason(lines[warningIndex]);
150
189
  const newReason = reason || 'validation failed';
151
190
  if (!shouldPreserveExistingWarning(existingReason, newReason)) {
@@ -155,6 +194,23 @@ function applySync(content, parsedTasks, results) {
155
194
  lines.splice(lastChildLineIndex + 1, 0, warningText);
156
195
  offset += 1;
157
196
  }
197
+
198
+ const recipeIndex = findVerificationRecipeIndex(lines, lineIndex);
199
+ if (result.passed) {
200
+ continue;
201
+ }
202
+ if (result.verificationRecipe) {
203
+ const recipeLine = formatVerificationRecipe(task.indent || '', result.verificationRecipe);
204
+ if (recipeIndex != null && recipeIndex >= 0 && recipeIndex < lines.length) {
205
+ lines[recipeIndex] = recipeLine;
206
+ } else {
207
+ lines.splice(lastChildLineIndex + 1, 0, recipeLine);
208
+ offset += 1;
209
+ }
210
+ } else if (recipeIndex != null && recipeIndex >= 0 && recipeIndex < lines.length) {
211
+ lines.splice(recipeIndex, 1);
212
+ offset -= 1;
213
+ }
158
214
  }
159
215
 
160
216
  return ensureTrailingNewline(lines.join('\n'));
@@ -13,6 +13,7 @@ const CODE_EXTENSIONS = new Set([
13
13
  ]);
14
14
  const TRANSLATION_DIR_SEGMENTS = ['locale', 'locales', 'i18n', 'translations'];
15
15
  const DEFAULT_EXCLUDED_PATH_PREFIXES = ['.claude/', '.agent/', 'roadmap-skill/'];
16
+ const GENERATED_OUTPUT_PREFIXES = ['dist-electron/', 'dist/', 'build/', 'out/', '.next/', 'coverage/'];
16
17
 
17
18
  // "docs" omitted from DOC_HINTS — it is a path prefix in scan tasks, not a doc-authoring keyword.
18
19
  const DOC_HINTS = ['readme', 'changelog', 'documentation', 'spec', 'diagram', 'runbook'];
@@ -151,6 +152,11 @@ function isMostlyUiStrings(content) {
151
152
  return stringLikeLines / lines.length > 0.8;
152
153
  }
153
154
 
155
+ function isGeneratedOutputPath(relativePath) {
156
+ const normalized = normalizePathForMatch(relativePath);
157
+ return GENERATED_OUTPUT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
158
+ }
159
+
154
160
  function readFileIndex(projectRoot, files, config) {
155
161
  const index = [];
156
162
  for (const relativePath of files) {
@@ -180,6 +186,7 @@ function readFileIndex(projectRoot, files, config) {
180
186
  absolutePath,
181
187
  ext,
182
188
  content,
189
+ generatedOutput: isGeneratedOutputPath(relativePath),
183
190
  isTestFile: /(^|\/)(__tests__|tests)\//.test(relativePath) || /\.test\.|\.spec\.|_test\.go$/.test(relativePath)
184
191
  });
185
192
  }
@@ -631,6 +638,7 @@ function findFilesBySymbols(symbolHints, fileIndex) {
631
638
  for (const symbol of symbolHints) {
632
639
  const regex = new RegExp(`\\b${escapeRegExp(symbol)}\\b`, 'i');
633
640
  for (const file of fileIndex) {
641
+ if (file.generatedOutput) continue;
634
642
  if (!CODE_EXTENSIONS.has(file.ext)) {
635
643
  continue;
636
644
  }
@@ -650,6 +658,7 @@ function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new
650
658
 
651
659
  const matches = new Set();
652
660
  for (const file of fileIndex) {
661
+ if (file.generatedOutput) continue;
653
662
  const pathSegments = normalizePathForMatch(file.relativePath).split('/').filter(Boolean);
654
663
  for (const token of tokens) {
655
664
  if (pathSegments.some((segment) => segment === token || segment.includes(token))) {
@@ -744,7 +753,7 @@ function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set()) {
744
753
 
745
754
  const matches = [];
746
755
  for (const file of fileIndex) {
747
- if (!CODE_EXTENSIONS.has(file.ext) || file.isTestFile) {
756
+ if (file.generatedOutput || !CODE_EXTENSIONS.has(file.ext) || file.isTestFile) {
748
757
  continue;
749
758
  }
750
759
 
@@ -819,7 +828,7 @@ function findTestEvidence(taskText, fileIndex, referencedPaths = []) {
819
828
  const matches = [];
820
829
 
821
830
  for (const file of fileIndex) {
822
- if (!file.isTestFile) continue;
831
+ if (file.generatedOutput || !file.isTestFile) continue;
823
832
 
824
833
  // A test file counts as evidence only when it imports a module whose path contains
825
834
  // one of the task's meaningful tokens. Content-keyword matching is intentionally absent:
@@ -897,6 +906,7 @@ function findArtifactEvidence(taskText, fileIndex) {
897
906
  ];
898
907
 
899
908
  for (const file of fileIndex) {
909
+ if (file.generatedOutput) continue;
900
910
  if (artifactPatterns.some((pattern) => pattern.test(file.relativePath)) && !files.includes(file.relativePath)) {
901
911
  files.push(file.relativePath);
902
912
  }
@@ -1273,6 +1283,303 @@ function evaluateRule(rule, task, context) {
1273
1283
  };
1274
1284
  }
1275
1285
 
1286
+ function parseVerificationFields(text) {
1287
+ const fields = {};
1288
+ for (const part of String(text || '').split(';')) {
1289
+ const separator = part.indexOf('=');
1290
+ if (separator <= 0) continue;
1291
+ const key = part.slice(0, separator).trim().toLowerCase();
1292
+ let value = part.slice(separator + 1).trim();
1293
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
1294
+ value = value.slice(1, -1);
1295
+ }
1296
+ if (key && value) fields[key] = value;
1297
+ }
1298
+ return fields;
1299
+ }
1300
+
1301
+ function stripCodeComments(content) {
1302
+ const source = String(content || '');
1303
+ let result = '';
1304
+ let quote = null;
1305
+ let escaped = false;
1306
+ let lineHasContent = false;
1307
+
1308
+ for (let index = 0; index < source.length; index += 1) {
1309
+ const char = source[index];
1310
+ const next = source[index + 1];
1311
+
1312
+ if (quote) {
1313
+ result += char;
1314
+ if (escaped) {
1315
+ escaped = false;
1316
+ } else if (char === '\\') {
1317
+ escaped = true;
1318
+ } else if (char === quote) {
1319
+ quote = null;
1320
+ }
1321
+ if (char === '\n') lineHasContent = false;
1322
+ continue;
1323
+ }
1324
+
1325
+ if (char === '"' || char === "'" || char === '`') {
1326
+ quote = char;
1327
+ result += char;
1328
+ lineHasContent = true;
1329
+ continue;
1330
+ }
1331
+
1332
+ if (char === '/' && next === '*') {
1333
+ const closeIndex = source.indexOf('*/', index + 2);
1334
+ index = closeIndex < 0 ? source.length : closeIndex + 1;
1335
+ continue;
1336
+ }
1337
+
1338
+ if (char === '/' && next === '/') {
1339
+ const newlineIndex = source.indexOf('\n', index + 2);
1340
+ if (newlineIndex < 0) break;
1341
+ result += '\n';
1342
+ lineHasContent = false;
1343
+ index = newlineIndex;
1344
+ continue;
1345
+ }
1346
+
1347
+ if (char === '#' && !lineHasContent) {
1348
+ const newlineIndex = source.indexOf('\n', index + 1);
1349
+ if (newlineIndex < 0) break;
1350
+ result += '\n';
1351
+ lineHasContent = false;
1352
+ index = newlineIndex;
1353
+ continue;
1354
+ }
1355
+
1356
+ result += char;
1357
+ if (char === '\n') {
1358
+ lineHasContent = false;
1359
+ } else if (!/\s/.test(char)) {
1360
+ lineHasContent = true;
1361
+ }
1362
+ }
1363
+
1364
+ return result;
1365
+ }
1366
+
1367
+ function findIndexedFile(relativePath, context) {
1368
+ const normalized = normalizeReferencedPath(relativePath);
1369
+ return context.fileIndex.find((file) => !file.generatedOutput && (
1370
+ normalizeReferencedPath(file.relativePath) === normalized ||
1371
+ normalizeReferencedPath(file.relativePath).endsWith('/' + normalized)
1372
+ ));
1373
+ }
1374
+
1375
+ function readTestReportRecords(projectRoot, validationConfig) {
1376
+ const reports = Array.isArray(validationConfig && validationConfig.testReports)
1377
+ ? validationConfig.testReports
1378
+ : [];
1379
+ const records = [];
1380
+
1381
+ function visit(value, reportPath, mtimeMs) {
1382
+ if (!value || typeof value !== 'object') return;
1383
+ if (Array.isArray(value)) {
1384
+ value.forEach((entry) => visit(entry, reportPath, mtimeMs));
1385
+ return;
1386
+ }
1387
+ const status = String(value.status || value.state || value.result || '').toLowerCase();
1388
+ const file = value.file || value.filepath || value.testFile || value.nameFile;
1389
+ const name = value.fullName || value.fullname || value.name || value.title;
1390
+ if (file && name && /^(pass|passed|success)$/.test(status)) {
1391
+ records.push({ file: String(file), name: String(name), reportPath, mtimeMs });
1392
+ }
1393
+ Object.values(value).forEach((entry) => visit(entry, reportPath, mtimeMs));
1394
+ }
1395
+
1396
+ for (const report of reports) {
1397
+ if (!report || report.format !== 'vitest-json' || !report.path) continue;
1398
+ const reportPath = path.resolve(projectRoot, report.path);
1399
+ try {
1400
+ const payload = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
1401
+ const mtimeMs = fs.statSync(reportPath).mtimeMs;
1402
+ visit(payload, reportPath, mtimeMs);
1403
+ } catch {
1404
+ // A missing or malformed optional report is simply unavailable evidence.
1405
+ }
1406
+ }
1407
+ return records;
1408
+ }
1409
+
1410
+ function timestampIsFresh(timestamp, files) {
1411
+ const verifiedAt = Date.parse(timestamp || '');
1412
+ if (!Number.isFinite(verifiedAt)) return false;
1413
+ return files.every((file) => {
1414
+ try {
1415
+ return verifiedAt >= fs.statSync(file.absolutePath).mtimeMs;
1416
+ } catch {
1417
+ return false;
1418
+ }
1419
+ });
1420
+ }
1421
+
1422
+ function findFreshTestProof(task, fields, context) {
1423
+ const testFile = findIndexedFile(fields.test || fields.file, context);
1424
+ const sourceFile = fields.source ? findIndexedFile(fields.source, context) : null;
1425
+ const caseName = fields.case;
1426
+ if (!testFile || !caseName) return { passed: false, available: false };
1427
+ const freshnessFiles = [testFile, ...(sourceFile ? [sourceFile] : [])];
1428
+ const evidenceLines = Array.isArray(task.testEvidenceLines) ? task.testEvidenceLines : [];
1429
+ for (const line of evidenceLines) {
1430
+ const evidence = parseVerificationFields(line.text);
1431
+ if (String(evidence.status || '').toUpperCase() !== 'PASS') continue;
1432
+ if (normalizeReferencedPath(evidence.file) !== normalizeReferencedPath(testFile.relativePath)) continue;
1433
+ if (evidence.case !== caseName) continue;
1434
+ if (timestampIsFresh(evidence.verifiedat, freshnessFiles)) {
1435
+ return { passed: true, available: true };
1436
+ }
1437
+ return { passed: false, available: true, stale: true };
1438
+ }
1439
+ for (const record of context.testReportRecords || []) {
1440
+ if (!referencedPathMatches(record.file, testFile.relativePath) || !record.name.includes(caseName)) continue;
1441
+ if (record.mtimeMs >= Math.max(...freshnessFiles.map((file) => fs.statSync(file.absolutePath).mtimeMs))) {
1442
+ return {
1443
+ passed: true,
1444
+ available: true,
1445
+ generatedEvidence: `file=${testFile.relativePath}; case=${caseName}; status=PASS; verifiedAt=${new Date(record.mtimeMs).toISOString()}`
1446
+ };
1447
+ }
1448
+ return { passed: false, available: true, stale: true };
1449
+ }
1450
+ return { passed: false, available: false };
1451
+ }
1452
+
1453
+ function testCoversEndpoint(testFile, route, context) {
1454
+ const normalizedRoute = String(route).replace(/\[([^\]]+)\]/g, '$1').toLowerCase();
1455
+ const normalizedContent = testFile.content.replace(/\[([^\]]+)\]/g, '$1').toLowerCase();
1456
+ if (normalizedContent.includes(normalizedRoute)) return true;
1457
+ const source = findIndexedFile(`src/app${route}/route.ts`, context);
1458
+ if (source && normalizedContent.includes(path.basename(source.relativePath, source.ext).toLowerCase())) return true;
1459
+ const segments = normalizedRoute.split('/').filter((segment) => segment.length >= 3);
1460
+ return segments.length > 0 && segments.every((segment) => normalizedContent.includes(segment));
1461
+ }
1462
+
1463
+ function findVerificationRecipe(task, context) {
1464
+ const patterns = [
1465
+ /disabled\s*=\s*\{[^}]+\}/i,
1466
+ /<(?:dialog|alertdialog)\b[^>]*\bopen\s*=/i,
1467
+ /abortcontroller|abortsignal\.timeout/i,
1468
+ /router\.push\s*\(/i
1469
+ ];
1470
+ for (const file of context.fileIndex) {
1471
+ if (file.generatedOutput || file.isTestFile || !CODE_EXTENSIONS.has(file.ext)) continue;
1472
+ const match = patterns.map((pattern) => pattern.exec(file.content)).find(Boolean);
1473
+ if (!match) continue;
1474
+ const line = file.content.slice(0, match.index).split(/\r?\n/).length;
1475
+ const command = context.config.validation && context.config.validation.recipeCommand
1476
+ ? `; run ${String(context.config.validation.recipeCommand).replace('{testFile}', '<test-file>')}`
1477
+ : '';
1478
+ return `${file.relativePath}:${line} inspect ${match[0].trim()}${command}`;
1479
+ }
1480
+ return null;
1481
+ }
1482
+
1483
+ function isBehavioralTask(taskText) {
1484
+ return /\b(mostrar|deshabilitar|confirmar|notificar|redirigir|imprimir|show|disable|confirm|notify|redirect|print)\b/i.test(String(taskText || ''));
1485
+ }
1486
+
1487
+ function evaluateDeterministicVerification(task, context) {
1488
+ const verifyLines = Array.isArray(task.verifyLines) ? task.verifyLines : [];
1489
+ if (verifyLines.length === 0) {
1490
+ if (!isBehavioralTask(task.text)) {
1491
+ return { applicable: false, passed: false, reasons: [], diagnostics: [], recipe: null };
1492
+ }
1493
+ const recipe = findVerificationRecipe(task, context);
1494
+ return {
1495
+ applicable: false,
1496
+ passed: false,
1497
+ reasons: [],
1498
+ diagnostics: [{
1499
+ code: recipe ? 'REQUIRES_HUMAN_EVIDENCE' : 'NO_STATIC_SIGNAL',
1500
+ severity: 'warning',
1501
+ message: recipe ? 'behavioral task requires explicit human or test evidence' : 'no static implementation signal was found for behavioral task'
1502
+ }],
1503
+ recipe
1504
+ };
1505
+ }
1506
+ const reasons = [];
1507
+ const diagnostics = [];
1508
+ let generatedTestEvidence = null;
1509
+
1510
+ for (const line of verifyLines) {
1511
+ const fields = parseVerificationFields(line.text);
1512
+ const kind = fields.kind;
1513
+ if (kind === 'contains' || kind === 'property') {
1514
+ const file = findIndexedFile(fields.file, context);
1515
+ if (!file) {
1516
+ reasons.push(`missing referenced file(s): ${fields.file || '<unspecified>'}`);
1517
+ continue;
1518
+ }
1519
+ const content = stripCodeComments(file.content);
1520
+ if (kind === 'contains') {
1521
+ if (!fields.expected || !content.includes(fields.expected)) {
1522
+ reasons.push(`no content match in ${file.relativePath}: expected ${fields.expected || '<unspecified>'}`);
1523
+ }
1524
+ continue;
1525
+ }
1526
+ const keyPattern = new RegExp(`\\b${escapeRegExp(fields.key || '')}\\s*:`);
1527
+ const exactPattern = new RegExp(`\\b${escapeRegExp(fields.key || '')}\\s*:\\s*${escapeRegExp(fields.equals || '')}(?=\\s*[,}\\n])`);
1528
+ if (!exactPattern.test(content)) {
1529
+ reasons.push(keyPattern.test(content)
1530
+ ? `wrong value for ${fields.key} in ${file.relativePath}: expected ${fields.equals}`
1531
+ : `no content match in ${file.relativePath}: expected ${fields.key}: ${fields.equals}`);
1532
+ }
1533
+ continue;
1534
+ }
1535
+ if (kind === 'endpoints') {
1536
+ const routes = String(fields.routes || '').split(',').map((value) => value.trim()).filter(Boolean);
1537
+ const tests = context.fileIndex.filter((file) => !file.generatedOutput && file.isTestFile);
1538
+ const covered = routes.filter((route) => tests.some((file) => testCoversEndpoint(file, route, context)));
1539
+ if (covered.length !== routes.length) {
1540
+ const missing = routes.filter((route) => !covered.includes(route));
1541
+ reasons.push(`partial endpoint coverage ${covered.length}/${routes.length}: missing ${missing.join(', ')}`);
1542
+ }
1543
+ continue;
1544
+ }
1545
+ if (kind === 'behavior') {
1546
+ const source = findIndexedFile(fields.source, context);
1547
+ const testFile = findIndexedFile(fields.test, context);
1548
+ const testContent = testFile ? testFile.content : '';
1549
+ const sourceReference = source && testContent.includes(path.basename(source.relativePath, source.ext));
1550
+ const exercise = fields.trigger && testContent.includes(fields.trigger);
1551
+ const assertion = fields.assertion && testContent.includes(fields.assertion);
1552
+ const namedCase = fields.case && testContent.includes(fields.case);
1553
+ const proof = findFreshTestProof(task, fields, context);
1554
+ if (!source || !testFile || !sourceReference || !exercise || !assertion || !namedCase) {
1555
+ diagnostics.push({ code: 'REQUIRES_HUMAN_EVIDENCE', severity: 'warning', message: 'behavior verification needs a source reference, named test, trigger, and assertion' });
1556
+ continue;
1557
+ }
1558
+ if (proof.stale) {
1559
+ diagnostics.push({ code: 'STALE_TEST_REPORT', severity: 'warning', message: `test result for ${testFile.relativePath} predates the verified source` });
1560
+ continue;
1561
+ }
1562
+ if (!proof.passed) {
1563
+ diagnostics.push({ code: 'REQUIRES_HUMAN_EVIDENCE', severity: 'warning', message: `no fresh passing result for ${testFile.relativePath}` });
1564
+ continue;
1565
+ }
1566
+ generatedTestEvidence = proof.generatedEvidence || generatedTestEvidence;
1567
+ continue;
1568
+ }
1569
+ reasons.push(`unsupported verification kind: ${kind || '<unspecified>'}`);
1570
+ }
1571
+
1572
+ const passed = reasons.length === 0 && diagnostics.length === 0;
1573
+ return {
1574
+ applicable: true,
1575
+ passed,
1576
+ reasons,
1577
+ diagnostics,
1578
+ generatedTestEvidence,
1579
+ recipe: passed ? null : findVerificationRecipe(task, context)
1580
+ };
1581
+ }
1582
+
1276
1583
  function buildValidationContext(projectRoot, config, plugins) {
1277
1584
  const files = walkFiles(projectRoot);
1278
1585
  const fileIndex = readFileIndex(projectRoot, files, config);
@@ -1286,10 +1593,70 @@ function buildValidationContext(projectRoot, config, plugins) {
1286
1593
  files,
1287
1594
  fileIndex,
1288
1595
  pathHintResolver,
1289
- testFrameworks
1596
+ testFrameworks,
1597
+ testReportRecords: readTestReportRecords(projectRoot, config.validation)
1290
1598
  };
1291
1599
  }
1292
1600
 
1601
+ function diagnosticCodeForReason(reason) {
1602
+ const normalized = String(reason || '').toLowerCase();
1603
+ if (normalized.includes('missing referenced file') || normalized.includes('evidence file(s) not found')) {
1604
+ return 'MISSING_REFERENCE';
1605
+ }
1606
+ if (normalized.includes('missing test evidence')) {
1607
+ return 'NO_TEST';
1608
+ }
1609
+ if (normalized.includes('wrong value')) {
1610
+ return 'WRONG_VALUE';
1611
+ }
1612
+ if (normalized.includes('partial endpoint coverage')) {
1613
+ return 'PARTIAL';
1614
+ }
1615
+ if (normalized.includes('no content match')) {
1616
+ return 'NOT_IMPLEMENTED';
1617
+ }
1618
+ if (
1619
+ normalized.includes('no code, test, or artifact evidence found') ||
1620
+ normalized.includes('implementation task requires evidence line') ||
1621
+ normalized.includes('weak path') ||
1622
+ normalized.includes('file reference shows implementation location')
1623
+ ) {
1624
+ return 'NOT_IMPLEMENTED';
1625
+ }
1626
+ return null;
1627
+ }
1628
+
1629
+ function buildDiagnostics(reasons, options = {}) {
1630
+ const diagnostics = [];
1631
+ const seen = new Set();
1632
+ for (const reason of Array.isArray(reasons) ? reasons : []) {
1633
+ const code = diagnosticCodeForReason(reason);
1634
+ if (!code || seen.has(code)) {
1635
+ continue;
1636
+ }
1637
+ seen.add(code);
1638
+ diagnostics.push({ code, severity: 'error', message: reason });
1639
+ }
1640
+ if (options.staleEvidence) {
1641
+ diagnostics.push({
1642
+ code: 'STALE_EVIDENCE',
1643
+ severity: 'warning',
1644
+ message: 'historical validation warning conflicts with fresh repository evidence'
1645
+ });
1646
+ }
1647
+ for (const diagnostic of options.extra || []) {
1648
+ if (!diagnostic || !diagnostic.code || diagnostics.some((item) => item.code === diagnostic.code)) continue;
1649
+ diagnostics.push(diagnostic);
1650
+ }
1651
+ return diagnostics;
1652
+ }
1653
+
1654
+ function buildDiscoveredEvidenceLine(evidence) {
1655
+ const files = unionArrays(evidence.codeFiles, evidence.testFiles)
1656
+ .sort((left, right) => left.localeCompare(right));
1657
+ return files.length > 0 ? files.join(', ') : null;
1658
+ }
1659
+
1293
1660
  function validateTask(task, context, config, plugins) {
1294
1661
  const {
1295
1662
  paths: pathHints,
@@ -1302,6 +1669,8 @@ function validateTask(task, context, config, plugins) {
1302
1669
  const standaloneFilenames = extractStandaloneFilenames(task.text);
1303
1670
  const symbolHints = extractSymbolHints(task.text);
1304
1671
  const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.pathHintResolver);
1672
+ const deterministicVerification = evaluateDeterministicVerification(task, context);
1673
+ const hasExplicitPendingItems = Array.isArray(task.explicitPendingItems) && task.explicitPendingItems.length > 0;
1305
1674
 
1306
1675
  const filesFromPaths = findFilesByPathHints(pathHints, context.pathHintResolver);
1307
1676
  const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.pathHintResolver);
@@ -1378,7 +1747,8 @@ function validateTask(task, context, config, plugins) {
1378
1747
  context.testFrameworks.length > 0 &&
1379
1748
  isCodeTask(task.text) &&
1380
1749
  !isDocTask(task.text) &&
1381
- !isHttpExpectationTask(task.text);
1750
+ !isHttpExpectationTask(task.text) &&
1751
+ !(task.verifyLines || []).some((line) => ['contains', 'property', 'endpoints', 'behavior'].includes(parseVerificationFields(line.text).kind));
1382
1752
  const configuredRules = Array.isArray(config.validators) ? config.validators : [];
1383
1753
  const pluginRules = collectPluginContributions(plugins || [], 'registerValidators', context);
1384
1754
  let overrideResult = null;
@@ -1412,6 +1782,18 @@ function validateTask(task, context, config, plugins) {
1412
1782
 
1413
1783
  let uniqueReasons = Array.from(new Set(reasons));
1414
1784
 
1785
+ if (deterministicVerification.passed) {
1786
+ uniqueReasons = uniqueReasons.filter((reason) => (
1787
+ reason !== 'no code, test, or artifact evidence found' &&
1788
+ reason !== 'missing test evidence' &&
1789
+ !reason.startsWith('weak path-')
1790
+ ));
1791
+ }
1792
+
1793
+ if (deterministicVerification.applicable && deterministicVerification.reasons.length > 0) {
1794
+ uniqueReasons = Array.from(new Set([...uniqueReasons, ...deterministicVerification.reasons]));
1795
+ }
1796
+
1415
1797
  if (overrideResult) {
1416
1798
  uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
1417
1799
  }
@@ -1467,7 +1849,22 @@ function validateTask(task, context, config, plugins) {
1467
1849
  // evidence, artifact evidence, or strong code+test threshold to pass.
1468
1850
  // Already-checked tasks with found path hints are preserved via shouldPreserveCheckedTask.
1469
1851
  const hasHighConfidenceImplementationEvidence = meetsStrongThreshold && evidence.code && evidence.test;
1470
- let passed = authoritativeEvidence.passed || hasArtifactTaskPass || hasTrustedRuleEvidencePass || meetsStrongThreshold;
1852
+ const hasFreshRepositoryEvidence = hasStrongEvidence || hasWeakEvidence;
1853
+ let staleEvidenceDetected = false;
1854
+ let staleEvidenceResolved = false;
1855
+ // Heuristic proximity remains useful to locate candidates, but is never sufficient to
1856
+ // complete an unchecked task. Completion requires human Evidence, a trusted rule, or a
1857
+ // typed deterministic verifier.
1858
+ let passed = authoritativeEvidence.passed || hasTrustedRuleEvidencePass || deterministicVerification.passed;
1859
+
1860
+ if (!task.checked && !authoritativeEvidence.passed && !hasTrustedRuleEvidencePass && !deterministicVerification.passed && hasHighConfidenceImplementationEvidence) {
1861
+ uniqueReasons.push('implementation task requires deterministic Verify metadata or explicit Evidence to be marked complete');
1862
+ uniqueReasons = Array.from(new Set(uniqueReasons));
1863
+ }
1864
+
1865
+ if (task.warningText && !task.checked && hasFreshRepositoryEvidence && !authoritativeEvidence.passed) {
1866
+ staleEvidenceDetected = true;
1867
+ }
1471
1868
 
1472
1869
  if (!passed && !task.checked && hasDirectReferencePass) {
1473
1870
  const locationReason = 'file reference shows implementation location, not confirmed completion';
@@ -1476,19 +1873,31 @@ function validateTask(task, context, config, plugins) {
1476
1873
  }
1477
1874
  }
1478
1875
 
1479
- // Unchecked tasks with an existing ⚠️ warning are preserved as failing unless an Evidence line
1480
- // explicitly confirms implementation. meetsStrongThreshold (token match) cannot override a
1481
- // human/agent judgment that the feature is incomplete.
1876
+ // Historical warnings are only cleared by independent, high-confidence repository evidence.
1482
1877
  if (task.warningText && !task.checked && passed && !authoritativeEvidence.passed) {
1483
- passed = false;
1484
- if (uniqueReasons.length === 0) {
1485
- uniqueReasons.push('validation failed');
1878
+ if ((deterministicVerification.passed || hasHighConfidenceImplementationEvidence) && negativeSignalMatches.length === 0 && uniqueReasons.length === 0) {
1879
+ staleEvidenceResolved = true;
1880
+ } else {
1881
+ passed = false;
1882
+ if (uniqueReasons.length === 0) {
1883
+ uniqueReasons.push('validation failed');
1884
+ }
1486
1885
  }
1487
1886
  }
1488
1887
  if (negativeSignalMatches.length > 0) {
1489
1888
  passed = false;
1490
1889
  }
1491
1890
 
1891
+ const extraDiagnostics = [...deterministicVerification.diagnostics];
1892
+ if (hasExplicitPendingItems) {
1893
+ passed = false;
1894
+ extraDiagnostics.push({
1895
+ code: 'HAS_EXPLICIT_PENDING',
1896
+ severity: 'warning',
1897
+ message: `task declares pending item(s): ${task.explicitPendingItems.map((item) => item.text || 'pending').join(', ')}`
1898
+ });
1899
+ }
1900
+
1492
1901
  // Unchecked implementation tasks need explicit evidence or high-confidence implementation
1493
1902
  // evidence. Weak token overlap, direct file references, or code-only matches are not enough.
1494
1903
  if (
@@ -1497,6 +1906,7 @@ function validateTask(task, context, config, plugins) {
1497
1906
  isImplementationTask(task.text) &&
1498
1907
  !authoritativeEvidence.passed &&
1499
1908
  !hasTrustedRuleEvidencePass &&
1909
+ !deterministicVerification.passed &&
1500
1910
  !hasArtifactTaskPass &&
1501
1911
  !hasHighConfidenceImplementationEvidence
1502
1912
  ) {
@@ -1533,17 +1943,23 @@ function validateTask(task, context, config, plugins) {
1533
1943
  // Used by auditValidation to flag implementation tasks that pass solely via documentation.
1534
1944
  const evidenceIsDocOnly = !evidence.code && !evidence.test && evidence.artifact && !isDocTask(task.text);
1535
1945
 
1946
+ const finalPassed = overrideResult ? (overrideResult.passed !== false && !hasExplicitPendingItems) : (passed && uniqueReasons.length === 0 && !hasExplicitPendingItems);
1536
1947
  return {
1537
1948
  taskId: task.id,
1538
- passed: overrideResult ? overrideResult.passed !== false : (passed && uniqueReasons.length === 0),
1949
+ passed: finalPassed,
1539
1950
  confidence,
1540
1951
  reasons: uniqueReasons,
1952
+ diagnostics: buildDiagnostics(uniqueReasons, { staleEvidence: staleEvidenceDetected, extra: extraDiagnostics }),
1541
1953
  evidence,
1542
1954
  evidenceIsDocOnly,
1543
1955
  requiresTest,
1544
1956
  hasEvidence: hasStrongEvidence || hasWeakEvidence,
1545
1957
  attempted,
1546
- preservedCheckedState
1958
+ preservedCheckedState,
1959
+ staleEvidenceResolved,
1960
+ discoveredEvidence: staleEvidenceResolved ? buildDiscoveredEvidenceLine(evidence) : null,
1961
+ verificationRecipe: deterministicVerification.recipe,
1962
+ generatedTestEvidence: deterministicVerification.generatedTestEvidence || null
1547
1963
  };
1548
1964
  }
1549
1965