@howaboua/opencode-roadmap-plugin 0.1.9 → 0.2.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 CHANGED
@@ -30,14 +30,14 @@ OpenCode installs it automatically on next launch.
30
30
 
31
31
  ### `createroadmap`
32
32
 
33
- Create or extend a project roadmap.
33
+ Create or extend a project roadmap. Requires a feature list and a short spec for each feature.
34
34
 
35
35
  ```
36
- "Create a roadmap for building user auth with login, signup, and password reset"
36
+ "Create a roadmap with features: 1) Auth, 2) Profiles. Specs: Auth uses OAuth and must support password reset; Profiles needs avatar uploads and privacy settings"
37
37
  ```
38
38
 
39
- - Features group related work (`"1"`, `"2"`, `"3"`)
40
- - Actions are concrete tasks (`"1.01"`, `"1.02"`) within features
39
+ - Features group related work (`"1"`, `"2"`, `"3"`) and include a brief spec
40
+ - Actions are markdown task list items (`- [ ] 1.01 ...`) within features
41
41
  - New actions always start as `pending`
42
42
  - Append-only: existing IDs never change
43
43
 
@@ -54,11 +54,11 @@ Before delegating work to Task tool subagents, instruct them to read the roadmap
54
54
 
55
55
  ### `updateroadmap`
56
56
 
57
- Change action status or description.
57
+ Change action status or description. Each update includes a brief note appended to the updates section.
58
58
 
59
59
  ```
60
- "Mark action 1.01 as in_progress"
61
- "Action 2.03 is completed"
60
+ "Mark action 1.01 as in_progress — Drafted schema notes"
61
+ "Action 2.03 is completed — Added tests for edge cases"
62
62
  ```
63
63
 
64
64
  **Statuses:** `pending` → `in_progress` → `completed` | `cancelled`
@@ -92,8 +92,8 @@ AI: Reads roadmap → sees Feature 1 has 4 actions → uses todowrite for immedi
92
92
 
93
93
  ## Storage
94
94
 
95
- - **Active:** `roadmap.json` in project root
96
- - **Archived:** `roadmap.archive.<timestamp>.json` when complete
95
+ - **Active:** Stored as a markdown roadmap alongside the project
96
+ - **Archived:** Snapshot archived when complete
97
97
 
98
98
  ## License
99
99
 
@@ -12,8 +12,8 @@ Structure: Features contain Actions. IDs are immutable once created.
12
12
 
13
13
  Inputs:
14
14
  - feature: short label for the overall roadmap
15
- - spec: natural-language spec for the overall direction
16
- - features/actions: structured tasks with numbered IDs
15
+ - spec: natural-language spec for the overall direction (scope, constraints, success criteria)
16
+ - features/actions: structured tasks with numbered IDs and clear, detailed descriptions
17
17
 
18
18
  Example:
