@nolto/cli 0.5.0 → 0.6.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 +50 -77
- package/dist/index.js +1072 -1646
- package/dist/skill/roadmap-progress/SKILL.md +83 -0
- package/dist/skill/roadmap-progress/agents/openai.yaml +4 -0
- package/dist/skill/roadmap-progress/references/schema.md +81 -0
- package/dist/skill/roadmap-progress/scripts/roadmap.mjs +247 -0
- package/package.json +7 -7
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: roadmap-progress
|
|
3
|
+
description: Maintain a local project's `.roadmap/roadmap.json` as work starts, completes, becomes blocked, or is replanned. Use when Codex or Claude plans project phases and tasks, begins or finishes an implementation task, reports project progress, records a blocker, or needs to keep the Roadmap Viewer current. Do not mark work done without verification. Attach plan documents to tasks with the plan command.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Roadmap Progress
|
|
7
|
+
|
|
8
|
+
Keep `.roadmap/roadmap.json` synchronized with verified repository work. Treat the file as the source of truth for progress reporting.
|
|
9
|
+
|
|
10
|
+
## Resolve the roadmap
|
|
11
|
+
|
|
12
|
+
1. Use the roadmap named by the user when provided.
|
|
13
|
+
2. Otherwise locate `.roadmap/roadmap.json` from the current directory upward.
|
|
14
|
+
3. If no roadmap exists and the user asked to plan or initialize the project, create one using [references/schema.md](references/schema.md). Otherwise report that it is missing; do not invent project scope.
|
|
15
|
+
4. Read [references/schema.md](references/schema.md) before creating phases or tasks, changing IDs, or repairing validation errors.
|
|
16
|
+
|
|
17
|
+
## Choose the workflow
|
|
18
|
+
|
|
19
|
+
### Report status
|
|
20
|
+
|
|
21
|
+
Read without modifying. Summarize overall completed/total tasks, current task, blocked tasks, and the next actionable todo whose dependencies are done. Run the bundled CLI `status` command when practical.
|
|
22
|
+
|
|
23
|
+
### Start work
|
|
24
|
+
|
|
25
|
+
Before implementation, set the selected task to `in-progress`, set `startedAt` when absent, set `currentTaskId`, update its phase status, refresh `summary`, and update `updatedAt`.
|
|
26
|
+
|
|
27
|
+
### Complete work
|
|
28
|
+
|
|
29
|
+
Run relevant verification first. Only after verification succeeds, set the task to `done`, set `completedAt`, clear `currentTaskId` when it points to that task, recompute the phase status, refresh `summary`, and update `updatedAt`. If verification fails, keep the task `in-progress` or mark it `blocked` with a concrete note.
|
|
30
|
+
|
|
31
|
+
### Record a blocker
|
|
32
|
+
|
|
33
|
+
Set the task to `blocked` and add a note that states the blocking condition and the next action or required input. Clear `currentTaskId` when it points to that task. Do not mark dependent tasks as blocked unless they have independently started; their `dependsOn` already represents the constraint.
|
|
34
|
+
|
|
35
|
+
### Replan
|
|
36
|
+
|
|
37
|
+
Edit phases and tasks directly while preserving stable IDs. Add dependencies only when they are real prerequisites. Keep the two-level `phases[] -> tasks[]` hierarchy; do not add epics, subtasks, or percentage fields. Progress is computed from done tasks.
|
|
38
|
+
|
|
39
|
+
### Attach a plan document
|
|
40
|
+
|
|
41
|
+
When an implementation plan exists as a markdown file in the repository,
|
|
42
|
+
link it to its task: `roadmap.mjs plan <task-id> --path docs/plans/feature.md`.
|
|
43
|
+
Paths are repository-relative (the repository root is the parent of `.roadmap/`).
|
|
44
|
+
The command warns if the file does not exist yet; create the plan file first
|
|
45
|
+
when possible.
|
|
46
|
+
|
|
47
|
+
## Use the bundled CLI
|
|
48
|
+
|
|
49
|
+
Resolve the script relative to this `SKILL.md`. Common repository installs are:
|
|
50
|
+
|
|
51
|
+
- Codex: `node .agents/skills/roadmap-progress/scripts/roadmap.mjs ...`
|
|
52
|
+
- Claude Code: `node .claude/skills/roadmap-progress/scripts/roadmap.mjs ...`
|
|
53
|
+
|
|
54
|
+
Supported commands:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
roadmap.mjs [--file path] status
|
|
58
|
+
roadmap.mjs [--file path] validate
|
|
59
|
+
roadmap.mjs [--file path] start <task-id> [--note text] [--summary text]
|
|
60
|
+
roadmap.mjs [--file path] done <task-id> [--note text] [--summary text]
|
|
61
|
+
roadmap.mjs [--file path] block <task-id> --note text [--summary text]
|
|
62
|
+
roadmap.mjs [--file path] todo <task-id> [--note text] [--summary text]
|
|
63
|
+
roadmap.mjs [--file path] summary --text text
|
|
64
|
+
roadmap.mjs [--file path] plan <task-id> --path <repo-relative-md>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use direct JSON edits to add, remove, rename, or reorder phases and tasks, then run `validate`. The CLI writes atomically and maintains timestamps and phase status for task state changes.
|
|
68
|
+
|
|
69
|
+
## Guardrails
|
|
70
|
+
|
|
71
|
+
- `schemaVersion` is 2. Files at version 1 still validate (with a warning) and
|
|
72
|
+
are migrated to 2 automatically by any mutation command. Never edit
|
|
73
|
+
`schemaVersion` by hand.
|
|
74
|
+
- This skill only writes local files. It never sends anything to a server —
|
|
75
|
+
syncing to Nolto is handled separately by `nolto watch` / `nolto sync`.
|
|
76
|
+
- Use only `todo`, `in-progress`, `done`, and `blocked`. Convert legacy `in_progress` when touching that entry.
|
|
77
|
+
- Keep all phase and task IDs unique across the file.
|
|
78
|
+
- Preserve task IDs across title changes so `dependsOn` remains stable.
|
|
79
|
+
- Keep exactly one `currentTaskId`; it must point to an `in-progress` task. Use `null` when nothing is active.
|
|
80
|
+
- Require `completedAt` for done tasks and `startedAt` for in-progress tasks.
|
|
81
|
+
- Keep notes factual and current; replace stale implementation notes instead of accumulating a log.
|
|
82
|
+
- Never rewrite repository history or source code solely to make the roadmap appear complete.
|
|
83
|
+
- Finish every mutation by running `validate` and reporting the task status changed.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Roadmap schema
|
|
2
|
+
|
|
3
|
+
Use this reference when creating or structurally editing `.roadmap/roadmap.json`.
|
|
4
|
+
|
|
5
|
+
## Shape
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"schemaVersion": 2,
|
|
10
|
+
"project": {
|
|
11
|
+
"id": "my-api",
|
|
12
|
+
"name": "My API",
|
|
13
|
+
"repository": "/absolute/path/to/my-api"
|
|
14
|
+
},
|
|
15
|
+
"updatedAt": "2026-07-20T14:30:00+09:00",
|
|
16
|
+
"currentTaskId": "task-004",
|
|
17
|
+
"summary": "セッション管理を実装中。",
|
|
18
|
+
"phases": [
|
|
19
|
+
{
|
|
20
|
+
"id": "phase-auth",
|
|
21
|
+
"title": "認証",
|
|
22
|
+
"status": "in-progress",
|
|
23
|
+
"plan": "docs/plans/auth.md",
|
|
24
|
+
"tasks": [
|
|
25
|
+
{
|
|
26
|
+
"id": "task-003",
|
|
27
|
+
"title": "ログインAPI",
|
|
28
|
+
"status": "done",
|
|
29
|
+
"completedAt": "2026-07-19T18:40:00+09:00"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "task-004",
|
|
33
|
+
"title": "セッション管理",
|
|
34
|
+
"status": "in-progress",
|
|
35
|
+
"startedAt": "2026-07-20T11:00:00+09:00",
|
|
36
|
+
"plan": "docs/plans/session-management.md",
|
|
37
|
+
"note": "Redisへの保存処理まで完了。期限切れ処理が未実装。"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "task-005",
|
|
41
|
+
"title": "ログアウトAPI",
|
|
42
|
+
"status": "todo",
|
|
43
|
+
"dependsOn": ["task-004"]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Fields
|
|
52
|
+
|
|
53
|
+
- `schemaVersion`: Always `2`.
|
|
54
|
+
- `project.id`: Stable lowercase identifier matching `^[a-z0-9][a-z0-9._-]*$`.
|
|
55
|
+
- `project.name`: Display name.
|
|
56
|
+
- `project.repository`: Absolute repository path when known.
|
|
57
|
+
- `updatedAt`: Date-time of the last meaningful roadmap mutation.
|
|
58
|
+
- `currentTaskId`: One active task ID or `null`.
|
|
59
|
+
- `summary`: One or two current sentences; do not use as a changelog.
|
|
60
|
+
- `phases`: Ordered delivery phases.
|
|
61
|
+
- `phase.status`: `todo`, `in-progress`, `done`, or `blocked`.
|
|
62
|
+
- `phase.plan`: Optional repository-relative path to a markdown plan document.
|
|
63
|
+
- `tasks`: Ordered work items. Each must be small enough to verify clearly.
|
|
64
|
+
- `task.status`: `todo`, `in-progress`, `done`, or `blocked`.
|
|
65
|
+
- `task.plan`: Optional repository-relative path to a markdown plan document.
|
|
66
|
+
- `startedAt`: Required in practice for `in-progress` tasks.
|
|
67
|
+
- `completedAt`: Required in practice for `done` tasks.
|
|
68
|
+
- `note`: Current implementation detail or blocker. Omit when it adds no value.
|
|
69
|
+
- `dependsOn`: IDs of prerequisite tasks in the same roadmap.
|
|
70
|
+
|
|
71
|
+
## Phase status derivation
|
|
72
|
+
|
|
73
|
+
After a task mutation, derive phase status in this order:
|
|
74
|
+
|
|
75
|
+
1. `done` when the phase has tasks and all are done.
|
|
76
|
+
2. `in-progress` when any task is in progress.
|
|
77
|
+
3. `blocked` when no task is in progress and any task is blocked.
|
|
78
|
+
4. `in-progress` when some tasks are done and work remains.
|
|
79
|
+
5. `todo` otherwise.
|
|
80
|
+
|
|
81
|
+
Overall and phase progress are computed as `done task count / total task count`. Do not store progress percentages in JSON.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { access, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const STATUSES = new Set(["todo", "in-progress", "done", "blocked"]);
|
|
6
|
+
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
7
|
+
const ALLOWED_KEYS = {
|
|
8
|
+
roadmap: new Set(["schemaVersion", "project", "updatedAt", "currentTaskId", "summary", "phases"]),
|
|
9
|
+
project: new Set(["id", "name", "repository"]),
|
|
10
|
+
phase: new Set(["id", "title", "status", "plan", "tasks"]),
|
|
11
|
+
task: new Set(["id", "title", "status", "startedAt", "completedAt", "note", "dependsOn", "plan"])
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function parseArguments(argv) {
|
|
15
|
+
const options = { file: null, note: undefined, summary: undefined, text: undefined, path: undefined, positional: [] };
|
|
16
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
17
|
+
const value = argv[index];
|
|
18
|
+
if (value === "--file") options.file = argv[++index];
|
|
19
|
+
else if (value === "--note") options.note = argv[++index];
|
|
20
|
+
else if (value === "--summary") options.summary = argv[++index];
|
|
21
|
+
else if (value === "--text") options.text = argv[++index];
|
|
22
|
+
else if (value === "--path") options.path = argv[++index];
|
|
23
|
+
else options.positional.push(value);
|
|
24
|
+
}
|
|
25
|
+
return options;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function findRoadmap(explicitPath) {
|
|
29
|
+
if (explicitPath) return path.resolve(explicitPath);
|
|
30
|
+
let directory = process.cwd();
|
|
31
|
+
while (true) {
|
|
32
|
+
const candidate = path.join(directory, ".roadmap", "roadmap.json");
|
|
33
|
+
try {
|
|
34
|
+
await access(candidate);
|
|
35
|
+
return candidate;
|
|
36
|
+
} catch {
|
|
37
|
+
const parent = path.dirname(directory);
|
|
38
|
+
if (parent === directory) break;
|
|
39
|
+
directory = parent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw new Error("No .roadmap/roadmap.json found. Pass --file <path>.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function localIsoNow() {
|
|
46
|
+
const date = new Date();
|
|
47
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
48
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
49
|
+
const pad = (value) => String(Math.abs(value)).padStart(2, "0");
|
|
50
|
+
const local = new Date(date.getTime() + offsetMinutes * 60_000).toISOString().slice(0, -1);
|
|
51
|
+
return `${local}${sign}${pad(Math.trunc(offsetMinutes / 60))}:${pad(offsetMinutes % 60)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkKeys(value, allowed, at, errors) {
|
|
55
|
+
for (const key of Object.keys(value ?? {})) {
|
|
56
|
+
if (!allowed.has(key)) errors.push(`${at} contains unsupported property "${key}".`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validate(roadmap) {
|
|
61
|
+
const errors = [];
|
|
62
|
+
const warnings = [];
|
|
63
|
+
if (!roadmap || typeof roadmap !== "object" || Array.isArray(roadmap)) return { errors: ["Root must be an object."], warnings };
|
|
64
|
+
checkKeys(roadmap, ALLOWED_KEYS.roadmap, "roadmap", errors);
|
|
65
|
+
if (roadmap.schemaVersion !== 1 && roadmap.schemaVersion !== 2) {
|
|
66
|
+
errors.push("schemaVersion must be 1 or 2.");
|
|
67
|
+
} else if (roadmap.schemaVersion === 1) {
|
|
68
|
+
warnings.push("schemaVersion 1 is legacy; the next mutation migrates this file to 2.");
|
|
69
|
+
}
|
|
70
|
+
checkKeys(roadmap.project, ALLOWED_KEYS.project, "project", errors);
|
|
71
|
+
if (!roadmap.project || !ID_PATTERN.test(roadmap.project.id ?? "")) errors.push("project.id is invalid.");
|
|
72
|
+
if (!roadmap.project?.name) errors.push("project.name is required.");
|
|
73
|
+
if (!roadmap.project?.repository) errors.push("project.repository is required.");
|
|
74
|
+
if (Number.isNaN(Date.parse(roadmap.updatedAt))) errors.push("updatedAt must be a valid date-time.");
|
|
75
|
+
if (typeof roadmap.summary !== "string") errors.push("summary must be a string.");
|
|
76
|
+
if (!Array.isArray(roadmap.phases)) errors.push("phases must be an array.");
|
|
77
|
+
|
|
78
|
+
const allIds = new Set();
|
|
79
|
+
const tasks = new Map();
|
|
80
|
+
for (const [phaseIndex, phase] of (Array.isArray(roadmap.phases) ? roadmap.phases : []).entries()) {
|
|
81
|
+
checkKeys(phase, ALLOWED_KEYS.phase, `phases[${phaseIndex}]`, errors);
|
|
82
|
+
if (!ID_PATTERN.test(phase.id ?? "")) errors.push(`phases[${phaseIndex}].id is invalid.`);
|
|
83
|
+
if (allIds.has(phase.id)) errors.push(`Duplicate id "${phase.id}".`);
|
|
84
|
+
allIds.add(phase.id);
|
|
85
|
+
if (!phase.title) errors.push(`phases[${phaseIndex}].title is required.`);
|
|
86
|
+
if (!STATUSES.has(phase.status)) errors.push(`phases[${phaseIndex}].status is invalid.`);
|
|
87
|
+
if (phase.plan !== undefined && (typeof phase.plan !== "string" || phase.plan.length === 0)) {
|
|
88
|
+
errors.push(`phases[${phaseIndex}].plan must be a non-empty string.`);
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(phase.tasks)) errors.push(`phases[${phaseIndex}].tasks must be an array.`);
|
|
91
|
+
for (const [taskIndex, task] of (Array.isArray(phase.tasks) ? phase.tasks : []).entries()) {
|
|
92
|
+
const at = `phases[${phaseIndex}].tasks[${taskIndex}]`;
|
|
93
|
+
checkKeys(task, ALLOWED_KEYS.task, at, errors);
|
|
94
|
+
if (!ID_PATTERN.test(task.id ?? "")) errors.push(`${at}.id is invalid.`);
|
|
95
|
+
if (allIds.has(task.id)) errors.push(`Duplicate id "${task.id}".`);
|
|
96
|
+
allIds.add(task.id);
|
|
97
|
+
tasks.set(task.id, task);
|
|
98
|
+
if (!task.title) errors.push(`${at}.title is required.`);
|
|
99
|
+
if (!STATUSES.has(task.status)) errors.push(`${at}.status is invalid.`);
|
|
100
|
+
if (task.status === "done" && !task.completedAt) warnings.push(`${task.id} is done without completedAt.`);
|
|
101
|
+
if (task.status === "in-progress" && !task.startedAt) warnings.push(`${task.id} is in-progress without startedAt.`);
|
|
102
|
+
if (task.plan !== undefined && (typeof task.plan !== "string" || task.plan.length === 0)) {
|
|
103
|
+
errors.push(`${at}.plan must be a non-empty string.`);
|
|
104
|
+
}
|
|
105
|
+
if (task.dependsOn !== undefined && !Array.isArray(task.dependsOn)) errors.push(`${at}.dependsOn must be an array.`);
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(phase.tasks) && STATUSES.has(phase.status)) {
|
|
108
|
+
const expected = derivePhaseStatus(phase);
|
|
109
|
+
if (phase.status !== expected) warnings.push(`${phase.id} status is ${phase.status}; task states derive ${expected}.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const task of tasks.values()) {
|
|
113
|
+
for (const dependency of task.dependsOn ?? []) {
|
|
114
|
+
if (!tasks.has(dependency)) warnings.push(`${task.id} depends on unknown task ${dependency}.`);
|
|
115
|
+
if (task.id === dependency) errors.push(`${task.id} cannot depend on itself.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (roadmap.currentTaskId !== null && roadmap.currentTaskId !== undefined) {
|
|
119
|
+
const current = tasks.get(roadmap.currentTaskId);
|
|
120
|
+
if (!current) errors.push(`currentTaskId ${roadmap.currentTaskId} does not exist.`);
|
|
121
|
+
else if (current.status !== "in-progress") warnings.push(`currentTaskId ${roadmap.currentTaskId} is not in-progress.`);
|
|
122
|
+
}
|
|
123
|
+
return { errors, warnings };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function derivePhaseStatus(phase) {
|
|
127
|
+
if (phase.tasks.length > 0 && phase.tasks.every((task) => task.status === "done")) return "done";
|
|
128
|
+
if (phase.tasks.some((task) => task.status === "in-progress")) return "in-progress";
|
|
129
|
+
if (phase.tasks.some((task) => task.status === "blocked")) return "blocked";
|
|
130
|
+
if (phase.tasks.some((task) => task.status === "done")) return "in-progress";
|
|
131
|
+
return "todo";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function findTask(roadmap, taskId) {
|
|
135
|
+
for (const phase of roadmap.phases) {
|
|
136
|
+
const task = phase.tasks.find((item) => item.id === taskId);
|
|
137
|
+
if (task) return { task, phase };
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`Task "${taskId}" not found.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function stats(roadmap) {
|
|
143
|
+
const counts = { todo: 0, "in-progress": 0, done: 0, blocked: 0 };
|
|
144
|
+
for (const phase of roadmap.phases) for (const task of phase.tasks) counts[task.status] += 1;
|
|
145
|
+
const total = Object.values(counts).reduce((sum, value) => sum + value, 0);
|
|
146
|
+
return { counts, total, progress: total ? Math.round((counts.done / total) * 100) : 0 };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function atomicWrite(filePath, roadmap) {
|
|
150
|
+
const temporary = `${filePath}.tmp-${process.pid}`;
|
|
151
|
+
await writeFile(temporary, `${JSON.stringify(roadmap, null, 2)}\n`, "utf8");
|
|
152
|
+
await rename(temporary, filePath);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function printStatus(roadmap) {
|
|
156
|
+
const result = stats(roadmap);
|
|
157
|
+
const current = roadmap.currentTaskId ? findTask(roadmap, roadmap.currentTaskId).task : null;
|
|
158
|
+
console.log(`${roadmap.project.name}: ${result.counts.done}/${result.total} done (${result.progress}%)`);
|
|
159
|
+
console.log(`Current: ${current ? `${current.id} — ${current.title}` : "none"}`);
|
|
160
|
+
const blocked = roadmap.phases.flatMap((phase) => phase.tasks).filter((task) => task.status === "blocked");
|
|
161
|
+
console.log(`Blocked: ${blocked.length ? blocked.map((task) => `${task.id} — ${task.title}`).join(", ") : "none"}`);
|
|
162
|
+
if (roadmap.summary) console.log(`Summary: ${roadmap.summary}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const options = parseArguments(process.argv.slice(2));
|
|
166
|
+
const [command = "status", taskId] = options.positional;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const filePath = await findRoadmap(options.file);
|
|
170
|
+
const roadmap = JSON.parse(await readFile(filePath, "utf8"));
|
|
171
|
+
|
|
172
|
+
if (command === "status") {
|
|
173
|
+
printStatus(roadmap);
|
|
174
|
+
} else if (command === "validate") {
|
|
175
|
+
const result = validate(roadmap);
|
|
176
|
+
for (const warning of result.warnings) console.warn(`WARN ${warning}`);
|
|
177
|
+
if (result.errors.length) {
|
|
178
|
+
for (const error of result.errors) console.error(`ERROR ${error}`);
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
} else {
|
|
181
|
+
console.log(`OK ${filePath}`);
|
|
182
|
+
}
|
|
183
|
+
} else if (command === "summary") {
|
|
184
|
+
if (options.text === undefined) throw new Error("summary requires --text.");
|
|
185
|
+
roadmap.summary = options.text;
|
|
186
|
+
roadmap.updatedAt = localIsoNow();
|
|
187
|
+
roadmap.schemaVersion = 2;
|
|
188
|
+
await atomicWrite(filePath, roadmap);
|
|
189
|
+
console.log(`Updated summary in ${filePath}`);
|
|
190
|
+
} else if (["start", "done", "block", "todo"].includes(command)) {
|
|
191
|
+
if (!taskId) throw new Error(`${command} requires a task id.`);
|
|
192
|
+
if (command === "block" && !options.note) throw new Error("block requires a non-empty --note.");
|
|
193
|
+
const { task, phase } = findTask(roadmap, taskId);
|
|
194
|
+
const now = localIsoNow();
|
|
195
|
+
if (command === "start") {
|
|
196
|
+
task.status = "in-progress";
|
|
197
|
+
task.startedAt ??= now;
|
|
198
|
+
delete task.completedAt;
|
|
199
|
+
roadmap.currentTaskId = task.id;
|
|
200
|
+
} else if (command === "done") {
|
|
201
|
+
task.status = "done";
|
|
202
|
+
task.completedAt = now;
|
|
203
|
+
if (roadmap.currentTaskId === task.id) roadmap.currentTaskId = null;
|
|
204
|
+
} else if (command === "block") {
|
|
205
|
+
task.status = "blocked";
|
|
206
|
+
delete task.completedAt;
|
|
207
|
+
if (roadmap.currentTaskId === task.id) roadmap.currentTaskId = null;
|
|
208
|
+
} else {
|
|
209
|
+
task.status = "todo";
|
|
210
|
+
delete task.startedAt;
|
|
211
|
+
delete task.completedAt;
|
|
212
|
+
if (roadmap.currentTaskId === task.id) roadmap.currentTaskId = null;
|
|
213
|
+
}
|
|
214
|
+
if (options.note !== undefined) task.note = options.note;
|
|
215
|
+
if (options.summary !== undefined) roadmap.summary = options.summary;
|
|
216
|
+
phase.status = derivePhaseStatus(phase);
|
|
217
|
+
roadmap.updatedAt = now;
|
|
218
|
+
roadmap.schemaVersion = 2;
|
|
219
|
+
const result = validate(roadmap);
|
|
220
|
+
if (result.errors.length) throw new Error(`Mutation failed validation: ${result.errors.join(" ")}`);
|
|
221
|
+
await atomicWrite(filePath, roadmap);
|
|
222
|
+
console.log(`${task.id} -> ${task.status}`);
|
|
223
|
+
for (const warning of result.warnings) console.warn(`WARN ${warning}`);
|
|
224
|
+
} else if (command === "plan") {
|
|
225
|
+
if (!taskId) throw new Error("plan requires a task id.");
|
|
226
|
+
if (!options.path) throw new Error("plan requires --path <repo-relative-md-path>.");
|
|
227
|
+
const { task } = findTask(roadmap, taskId);
|
|
228
|
+
task.plan = options.path;
|
|
229
|
+
roadmap.schemaVersion = 2;
|
|
230
|
+
roadmap.updatedAt = localIsoNow();
|
|
231
|
+
const repoRoot = path.dirname(path.dirname(filePath));
|
|
232
|
+
try {
|
|
233
|
+
await access(path.join(repoRoot, options.path));
|
|
234
|
+
} catch {
|
|
235
|
+
console.warn(`WARN plan file not found at ${path.join(repoRoot, options.path)}`);
|
|
236
|
+
}
|
|
237
|
+
const result = validate(roadmap);
|
|
238
|
+
if (result.errors.length) throw new Error(`Mutation failed validation: ${result.errors.join(" ")}`);
|
|
239
|
+
await atomicWrite(filePath, roadmap);
|
|
240
|
+
console.log(`${task.id} plan -> ${options.path}`);
|
|
241
|
+
} else {
|
|
242
|
+
throw new Error(`Unknown command "${command}".`);
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error(`roadmap-progress: ${error.message}`);
|
|
246
|
+
process.exitCode = 1;
|
|
247
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nolto/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "CLI for syncing repository roadmaps with Nolto.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"homepage": "https://nolto.app",
|
|
26
26
|
"keywords": [
|
|
27
27
|
"nolto",
|
|
28
|
-
"mcp",
|
|
29
28
|
"cli",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
29
|
+
"roadmap",
|
|
30
|
+
"sync"
|
|
32
31
|
],
|
|
33
32
|
"scripts": {
|
|
34
|
-
"build": "tsup",
|
|
33
|
+
"build": "tsup && node scripts/copy-skill.mjs",
|
|
35
34
|
"typecheck": "tsc -p tsconfig.json",
|
|
36
35
|
"test": "vitest run --coverage",
|
|
37
36
|
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
38
37
|
},
|
|
39
38
|
"dependencies": {
|
|
40
|
-
"@
|
|
39
|
+
"@nolto/roadmap-schema": "0.1.0",
|
|
40
|
+
"chokidar": "^4.0.3",
|
|
41
41
|
"commander": "^14.0.0",
|
|
42
42
|
"zod": "^3.24.1"
|
|
43
43
|
},
|