@howaboua/opencode-roadmap-plugin 0.1.6 → 0.1.7

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,11 +1,21 @@
1
1
  import type { Roadmap, RoadmapStorage, ValidationError } from "./types.js";
2
+ type UpdateResult<T> = {
3
+ roadmap: Roadmap;
4
+ buildResult: (archiveName: string | null) => T;
5
+ archive?: boolean;
6
+ };
2
7
  export declare class FileStorage implements RoadmapStorage {
3
8
  private readonly directory;
4
9
  constructor(directory: string);
5
10
  exists(): Promise<boolean>;
6
11
  read(): Promise<Roadmap | null>;
12
+ private readFromDisk;
7
13
  private acquireLock;
14
+ private fsyncDir;
15
+ private writeAtomic;
16
+ private archiveUnlocked;
8
17
  write(roadmap: Roadmap): Promise<void>;
18
+ update<T>(fn: (current: Roadmap | null) => Promise<UpdateResult<T>> | UpdateResult<T>): Promise<T>;
9
19
  archive(): Promise<string>;
10
20
  }
11
21
  export declare class RoadmapValidator {
@@ -24,3 +34,4 @@ export declare class RoadmapValidator {
24
34
  static validateDescription(description: string, fieldType: "feature" | "action"): ValidationError | null;
25
35
  static validateStatusProgression(currentStatus: string, newStatus: string): ValidationError | null;
26
36
  }
37
+ export {};
@@ -10,6 +10,7 @@ const ROADMAP_FILE = "roadmap.json";
10
10
  const LOCK_FILE = `${ROADMAP_FILE}.lock`;
11
11
  const LOCK_TIMEOUT_MS = 5000;
12
12
  const LOCK_RETRY_MS = 50;
13
+ const LOCK_STALE_MS = 30000;
13
14
  export class FileStorage {
14
15
  directory;
15
16
  constructor(directory) {
@@ -25,6 +26,9 @@ export class FileStorage {
25
26
  }
26
27
  }
27
28
  async read() {
29
+ return this.readFromDisk();
30
+ }
31
+ async readFromDisk() {
28
32
  try {
29
33
  const filePath = join(this.directory, ROADMAP_FILE);
30
34
  const data = await fs.readFile(filePath, "utf-8");
@@ -63,30 +67,80 @@ export class FileStorage {
63
67
  };
64
68
  }
65
69
  catch {
66
- await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
70
+ const isStale = await fs
71
+ .stat(lockPath)
72
+ .then((stat) => Date.now() - stat.mtimeMs > LOCK_STALE_MS)
73
+ .catch(() => false);
74
+ if (isStale) {
75
+ await fs.unlink(lockPath).catch(() => { });
76
+ continue;
77
+ }
78
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
67
79
  }
68
80
  }
69
81
  throw new Error("Could not acquire lock on roadmap file. Another operation may be in progress.");
70
82
  }
83
+ async fsyncDir() {
84
+ const handle = await fs.open(this.directory, "r");
85
+ try {
86
+ await handle.sync();
87
+ }
88
+ finally {
89
+ await handle.close().catch(() => { });
90
+ }
91
+ }
92
+ async writeAtomic(filePath, data) {
93
+ const randomSuffix = Math.random().toString(36).slice(2, 8);
94
+ const tempPath = join(this.directory, `${ROADMAP_FILE}.tmp.${Date.now()}.${randomSuffix}`);
95
+ const handle = await fs.open(tempPath, "w");
96
+ try {
97
+ await handle.writeFile(data, "utf-8");
98
+ await handle.sync();
99
+ await handle.close();
100
+ await fs.rename(tempPath, filePath);
101
+ await this.fsyncDir();
102
+ }
103
+ catch (error) {
104
+ await handle.close().catch(() => { });
105
+ await fs.unlink(tempPath).catch(() => { });
106
+ if (error instanceof Error) {
107
+ throw error;
108
+ }
109
+ throw new Error("Unknown error while writing roadmap");
110
+ }
111
+ }
112
+ async archiveUnlocked() {
113
+ const filePath = join(this.directory, ROADMAP_FILE);
114
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
115
+ const archiveFilename = `roadmap.archive.${timestamp}.json`;
116
+ const archivePath = join(this.directory, archiveFilename);
117
+ await fs.rename(filePath, archivePath);
118
+ await this.fsyncDir();
119
+ return archiveFilename;
120
+ }
71
121
  async write(roadmap) {
72
122
  await fs.mkdir(this.directory, { recursive: true }).catch(() => { });
73
123
  const unlock = await this.acquireLock();
74
124
  try {
125
+ const data = JSON.stringify(roadmap, null, 2);
75
126
  const filePath = join(this.directory, ROADMAP_FILE);
76
- const randomSuffix = Math.random().toString(36).slice(2, 8);
77
- const tempPath = join(this.directory, `${ROADMAP_FILE}.tmp.${Date.now()}.${randomSuffix}`);
78
- try {
79
- const data = JSON.stringify(roadmap, null, 2);
80
- await fs.writeFile(tempPath, data, "utf-8");
81
- await fs.rename(tempPath, filePath);
82
- }
83
- catch (error) {
84
- await fs.unlink(tempPath).catch(() => { });
85
- if (error instanceof Error) {
86
- throw error;
87
- }
88
- throw new Error("Unknown error while writing roadmap");
89
- }
127
+ await this.writeAtomic(filePath, data);
128
+ }
129
+ finally {
130
+ await unlock();
131
+ }
132
+ }
133
+ async update(fn) {
134
+ await fs.mkdir(this.directory, { recursive: true }).catch(() => { });
135
+ const unlock = await this.acquireLock();
136
+ try {
137
+ const current = await this.readFromDisk();
138
+ const outcome = await fn(current);
139
+ const data = JSON.stringify(outcome.roadmap, null, 2);
140
+ const filePath = join(this.directory, ROADMAP_FILE);
141
+ await this.writeAtomic(filePath, data);
142
+ const archiveName = outcome.archive ? await this.archiveUnlocked() : null;
143
+ return outcome.buildResult(archiveName);
90
144
  }
91
145
  finally {
92
146
  await unlock();
@@ -96,12 +150,13 @@ export class FileStorage {
96
150
  if (!(await this.exists())) {
97
151
  throw new Error("Cannot archive: roadmap file does not exist");
98
152
  }
99
- const filePath = join(this.directory, ROADMAP_FILE);
100
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
101
- const archiveFilename = `roadmap.archive.${timestamp}.json`;
102
- const archivePath = join(this.directory, archiveFilename);
103
- await fs.rename(filePath, archivePath);
104
- return archiveFilename;
153
+ const unlock = await this.acquireLock();
154
+ try {
155
+ return await this.archiveUnlocked();
156
+ }
157
+ finally {
158
+ await unlock();
159
+ }
105
160
  }
106
161
  }
107
162
  export class RoadmapValidator {
@@ -237,7 +292,7 @@ export class RoadmapValidator {
237
292
  if (currentStatus === "cancelled") {
238
293
  return {
239
294
  code: "INVALID_STATUS_TRANSITION",
240
- message: `Cannot change status of cancelled action. Create a new action instead.`,
295
+ message: "Cannot change status of cancelled action. Create a new action instead.",
241
296
  };
242
297
  }
243
298
  return null;
@@ -30,117 +30,110 @@ export async function createCreateRoadmapTool(directory) {
30
30
  },
31
31
  async execute(args) {
32
32
  const storage = new FileStorage(directory);
33
- let roadmap;
34
- let isUpdate = false;
35
- if (await storage.exists()) {
36
- const existing = await storage.read();
37
- if (!existing) {
38
- throw new Error("Existing roadmap file is corrupted. Please fix manually.");
39
- }
40
- roadmap = existing;
41
- isUpdate = true;
42
- }
43
- else {
44
- roadmap = { features: [] };
45
- }
46
33
  if (!args.features || args.features.length === 0) {
47
34
  throw new Error('Roadmap must have at least one feature with at least one action. Example: {"features": [{"number": "1", "title": "Feature 1", "description": "Description", "actions": [{"number": "1.01", "description": "Action 1", "status": "pending"}]}]}');
48
35
  }
49
- const validationErrors = [];
50
- // First pass: structural validation of input
51
- for (const feature of args.features) {
52
- if (!feature.actions || feature.actions.length === 0) {
53
- throw new Error(`Feature "${feature.number}" must have at least one action. Each feature needs at least one action to be valid.`);
36
+ return await storage.update(async (current) => {
37
+ const roadmap = current ?? { features: [] };
38
+ const isUpdate = current !== null;
39
+ const validationErrors = [];
40
+ // First pass: structural validation of input
41
+ for (const feature of args.features) {
42
+ if (!feature.actions || feature.actions.length === 0) {
43
+ throw new Error(`Feature "${feature.number}" must have at least one action. Each feature needs at least one action to be valid.`);
44
+ }
45
+ const titleError = RoadmapValidator.validateTitle(feature.title, "feature");
46
+ if (titleError)
47
+ validationErrors.push(titleError);
48
+ const descError = RoadmapValidator.validateDescription(feature.description, "feature");
49
+ if (descError)
50
+ validationErrors.push(descError);
51
+ for (const action of feature.actions) {
52
+ const actionTitleError = RoadmapValidator.validateTitle(action.description, "action");
53
+ if (actionTitleError)
54
+ validationErrors.push(actionTitleError);
55
+ }
54
56
  }
55
- const titleError = RoadmapValidator.validateTitle(feature.title, "feature");
56
- if (titleError)
57
- validationErrors.push(titleError);
58
- const descError = RoadmapValidator.validateDescription(feature.description, "feature");
59
- if (descError)
60
- validationErrors.push(descError);
61
- for (const action of feature.actions) {
62
- const actionTitleError = RoadmapValidator.validateTitle(action.description, "action");
63
- if (actionTitleError)
64
- validationErrors.push(actionTitleError);
57
+ // Validate sequence consistency of input (internal consistency)
58
+ const sequenceErrors = RoadmapValidator.validateFeatureSequence(args.features);
59
+ validationErrors.push(...sequenceErrors);
60
+ if (validationErrors.length > 0) {
61
+ const errorMessages = validationErrors.map((err) => err.message).join("\n");
62
+ throw new Error(`Validation errors:\n${errorMessages}\n\nPlease fix these issues and try again.`);
65
63
  }
66
- }
67
- // Validate sequence consistency of input (internal consistency)
68
- const sequenceErrors = RoadmapValidator.validateFeatureSequence(args.features);
69
- validationErrors.push(...sequenceErrors);
70
- if (validationErrors.length > 0) {
71
- const errorMessages = validationErrors.map((err) => err.message).join("\n");
72
- throw new Error(`Validation errors:\n${errorMessages}\n\nPlease fix these issues and try again.`);
73
- }
74
- // Merge Logic
75
- for (const inputFeature of args.features) {
76
- const existingFeature = roadmap.features.find((f) => f.number === inputFeature.number);
77
- if (existingFeature) {
78
- // Feature exists: Validate Immutability
79
- if (existingFeature.title !== inputFeature.title ||
80
- existingFeature.description !== inputFeature.description) {
81
- const msg = await getErrorMessage("immutable_feature", {
82
- id: inputFeature.number,
83
- oldTitle: existingFeature.title,
84
- oldDesc: existingFeature.description,
85
- newTitle: inputFeature.title,
86
- newDesc: inputFeature.description,
87
- });
88
- throw new Error(msg);
89
- }
90
- // Process Actions
91
- for (const inputAction of inputFeature.actions) {
92
- const existingAction = existingFeature.actions.find((a) => a.number === inputAction.number);
93
- if (existingAction) {
94
- // Action exists: skip (immutable)
95
- continue;
96
- }
97
- else {
98
- // New Action: Append
99
- existingFeature.actions.push({
100
- number: inputAction.number,
101
- description: inputAction.description,
102
- status: inputAction.status,
64
+ // Merge Logic
65
+ for (const inputFeature of args.features) {
66
+ const existingFeature = roadmap.features.find((f) => f.number === inputFeature.number);
67
+ if (existingFeature) {
68
+ // Feature exists: Validate Immutability
69
+ if (existingFeature.title !== inputFeature.title ||
70
+ existingFeature.description !== inputFeature.description) {
71
+ const msg = await getErrorMessage("immutable_feature", {
72
+ id: inputFeature.number,
73
+ oldTitle: existingFeature.title,
74
+ oldDesc: existingFeature.description,
75
+ newTitle: inputFeature.title,
76
+ newDesc: inputFeature.description,
103
77
  });
104
- // Sort actions to ensure order
105
- existingFeature.actions.sort((a, b) => parseFloat(a.number) - parseFloat(b.number));
78
+ throw new Error(msg);
79
+ }
80
+ // Process Actions
81
+ for (const inputAction of inputFeature.actions) {
82
+ const existingAction = existingFeature.actions.find((a) => a.number === inputAction.number);
83
+ if (existingAction) {
84
+ // Action exists: skip (immutable)
85
+ continue;
86
+ }
87
+ else {
88
+ // New Action: Append
89
+ existingFeature.actions.push({
90
+ number: inputAction.number,
91
+ description: inputAction.description,
92
+ status: inputAction.status,
93
+ });
94
+ // Sort actions to ensure order
95
+ existingFeature.actions.sort((a, b) => parseFloat(a.number) - parseFloat(b.number));
96
+ }
106
97
  }
107
98
  }
99
+ else {
100
+ // New Feature: Append
101
+ roadmap.features.push({
102
+ number: inputFeature.number,
103
+ title: inputFeature.title,
104
+ description: inputFeature.description,
105
+ actions: inputFeature.actions.map((a) => ({
106
+ number: a.number,
107
+ description: a.description,
108
+ status: a.status,
109
+ })),
110
+ });
111
+ }
108
112
  }
109
- else {
110
- // New Feature: Append
111
- roadmap.features.push({
112
- number: inputFeature.number,
113
- title: inputFeature.title,
114
- description: inputFeature.description,
115
- actions: inputFeature.actions.map((a) => ({
116
- number: a.number,
117
- description: a.description,
118
- status: a.status,
119
- })),
120
- });
113
+ // Final Sort of Features
114
+ roadmap.features.sort((a, b) => parseInt(a.number) - parseInt(b.number));
115
+ // Safety check: ensure no feature ended up with zero actions after merge
116
+ for (const feature of roadmap.features) {
117
+ if (feature.actions.length === 0) {
118
+ throw new Error(`Feature "${feature.number}" has no actions. This indicates a merge error.`);
119
+ }
121
120
  }
122
- }
123
- // Final Sort of Features
124
- roadmap.features.sort((a, b) => parseInt(a.number) - parseInt(b.number));
125
- // Safety check: ensure no feature ended up with zero actions after merge
126
- for (const feature of roadmap.features) {
127
- if (feature.actions.length === 0) {
128
- throw new Error(`Feature "${feature.number}" has no actions. This indicates a merge error.`);
121
+ // Final Validation of the Merged Roadmap
122
+ const finalErrors = RoadmapValidator.validateFeatureSequence(roadmap.features);
123
+ if (finalErrors.length > 0) {
124
+ throw new Error(`Resulting roadmap would be invalid:\n${finalErrors.map((e) => e.message).join("\n")}`);
129
125
  }
130
- }
131
- // Final Validation of the Merged Roadmap
132
- const finalErrors = RoadmapValidator.validateFeatureSequence(roadmap.features);
133
- if (finalErrors.length > 0) {
134
- throw new Error(`Resulting roadmap would be invalid:\n${finalErrors.map((e) => e.message).join("\n")}`);
135
- }
136
- await storage.write(roadmap);
137
- const totalActions = roadmap.features.reduce((sum, feature) => sum + feature.actions.length, 0);
138
- const action = isUpdate ? "Updated" : "Created";
139
- const summary = `${action} roadmap with ${roadmap.features.length} features and ${totalActions} actions:\n` +
140
- roadmap.features
141
- .map((feature) => ` Feature ${feature.number}: ${feature.title} (${feature.actions.length} actions)`)
142
- .join("\n");
143
- return summary;
126
+ const totalActions = roadmap.features.reduce((sum, feature) => sum + feature.actions.length, 0);
127
+ const action = isUpdate ? "Updated" : "Created";
128
+ const summary = `${action} roadmap with ${roadmap.features.length} features and ${totalActions} actions:\n` +
129
+ roadmap.features
130
+ .map((feature) => ` Feature ${feature.number}: ${feature.title} (${feature.actions.length} actions)`)
131
+ .join("\n");
132
+ return {
133
+ roadmap,
134
+ buildResult: () => summary,
135
+ };
136
+ });
144
137
  },
145
138
  });
146
139
  }
@@ -22,96 +22,97 @@ export async function createUpdateRoadmapTool(directory) {
22
22
  },
23
23
  async execute(args) {
24
24
  const storage = new FileStorage(directory);
25
- if (!(await storage.exists())) {
26
- throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
27
- }
28
- const roadmap = await storage.read();
29
- if (!roadmap) {
30
- throw new Error("Roadmap file is corrupted. Please fix manually.");
31
- }
32
25
  const actionNumberError = RoadmapValidator.validateActionNumber(args.actionNumber);
33
26
  if (actionNumberError) {
34
27
  throw new Error(`${actionNumberError.message} Use ReadRoadmap to see valid action numbers.`);
35
28
  }
36
- let targetAction = null;
37
- let targetFeature = null;
38
- let actionFound = false;
39
- for (const feature of roadmap.features) {
40
- const action = feature.actions.find((a) => a.number === args.actionNumber);
41
- if (action) {
42
- targetAction = action;
43
- targetFeature = feature;
44
- actionFound = true;
45
- break;
29
+ return await storage.update((roadmap) => {
30
+ if (!roadmap) {
31
+ throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
46
32
  }
47
- }
48
- if (!actionFound) {
49
- throw new Error(`Action "${args.actionNumber}" not found. Use ReadRoadmap to see valid action numbers.`);
50
- }
51
- // TypeScript: we know targetAction and targetFeature are not null here
52
- if (!targetAction || !targetFeature) {
53
- throw new Error("Internal error: target action not found.");
54
- }
55
- // Validate that at least one field is being updated
56
- if (args.description === undefined && args.status === undefined) {
57
- throw new Error("No changes specified. Please provide description and/or status.");
58
- }
59
- const oldStatus = targetAction.status;
60
- const oldDescription = targetAction.description;
61
- // Validate description if provided
62
- if (args.description !== undefined) {
63
- const descError = RoadmapValidator.validateDescription(args.description, "action");
64
- if (descError) {
65
- throw new Error(`${descError.message}`);
33
+ let targetAction = null;
34
+ let targetFeature = null;
35
+ let actionFound = false;
36
+ for (const feature of roadmap.features) {
37
+ const action = feature.actions.find((a) => a.number === args.actionNumber);
38
+ if (action) {
39
+ targetAction = action;
40
+ targetFeature = feature;
41
+ actionFound = true;
42
+ break;
43
+ }
66
44
  }
67
- targetAction.description = args.description;
68
- }
69
- // Validate and update status if provided
70
- if (args.status !== undefined) {
71
- const statusTransitionError = RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
72
- if (statusTransitionError) {
73
- throw new Error(`${statusTransitionError.message} Current status: "${targetAction.status}", requested: "${args.status}"`);
45
+ if (!actionFound) {
46
+ throw new Error(`Action "${args.actionNumber}" not found. Use ReadRoadmap to see valid action numbers.`);
74
47
  }
75
- targetAction.status = args.status;
76
- }
77
- await storage.write(roadmap);
78
- const changes = [];
79
- if (args.description !== undefined && oldDescription !== args.description) {
80
- changes.push(`description updated`);
81
- }
82
- if (args.status !== undefined && oldStatus !== args.status) {
83
- changes.push(`status: "${oldStatus}" → "${args.status}"`);
84
- }
85
- if (changes.length === 0) {
86
- return `Action ${args.actionNumber} unchanged. Provided values match current state.`;
87
- }
88
- // Check if all actions are completed
89
- let allCompleted = true;
90
- for (const feature of roadmap.features) {
91
- for (const action of feature.actions) {
92
- if (action.status !== "completed") {
93
- allCompleted = false;
94
- break;
48
+ if (!targetAction || !targetFeature) {
49
+ throw new Error("Internal error: target action not found.");
50
+ }
51
+ // Validate that at least one field is being updated
52
+ if (args.description === undefined && args.status === undefined) {
53
+ throw new Error("No changes specified. Please provide description and/or status.");
54
+ }
55
+ const oldStatus = targetAction.status;
56
+ const oldDescription = targetAction.description;
57
+ // Validate description if provided
58
+ if (args.description !== undefined) {
59
+ const descError = RoadmapValidator.validateDescription(args.description, "action");
60
+ if (descError) {
61
+ throw new Error(`${descError.message}`);
95
62
  }
63
+ targetAction.description = args.description;
96
64
  }
97
- if (!allCompleted)
98
- break;
99
- }
100
- let archiveMsg = "";
101
- if (allCompleted) {
102
- const archiveName = await storage.archive();
103
- archiveMsg = `\n\nšŸŽ‰ All actions completed! Roadmap archived to "${archiveName}".`;
104
- }
105
- // Format feature context
106
- const featureCompleted = targetFeature.actions.filter((a) => a.status === "completed").length;
107
- const featureTotal = targetFeature.actions.length;
108
- let featureContext = `\n\nFeature ${targetFeature.number}: ${targetFeature.title} (${featureCompleted}/${featureTotal} complete)\n`;
109
- featureContext += `Description: ${targetFeature.description}\n`;
110
- for (const action of targetFeature.actions) {
111
- const statusIcon = action.status === "completed" ? "āœ“" : action.status === "in_progress" ? "→" : "ā—‹";
112
- featureContext += `${action.number} ${statusIcon} ${action.description} [${action.status}]\n`;
113
- }
114
- return `Updated action ${args.actionNumber} in feature "${targetFeature.title}": ${changes.join(", ")}${featureContext}${archiveMsg}`;
65
+ // Validate and update status if provided
66
+ if (args.status !== undefined) {
67
+ const statusTransitionError = RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
68
+ if (statusTransitionError) {
69
+ throw new Error(`${statusTransitionError.message} Current status: "${targetAction.status}", requested: "${args.status}"`);
70
+ }
71
+ targetAction.status = args.status;
72
+ }
73
+ const changes = [];
74
+ if (args.description !== undefined && oldDescription !== args.description) {
75
+ changes.push("description updated");
76
+ }
77
+ if (args.status !== undefined && oldStatus !== args.status) {
78
+ changes.push(`status: "${oldStatus}" → "${args.status}"`);
79
+ }
80
+ if (changes.length === 0) {
81
+ return {
82
+ roadmap,
83
+ buildResult: () => `Action ${args.actionNumber} unchanged. Provided values match current state.`,
84
+ };
85
+ }
86
+ // Check if all actions are completed
87
+ let allCompleted = true;
88
+ for (const feature of roadmap.features) {
89
+ for (const action of feature.actions) {
90
+ if (action.status !== "completed") {
91
+ allCompleted = false;
92
+ break;
93
+ }
94
+ }
95
+ if (!allCompleted)
96
+ break;
97
+ }
98
+ // Format feature context
99
+ const featureCompleted = targetFeature.actions.filter((a) => a.status === "completed").length;
100
+ const featureTotal = targetFeature.actions.length;
101
+ let featureContext = `\n\nFeature ${targetFeature.number}: ${targetFeature.title} (${featureCompleted}/${featureTotal} complete)\n`;
102
+ featureContext += `Description: ${targetFeature.description}\n`;
103
+ for (const action of targetFeature.actions) {
104
+ const statusIcon = action.status === "completed" ? "āœ“" : action.status === "in_progress" ? "→" : "ā—‹";
105
+ featureContext += `${action.number} ${statusIcon} ${action.description} [${action.status}]\n`;
106
+ }
107
+ return {
108
+ roadmap,
109
+ archive: allCompleted,
110
+ buildResult: (archiveName) => {
111
+ const archiveMsg = archiveName ? `\n\nAll actions completed! Roadmap archived to "${archiveName}".` : "";
112
+ return `Updated action ${args.actionNumber} in feature "${targetFeature.title}": ${changes.join(", ")}${featureContext}${archiveMsg}`;
113
+ },
114
+ };
115
+ });
115
116
  },
116
117
  });
117
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/opencode-roadmap-plugin",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Strategic roadmap planning and multi-agent coordination for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",