@howaboua/opencode-roadmap-plugin 0.1.4 → 0.1.6

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,7 +1,10 @@
1
+ /**
2
+ * Tool for reading roadmap state, progress, and details.
3
+ * Supports filtering by feature or action number.
4
+ */
1
5
  import { tool } from "@opencode-ai/plugin";
2
6
  import { FileStorage, RoadmapValidator } from "../storage.js";
3
7
  import { loadDescription } from "../descriptions/index.js";
4
- import { getErrorMessage } from "../errors/loader.js";
5
8
  export async function createReadRoadmapTool(directory) {
6
9
  const description = await loadDescription("readroadmap.txt");
7
10
  return tool({
@@ -19,17 +22,17 @@ export async function createReadRoadmapTool(directory) {
19
22
  async execute(args) {
20
23
  const storage = new FileStorage(directory);
21
24
  if (!(await storage.exists())) {
22
- throw new Error(await getErrorMessage("roadmap_not_found"));
25
+ throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
23
26
  }
24
27
  const roadmap = await storage.read();
25
28
  if (!roadmap) {
26
- throw new Error(await getErrorMessage("roadmap_corrupted"));
29
+ throw new Error("Roadmap file is corrupted. Please fix manually.");
27
30
  }
28
31
  if (args.actionNumber && args.featureNumber) {
29
32
  throw new Error("Cannot specify both actionNumber and featureNumber. Use one or the other, or neither for full roadmap.");
30
33
  }
31
34
  if (args.actionNumber) {
32
- const actionNumberError = await RoadmapValidator.validateActionNumber(args.actionNumber);
35
+ const actionNumberError = RoadmapValidator.validateActionNumber(args.actionNumber);
33
36
  if (actionNumberError) {
34
37
  throw new Error(`${actionNumberError.message} Use ReadRoadmap to see valid action numbers.`);
35
38
  }
@@ -46,7 +49,7 @@ export async function createReadRoadmapTool(directory) {
46
49
  throw new Error(`Action "${args.actionNumber}" not found. Use ReadRoadmap with no arguments to see all available actions.`);
47
50
  }
48
51
  if (args.featureNumber) {
49
- const featureNumberError = await RoadmapValidator.validateFeatureNumber(args.featureNumber);
52
+ const featureNumberError = RoadmapValidator.validateFeatureNumber(args.featureNumber);
50
53
  if (featureNumberError) {
51
54
  throw new Error(`${featureNumberError.message} Use ReadRoadmap to see valid feature numbers.`);
52
55
  }
@@ -1,2 +1,6 @@
1
+ /**
2
+ * Tool for updating action status and descriptions in a roadmap.
3
+ * Enforces forward-only status progression and archives when complete.
4
+ */
1
5
  import { type ToolDefinition } from "@opencode-ai/plugin";
2
6
  export declare function createUpdateRoadmapTool(directory: string): Promise<ToolDefinition>;
@@ -1,7 +1,10 @@
1
+ /**
2
+ * Tool for updating action status and descriptions in a roadmap.
3
+ * Enforces forward-only status progression and archives when complete.
4
+ */
1
5
  import { tool } from "@opencode-ai/plugin";
2
6
  import { FileStorage, RoadmapValidator } from "../storage.js";
3
7
  import { loadDescription } from "../descriptions/index.js";
4
- import { getErrorMessage } from "../errors/loader.js";
5
8
  export async function createUpdateRoadmapTool(directory) {
6
9
  const description = await loadDescription("updateroadmap.txt");
7
10
  return tool({
@@ -13,20 +16,20 @@ export async function createUpdateRoadmapTool(directory) {
13
16
  .optional()
14
17
  .describe("New action description (full overwrite). If not provided, only status is updated."),
15
18
  status: tool.schema
16
- .enum(["pending", "in_progress", "completed"])
19
+ .enum(["pending", "in_progress", "completed", "cancelled"])
17
20
  .optional()
18
- .describe("New action status - optional if only updating description"),
21
+ .describe("New action status. Flexible transitions allowed except from cancelled."),
19
22
  },
20
23
  async execute(args) {
21
24
  const storage = new FileStorage(directory);
22
25
  if (!(await storage.exists())) {
23
- throw new Error(await getErrorMessage("roadmap_not_found"));
26
+ throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
24
27
  }
25
28
  const roadmap = await storage.read();
26
29
  if (!roadmap) {
27
- throw new Error(await getErrorMessage("roadmap_corrupted"));
30
+ throw new Error("Roadmap file is corrupted. Please fix manually.");
28
31
  }
29
- const actionNumberError = await RoadmapValidator.validateActionNumber(args.actionNumber);
32
+ const actionNumberError = RoadmapValidator.validateActionNumber(args.actionNumber);
30
33
  if (actionNumberError) {
31
34
  throw new Error(`${actionNumberError.message} Use ReadRoadmap to see valid action numbers.`);
32
35
  }
@@ -43,17 +46,21 @@ export async function createUpdateRoadmapTool(directory) {
43
46
  }
44
47
  }
45
48
  if (!actionFound) {
46
- throw new Error(await getErrorMessage("action_not_found", { id: args.actionNumber }));
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.");
47
54
  }
48
55
  // Validate that at least one field is being updated
49
56
  if (args.description === undefined && args.status === undefined) {
50
- throw new Error(await getErrorMessage("no_changes_specified"));
57
+ throw new Error("No changes specified. Please provide description and/or status.");
51
58
  }
52
59
  const oldStatus = targetAction.status;
53
60
  const oldDescription = targetAction.description;
54
61
  // Validate description if provided
55
62
  if (args.description !== undefined) {
56
- const descError = await RoadmapValidator.validateDescription(args.description, "action");
63
+ const descError = RoadmapValidator.validateDescription(args.description, "action");
57
64
  if (descError) {
58
65
  throw new Error(`${descError.message}`);
59
66
  }
@@ -61,7 +68,7 @@ export async function createUpdateRoadmapTool(directory) {
61
68
  }
62
69
  // Validate and update status if provided
63
70
  if (args.status !== undefined) {
64
- const statusTransitionError = await RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
71
+ const statusTransitionError = RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
65
72
  if (statusTransitionError) {
66
73
  throw new Error(`${statusTransitionError.message} Current status: "${targetAction.status}", requested: "${args.status}"`);
67
74
  }
@@ -75,6 +82,9 @@ export async function createUpdateRoadmapTool(directory) {
75
82
  if (args.status !== undefined && oldStatus !== args.status) {
76
83
  changes.push(`status: "${oldStatus}" → "${args.status}"`);
77
84
  }
85
+ if (changes.length === 0) {
86
+ return `Action ${args.actionNumber} unchanged. Provided values match current state.`;
87
+ }
78
88
  // Check if all actions are completed
79
89
  let allCompleted = true;
80
90
  for (const feature of roadmap.features) {
@@ -1,17 +1,17 @@
1
1
  import { z } from "zod";
2
- export declare const ActionStatus: z.ZodEnum<["pending", "in_progress", "completed"]>;
2
+ export declare const ActionStatus: z.ZodEnum<["pending", "in_progress", "completed", "cancelled"]>;
3
3
  export type ActionStatus = z.infer<typeof ActionStatus>;
4
4
  export declare const Action: z.ZodObject<{
5
5
  number: z.ZodString;
6
6
  description: z.ZodString;
7
- status: z.ZodEnum<["pending", "in_progress", "completed"]>;
7
+ status: z.ZodEnum<["pending", "in_progress", "completed", "cancelled"]>;
8
8
  }, "strip", z.ZodTypeAny, {
9
9
  number: string;
10
- status: "pending" | "in_progress" | "completed";
10
+ status: "pending" | "in_progress" | "completed" | "cancelled";
11
11
  description: string;
12
12
  }, {
13
13
  number: string;
14
- status: "pending" | "in_progress" | "completed";
14
+ status: "pending" | "in_progress" | "completed" | "cancelled";
15
15
  description: string;
16
16
  }>;
17
17
  export type Action = z.infer<typeof Action>;
@@ -22,14 +22,14 @@ export declare const Feature: z.ZodObject<{
22
22
  actions: z.ZodArray<z.ZodObject<{
23
23
  number: z.ZodString;
24
24
  description: z.ZodString;
25
- status: z.ZodEnum<["pending", "in_progress", "completed"]>;
25
+ status: z.ZodEnum<["pending", "in_progress", "completed", "cancelled"]>;
26
26
  }, "strip", z.ZodTypeAny, {
27
27
  number: string;
28
- status: "pending" | "in_progress" | "completed";
28
+ status: "pending" | "in_progress" | "completed" | "cancelled";
29
29
  description: string;
30
30
  }, {
31
31
  number: string;
32
- status: "pending" | "in_progress" | "completed";
32
+ status: "pending" | "in_progress" | "completed" | "cancelled";
33
33
  description: string;
34
34
  }>, "many">;
35
35
  }, "strip", z.ZodTypeAny, {
@@ -38,7 +38,7 @@ export declare const Feature: z.ZodObject<{
38
38
  title: string;
39
39
  actions: {
40
40
  number: string;
41
- status: "pending" | "in_progress" | "completed";
41
+ status: "pending" | "in_progress" | "completed" | "cancelled";
42
42
  description: string;
43
43
  }[];
44
44
  }, {
@@ -47,7 +47,7 @@ export declare const Feature: z.ZodObject<{
47
47
  title: string;
48
48
  actions: {
49
49
  number: string;
50
- status: "pending" | "in_progress" | "completed";
50
+ status: "pending" | "in_progress" | "completed" | "cancelled";
51
51
  description: string;
52
52
  }[];
53
53
  }>;
@@ -60,14 +60,14 @@ export declare const Roadmap: z.ZodObject<{
60
60
  actions: z.ZodArray<z.ZodObject<{
61
61
  number: z.ZodString;
62
62
  description: z.ZodString;
63
- status: z.ZodEnum<["pending", "in_progress", "completed"]>;
63
+ status: z.ZodEnum<["pending", "in_progress", "completed", "cancelled"]>;
64
64
  }, "strip", z.ZodTypeAny, {
65
65
  number: string;
66
- status: "pending" | "in_progress" | "completed";
66
+ status: "pending" | "in_progress" | "completed" | "cancelled";
67
67
  description: string;
68
68
  }, {
69
69
  number: string;
70
- status: "pending" | "in_progress" | "completed";
70
+ status: "pending" | "in_progress" | "completed" | "cancelled";
71
71
  description: string;
72
72
  }>, "many">;
73
73
  }, "strip", z.ZodTypeAny, {
@@ -76,7 +76,7 @@ export declare const Roadmap: z.ZodObject<{
76
76
  title: string;
77
77
  actions: {
78
78
  number: string;
79
- status: "pending" | "in_progress" | "completed";
79
+ status: "pending" | "in_progress" | "completed" | "cancelled";
80
80
  description: string;
81
81
  }[];
82
82
  }, {
@@ -85,7 +85,7 @@ export declare const Roadmap: z.ZodObject<{
85
85
  title: string;
86
86
  actions: {
87
87
  number: string;
88
- status: "pending" | "in_progress" | "completed";
88
+ status: "pending" | "in_progress" | "completed" | "cancelled";
89
89
  description: string;
90
90
  }[];
91
91
  }>, "many">;
@@ -96,7 +96,7 @@ export declare const Roadmap: z.ZodObject<{
96
96
  title: string;
97
97
  actions: {
98
98
  number: string;
99
- status: "pending" | "in_progress" | "completed";
99
+ status: "pending" | "in_progress" | "completed" | "cancelled";
100
100
  description: string;
101
101
  }[];
102
102
  }[];
@@ -107,7 +107,7 @@ export declare const Roadmap: z.ZodObject<{
107
107
  title: string;
108
108
  actions: {
109
109
  number: string;
110
- status: "pending" | "in_progress" | "completed";
110
+ status: "pending" | "in_progress" | "completed" | "cancelled";
111
111
  description: string;
112
112
  }[];
113
113
  }[];
@@ -129,21 +129,21 @@ export interface ValidationResult {
129
129
  errors: ValidationError[];
130
130
  }
131
131
  export interface CreateRoadmapInput {
132
- features: Array<{
132
+ features: {
133
133
  number: string;
134
134
  title: string;
135
135
  description: string;
136
- actions: Array<{
136
+ actions: {
137
137
  number: string;
138
138
  description: string;
139
139
  status: "pending";
140
- }>;
141
- }>;
140
+ }[];
141
+ }[];
142
142
  }
143
143
  export interface UpdateRoadmapInput {
144
144
  actionNumber: string;
145
145
  description?: string;
146
- status: ActionStatus;
146
+ status?: ActionStatus;
147
147
  }
148
148
  export interface ReadRoadmapInput {
149
149
  actionNumber?: string;
package/dist/src/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export const ActionStatus = z.enum(["pending", "in_progress", "completed"]);
2
+ export const ActionStatus = z.enum(["pending", "in_progress", "completed", "cancelled"]);
3
3
  export const Action = z.object({
4
4
  number: z
5
5
  .string()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/opencode-roadmap-plugin",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Strategic roadmap planning and multi-agent coordination for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,18 +25,24 @@
25
25
  "devDependencies": {
26
26
  "@opencode-ai/plugin": "^1.0.0",
27
27
  "@types/node": "^20.0.0",
28
+ "@typescript-eslint/eslint-plugin": "^8.48.1",
29
+ "@typescript-eslint/parser": "^8.48.1",
30
+ "eslint": "^9.39.1",
31
+ "eslint-config-prettier": "^10.1.8",
28
32
  "typescript": "^5.0.0"
29
33
  },
30
34
  "dependencies": {
31
- "zod": "^3.22.0",
32
- "typescript": "^5.0.0"
35
+ "zod": "^3.22.0"
33
36
  },
34
-
35
37
  "scripts": {
36
38
  "build": "tsc -p tsconfig.json && npm run copy-assets",
37
39
  "copy-assets": "mkdir -p dist/src/descriptions dist/src/errors && cp src/descriptions/*.txt dist/src/descriptions/ && cp src/errors/*.txt dist/src/errors/",
40
+ "lint": "eslint . --ext .ts",
38
41
  "prepublishOnly": "npm run build"
39
42
  },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
40
46
  "opencode": {
41
47
  "type": "plugin",
42
48
  "tools": [
@@ -1 +0,0 @@
1
- export { loadDescription } from "./loader";
@@ -1 +0,0 @@
1
- export { loadDescription } from "./loader.js";
@@ -1 +0,0 @@
1
- export declare function loadDescription(filename: string): Promise<string>;
@@ -1,17 +0,0 @@
1
- import { promises as fs } from "fs";
2
- import { join } from "path";
3
- export async function loadDescription(filename) {
4
- // In the compiled output, __dirname is .../dist/descriptions, but the assets are in .../src/descriptions.
5
- // This path adjustment ensures the assets are found regardless of the build process.
6
- const descriptionsDir = join(__dirname, "..", "..", "src", "descriptions");
7
- const filePath = join(descriptionsDir, filename);
8
- try {
9
- return await fs.readFile(filePath, "utf-8");
10
- }
11
- catch (error) {
12
- if (error.code === "ENOENT") {
13
- throw new Error(`Description file not found: ${filename}. Looked in: ${filePath}. Please ensure asset files are correctly located.`);
14
- }
15
- throw error;
16
- }
17
- }
@@ -1,2 +0,0 @@
1
- export declare function loadErrorTemplate(filename: string): Promise<string>;
2
- export declare function getErrorMessage(filename: string, params?: Record<string, string>): Promise<string>;
@@ -1,24 +0,0 @@
1
- import { promises as fs } from "fs";
2
- import { join } from "path";
3
- const ERROR_CACHE = {};
4
- export async function loadErrorTemplate(filename) {
5
- if (ERROR_CACHE[filename])
6
- return ERROR_CACHE[filename];
7
- const errorsDir = join(__dirname, "..", "..", "src", "errors");
8
- const filePath = join(errorsDir, filename + ".txt");
9
- try {
10
- const content = await fs.readFile(filePath, "utf-8");
11
- ERROR_CACHE[filename] = content.trim();
12
- return ERROR_CACHE[filename];
13
- }
14
- catch (error) {
15
- if (error.code === "ENOENT") {
16
- throw new Error(`Error template not found: ${filename} at ${filePath}`);
17
- }
18
- throw error;
19
- }
20
- }
21
- export async function getErrorMessage(filename, params = {}) {
22
- const template = await loadErrorTemplate(filename);
23
- return template.replace(/\{(\w+)\}/g, (_, key) => params[key] || `{${key}}`);
24
- }
package/dist/storage.d.ts DELETED
@@ -1,25 +0,0 @@
1
- import type { Roadmap, RoadmapStorage, ValidationError } from "./types";
2
- export declare class FileStorage implements RoadmapStorage {
3
- private directory;
4
- constructor(directory: string);
5
- exists(): Promise<boolean>;
6
- read(): Promise<Roadmap | null>;
7
- write(roadmap: Roadmap): Promise<void>;
8
- archive(): Promise<string>;
9
- }
10
- export declare class RoadmapValidator {
11
- static validateFeatureNumber(number: string): Promise<ValidationError | null>;
12
- static validateActionNumber(number: string): Promise<ValidationError | null>;
13
- static validateActionSequence(actions: Array<{
14
- number: string;
15
- }>, globalSeenNumbers?: Set<string>, featureNumber?: string): Promise<ValidationError[]>;
16
- static validateFeatureSequence(features: Array<{
17
- number: string;
18
- actions: Array<{
19
- number: string;
20
- }>;
21
- }>): Promise<ValidationError[]>;
22
- static validateTitle(title: string, fieldType: "feature" | "action"): Promise<ValidationError | null>;
23
- static validateDescription(description: string, fieldType: "feature" | "action"): Promise<ValidationError | null>;
24
- static validateStatusProgression(currentStatus: string, newStatus: string): Promise<ValidationError | null>;
25
- }
package/dist/storage.js DELETED
@@ -1,214 +0,0 @@
1
- import { promises as fs } from "fs";
2
- import { join } from "path";
3
- import { getErrorMessage } from "./errors/loader.js";
4
- const ROADMAP_FILE = "roadmap.json";
5
- export class FileStorage {
6
- directory;
7
- constructor(directory) {
8
- this.directory = directory;
9
- }
10
- async exists() {
11
- try {
12
- await fs.access(join(this.directory, ROADMAP_FILE));
13
- return true;
14
- }
15
- catch {
16
- return false;
17
- }
18
- }
19
- async read() {
20
- try {
21
- const filePath = join(this.directory, ROADMAP_FILE);
22
- const data = await fs.readFile(filePath, "utf-8");
23
- if (!data.trim()) {
24
- return null;
25
- }
26
- const parsed = JSON.parse(data);
27
- if (!parsed || typeof parsed !== "object") {
28
- throw new Error("Invalid roadmap format: not an object");
29
- }
30
- if (!Array.isArray(parsed.features)) {
31
- throw new Error("Invalid roadmap format: missing or invalid features array");
32
- }
33
- return parsed;
34
- }
35
- catch (error) {
36
- if (error instanceof SyntaxError) {
37
- throw new Error("Roadmap file contains invalid JSON. File may be corrupted.");
38
- }
39
- if (error instanceof Error && error.message.includes("ENOENT")) {
40
- return null;
41
- }
42
- throw error;
43
- }
44
- }
45
- async write(roadmap) {
46
- const filePath = join(this.directory, ROADMAP_FILE);
47
- const tempPath = join(this.directory, `${ROADMAP_FILE}.tmp.${Date.now()}`);
48
- try {
49
- const data = JSON.stringify(roadmap, null, 2);
50
- await fs.writeFile(tempPath, data, "utf-8");
51
- await fs.rename(tempPath, filePath);
52
- }
53
- catch (error) {
54
- try {
55
- await fs.unlink(tempPath);
56
- }
57
- catch {
58
- // Ignore cleanup errors
59
- }
60
- throw error;
61
- }
62
- }
63
- async archive() {
64
- const filePath = join(this.directory, ROADMAP_FILE);
65
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
66
- const archiveFilename = `roadmap.archive.${timestamp}.json`;
67
- const archivePath = join(this.directory, archiveFilename);
68
- await fs.rename(filePath, archivePath);
69
- return archiveFilename;
70
- }
71
- }
72
- export class RoadmapValidator {
73
- static async validateFeatureNumber(number) {
74
- if (!number || typeof number !== "string") {
75
- return {
76
- code: "INVALID_FEATURE_NUMBER",
77
- message: await getErrorMessage("invalid_feature_id", { id: "undefined" }),
78
- };
79
- }
80
- if (!/^\d+$/.test(number)) {
81
- return {
82
- code: "INVALID_FEATURE_NUMBER_FORMAT",
83
- message: await getErrorMessage("invalid_feature_id", { id: number }),
84
- };
85
- }
86
- return null;
87
- }
88
- static async validateActionNumber(number) {
89
- if (!number || typeof number !== "string") {
90
- return {
91
- code: "INVALID_ACTION_NUMBER",
92
- message: await getErrorMessage("invalid_action_id", { id: "undefined" }),
93
- };
94
- }
95
- if (!/^\d+\.\d{2}$/.test(number)) {
96
- return {
97
- code: "INVALID_ACTION_NUMBER_FORMAT",
98
- message: await getErrorMessage("invalid_action_id", { id: number }),
99
- };
100
- }
101
- return null;
102
- }
103
- static async validateActionSequence(actions, globalSeenNumbers, featureNumber) {
104
- const errors = [];
105
- const seenNumbers = new Set();
106
- for (let i = 0; i < actions.length; i++) {
107
- const action = actions[i];
108
- const numberError = await this.validateActionNumber(action.number);
109
- if (numberError) {
110
- errors.push(numberError);
111
- continue;
112
- }
113
- // Check action-feature mismatch
114
- if (featureNumber) {
115
- const actionFeaturePrefix = action.number.split('.')[0];
116
- if (actionFeaturePrefix !== featureNumber) {
117
- errors.push({
118
- code: "ACTION_FEATURE_MISMATCH",
119
- message: await getErrorMessage("action_mismatch", { action: action.number, feature: featureNumber }),
120
- });
121
- }
122
- }
123
- // Check for duplicates within this feature
124
- if (seenNumbers.has(action.number)) {
125
- errors.push({
126
- code: "DUPLICATE_ACTION_NUMBER",
127
- message: `Duplicate action ID "${action.number}".`,
128
- });
129
- }
130
- // Check for global duplicates
131
- if (globalSeenNumbers?.has(action.number)) {
132
- errors.push({
133
- code: "DUPLICATE_ACTION_NUMBER_GLOBAL",
134
- message: `Duplicate action ID "${action.number}" (exists in another feature).`,
135
- });
136
- }
137
- seenNumbers.add(action.number);
138
- globalSeenNumbers?.add(action.number);
139
- }
140
- return errors;
141
- }
142
- static async validateFeatureSequence(features) {
143
- const errors = [];
144
- const seenNumbers = new Set();
145
- const seenActionNumbers = new Set();
146
- for (let i = 0; i < features.length; i++) {
147
- const feature = features[i];
148
- const numberError = await this.validateFeatureNumber(feature.number);
149
- if (numberError) {
150
- errors.push(numberError);
151
- continue;
152
- }
153
- if (seenNumbers.has(feature.number)) {
154
- errors.push({
155
- code: "DUPLICATE_FEATURE_NUMBER",
156
- message: `Duplicate feature ID "${feature.number}".`,
157
- });
158
- }
159
- seenNumbers.add(feature.number);
160
- const actionErrors = await this.validateActionSequence(feature.actions, seenActionNumbers, feature.number);
161
- errors.push(...actionErrors);
162
- }
163
- return errors;
164
- }
165
- static async validateTitle(title, fieldType) {
166
- if (!title || typeof title !== "string") {
167
- return {
168
- code: "INVALID_TITLE",
169
- message: `Invalid ${fieldType} title. Must be non-empty string.`,
170
- };
171
- }
172
- if (title.trim() === "") {
173
- return {
174
- code: "EMPTY_TITLE",
175
- message: `${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)} title cannot be empty.`,
176
- };
177
- }
178
- return null;
179
- }
180
- static async validateDescription(description, fieldType) {
181
- if (!description || typeof description !== "string") {
182
- return {
183
- code: "INVALID_DESCRIPTION",
184
- message: `Invalid ${fieldType} description. Must be non-empty string.`,
185
- };
186
- }
187
- if (description.trim() === "") {
188
- return {
189
- code: "EMPTY_DESCRIPTION",
190
- message: `${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)} description cannot be empty.`,
191
- };
192
- }
193
- return null;
194
- }
195
- static async validateStatusProgression(currentStatus, newStatus) {
196
- const statusFlow = {
197
- pending: ["in_progress", "completed"],
198
- in_progress: ["completed"],
199
- completed: [],
200
- };
201
- const allowedTransitions = statusFlow[currentStatus] || [];
202
- if (!allowedTransitions.includes(newStatus)) {
203
- return {
204
- code: "INVALID_STATUS_TRANSITION",
205
- message: await getErrorMessage("invalid_transition", {
206
- from: currentStatus,
207
- to: newStatus,
208
- allowed: allowedTransitions.length > 0 ? allowedTransitions.join(", ") : "None (terminal state)"
209
- }),
210
- };
211
- }
212
- return null;
213
- }
214
- }
@@ -1,2 +0,0 @@
1
- import { type ToolDefinition } from "@opencode-ai/plugin";
2
- export declare function createCreateRoadmapTool(directory: string): Promise<ToolDefinition>;