19
19
  {
@@ -7,4 +7,4 @@ Use after completing work on an action. When delegating to Task tool subagents,
7
7
 
8
8
  Archives roadmap automatically when all actions reach completed.
9
9
 
10
- Input: actionNumber (required), status (optional), description (optional).
10
+ Input: actionNumber (required), note (required), status (optional), description (optional).
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Parses and renders the roadmap markdown document.
3
- * Keeps frontmatter simple and the task block machine-readable.
3
+ * Keeps frontmatter simple and the task list machine-readable.
4
4
  * Exposes a narrow API for the storage layer.
5
5
  */
6
6
  import type { RoadmapDocument } from "../types.js";
@@ -1,19 +1,25 @@
1
1
  import { Roadmap as RoadmapSchema } from "../types.js";
2
2
  const FRONTMATTER_START = "---\n";
3
3
  const FRONTMATTER_END = "\n---\n";
4
- const TASK_FENCE = "```json";
5
- const TASK_FENCE_END = "\n```";
4
+ const TASK_LIST_HEADER = "## Task List";
5
+ const STATUS_MAP = {
6
+ " ": "pending",
7
+ x: "completed",
8
+ X: "completed",
9
+ "~": "in_progress",
10
+ "-": "cancelled",
11
+ };
6
12
  export const parseDocument = (data) => {
7
13
  const { frontmatter, body } = splitFrontmatter(data);
8
14
  const { feature, spec } = parseFrontmatter(frontmatter);
9
- const roadmap = parseRoadmapBody(body);
15
+ const roadmap = parseTaskList(body);
10
16
  return { feature, spec, roadmap };
11
17
  };
12
18
  export const buildDocument = (document) => {
13
19
  const specValue = document.spec.trimEnd();
14
20
  const specLines = specValue === "" ? [""] : specValue.split("\n");
15
21
  const specBlock = specLines.map((line) => ` ${line}`).join("\n");
16
- const tasks = JSON.stringify(document.roadmap, null, 2);
22
+ const taskList = buildTaskList(document.roadmap);
17
23
  return [
18
24
  "---",
19
25
  `feature: ${JSON.stringify(document.feature)}`,
@@ -21,12 +27,19 @@ export const buildDocument = (document) => {
21
27
  specBlock,
22
28
  "---",
23
29
  "",
24
- TASK_FENCE,
25
- tasks,
26
- "```",
30
+ TASK_LIST_HEADER,
27
31
  "",
32
+ taskList,
28
33
  ].join("\n");
29
34
  };
35
+ export const ensureDocument = (document) => {
36
+ const validated = RoadmapSchema.parse(document.roadmap);
37
+ return {
38
+ feature: document.feature,
39
+ spec: document.spec,
40
+ roadmap: validated,
41
+ };
42
+ };
30
43
  const splitFrontmatter = (data) => {
31
44
  if (!data.startsWith(FRONTMATTER_START)) {
32
45
  throw new Error("Roadmap format is invalid. Missing frontmatter.");
@@ -90,28 +103,97 @@ const normalizeSpec = (lines) => {
90
103
  const normalized = lines.map((line) => line.slice(indent));
91
104
  return normalized.join("\n").trimEnd();
92
105
  };
93
- const parseRoadmapBody = (body) => {
94
- const fenceStart = body.indexOf(TASK_FENCE);
95
- if (fenceStart === -1) {
96
- throw new Error("Roadmap format is invalid. Missing task block.");
106
+ const parseTaskList = (body) => {
107
+ const lines = body.split("\n");
108
+ const features = [];
109
+ let currentFeature = null;
110
+ for (const line of lines) {
111
+ const trimmed = line.trim();
112
+ if (trimmed === "" || trimmed === TASK_LIST_HEADER) {
113
+ continue;
114
+ }
115
+ const featureMatch = trimmed.match(/^#+\s*Feature\s+(\d+)\s*:\s*(.+)$/);
116
+ if (featureMatch) {
117
+ if (currentFeature) {
118
+ features.push(currentFeature);
119
+ }
120
+ currentFeature = {
121
+ number: featureMatch[1],
122
+ title: featureMatch[2].trim(),
123
+ description: "",
124
+ actions: [],
125
+ };
126
+ continue;
127
+ }
128
+ const descriptionMatch = trimmed.match(/^Description:\s*(.+)$/);
129
+ if (descriptionMatch) {
130
+ if (!currentFeature) {
131
+ throw new Error("Roadmap format is invalid. Description must follow a feature header.");
132
+ }
133
+ currentFeature.description = descriptionMatch[1].trim();
134
+ continue;
135
+ }
136
+ const actionMatch = trimmed.match(/^-\s*\[([ xX~-])\]\s+(\d+\.\d{2})\s+(.+)$/);
137
+ if (actionMatch) {
138
+ if (!currentFeature) {
139
+ throw new Error("Roadmap format is invalid. Action must follow a feature header.");
140
+ }
141
+ const statusToken = actionMatch[1];
142
+ const status = STATUS_MAP[statusToken];
143
+ if (!status) {
144
+ throw new Error("Roadmap format is invalid. Unsupported status marker.");
145
+ }
146
+ const number = actionMatch[2];
147
+ const numberPrefix = number.split(".")[0];
148
+ if (numberPrefix !== currentFeature.number) {
149
+ throw new Error(`Action "${number}" does not belong to feature "${currentFeature.number}".`);
150
+ }
151
+ currentFeature.actions.push({
152
+ number,
153
+ description: actionMatch[3].trim(),
154
+ status,
155
+ });
156
+ continue;
157
+ }
97
158
  }
98
- const jsonStart = body.indexOf("\n", fenceStart + TASK_FENCE.length);
99
- if (jsonStart === -1) {
100
- throw new Error("Roadmap format is invalid. Task block is incomplete.");
159
+ if (currentFeature) {
160
+ features.push(currentFeature);
101
161
  }
102
- const fenceEnd = body.indexOf(TASK_FENCE_END, jsonStart + 1);
103
- if (fenceEnd === -1) {
104
- throw new Error("Roadmap format is invalid. Task block is not closed.");
162
+ if (features.length === 0) {
163
+ throw new Error("Roadmap format is invalid. Missing task list.");
164
+ }
165
+ for (const feature of features) {
166
+ if (!feature.description) {
167
+ throw new Error(`Feature "${feature.number}" is missing a description.`);
168
+ }
169
+ if (feature.actions.length === 0) {
170
+ throw new Error(`Feature "${feature.number}" must include at least one action.`);
171
+ }
105
172
  }
106
- const jsonText = body.slice(jsonStart + 1, fenceEnd).trim();
107
- const parsed = JSON.parse(jsonText);
108
- return RoadmapSchema.parse(parsed);
173
+ return RoadmapSchema.parse({ features });
109
174
  };
110
- export const ensureDocument = (document) => {
111
- const validated = RoadmapSchema.parse(document.roadmap);
112
- return {
113
- feature: document.feature,
114
- spec: document.spec,
115
- roadmap: validated,
116
- };
175
+ const buildTaskList = (roadmap) => {
176
+ const lines = [];
177
+ for (const feature of roadmap.features) {
178
+ lines.push(`### Feature ${feature.number}: ${feature.title}`);
179
+ lines.push(`Description: ${feature.description}`);
180
+ for (const action of feature.actions) {
181
+ lines.push(`${renderAction(action.status)} ${action.number} ${action.description}`);
182
+ }
183
+ lines.push("");
184
+ }
185
+ return lines.join("\n");
186
+ };
187
+ const renderAction = (status) => {
188
+ switch (status) {
189
+ case "completed":
190
+ return "- [x]";
191
+ case "in_progress":
192
+ return "- [~]";
193
+ case "cancelled":
194
+ return "- [-]";
195
+ case "pending":
196
+ default:
197
+ return "- [ ]";
198
+ }
117
199
  };
@@ -20,6 +20,7 @@ export async function createUpdateRoadmapTool(directory) {
20
20
  .enum(["pending", "in_progress", "completed", "cancelled"])
21
21
  .optional()
22
22
  .describe("New action status. Flexible transitions allowed except from cancelled."),
23
+ note: tool.schema.string().describe("Required update note to append to the action."),
23
24
  },
24
25
  async execute(args) {
25
26
  const storage = new FileStorage(directory);
@@ -54,6 +55,10 @@ export async function createUpdateRoadmapTool(directory) {
54
55
  if (args.description === undefined && args.status === undefined) {
55
56
  throw new Error("No changes specified. Please provide description and/or status.");
56
57
  }
58
+ const noteError = RoadmapValidator.validateDescription(args.note, "action");
59
+ if (noteError) {
60
+ throw new Error(`${noteError.message}`);
61
+ }
57
62
  const oldStatus = targetAction.status;
58
63
  const oldDescription = targetAction.description;
59
64
  // Validate description if provided
@@ -64,6 +69,7 @@ export async function createUpdateRoadmapTool(directory) {
64
69
  }
65
70
  targetAction.description = args.description;
66
71
  }
72
+ targetAction.description = `${targetAction.description} (note: ${args.note})`;
67
73
  // Validate and update status if provided
68
74
  if (args.status !== undefined) {
69
75
  const statusTransitionError = RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
@@ -79,6 +85,9 @@ export async function createUpdateRoadmapTool(directory) {
79
85
  if (args.status !== undefined && oldStatus !== args.status) {
80
86
  changes.push(`status: "${oldStatus}" → "${args.status}"`);
81
87
  }
88
+ if (oldDescription !== targetAction.description) {
89
+ changes.push("note added");
90
+ }
82
91
  if (changes.length === 0) {
83
92
  return {
84
93
  document: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/opencode-roadmap-plugin",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "Strategic roadmap planning and multi-agent coordination for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",