@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 +20 -0
- package/dist/fs.js +118 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/maturity.d.ts +2 -0
- package/dist/maturity.js +15 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +32 -0
- package/dist/mcp/tools/backlog.d.ts +2 -0
- package/dist/mcp/tools/backlog.js +194 -0
- package/dist/mcp/tools/issues.d.ts +2 -0
- package/dist/mcp/tools/issues.js +313 -0
- package/dist/mcp/tools/topics.d.ts +2 -0
- package/dist/mcp/tools/topics.js +80 -0
- package/dist/mcp/tools/tracks.d.ts +2 -0
- package/dist/mcp/tools/tracks.js +118 -0
- package/dist/schema.d.ts +78 -0
- package/dist/schema.js +3 -0
- package/dist/track.d.ts +2 -0
- package/dist/track.js +30 -0
- package/package.json +39 -1
- package/schemas/backlog.yaml +23 -0
- package/schemas/config.yaml +28 -0
- package/schemas/issue.yaml +61 -0
- package/schemas/topic.yaml +9 -0
- package/skills/display-backlog.md +41 -0
- package/skills/display-issue.md +72 -0
- package/skills/display-list.md +39 -0
- package/skills/display-weekly-summary.md +50 -0
- package/.github/workflows/publish.yml +0 -31
|
@@ -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,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,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
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -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
package/dist/track.d.ts
ADDED
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
|
+
}
|