@dleangen/cage-issues 0.0.1 → 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/dist/fs.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { Backlog, Config, Issue, Topic } from './schema';
2
+ export declare function readYaml<T>(filePath: string): T;
3
+ export declare function writeYaml(filePath: string, data: unknown): void;
4
+ export declare function configPath(root: string): string;
5
+ export declare function topicPath(root: string, topicId: string): string;
6
+ export declare function backlogPath(root: string, topicId: string): string;
7
+ export declare function issueDir(root: string, topicId: string): string;
8
+ export declare function issuePath(root: string, issueId: string): string;
9
+ export declare function readConfig(root: string): Config;
10
+ export declare function readTopic(root: string, topicId: string): Topic;
11
+ export declare function writeTopic(root: string, topic: Topic): void;
12
+ export declare function readBacklog(root: string, topicId: string): Backlog;
13
+ export declare function writeBacklog(root: string, topicId: string, backlog: Backlog): void;
14
+ export declare function readIssue(root: string, issueId: string): Issue;
15
+ export declare function writeIssue(root: string, issue: Issue): void;
16
+ export declare function topicExists(root: string, topicId: string): boolean;
17
+ export declare function issueExists(root: string, issueId: string): boolean;
18
+ export declare function listTopicIds(root: string): string[];
19
+ export declare function listIssueIds(root: string, topicId: string): string[];
20
+ export declare function nextIssueId(root: string, topicId: string): string;
package/dist/fs.js ADDED
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readYaml = readYaml;
7
+ exports.writeYaml = writeYaml;
8
+ exports.configPath = configPath;
9
+ exports.topicPath = topicPath;
10
+ exports.backlogPath = backlogPath;
11
+ exports.issueDir = issueDir;
12
+ exports.issuePath = issuePath;
13
+ exports.readConfig = readConfig;
14
+ exports.readTopic = readTopic;
15
+ exports.writeTopic = writeTopic;
16
+ exports.readBacklog = readBacklog;
17
+ exports.writeBacklog = writeBacklog;
18
+ exports.readIssue = readIssue;
19
+ exports.writeIssue = writeIssue;
20
+ exports.topicExists = topicExists;
21
+ exports.issueExists = issueExists;
22
+ exports.listTopicIds = listTopicIds;
23
+ exports.listIssueIds = listIssueIds;
24
+ exports.nextIssueId = nextIssueId;
25
+ const js_yaml_1 = __importDefault(require("js-yaml"));
26
+ const node_fs_1 = __importDefault(require("node:fs"));
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ function readYaml(filePath) {
29
+ return js_yaml_1.default.load(node_fs_1.default.readFileSync(filePath, 'utf8'));
30
+ }
31
+ function writeYaml(filePath, data) {
32
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
33
+ node_fs_1.default.writeFileSync(filePath, js_yaml_1.default.dump(data, { lineWidth: 100 }));
34
+ }
35
+ // Path helpers
36
+ function configPath(root) {
37
+ return node_path_1.default.join(root, 'config.yaml');
38
+ }
39
+ function topicPath(root, topicId) {
40
+ return node_path_1.default.join(root, 'topics', topicId, 'topic.yaml');
41
+ }
42
+ function backlogPath(root, topicId) {
43
+ return node_path_1.default.join(root, 'topics', topicId, 'backlog.yaml');
44
+ }
45
+ function issueDir(root, topicId) {
46
+ return node_path_1.default.join(root, 'topics', topicId, 'issues');
47
+ }
48
+ function issuePath(root, issueId) {
49
+ const topicId = issueId.split('-')[0];
50
+ return node_path_1.default.join(issueDir(root, topicId), `${issueId}.yaml`);
51
+ }
52
+ // Read helpers
53
+ function readConfig(root) {
54
+ const p = configPath(root);
55
+ if (!node_fs_1.default.existsSync(p)) {
56
+ return {
57
+ priority_labels: { p1: 'p1', p2: 'p2', p3: 'p3', p4: 'p4' },
58
+ impact_labels: { low: 'low', medium: 'medium', high: 'high', critical: 'critical' },
59
+ tracks: [],
60
+ };
61
+ }
62
+ return readYaml(p);
63
+ }
64
+ function readTopic(root, topicId) {
65
+ return readYaml(topicPath(root, topicId));
66
+ }
67
+ function writeTopic(root, topic) {
68
+ writeYaml(topicPath(root, topic.id), topic);
69
+ }
70
+ function readBacklog(root, topicId) {
71
+ const p = backlogPath(root, topicId);
72
+ if (!node_fs_1.default.existsSync(p))
73
+ return { active: [], up_next: [], deferred: [] };
74
+ return readYaml(p);
75
+ }
76
+ function writeBacklog(root, topicId, backlog) {
77
+ writeYaml(backlogPath(root, topicId), backlog);
78
+ }
79
+ function readIssue(root, issueId) {
80
+ return readYaml(issuePath(root, issueId));
81
+ }
82
+ function writeIssue(root, issue) {
83
+ writeYaml(issuePath(root, issue.id), issue);
84
+ }
85
+ function topicExists(root, topicId) {
86
+ return node_fs_1.default.existsSync(topicPath(root, topicId));
87
+ }
88
+ function issueExists(root, issueId) {
89
+ return node_fs_1.default.existsSync(issuePath(root, issueId));
90
+ }
91
+ // List helpers
92
+ function listTopicIds(root) {
93
+ const topicsDir = node_path_1.default.join(root, 'topics');
94
+ if (!node_fs_1.default.existsSync(topicsDir))
95
+ return [];
96
+ return node_fs_1.default
97
+ .readdirSync(topicsDir)
98
+ .filter((d) => node_fs_1.default.existsSync(node_path_1.default.join(topicsDir, d, 'topic.yaml')));
99
+ }
100
+ function listIssueIds(root, topicId) {
101
+ const dir = issueDir(root, topicId);
102
+ if (!node_fs_1.default.existsSync(dir))
103
+ return [];
104
+ return node_fs_1.default
105
+ .readdirSync(dir)
106
+ .filter((f) => f.endsWith('.yaml'))
107
+ .map((f) => f.replace('.yaml', ''));
108
+ }
109
+ // Sequence generation: {TOPIC}-{YEAR}-{SEQUENCE}
110
+ function nextIssueId(root, topicId) {
111
+ const year = new Date().getFullYear();
112
+ const ids = listIssueIds(root, topicId).filter((id) => id.includes(`-${year}-`));
113
+ if (ids.length === 0)
114
+ return `${topicId}-${year}-0001`;
115
+ const sequences = ids.map((id) => Number.parseInt(id.split('-').at(-1) ?? '0', 10));
116
+ const next = Math.max(...sequences) + 1;
117
+ return `${topicId}-${year}-${String(next).padStart(4, '0')}`;
118
+ }
@@ -0,0 +1,3 @@
1
+ export type { Backlog, BacklogEntry, Config, Issue, IssueClassification, IssueImpact, IssueMaturity, IssuePriority, IssueStatus, IssueWithDerived, Signoff, StatusHistoryEntry, Task, TaskPriority, TaskStatus, Topic, TopicStatus, TrackDefinition, } from './schema';
2
+ export { calculateMaturity } from './maturity';
3
+ export { deriveTrack } from './track';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deriveTrack = exports.calculateMaturity = void 0;
4
+ var maturity_1 = require("./maturity");
5
+ Object.defineProperty(exports, "calculateMaturity", { enumerable: true, get: function () { return maturity_1.calculateMaturity; } });
6
+ var track_1 = require("./track");
7
+ Object.defineProperty(exports, "deriveTrack", { enumerable: true, get: function () { return track_1.deriveTrack; } });
@@ -0,0 +1,2 @@
1
+ import type { Issue, IssueMaturity } from './schema';
2
+ export declare function calculateMaturity(issue: Issue): IssueMaturity;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateMaturity = calculateMaturity;
4
+ function calculateMaturity(issue) {
5
+ // draft → defined: add priority + what_to_resolve
6
+ if (!issue.priority || !issue.what_to_resolve)
7
+ return 'draft';
8
+ // defined → ready: add at least one task and one affects entry
9
+ if ((issue.tasks?.length ?? 0) === 0 || (issue.affects?.length ?? 0) === 0)
10
+ return 'defined';
11
+ // ready → verified: add at least one signoff
12
+ if ((issue.signoffs?.length ?? 0) === 0)
13
+ return 'ready';
14
+ return 'verified';
15
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const issues_1 = require("./tools/issues");
7
+ const topics_1 = require("./tools/topics");
8
+ const backlog_1 = require("./tools/backlog");
9
+ const tracks_1 = require("./tools/tracks");
10
+ function parseRoot() {
11
+ const idx = process.argv.indexOf('--root');
12
+ if (idx !== -1 && process.argv[idx + 1])
13
+ return process.argv[idx + 1];
14
+ return process.cwd();
15
+ }
16
+ async function main() {
17
+ const root = parseRoot();
18
+ const server = new mcp_js_1.McpServer({
19
+ name: 'cage-issues',
20
+ version: '0.0.1',
21
+ });
22
+ (0, issues_1.registerIssueTools)(server, root);
23
+ (0, topics_1.registerTopicTools)(server, root);
24
+ (0, tracks_1.registerTrackTools)(server, root);
25
+ (0, backlog_1.registerBacklogTools)(server, root);
26
+ const transport = new stdio_js_1.StdioServerTransport();
27
+ await server.connect(transport);
28
+ }
29
+ main().catch((err) => {
30
+ console.error(err);
31
+ process.exit(1);
32
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerBacklogTools(server: McpServer, root: string): void;
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerBacklogTools = registerBacklogTools;
37
+ const zod_1 = require("zod");
38
+ const fsHelper = __importStar(require("../../fs"));
39
+ function entryFromIssue(root, issueId) {
40
+ const issue = fsHelper.readIssue(root, issueId);
41
+ return { id: issue.id, title: issue.title, rationale: '' };
42
+ }
43
+ function registerBacklogTools(server, root) {
44
+ // get_backlog
45
+ server.tool('get_backlog', 'Get the backlog. If topic is omitted, returns an aggregated view across all topics.', {
46
+ topic: zod_1.z.string().optional().describe('Topic ID — omit for cross-topic aggregate'),
47
+ track: zod_1.z.string().optional().describe('Filter entries by derived track'),
48
+ }, async ({ topic, track }) => {
49
+ const topicIds = topic ? [topic] : fsHelper.listTopicIds(root);
50
+ if (topicIds.length === 0) {
51
+ return { content: [{ type: 'text', text: JSON.stringify({ active: [], up_next: [], deferred: [] }, null, 2) }] };
52
+ }
53
+ // Aggregate across topics
54
+ const aggregate = { active: [], up_next: [], deferred: [] };
55
+ const config = fsHelper.readConfig(root);
56
+ for (const tid of topicIds) {
57
+ if (!fsHelper.topicExists(root, tid))
58
+ continue;
59
+ const backlog = fsHelper.readBacklog(root, tid);
60
+ for (const section of ['active', 'up_next', 'deferred']) {
61
+ for (const entry of backlog[section]) {
62
+ if (track) {
63
+ // Apply track filter — derive track from the issue
64
+ try {
65
+ const issue = fsHelper.readIssue(root, entry.id);
66
+ const { deriveTrack } = await Promise.resolve().then(() => __importStar(require('../../track')));
67
+ const derived = deriveTrack(issue.affects, config);
68
+ if (derived !== track)
69
+ continue;
70
+ }
71
+ catch {
72
+ // Issue file missing — include anyway
73
+ }
74
+ }
75
+ aggregate[section].push(entry);
76
+ }
77
+ }
78
+ }
79
+ return { content: [{ type: 'text', text: JSON.stringify(aggregate, null, 2) }] };
80
+ });
81
+ // update_backlog
82
+ server.tool('update_backlog', 'Modify a topic backlog. Operations: promote (up_next→active or deferred→up_next), defer (active→deferred), reorder (move within section), resolve (remove from backlog entirely).', {
83
+ topic: zod_1.z.string().describe('Topic ID'),
84
+ operation: zod_1.z.enum(['promote', 'defer', 'reorder', 'resolve']),
85
+ issue_id: zod_1.z.string().describe('Issue ID to act on'),
86
+ position: zod_1.z
87
+ .number()
88
+ .int()
89
+ .nonnegative()
90
+ .optional()
91
+ .describe('Target position (0-based) for reorder operation'),
92
+ }, async ({ topic, operation, issue_id, position }) => {
93
+ const topicId = topic.toUpperCase();
94
+ if (!fsHelper.topicExists(root, topicId)) {
95
+ return { content: [{ type: 'text', text: `Error: topic "${topicId}" not found` }] };
96
+ }
97
+ const backlog = fsHelper.readBacklog(root, topicId);
98
+ const sections = ['active', 'up_next', 'deferred'];
99
+ let currentSection = null;
100
+ for (const s of sections) {
101
+ if (backlog[s].some((e) => e.id === issue_id)) {
102
+ currentSection = s;
103
+ break;
104
+ }
105
+ }
106
+ switch (operation) {
107
+ case 'promote': {
108
+ if (!currentSection) {
109
+ return {
110
+ content: [{ type: 'text', text: `Error: issue "${issue_id}" not found in backlog` }],
111
+ };
112
+ }
113
+ const entry = backlog[currentSection].find((e) => e.id === issue_id);
114
+ backlog[currentSection] = backlog[currentSection].filter((e) => e.id !== issue_id);
115
+ if (currentSection === 'deferred') {
116
+ backlog.up_next.push(entry);
117
+ }
118
+ else if (currentSection === 'up_next') {
119
+ backlog.active.push(entry);
120
+ }
121
+ else {
122
+ return { content: [{ type: 'text', text: `Error: issue "${issue_id}" is already in active` }] };
123
+ }
124
+ break;
125
+ }
126
+ case 'defer': {
127
+ if (!currentSection) {
128
+ return {
129
+ content: [{ type: 'text', text: `Error: issue "${issue_id}" not found in backlog` }],
130
+ };
131
+ }
132
+ if (currentSection === 'deferred') {
133
+ return { content: [{ type: 'text', text: `Error: issue "${issue_id}" is already deferred` }] };
134
+ }
135
+ const entry = backlog[currentSection].find((e) => e.id === issue_id);
136
+ backlog[currentSection] = backlog[currentSection].filter((e) => e.id !== issue_id);
137
+ backlog.deferred.push(entry);
138
+ break;
139
+ }
140
+ case 'reorder': {
141
+ if (!currentSection) {
142
+ return {
143
+ content: [{ type: 'text', text: `Error: issue "${issue_id}" not found in backlog` }],
144
+ };
145
+ }
146
+ if (position === undefined) {
147
+ return { content: [{ type: 'text', text: 'Error: position is required for reorder' }] };
148
+ }
149
+ const items = backlog[currentSection];
150
+ const idx = items.findIndex((e) => e.id === issue_id);
151
+ const [entry] = items.splice(idx, 1);
152
+ items.splice(Math.min(position, items.length), 0, entry);
153
+ break;
154
+ }
155
+ case 'resolve': {
156
+ if (!currentSection) {
157
+ // Already removed — idempotent
158
+ return { content: [{ type: 'text', text: JSON.stringify(backlog, null, 2) }] };
159
+ }
160
+ backlog[currentSection] = backlog[currentSection].filter((e) => e.id !== issue_id);
161
+ break;
162
+ }
163
+ }
164
+ fsHelper.writeBacklog(root, topicId, backlog);
165
+ // Also add issue to backlog if it's a new entry (create_issue doesn't add it automatically)
166
+ return { content: [{ type: 'text', text: JSON.stringify(backlog, null, 2) }] };
167
+ });
168
+ // add_to_backlog — convenience tool to add an existing issue to a topic's backlog
169
+ server.tool('add_to_backlog', 'Add an existing issue to its topic backlog in the specified section.', {
170
+ issue_id: zod_1.z.string().describe('Issue ID'),
171
+ section: zod_1.z.enum(['active', 'up_next', 'deferred']),
172
+ rationale: zod_1.z.string().optional().describe('Why this issue is in this section'),
173
+ }, async ({ issue_id, section, rationale }) => {
174
+ const topicId = issue_id.split('-')[0];
175
+ if (!fsHelper.topicExists(root, topicId)) {
176
+ return { content: [{ type: 'text', text: `Error: topic "${topicId}" not found` }] };
177
+ }
178
+ if (!fsHelper.issueExists(root, issue_id)) {
179
+ return { content: [{ type: 'text', text: `Error: issue "${issue_id}" not found` }] };
180
+ }
181
+ const backlog = fsHelper.readBacklog(root, topicId);
182
+ // Check not already present
183
+ const all = [...backlog.active, ...backlog.up_next, ...backlog.deferred];
184
+ if (all.some((e) => e.id === issue_id)) {
185
+ return { content: [{ type: 'text', text: `Error: issue "${issue_id}" is already in the backlog` }] };
186
+ }
187
+ const entry = entryFromIssue(root, issue_id);
188
+ if (rationale)
189
+ entry.rationale = rationale;
190
+ backlog[section].push(entry);
191
+ fsHelper.writeBacklog(root, topicId, backlog);
192
+ return { content: [{ type: 'text', text: JSON.stringify(backlog, null, 2) }] };
193
+ });
194
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerIssueTools(server: McpServer, root: string): void;