roadmapsmith 0.1.0
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/README.md +219 -0
- package/bin/cli.js +212 -0
- package/package.json +41 -0
- package/src/config.js +188 -0
- package/src/generator/index.js +436 -0
- package/src/index.js +8 -0
- package/src/io.js +228 -0
- package/src/match.js +86 -0
- package/src/model.js +28 -0
- package/src/parser/index.js +109 -0
- package/src/sync/index.js +59 -0
- package/src/templates/index.js +31 -0
- package/src/utils.js +143 -0
- package/src/validator/index.js +401 -0
- package/templates/agents.template.md +22 -0
- package/templates/roadmap.template.md +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# roadmap-skill
|
|
2
|
+
|
|
3
|
+
Production-grade roadmap generator and sync tool for agent-driven projects.
|
|
4
|
+
|
|
5
|
+
## Install Status
|
|
6
|
+
|
|
7
|
+
This package and skill are in private/internal development. Public install flows are not available yet.
|
|
8
|
+
|
|
9
|
+
### Primary install path: agent skill (available for private repository users)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This install command adds the `roadmap-sync` agent skill. It does not install the CLI package.
|
|
16
|
+
|
|
17
|
+
### Optional CLI package install (future, unavailable until npm publication)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g roadmapsmith
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Local development usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd roadmap-skill
|
|
27
|
+
node bin/cli.js --help
|
|
28
|
+
node bin/cli.js init --dry-run
|
|
29
|
+
node bin/cli.js generate --project-root . --dry-run --audit
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Legacy/internal alias
|
|
33
|
+
|
|
34
|
+
The `roadmap-skill` CLI alias remains for backward compatibility and internal use, but public examples use `roadmapsmith`.
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]
|
|
40
|
+
roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit]
|
|
41
|
+
roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]
|
|
42
|
+
roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Behavior
|
|
46
|
+
|
|
47
|
+
- Generates deterministic `ROADMAP.md` with fixed section order.
|
|
48
|
+
- Uses stable task IDs: `<!-- rs:task=<slug> -->`.
|
|
49
|
+
- Sync marks `[x]` only when validation passes.
|
|
50
|
+
- Validation evidence gate:
|
|
51
|
+
- code OR test OR artifact evidence required.
|
|
52
|
+
- test evidence required for code tasks when test frameworks are detected.
|
|
53
|
+
- Validation failures in sync write warning lines:
|
|
54
|
+
- `- ⚠️ attempted but validation failed: <reason>`
|
|
55
|
+
- Preserves unmanaged markdown content by updating only the managed roadmap block.
|
|
56
|
+
|
|
57
|
+
## Defaults
|
|
58
|
+
|
|
59
|
+
- Roadmap file: `./ROADMAP.md` (falls back to `./roadmap.md` when only the legacy file exists)
|
|
60
|
+
- Agent rules file: `./AGENTS.md` (falls back to `./CLAUDE.md` when present)
|
|
61
|
+
- Config file: `./roadmap-skill.config.json`
|
|
62
|
+
|
|
63
|
+
Roadmap resolution precedence:
|
|
64
|
+
|
|
65
|
+
1. `--roadmap-file` CLI flag
|
|
66
|
+
2. `config.roadmapFile` in `roadmap-skill.config.json`
|
|
67
|
+
3. Existing `./ROADMAP.md`
|
|
68
|
+
4. Existing `./roadmap.md` (legacy fallback)
|
|
69
|
+
5. `./ROADMAP.md` when neither file exists
|
|
70
|
+
|
|
71
|
+
## Config
|
|
72
|
+
|
|
73
|
+
Create `roadmap-skill.config.json`:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"roadmapFile": "./ROADMAP.md",
|
|
78
|
+
"agentsFile": "./AGENTS.md",
|
|
79
|
+
"taskMatchers": [
|
|
80
|
+
{
|
|
81
|
+
"pattern": "src/payments/",
|
|
82
|
+
"task": "Complete payment flow hardening",
|
|
83
|
+
"phase": "P0",
|
|
84
|
+
"priority": "P0"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"validators": [
|
|
88
|
+
{
|
|
89
|
+
"type": "file-exists",
|
|
90
|
+
"when": "migration",
|
|
91
|
+
"path": "db/migrations"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"customSections": [
|
|
95
|
+
{
|
|
96
|
+
"title": "Compliance",
|
|
97
|
+
"items": [
|
|
98
|
+
"- [ ] Complete SOC2 evidence packet <!-- rs:task=compliance-soc2-evidence -->"
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"plugins": [
|
|
103
|
+
"./plugins/roadmap.plugin.js"
|
|
104
|
+
],
|
|
105
|
+
"milestones": [
|
|
106
|
+
{ "version": "v0.1", "goal": "Foundation" },
|
|
107
|
+
{ "version": "v0.2", "goal": "Reliability" },
|
|
108
|
+
{ "version": "v0.3", "goal": "Release candidate" },
|
|
109
|
+
{ "version": "v1.0", "goal": "General availability" }
|
|
110
|
+
],
|
|
111
|
+
"phaseTemplates": {
|
|
112
|
+
"P0": ["Stabilize critical path"],
|
|
113
|
+
"P1": ["Expand reliability"],
|
|
114
|
+
"P2": ["Finalize release hardening"]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Plugin API
|
|
120
|
+
|
|
121
|
+
Plugin module path(s) are loaded from `config.plugins` in deterministic order.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
module.exports = {
|
|
125
|
+
registerTaskDetectors(ctx) {
|
|
126
|
+
return [
|
|
127
|
+
{ text: 'Implement billing retries', phase: 'P1', priority: 'P1' }
|
|
128
|
+
];
|
|
129
|
+
},
|
|
130
|
+
registerSectionGenerators(ctx) {
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
title: 'Platform Notes',
|
|
134
|
+
items: ['- [ ] Verify deployment rollback path <!-- rs:task=verify-deployment-rollback-path -->']
|
|
135
|
+
}
|
|
136
|
+
];
|
|
137
|
+
},
|
|
138
|
+
registerValidators(ctx) {
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
type: 'symbol',
|
|
142
|
+
when: 'billing',
|
|
143
|
+
pattern: 'retry',
|
|
144
|
+
message: 'billing retry symbol not found'
|
|
145
|
+
}
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Example Usage
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
roadmapsmith init
|
|
155
|
+
roadmapsmith generate --project-root .
|
|
156
|
+
roadmapsmith validate --json
|
|
157
|
+
roadmapsmith sync --audit
|
|
158
|
+
roadmapsmith sync --dry-run
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Dry Run and Audit
|
|
162
|
+
|
|
163
|
+
- `--dry-run`: shows file diff preview without writing.
|
|
164
|
+
- `--audit`: reports roadmap/code mismatches:
|
|
165
|
+
- checked without evidence
|
|
166
|
+
- ready but unchecked
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npm test
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Publishing (Future, Not Yet)
|
|
175
|
+
|
|
176
|
+
### npm (public package, future)
|
|
177
|
+
|
|
178
|
+
1. Run tests.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npm test
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
2. Bump version using semver.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npm version patch
|
|
188
|
+
# or: npm version minor
|
|
189
|
+
# or: npm version major
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
3. Publish package.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npm publish --access public
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
4. Push tags.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
git push origin main --follow-tags
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### skills.sh / `npx skills add` discoverability (future)
|
|
205
|
+
|
|
206
|
+
1. After internal release-readiness is complete, make the repository public with `skills/roadmap-sync/SKILL.md` and `skills.json`.
|
|
207
|
+
2. Verify install flow from a clean project:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
3. Confirm the installed skill appears in your agent skill list. This install command adds the agent skill, not the CLI package.
|
|
214
|
+
|
|
215
|
+
## Versioning Strategy
|
|
216
|
+
|
|
217
|
+
- `patch`: bug fixes and non-breaking validation/generation improvements.
|
|
218
|
+
- `minor`: backward-compatible features (new flags, new plugin hooks, additive config fields).
|
|
219
|
+
- `major`: breaking CLI/config behavior or marker/format changes.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { parseArgv } = require('../src/utils');
|
|
7
|
+
const { loadConfig, resolveRoadmapFile, resolveAgentsFile, loadPlugins } = require('../src/config');
|
|
8
|
+
const { readTextIfExists, writeText, printDryRunDiff } = require('../src/io');
|
|
9
|
+
const { renderRoadmapTemplate, renderAgentsTemplate } = require('../src/templates');
|
|
10
|
+
const { generateRoadmapDocument } = require('../src/generator');
|
|
11
|
+
const { parseRoadmap } = require('../src/parser');
|
|
12
|
+
const { buildValidationContext, validateTasks, auditValidation } = require('../src/validator');
|
|
13
|
+
const { applySync } = require('../src/sync');
|
|
14
|
+
|
|
15
|
+
function printHelp() {
|
|
16
|
+
console.log([
|
|
17
|
+
'Usage:',
|
|
18
|
+
' roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]',
|
|
19
|
+
' roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit]',
|
|
20
|
+
' roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]',
|
|
21
|
+
' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]'
|
|
22
|
+
].join('\n'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isEnabled(value) {
|
|
26
|
+
if (value === true) return true;
|
|
27
|
+
if (typeof value !== 'string') return false;
|
|
28
|
+
const normalized = value.toLowerCase();
|
|
29
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatResultLine(task, result) {
|
|
33
|
+
const status = result.passed ? 'PASS' : 'FAIL';
|
|
34
|
+
const reason = result.reasons.length > 0 ? ` :: ${result.reasons.join('; ')}` : '';
|
|
35
|
+
return `${status} [${task.id}] ${task.text}${reason}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function maybeFilterTasks(tasks, filterValue) {
|
|
39
|
+
if (!filterValue) return tasks;
|
|
40
|
+
const normalized = String(filterValue).toLowerCase();
|
|
41
|
+
return tasks.filter((task) => {
|
|
42
|
+
return task.id.toLowerCase() === normalized || task.text.toLowerCase().includes(normalized);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function printAudit(audit) {
|
|
47
|
+
console.log(`Audit summary: ${audit.checkedWithoutEvidence.length} checked-without-evidence, ${audit.readyButUnchecked.length} ready-but-unchecked.`);
|
|
48
|
+
if (audit.checkedWithoutEvidence.length > 0) {
|
|
49
|
+
console.log('Checked without evidence:');
|
|
50
|
+
audit.checkedWithoutEvidence.forEach((item) => {
|
|
51
|
+
console.log(`- [${item.task.id}] ${item.task.text}`);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (audit.readyButUnchecked.length > 0) {
|
|
55
|
+
console.log('Ready but unchecked:');
|
|
56
|
+
audit.readyButUnchecked.forEach((item) => {
|
|
57
|
+
console.log(`- [${item.task.id}] ${item.task.text}`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function run() {
|
|
63
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
64
|
+
const command = parsed.command;
|
|
65
|
+
const flags = parsed.flags;
|
|
66
|
+
|
|
67
|
+
if (!command || isEnabled(flags.help) || isEnabled(flags.h)) {
|
|
68
|
+
printHelp();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (command === 'init') {
|
|
73
|
+
const projectRoot = process.cwd();
|
|
74
|
+
const config = loadConfig({ projectRoot });
|
|
75
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
76
|
+
const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
|
|
77
|
+
const dryRun = isEnabled(flags['dry-run']);
|
|
78
|
+
|
|
79
|
+
const roadmapExists = fs.existsSync(roadmapFile);
|
|
80
|
+
const agentsExists = fs.existsSync(agentsFile);
|
|
81
|
+
|
|
82
|
+
if (!roadmapExists) {
|
|
83
|
+
const roadmap = renderRoadmapTemplate();
|
|
84
|
+
const result = writeText(roadmapFile, roadmap, { dryRun });
|
|
85
|
+
if (dryRun && result.changed) {
|
|
86
|
+
printDryRunDiff(roadmapFile, result.before, result.after);
|
|
87
|
+
}
|
|
88
|
+
console.log(`${dryRun ? 'Would create' : 'Created'} ${roadmapFile}`);
|
|
89
|
+
} else {
|
|
90
|
+
console.log(`Skipped existing ${roadmapFile}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!agentsExists) {
|
|
94
|
+
const agents = renderAgentsTemplate({ roadmapPath: path.basename(roadmapFile) });
|
|
95
|
+
const result = writeText(agentsFile, agents, { dryRun });
|
|
96
|
+
if (dryRun && result.changed) {
|
|
97
|
+
printDryRunDiff(agentsFile, result.before, result.after);
|
|
98
|
+
}
|
|
99
|
+
console.log(`${dryRun ? 'Would create' : 'Created'} ${agentsFile}`);
|
|
100
|
+
} else {
|
|
101
|
+
console.log(`Skipped existing ${agentsFile}`);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (command === 'generate') {
|
|
107
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
108
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
109
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
110
|
+
const plugins = loadPlugins(projectRoot, config.plugins);
|
|
111
|
+
const existingContent = readTextIfExists(roadmapFile) || '';
|
|
112
|
+
const dryRun = isEnabled(flags['dry-run']);
|
|
113
|
+
|
|
114
|
+
const document = generateRoadmapDocument({
|
|
115
|
+
projectRoot,
|
|
116
|
+
roadmapPath: roadmapFile,
|
|
117
|
+
existingContent,
|
|
118
|
+
config,
|
|
119
|
+
plugins
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const writeResult = writeText(roadmapFile, document, { dryRun });
|
|
123
|
+
if (dryRun) {
|
|
124
|
+
if (writeResult.changed) {
|
|
125
|
+
printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
|
|
126
|
+
} else {
|
|
127
|
+
console.log(`No changes for ${roadmapFile}`);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isEnabled(flags.audit)) {
|
|
134
|
+
const parsedRoadmap = parseRoadmap(document);
|
|
135
|
+
const validationContext = buildValidationContext(projectRoot, config, plugins);
|
|
136
|
+
const results = validateTasks(parsedRoadmap.tasks, validationContext, config, plugins);
|
|
137
|
+
const audit = auditValidation(parsedRoadmap.tasks, results);
|
|
138
|
+
printAudit(audit);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (command === 'sync') {
|
|
144
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
145
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
146
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
147
|
+
const content = readTextIfExists(roadmapFile);
|
|
148
|
+
if (content == null) {
|
|
149
|
+
throw new Error(`Roadmap not found: ${roadmapFile}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const parsedRoadmap = parseRoadmap(content);
|
|
153
|
+
const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
|
|
154
|
+
const results = validateTasks(parsedRoadmap.tasks, validationContext, config, validationContext.plugins);
|
|
155
|
+
const next = applySync(content, parsedRoadmap.tasks, results);
|
|
156
|
+
const dryRun = isEnabled(flags['dry-run']);
|
|
157
|
+
const writeResult = writeText(roadmapFile, next, { dryRun });
|
|
158
|
+
|
|
159
|
+
if (dryRun) {
|
|
160
|
+
if (writeResult.changed) {
|
|
161
|
+
printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
|
|
162
|
+
} else {
|
|
163
|
+
console.log(`No changes for ${roadmapFile}`);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (isEnabled(flags.audit)) {
|
|
170
|
+
const audit = auditValidation(parsedRoadmap.tasks, results);
|
|
171
|
+
printAudit(audit);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (command === 'validate') {
|
|
177
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
178
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
179
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
180
|
+
const content = readTextIfExists(roadmapFile);
|
|
181
|
+
if (content == null) {
|
|
182
|
+
throw new Error(`Roadmap not found: ${roadmapFile}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const parsedRoadmap = parseRoadmap(content);
|
|
186
|
+
const tasks = maybeFilterTasks(parsedRoadmap.tasks, flags.task);
|
|
187
|
+
const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
|
|
188
|
+
const results = validateTasks(tasks, validationContext, config, validationContext.plugins);
|
|
189
|
+
|
|
190
|
+
if (isEnabled(flags.json)) {
|
|
191
|
+
const payload = tasks.map((task) => ({ task, result: results[task.id] }));
|
|
192
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
193
|
+
} else {
|
|
194
|
+
tasks.forEach((task) => {
|
|
195
|
+
console.log(formatResultLine(task, results[task.id]));
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const failed = tasks.some((task) => !results[task.id].passed);
|
|
200
|
+
if (failed) {
|
|
201
|
+
process.exitCode = 1;
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new Error(`Unknown command: ${command}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
run().catch((error) => {
|
|
210
|
+
console.error(error.message);
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "roadmapsmith",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate, sync, and validate deterministic project roadmaps for agent-driven execution.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"roadmap-skill": "bin/cli.js",
|
|
8
|
+
"roadmapsmith": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"type": "commonjs",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test test/*.test.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"roadmap",
|
|
16
|
+
"skills",
|
|
17
|
+
"agent",
|
|
18
|
+
"cli",
|
|
19
|
+
"planning"
|
|
20
|
+
],
|
|
21
|
+
"author": "PapiScholz",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/PapiScholz/roadmapsmith.git",
|
|
26
|
+
"directory": "roadmap-skill"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/PapiScholz/roadmapsmith/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/PapiScholz/roadmapsmith#readme",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin",
|
|
37
|
+
"src",
|
|
38
|
+
"templates",
|
|
39
|
+
"README.md"
|
|
40
|
+
]
|
|
41
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { readTextIfExists } = require('./io');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
roadmapFile: './ROADMAP.md',
|
|
9
|
+
agentsFile: './AGENTS.md',
|
|
10
|
+
taskMatchers: [],
|
|
11
|
+
validators: [],
|
|
12
|
+
customSections: [],
|
|
13
|
+
plugins: [],
|
|
14
|
+
milestones: [
|
|
15
|
+
{ version: 'v0.1', goal: 'Foundation baseline complete' },
|
|
16
|
+
{ version: 'v0.2', goal: 'Core feature coverage stabilized' },
|
|
17
|
+
{ version: 'v0.3', goal: 'Release candidate hardening complete' },
|
|
18
|
+
{ version: 'v1.0', goal: 'Production readiness exit criteria met' }
|
|
19
|
+
],
|
|
20
|
+
phaseTemplates: {
|
|
21
|
+
P0: [
|
|
22
|
+
'Stabilize project baseline and unblock high-risk delivery paths',
|
|
23
|
+
'Implement critical tasks required for milestone v0.1'
|
|
24
|
+
],
|
|
25
|
+
P1: [
|
|
26
|
+
'Expand feature completeness and improve reliability',
|
|
27
|
+
'Reduce operational risk before v0.3'
|
|
28
|
+
],
|
|
29
|
+
P2: [
|
|
30
|
+
'Complete final hardening and release readiness for v1.0',
|
|
31
|
+
'Close non-critical backlog aligned to anti-goals'
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function safeParseJson(content, filePath) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(content);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function mergeConfig(userConfig) {
|
|
45
|
+
return {
|
|
46
|
+
...DEFAULT_CONFIG,
|
|
47
|
+
...userConfig,
|
|
48
|
+
taskMatchers: Array.isArray(userConfig.taskMatchers) ? userConfig.taskMatchers : DEFAULT_CONFIG.taskMatchers,
|
|
49
|
+
validators: Array.isArray(userConfig.validators) ? userConfig.validators : DEFAULT_CONFIG.validators,
|
|
50
|
+
customSections: Array.isArray(userConfig.customSections) ? userConfig.customSections : DEFAULT_CONFIG.customSections,
|
|
51
|
+
plugins: Array.isArray(userConfig.plugins) ? userConfig.plugins : DEFAULT_CONFIG.plugins,
|
|
52
|
+
milestones: Array.isArray(userConfig.milestones) ? userConfig.milestones : DEFAULT_CONFIG.milestones,
|
|
53
|
+
phaseTemplates: {
|
|
54
|
+
...DEFAULT_CONFIG.phaseTemplates,
|
|
55
|
+
...((userConfig && userConfig.phaseTemplates) || {})
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadConfig(options = {}) {
|
|
61
|
+
const projectRoot = path.resolve(options.projectRoot || process.cwd());
|
|
62
|
+
const resolvedConfigPath = options.configPath
|
|
63
|
+
? path.resolve(projectRoot, String(options.configPath))
|
|
64
|
+
: path.resolve(projectRoot, 'roadmap-skill.config.json');
|
|
65
|
+
|
|
66
|
+
const content = readTextIfExists(resolvedConfigPath);
|
|
67
|
+
let userConfig = {};
|
|
68
|
+
if (!content) {
|
|
69
|
+
const merged = mergeConfig(userConfig);
|
|
70
|
+
Object.defineProperty(merged, '__roadmapFileExplicit', {
|
|
71
|
+
value: false,
|
|
72
|
+
enumerable: false,
|
|
73
|
+
configurable: false,
|
|
74
|
+
writable: false
|
|
75
|
+
});
|
|
76
|
+
return merged;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
userConfig = safeParseJson(content, resolvedConfigPath);
|
|
80
|
+
const merged = mergeConfig(userConfig);
|
|
81
|
+
Object.defineProperty(merged, '__roadmapFileExplicit', {
|
|
82
|
+
value: Object.prototype.hasOwnProperty.call(userConfig, 'roadmapFile'),
|
|
83
|
+
enumerable: false,
|
|
84
|
+
configurable: false,
|
|
85
|
+
writable: false
|
|
86
|
+
});
|
|
87
|
+
return merged;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveRoadmapFile(projectRoot, config, overridePath) {
|
|
91
|
+
if (overridePath) {
|
|
92
|
+
return path.resolve(projectRoot, overridePath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const configuredRoadmapFile =
|
|
96
|
+
config && typeof config.roadmapFile === 'string' ? config.roadmapFile.trim() : '';
|
|
97
|
+
const hasExplicitRoadmapFile = Boolean(config && config.__roadmapFileExplicit);
|
|
98
|
+
const hasCustomConfigRoadmapFile =
|
|
99
|
+
configuredRoadmapFile.length > 0 &&
|
|
100
|
+
(hasExplicitRoadmapFile || configuredRoadmapFile !== DEFAULT_CONFIG.roadmapFile);
|
|
101
|
+
|
|
102
|
+
if (hasCustomConfigRoadmapFile) {
|
|
103
|
+
return path.resolve(projectRoot, configuredRoadmapFile);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let rootEntries = null;
|
|
107
|
+
try {
|
|
108
|
+
rootEntries = fs.readdirSync(projectRoot);
|
|
109
|
+
} catch {
|
|
110
|
+
rootEntries = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const canonicalRoadmapPath = path.resolve(projectRoot, DEFAULT_CONFIG.roadmapFile);
|
|
114
|
+
const legacyRoadmapPath = path.resolve(projectRoot, './roadmap.md');
|
|
115
|
+
|
|
116
|
+
const hasCanonicalRoadmap = Array.isArray(rootEntries)
|
|
117
|
+
? rootEntries.includes('ROADMAP.md')
|
|
118
|
+
: readTextIfExists(canonicalRoadmapPath) != null;
|
|
119
|
+
const hasLegacyRoadmap = Array.isArray(rootEntries)
|
|
120
|
+
? rootEntries.includes('roadmap.md')
|
|
121
|
+
: readTextIfExists(legacyRoadmapPath) != null;
|
|
122
|
+
|
|
123
|
+
if (hasCanonicalRoadmap) {
|
|
124
|
+
return canonicalRoadmapPath;
|
|
125
|
+
}
|
|
126
|
+
if (hasLegacyRoadmap) {
|
|
127
|
+
return legacyRoadmapPath;
|
|
128
|
+
}
|
|
129
|
+
return canonicalRoadmapPath;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveAgentsFile(projectRoot, config, overridePath) {
|
|
133
|
+
if (overridePath) {
|
|
134
|
+
return path.resolve(projectRoot, overridePath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const agentsPath = path.resolve(projectRoot, config.agentsFile || './AGENTS.md');
|
|
138
|
+
const claudePath = path.resolve(projectRoot, './CLAUDE.md');
|
|
139
|
+
|
|
140
|
+
if (readTextIfExists(agentsPath) != null) {
|
|
141
|
+
return agentsPath;
|
|
142
|
+
}
|
|
143
|
+
if (readTextIfExists(claudePath) != null) {
|
|
144
|
+
return claudePath;
|
|
145
|
+
}
|
|
146
|
+
return agentsPath;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function loadPlugins(projectRoot, pluginEntries) {
|
|
150
|
+
const plugins = [];
|
|
151
|
+
for (const entry of pluginEntries || []) {
|
|
152
|
+
const pluginPath = path.resolve(projectRoot, entry);
|
|
153
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
154
|
+
const pluginModule = require(pluginPath);
|
|
155
|
+
plugins.push({
|
|
156
|
+
name: path.basename(pluginPath),
|
|
157
|
+
path: pluginPath,
|
|
158
|
+
module: pluginModule
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return plugins;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function collectPluginContributions(plugins, hookName, context) {
|
|
165
|
+
const contributions = [];
|
|
166
|
+
for (const plugin of plugins || []) {
|
|
167
|
+
const hook = plugin.module && plugin.module[hookName];
|
|
168
|
+
if (typeof hook !== 'function') {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const result = hook(context);
|
|
172
|
+
if (Array.isArray(result)) {
|
|
173
|
+
for (const item of result) {
|
|
174
|
+
contributions.push(item);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return contributions;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
DEFAULT_CONFIG,
|
|
183
|
+
collectPluginContributions,
|
|
184
|
+
loadConfig,
|
|
185
|
+
loadPlugins,
|
|
186
|
+
resolveAgentsFile,
|
|
187
|
+
resolveRoadmapFile
|
|
188
|
+
};
|