@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.
@@ -0,0 +1,313 @@
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.registerIssueTools = registerIssueTools;
37
+ const zod_1 = require("zod");
38
+ const fsHelper = __importStar(require("../../fs"));
39
+ const maturity_1 = require("../../maturity");
40
+ const track_1 = require("../../track");
41
+ function withDerived(issue, root) {
42
+ const config = fsHelper.readConfig(root);
43
+ return {
44
+ ...issue,
45
+ maturity: (0, maturity_1.calculateMaturity)(issue),
46
+ track: (0, track_1.deriveTrack)(issue.affects, config),
47
+ };
48
+ }
49
+ function wordSet(text) {
50
+ return new Set(text
51
+ .toLowerCase()
52
+ .split(/\W+/)
53
+ .filter((w) => w.length > 3));
54
+ }
55
+ function similarityScore(a, b) {
56
+ const setA = wordSet(a);
57
+ const setB = wordSet(b);
58
+ const intersection = [...setA].filter((w) => setB.has(w)).length;
59
+ const union = new Set([...setA, ...setB]).size;
60
+ return union === 0 ? 0 : intersection / union;
61
+ }
62
+ function findSimilar(root, title, description, topicId) {
63
+ const topicIds = topicId ? [topicId] : fsHelper.listTopicIds(root);
64
+ const query = title + ' ' + description;
65
+ const results = [];
66
+ for (const tid of topicIds) {
67
+ for (const id of fsHelper.listIssueIds(root, tid)) {
68
+ const issue = fsHelper.readIssue(root, id);
69
+ const candidate = issue.title + ' ' + issue.description;
70
+ const score = similarityScore(query, candidate);
71
+ if (score > 0.2) {
72
+ results.push({
73
+ confidence: score > 0.6 ? 'high' : score > 0.35 ? 'medium' : 'low',
74
+ id: issue.id,
75
+ score,
76
+ title: issue.title,
77
+ });
78
+ }
79
+ }
80
+ }
81
+ return results.sort((a, b) => b.score - a.score);
82
+ }
83
+ function registerIssueTools(server, root) {
84
+ // create_issue
85
+ server.tool('create_issue', 'Create a new issue. Automatically checks for duplicates before creating.', {
86
+ topic: zod_1.z.string().describe('Topic ID (must be active)'),
87
+ title: zod_1.z.string().describe('Short description'),
88
+ description: zod_1.z.string().describe('Full prose description'),
89
+ priority: zod_1.z.enum(['p1', 'p2', 'p3', 'p4']).optional(),
90
+ what_to_resolve: zod_1.z.string().optional(),
91
+ focus: zod_1.z.string().optional(),
92
+ facet: zod_1.z.string().optional(),
93
+ impact: zod_1.z.enum(['low', 'medium', 'high', 'critical']).optional(),
94
+ impact_note: zod_1.z.string().optional(),
95
+ classification: zod_1.z.enum(['standard', 'mechanical-refactor', 'architectural']).optional(),
96
+ base_branch: zod_1.z.string().optional(),
97
+ related: zod_1.z.array(zod_1.z.string()).optional(),
98
+ dependencies: zod_1.z.array(zod_1.z.string()).optional(),
99
+ }, async (input) => {
100
+ if (!fsHelper.topicExists(root, input.topic)) {
101
+ return { content: [{ type: 'text', text: `Error: topic "${input.topic}" not found` }] };
102
+ }
103
+ const topic = fsHelper.readTopic(root, input.topic);
104
+ if (topic.status === 'closed') {
105
+ return { content: [{ type: 'text', text: `Error: topic "${input.topic}" is closed` }] };
106
+ }
107
+ const duplicates = findSimilar(root, input.title, input.description, input.topic);
108
+ const warnings = duplicates.length > 0
109
+ ? `Warning: possible duplicates found:\n${duplicates.map((d) => ` ${d.id}: ${d.title} (confidence: ${d.confidence})`).join('\n')}\n\n`
110
+ : '';
111
+ const id = fsHelper.nextIssueId(root, input.topic);
112
+ const now = new Date().toISOString();
113
+ const issue = {
114
+ id,
115
+ date: now.split('T')[0],
116
+ title: input.title,
117
+ description: input.description,
118
+ status: 'open',
119
+ status_history: [{ status: 'open', timestamp: now, by: 'system' }],
120
+ ...(input.priority && { priority: input.priority }),
121
+ ...(input.what_to_resolve && { what_to_resolve: input.what_to_resolve }),
122
+ ...(input.focus && { focus: input.focus }),
123
+ ...(input.facet && { facet: input.facet }),
124
+ ...(input.impact && { impact: input.impact }),
125
+ ...(input.impact_note && { impact_note: input.impact_note }),
126
+ ...(input.classification && { classification: input.classification }),
127
+ ...(input.base_branch && { base_branch: input.base_branch }),
128
+ ...(input.related && { related: input.related }),
129
+ ...(input.dependencies && { dependencies: input.dependencies }),
130
+ };
131
+ fsHelper.writeIssue(root, issue);
132
+ const result = withDerived(issue, root);
133
+ return {
134
+ content: [{ type: 'text', text: warnings + JSON.stringify(result, null, 2) }],
135
+ };
136
+ });
137
+ // get_issue
138
+ server.tool('get_issue', 'Get a single issue by ID, including derived maturity and track.', { id: zod_1.z.string().describe('Issue ID, e.g. SHELL-2026-0001') }, async ({ id }) => {
139
+ if (!fsHelper.issueExists(root, id)) {
140
+ return { content: [{ type: 'text', text: `Error: issue "${id}" not found` }] };
141
+ }
142
+ const issue = fsHelper.readIssue(root, id);
143
+ return { content: [{ type: 'text', text: JSON.stringify(withDerived(issue, root), null, 2) }] };
144
+ });
145
+ // update_issue
146
+ server.tool('update_issue', 'Partially update an issue. Only provided fields are changed. Status changes append to status_history. Task updates merge by task id.', {
147
+ id: zod_1.z.string().describe('Issue ID'),
148
+ title: zod_1.z.string().optional(),
149
+ description: zod_1.z.string().optional(),
150
+ status: zod_1.z.enum(['open', 'in_progress', 'resolved', 'cancelled']).optional(),
151
+ priority: zod_1.z.enum(['p1', 'p2', 'p3', 'p4']).optional(),
152
+ what_to_resolve: zod_1.z.string().optional(),
153
+ tasks: zod_1.z
154
+ .array(zod_1.z.object({
155
+ id: zod_1.z.string(),
156
+ description: zod_1.z.string().optional(),
157
+ status: zod_1.z.enum(['open', 'in_progress', 'done']).optional(),
158
+ priority: zod_1.z.enum(['must-have', 'nice-to-have', 'optional']).optional(),
159
+ }))
160
+ .optional()
161
+ .describe('Task updates — merged by id, not replaced'),
162
+ affects: zod_1.z.array(zod_1.z.string()).optional(),
163
+ focus: zod_1.z.string().optional(),
164
+ facet: zod_1.z.string().optional(),
165
+ impact: zod_1.z.enum(['low', 'medium', 'high', 'critical']).optional(),
166
+ impact_note: zod_1.z.string().optional(),
167
+ classification: zod_1.z.enum(['standard', 'mechanical-refactor', 'architectural']).optional(),
168
+ base_branch: zod_1.z.string().optional(),
169
+ related: zod_1.z.array(zod_1.z.string()).optional(),
170
+ dependencies: zod_1.z.array(zod_1.z.string()).optional(),
171
+ pull_request: zod_1.z.string().optional(),
172
+ }, async (input) => {
173
+ if (!fsHelper.issueExists(root, input.id)) {
174
+ return { content: [{ type: 'text', text: `Error: issue "${input.id}" not found` }] };
175
+ }
176
+ const issue = fsHelper.readIssue(root, input.id);
177
+ // Apply scalar field updates
178
+ const scalarFields = [
179
+ 'title', 'description', 'priority', 'what_to_resolve', 'affects',
180
+ 'focus', 'facet', 'impact', 'impact_note', 'classification',
181
+ 'base_branch', 'related', 'dependencies', 'pull_request',
182
+ ];
183
+ for (const field of scalarFields) {
184
+ if (input[field] !== undefined) {
185
+ issue[field] = input[field];
186
+ }
187
+ }
188
+ // Status change: append history entry
189
+ if (input.status && input.status !== issue.status) {
190
+ issue.status = input.status;
191
+ issue.status_history = [
192
+ ...(issue.status_history ?? []),
193
+ { status: input.status, timestamp: new Date().toISOString(), by: 'agent' },
194
+ ];
195
+ }
196
+ // Task updates: merge by id
197
+ if (input.tasks) {
198
+ const existing = new Map((issue.tasks ?? []).map((t) => [t.id, t]));
199
+ for (const update of input.tasks) {
200
+ const task = existing.get(update.id);
201
+ if (task) {
202
+ Object.assign(task, Object.fromEntries(Object.entries(update).filter(([, v]) => v !== undefined)));
203
+ }
204
+ else {
205
+ // New task — require all fields
206
+ existing.set(update.id, update);
207
+ }
208
+ }
209
+ issue.tasks = [...existing.values()];
210
+ }
211
+ fsHelper.writeIssue(root, issue);
212
+ return { content: [{ type: 'text', text: JSON.stringify(withDerived(issue, root), null, 2) }] };
213
+ });
214
+ // list_issues
215
+ server.tool('list_issues', 'List issues with optional filters.', {
216
+ topic: zod_1.z.string().optional().describe('Filter by topic ID'),
217
+ status: zod_1.z.enum(['open', 'in_progress', 'resolved', 'cancelled']).optional(),
218
+ priority: zod_1.z.enum(['p1', 'p2', 'p3', 'p4']).optional(),
219
+ maturity: zod_1.z.enum(['draft', 'defined', 'ready', 'verified']).optional(),
220
+ track: zod_1.z.string().optional(),
221
+ date_from: zod_1.z.string().optional().describe('ISO date, inclusive'),
222
+ date_to: zod_1.z.string().optional().describe('ISO date, inclusive'),
223
+ }, async (input) => {
224
+ const topicIds = input.topic ? [input.topic] : fsHelper.listTopicIds(root);
225
+ const results = [];
226
+ for (const tid of topicIds) {
227
+ for (const id of fsHelper.listIssueIds(root, tid)) {
228
+ const issue = fsHelper.readIssue(root, id);
229
+ const derived = withDerived(issue, root);
230
+ if (input.status && derived.status !== input.status)
231
+ continue;
232
+ if (input.priority && derived.priority !== input.priority)
233
+ continue;
234
+ if (input.maturity && derived.maturity !== input.maturity)
235
+ continue;
236
+ if (input.track && derived.track !== input.track)
237
+ continue;
238
+ if (input.date_from && derived.date < input.date_from)
239
+ continue;
240
+ if (input.date_to && derived.date > input.date_to)
241
+ continue;
242
+ results.push(derived);
243
+ }
244
+ }
245
+ results.sort((a, b) => b.date.localeCompare(a.date));
246
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
247
+ });
248
+ // search_issues
249
+ server.tool('search_issues', 'Free-text search across issue titles and descriptions, ranked by relevance.', {
250
+ query: zod_1.z.string(),
251
+ topic: zod_1.z.string().optional().describe('Scope search to a single topic'),
252
+ }, async ({ query, topic }) => {
253
+ const topicIds = topic ? [topic] : fsHelper.listTopicIds(root);
254
+ const results = [];
255
+ for (const tid of topicIds) {
256
+ for (const id of fsHelper.listIssueIds(root, tid)) {
257
+ const issue = fsHelper.readIssue(root, id);
258
+ const score = similarityScore(query, issue.title + ' ' + issue.description);
259
+ if (score > 0)
260
+ results.push({ issue: withDerived(issue, root), score });
261
+ }
262
+ }
263
+ results.sort((a, b) => b.score - a.score);
264
+ return {
265
+ content: [{ type: 'text', text: JSON.stringify(results.map((r) => r.issue), null, 2) }],
266
+ };
267
+ });
268
+ // find_duplicates
269
+ server.tool('find_duplicates', 'Check for existing issues similar to a proposed new one.', {
270
+ title: zod_1.z.string(),
271
+ description: zod_1.z.string(),
272
+ topic: zod_1.z.string().optional().describe('Scope to a single topic'),
273
+ }, async ({ title, description, topic }) => {
274
+ const results = findSimilar(root, title, description, topic);
275
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
276
+ });
277
+ // validate_issue
278
+ server.tool('validate_issue', 'Record a validation result. On pass, appends a signoff. On fail, no trace is recorded.', {
279
+ id: zod_1.z.string().describe('Issue ID'),
280
+ role: zod_1.z.string().describe('Validating agent role, e.g. Architect, Critic'),
281
+ result: zod_1.z.enum(['pass', 'fail']),
282
+ note: zod_1.z.string().describe('What was checked and why it passed or failed'),
283
+ }, async ({ id, role, result, note }) => {
284
+ if (!fsHelper.issueExists(root, id)) {
285
+ return { content: [{ type: 'text', text: `Error: issue "${id}" not found` }] };
286
+ }
287
+ const issue = fsHelper.readIssue(root, id);
288
+ const maturity = (0, maturity_1.calculateMaturity)(issue);
289
+ if (maturity === 'draft' || maturity === 'defined') {
290
+ return {
291
+ content: [
292
+ {
293
+ type: 'text',
294
+ text: `Error: issue "${id}" is at maturity "${maturity}" — must be Ready or above to validate`,
295
+ },
296
+ ],
297
+ };
298
+ }
299
+ if (result === 'fail') {
300
+ return {
301
+ content: [
302
+ { type: 'text', text: `Validation failed. No trace recorded. Feedback: ${note}` },
303
+ ],
304
+ };
305
+ }
306
+ issue.signoffs = [
307
+ ...(issue.signoffs ?? []),
308
+ { role, timestamp: new Date().toISOString(), note },
309
+ ];
310
+ fsHelper.writeIssue(root, issue);
311
+ return { content: [{ type: 'text', text: JSON.stringify(withDerived(issue, root), null, 2) }] };
312
+ });
313
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerTopicTools(server: McpServer, root: string): void;
@@ -0,0 +1,80 @@
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.registerTopicTools = registerTopicTools;
37
+ const zod_1 = require("zod");
38
+ const fsHelper = __importStar(require("../../fs"));
39
+ function registerTopicTools(server, root) {
40
+ // create_topic
41
+ server.tool('create_topic', 'Create a new topic and its directory structure (topic.yaml, backlog.yaml, issues/).', {
42
+ id: zod_1.z.string().describe('Topic ID — uppercase, e.g. SHELL'),
43
+ name: zod_1.z.string().describe('Human-readable name'),
44
+ description: zod_1.z.string().describe('What this topic covers'),
45
+ owner: zod_1.z.string().describe('Owner email or name'),
46
+ }, async ({ id, name, description, owner }) => {
47
+ const topicId = id.toUpperCase();
48
+ if (fsHelper.topicExists(root, topicId)) {
49
+ return { content: [{ type: 'text', text: `Error: topic "${topicId}" already exists` }] };
50
+ }
51
+ const topic = { id: topicId, name, description, owner, status: 'active' };
52
+ fsHelper.writeTopic(root, topic);
53
+ fsHelper.writeBacklog(root, topicId, { active: [], up_next: [], deferred: [] });
54
+ // Ensure issues/ dir exists
55
+ const issuesDirPath = fsHelper.issueDir(root, topicId);
56
+ const { mkdirSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
57
+ mkdirSync(issuesDirPath, { recursive: true });
58
+ return { content: [{ type: 'text', text: JSON.stringify(topic, null, 2) }] };
59
+ });
60
+ // list_topics
61
+ server.tool('list_topics', 'List all topics with their status.', {}, async () => {
62
+ const topicIds = fsHelper.listTopicIds(root);
63
+ const topics = topicIds.map((id) => fsHelper.readTopic(root, id));
64
+ return { content: [{ type: 'text', text: JSON.stringify(topics, null, 2) }] };
65
+ });
66
+ // close_topic
67
+ server.tool('close_topic', 'Permanently close a topic. No new issues may be filed. Existing issues are preserved.', { id: zod_1.z.string().describe('Topic ID') }, async ({ id }) => {
68
+ const topicId = id.toUpperCase();
69
+ if (!fsHelper.topicExists(root, topicId)) {
70
+ return { content: [{ type: 'text', text: `Error: topic "${topicId}" not found` }] };
71
+ }
72
+ const topic = fsHelper.readTopic(root, topicId);
73
+ if (topic.status === 'closed') {
74
+ return { content: [{ type: 'text', text: `Error: topic "${topicId}" is already closed` }] };
75
+ }
76
+ topic.status = 'closed';
77
+ fsHelper.writeTopic(root, topic);
78
+ return { content: [{ type: 'text', text: JSON.stringify(topic, null, 2) }] };
79
+ });
80
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerTrackTools(server: McpServer, root: string): void;
@@ -0,0 +1,118 @@
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.registerTrackTools = registerTrackTools;
37
+ const zod_1 = require("zod");
38
+ const fsHelper = __importStar(require("../../fs"));
39
+ function registerTrackTools(server, root) {
40
+ // create_track
41
+ server.tool('create_track', 'Add a project-specific track definition to config.yaml.', {
42
+ id: zod_1.z.string().describe('Track ID, e.g. frontend'),
43
+ name: zod_1.z.string().describe('Human-readable name'),
44
+ affects_patterns: zod_1.z
45
+ .array(zod_1.z.string())
46
+ .describe('Path prefixes that map to this track, e.g. ["src/", "components/"]'),
47
+ }, async ({ id, name, affects_patterns }) => {
48
+ const config = fsHelper.readConfig(root);
49
+ if (config.tracks.some((t) => t.id === id)) {
50
+ return { content: [{ type: 'text', text: `Error: track "${id}" already exists` }] };
51
+ }
52
+ const track = { id, name, affects_patterns };
53
+ config.tracks.push(track);
54
+ fsHelper.writeYaml(fsHelper.configPath(root), config);
55
+ return { content: [{ type: 'text', text: JSON.stringify(track, null, 2) }] };
56
+ });
57
+ // list_tracks
58
+ server.tool('list_tracks', 'List all track definitions — both built-in generic tracks and project-specific tracks from config.yaml.', {}, async () => {
59
+ const GENERIC_TRACKS = [
60
+ { id: 'data', name: 'Data', affects_patterns: ['data/'] },
61
+ { id: 'docs', name: 'Docs', affects_patterns: ['README.md', 'docs/'] },
62
+ { id: 'meta', name: 'Meta', affects_patterns: ['agents/', 'skills/', 'adr/', 'retrospectives/'] },
63
+ { id: 'issues', name: 'Issues', affects_patterns: ['issues/', 'topics/'] },
64
+ ];
65
+ const config = fsHelper.readConfig(root);
66
+ const result = {
67
+ generic: GENERIC_TRACKS,
68
+ project: config.tracks,
69
+ };
70
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
71
+ });
72
+ // update_track
73
+ server.tool('update_track', 'Update a project-specific track definition in config.yaml.', {
74
+ id: zod_1.z.string().describe('Track ID to update'),
75
+ name: zod_1.z.string().optional(),
76
+ affects_patterns: zod_1.z.array(zod_1.z.string()).optional(),
77
+ }, async ({ id, name, affects_patterns }) => {
78
+ const config = fsHelper.readConfig(root);
79
+ const track = config.tracks.find((t) => t.id === id);
80
+ if (!track) {
81
+ return {
82
+ content: [
83
+ {
84
+ type: 'text',
85
+ text: `Error: track "${id}" not found — only project-specific tracks can be updated`,
86
+ },
87
+ ],
88
+ };
89
+ }
90
+ if (name !== undefined)
91
+ track.name = name;
92
+ if (affects_patterns !== undefined)
93
+ track.affects_patterns = affects_patterns;
94
+ fsHelper.writeYaml(fsHelper.configPath(root), config);
95
+ return { content: [{ type: 'text', text: JSON.stringify(track, null, 2) }] };
96
+ });
97
+ // close_track
98
+ server.tool('close_track', 'Deactivate a project-specific track. Existing issues retain their derived track value.', { id: zod_1.z.string().describe('Track ID to deactivate') }, async ({ id }) => {
99
+ const config = fsHelper.readConfig(root);
100
+ const track = config.tracks.find((t) => t.id === id);
101
+ if (!track) {
102
+ return {
103
+ content: [
104
+ {
105
+ type: 'text',
106
+ text: `Error: track "${id}" not found — only project-specific tracks can be closed`,
107
+ },
108
+ ],
109
+ };
110
+ }
111
+ if (track.active === false) {
112
+ return { content: [{ type: 'text', text: `Error: track "${id}" is already inactive` }] };
113
+ }
114
+ track.active = false;
115
+ fsHelper.writeYaml(fsHelper.configPath(root), config);
116
+ return { content: [{ type: 'text', text: JSON.stringify(track, null, 2) }] };
117
+ });
118
+ }
@@ -0,0 +1,78 @@
1
+ export type IssueStatus = 'open' | 'in_progress' | 'resolved' | 'cancelled';
2
+ export type IssuePriority = 'p1' | 'p2' | 'p3' | 'p4';
3
+ export type IssueImpact = 'low' | 'medium' | 'high' | 'critical';
4
+ export type IssueMaturity = 'draft' | 'defined' | 'ready' | 'verified';
5
+ export type IssueClassification = 'standard' | 'mechanical-refactor' | 'architectural';
6
+ export type TaskStatus = 'open' | 'in_progress' | 'done';
7
+ export type TaskPriority = 'must-have' | 'nice-to-have' | 'optional';
8
+ export type TopicStatus = 'active' | 'closed';
9
+ export interface Task {
10
+ id: string;
11
+ description: string;
12
+ status: TaskStatus;
13
+ priority: TaskPriority;
14
+ }
15
+ export interface Signoff {
16
+ role: string;
17
+ timestamp: string;
18
+ note: string;
19
+ }
20
+ export interface StatusHistoryEntry {
21
+ status: IssueStatus;
22
+ timestamp: string;
23
+ by: string;
24
+ }
25
+ export interface Issue {
26
+ id: string;
27
+ date: string;
28
+ title: string;
29
+ description: string;
30
+ status: IssueStatus;
31
+ status_history: StatusHistoryEntry[];
32
+ priority?: IssuePriority;
33
+ what_to_resolve?: string;
34
+ tasks?: Task[];
35
+ affects?: string[];
36
+ signoffs?: Signoff[];
37
+ focus?: string;
38
+ facet?: string;
39
+ impact?: IssueImpact;
40
+ impact_note?: string;
41
+ classification?: IssueClassification;
42
+ base_branch?: string;
43
+ related?: string[];
44
+ dependencies?: string[];
45
+ pull_request?: string;
46
+ }
47
+ export interface IssueWithDerived extends Issue {
48
+ maturity: IssueMaturity;
49
+ track: string | null;
50
+ }
51
+ export interface Topic {
52
+ id: string;
53
+ name: string;
54
+ description: string;
55
+ owner: string;
56
+ status: TopicStatus;
57
+ }
58
+ export interface BacklogEntry {
59
+ id: string;
60
+ title: string;
61
+ rationale: string;
62
+ }
63
+ export interface Backlog {
64
+ active: BacklogEntry[];
65
+ up_next: BacklogEntry[];
66
+ deferred: BacklogEntry[];
67
+ }
68
+ export interface TrackDefinition {
69
+ id: string;
70
+ name: string;
71
+ affects_patterns: string[];
72
+ active?: boolean;
73
+ }
74
+ export interface Config {
75
+ priority_labels: Record<IssuePriority, string>;
76
+ impact_labels: Record<IssueImpact, string>;
77
+ tracks: TrackDefinition[];
78
+ }
package/dist/schema.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // Core enumerations
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ import type { Config } from './schema';
2
+ export declare function deriveTrack(affects: string[] | undefined, config: Config): string | null;
package/dist/track.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deriveTrack = deriveTrack;
4
+ const GENERIC_TRACKS = [
5
+ { id: 'data', affects_patterns: ['data/'] },
6
+ { id: 'docs', affects_patterns: ['README.md', 'docs/'] },
7
+ { id: 'meta', affects_patterns: ['agents/', 'skills/', 'adr/', 'retrospectives/'] },
8
+ { id: 'issues', affects_patterns: ['issues/', 'topics/'] },
9
+ ];
10
+ function deriveTrack(affects, config) {
11
+ if (!affects || affects.length === 0)
12
+ return null;
13
+ const primary = affects[0];
14
+ // Project-specific tracks take priority over generic tracks
15
+ for (const track of config.tracks) {
16
+ if (track.active === false)
17
+ continue;
18
+ for (const pattern of track.affects_patterns) {
19
+ if (primary.startsWith(pattern) || primary === pattern)
20
+ return track.id;
21
+ }
22
+ }
23
+ for (const track of GENERIC_TRACKS) {
24
+ for (const pattern of track.affects_patterns) {
25
+ if (primary.startsWith(pattern) || primary === pattern)
26
+ return track.id;
27
+ }
28
+ }
29
+ return null;
30
+ }