superintent 0.0.1
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/LICENSE +21 -0
- package/README.md +226 -0
- package/bin/superintent.js +2 -0
- package/dist/commands/extract.d.ts +2 -0
- package/dist/commands/extract.js +66 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +56 -0
- package/dist/commands/knowledge.d.ts +2 -0
- package/dist/commands/knowledge.js +647 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +153 -0
- package/dist/commands/spec.d.ts +2 -0
- package/dist/commands/spec.js +283 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +43 -0
- package/dist/commands/ticket.d.ts +4 -0
- package/dist/commands/ticket.js +942 -0
- package/dist/commands/ui.d.ts +2 -0
- package/dist/commands/ui.js +954 -0
- package/dist/db/client.d.ts +4 -0
- package/dist/db/client.js +26 -0
- package/dist/db/init-schema.d.ts +2 -0
- package/dist/db/init-schema.js +28 -0
- package/dist/db/parsers.d.ts +24 -0
- package/dist/db/parsers.js +79 -0
- package/dist/db/schema.d.ts +7 -0
- package/dist/db/schema.js +64 -0
- package/dist/db/usage.d.ts +8 -0
- package/dist/db/usage.js +24 -0
- package/dist/embed/model.d.ts +5 -0
- package/dist/embed/model.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +31 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +1 -0
- package/dist/ui/components/index.d.ts +6 -0
- package/dist/ui/components/index.js +13 -0
- package/dist/ui/components/knowledge.d.ts +33 -0
- package/dist/ui/components/knowledge.js +238 -0
- package/dist/ui/components/layout.d.ts +1 -0
- package/dist/ui/components/layout.js +323 -0
- package/dist/ui/components/search.d.ts +15 -0
- package/dist/ui/components/search.js +114 -0
- package/dist/ui/components/spec.d.ts +11 -0
- package/dist/ui/components/spec.js +253 -0
- package/dist/ui/components/ticket.d.ts +90 -0
- package/dist/ui/components/ticket.js +604 -0
- package/dist/ui/components/utils.d.ts +26 -0
- package/dist/ui/components/utils.js +34 -0
- package/dist/ui/styles.css +2 -0
- package/dist/utils/cli.d.ts +21 -0
- package/dist/utils/cli.js +31 -0
- package/dist/utils/config.d.ts +12 -0
- package/dist/utils/config.js +116 -0
- package/dist/utils/id.d.ts +6 -0
- package/dist/utils/id.js +13 -0
- package/dist/utils/io.d.ts +8 -0
- package/dist/utils/io.js +15 -0
- package/package.json +60 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { getClient, closeClient } from '../db/client.js';
|
|
4
|
+
import { parseTicketRow } from '../db/parsers.js';
|
|
5
|
+
import { getProjectNamespace } from '../utils/config.js';
|
|
6
|
+
import { readStdin } from '../utils/io.js';
|
|
7
|
+
import { generateId } from '../utils/id.js';
|
|
8
|
+
/**
|
|
9
|
+
* Infer ticket type from intent keywords
|
|
10
|
+
*/
|
|
11
|
+
function inferTicketType(intent) {
|
|
12
|
+
const lower = intent.toLowerCase();
|
|
13
|
+
// Bugfix patterns
|
|
14
|
+
if (/\b(fix|bug|issue|error|broken|crash|fail|wrong|incorrect)\b/.test(lower)) {
|
|
15
|
+
return 'bugfix';
|
|
16
|
+
}
|
|
17
|
+
// Refactor patterns
|
|
18
|
+
if (/\b(refactor|restructure|reorganize|clean\s?up|simplify|improve\s+code|optimize)\b/.test(lower)) {
|
|
19
|
+
return 'refactor';
|
|
20
|
+
}
|
|
21
|
+
// Docs patterns
|
|
22
|
+
if (/\b(document|docs?|readme|comment|explain|guide)\b/.test(lower)) {
|
|
23
|
+
return 'docs';
|
|
24
|
+
}
|
|
25
|
+
// Test patterns
|
|
26
|
+
if (/\b(test|spec|coverage|unit\s+test|e2e|integration\s+test)\b/.test(lower)) {
|
|
27
|
+
return 'test';
|
|
28
|
+
}
|
|
29
|
+
// Chore patterns
|
|
30
|
+
if (/\b(chore|update\s+dep|upgrade|migrate|config|setup|ci|cd|build)\b/.test(lower)) {
|
|
31
|
+
return 'chore';
|
|
32
|
+
}
|
|
33
|
+
// Default to feature
|
|
34
|
+
return 'feature';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate a parsed ticket — errors block creation, warnings are informational
|
|
38
|
+
*/
|
|
39
|
+
function validateParsedTicket(parsed) {
|
|
40
|
+
const issues = [];
|
|
41
|
+
if (!parsed.intent) {
|
|
42
|
+
issues.push({ field: 'intent', severity: 'error', message: 'Missing **Intent:** field' });
|
|
43
|
+
}
|
|
44
|
+
return issues;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse markdown ticket format matching SKILL.md ticket format
|
|
48
|
+
*/
|
|
49
|
+
function parseMarkdownTicket(content) {
|
|
50
|
+
// Check for ## Plan section and split content
|
|
51
|
+
const planMatch = content.match(/^##\s*Plan\s*$/im);
|
|
52
|
+
let ticketContent = content;
|
|
53
|
+
let planContent;
|
|
54
|
+
if (planMatch && planMatch.index !== undefined) {
|
|
55
|
+
ticketContent = content.substring(0, planMatch.index);
|
|
56
|
+
planContent = content.substring(planMatch.index + planMatch[0].length);
|
|
57
|
+
}
|
|
58
|
+
const lines = ticketContent.split('\n');
|
|
59
|
+
const ticket = { intent: '', planContent };
|
|
60
|
+
let currentSection = '';
|
|
61
|
+
let contextLines = [];
|
|
62
|
+
let inContext = false;
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
const line = lines[i];
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
// Parse title: # {intent summary}
|
|
67
|
+
if (trimmed.startsWith('# ') && !trimmed.startsWith('## ')) {
|
|
68
|
+
ticket.title = trimmed.substring(2).trim();
|
|
69
|
+
}
|
|
70
|
+
// Parse fields — **Field:** format only (matches SKILL.md)
|
|
71
|
+
const isType = trimmed.startsWith('**Type:**');
|
|
72
|
+
const isIntent = trimmed.startsWith('**Intent:**');
|
|
73
|
+
const isContext = trimmed.startsWith('**Context:**');
|
|
74
|
+
const isConstraints = trimmed.startsWith('**Constraints:**');
|
|
75
|
+
const isAssumptions = trimmed.startsWith('**Assumptions:**');
|
|
76
|
+
const isChangeClass = trimmed.startsWith('**Change Class:**');
|
|
77
|
+
if (isType) {
|
|
78
|
+
const typeValue = trimmed.replace(/^\*\*Type:\*\*\s*/, '').trim().toLowerCase();
|
|
79
|
+
if (['feature', 'bugfix', 'refactor', 'docs', 'chore', 'test'].includes(typeValue)) {
|
|
80
|
+
ticket.type = typeValue;
|
|
81
|
+
}
|
|
82
|
+
inContext = false;
|
|
83
|
+
currentSection = '';
|
|
84
|
+
}
|
|
85
|
+
else if (isIntent) {
|
|
86
|
+
ticket.intent = trimmed.replace(/^\*\*Intent:\*\*\s*/, '').trim();
|
|
87
|
+
inContext = false;
|
|
88
|
+
currentSection = '';
|
|
89
|
+
}
|
|
90
|
+
else if (isContext) {
|
|
91
|
+
ticket.context = trimmed.replace(/^\*\*Context:\*\*\s*/, '').trim();
|
|
92
|
+
inContext = true;
|
|
93
|
+
currentSection = '';
|
|
94
|
+
contextLines = [];
|
|
95
|
+
}
|
|
96
|
+
else if (isConstraints) {
|
|
97
|
+
inContext = false;
|
|
98
|
+
currentSection = 'constraints';
|
|
99
|
+
const parts = trimmed.replace(/^\*\*Constraints:\*\*\s*/, '').trim();
|
|
100
|
+
if (parts) {
|
|
101
|
+
const useMatch = parts.match(/Use:\s*([^|]+)/i);
|
|
102
|
+
const avoidMatch = parts.match(/Avoid:\s*(.+)/i);
|
|
103
|
+
if (useMatch) {
|
|
104
|
+
ticket.constraintsUse = useMatch[1].replace(/[[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
if (avoidMatch) {
|
|
107
|
+
ticket.constraintsAvoid = avoidMatch[1].replace(/[[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (isAssumptions) {
|
|
112
|
+
inContext = false;
|
|
113
|
+
currentSection = 'assumptions';
|
|
114
|
+
const assumptionText = trimmed.replace(/^\*\*Assumptions:\*\*\s*/, '').trim();
|
|
115
|
+
if (assumptionText) {
|
|
116
|
+
ticket.assumptions = assumptionText.replace(/[[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
ticket.assumptions = [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (isChangeClass) {
|
|
123
|
+
inContext = false;
|
|
124
|
+
currentSection = '';
|
|
125
|
+
const classLine = trimmed.replace(/^\*\*Change Class:\*\*\s*/, '').trim();
|
|
126
|
+
const dashIndex = classLine.indexOf('-');
|
|
127
|
+
if (dashIndex > -1) {
|
|
128
|
+
ticket.changeClass = classLine.substring(0, dashIndex).trim();
|
|
129
|
+
ticket.changeClassReason = classLine.substring(dashIndex + 1).trim();
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
ticket.changeClass = classLine;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (trimmed.startsWith('- ')) {
|
|
136
|
+
const text = trimmed.substring(2).trim();
|
|
137
|
+
if (currentSection === 'constraints') {
|
|
138
|
+
if (text.toLowerCase().startsWith('use:')) {
|
|
139
|
+
const items = text.substring(4).trim().split(',').map(s => s.trim()).filter(Boolean);
|
|
140
|
+
ticket.constraintsUse = ticket.constraintsUse || [];
|
|
141
|
+
ticket.constraintsUse.push(...items);
|
|
142
|
+
}
|
|
143
|
+
else if (text.toLowerCase().startsWith('avoid:')) {
|
|
144
|
+
const items = text.substring(6).trim().split(',').map(s => s.trim()).filter(Boolean);
|
|
145
|
+
ticket.constraintsAvoid = ticket.constraintsAvoid || [];
|
|
146
|
+
ticket.constraintsAvoid.push(...items);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (currentSection === 'assumptions') {
|
|
150
|
+
ticket.assumptions?.push(text);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (trimmed.startsWith('**') && !trimmed.startsWith('**Status')) {
|
|
154
|
+
inContext = false;
|
|
155
|
+
currentSection = '';
|
|
156
|
+
}
|
|
157
|
+
else if (inContext && trimmed && !trimmed.startsWith('**')) {
|
|
158
|
+
contextLines.push(trimmed);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Append multi-line context
|
|
162
|
+
if (contextLines.length > 0 && ticket.context) {
|
|
163
|
+
ticket.context = ticket.context + '\n' + contextLines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
return ticket;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse plan from markdown format (synced with ticket structure):
|
|
169
|
+
*
|
|
170
|
+
* **Files:** src/api.ts, src/utils.ts
|
|
171
|
+
*
|
|
172
|
+
* **Tasks → Steps:**
|
|
173
|
+
* - task: Implement API endpoint
|
|
174
|
+
* - Step 1: Create route handler
|
|
175
|
+
* - Step 2: Add validation
|
|
176
|
+
* - task: Add tests
|
|
177
|
+
* - Step 1: Unit tests
|
|
178
|
+
*
|
|
179
|
+
* **DoD → Verification:**
|
|
180
|
+
* - dod: API returns correct data | verify: Run integration tests
|
|
181
|
+
* - dod: No TypeScript errors | verify: npx tsc --noEmit
|
|
182
|
+
*
|
|
183
|
+
* **Decisions:**
|
|
184
|
+
* - choice: Use cursor pagination | reason: Better for large datasets
|
|
185
|
+
*/
|
|
186
|
+
function parsePlanMarkdown(content) {
|
|
187
|
+
const plan = {
|
|
188
|
+
files: [],
|
|
189
|
+
taskSteps: [],
|
|
190
|
+
dodVerification: [],
|
|
191
|
+
decisions: [],
|
|
192
|
+
tradeOffs: [],
|
|
193
|
+
rollback: undefined,
|
|
194
|
+
irreversibleActions: [],
|
|
195
|
+
edgeCases: [],
|
|
196
|
+
};
|
|
197
|
+
const lines = content.split('\n');
|
|
198
|
+
let currentSection = '';
|
|
199
|
+
let currentTaskSteps = null;
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
const trimmed = line.trim();
|
|
202
|
+
// Parse section headers
|
|
203
|
+
if (trimmed.startsWith('**Files to Edit:**') || trimmed.startsWith('**Files:**') || trimmed.toLowerCase().startsWith('files to edit:') || trimmed.toLowerCase().startsWith('files:')) {
|
|
204
|
+
const filesStr = trimmed.replace(/^\*?\*?Files( to Edit)?:\*?\*?\s*/i, '').trim();
|
|
205
|
+
if (filesStr) {
|
|
206
|
+
plan.files = filesStr.split(',').map(f => f.trim()).filter(Boolean);
|
|
207
|
+
}
|
|
208
|
+
currentSection = 'files';
|
|
209
|
+
if (currentTaskSteps) {
|
|
210
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
211
|
+
currentTaskSteps = null;
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (trimmed.match(/^\*?\*?Tasks?\s*(→|->)\s*Steps?:?\*?\*?$/i) || trimmed.toLowerCase().startsWith('tasks → steps:') || trimmed.toLowerCase().startsWith('tasks -> steps:')) {
|
|
216
|
+
currentSection = 'taskSteps';
|
|
217
|
+
if (currentTaskSteps) {
|
|
218
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
219
|
+
currentTaskSteps = null;
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (trimmed.match(/^\*?\*?(DoD|Definition of Done)\s*(→|->)\s*Verification:?\*?\*?$/i) || trimmed.toLowerCase().includes('→ verification:') || trimmed.toLowerCase().includes('-> verification:')) {
|
|
224
|
+
currentSection = 'dodVerification';
|
|
225
|
+
if (currentTaskSteps) {
|
|
226
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
227
|
+
currentTaskSteps = null;
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (trimmed.startsWith('**Decisions:**') || trimmed.toLowerCase().startsWith('decisions:')) {
|
|
232
|
+
currentSection = 'decisions';
|
|
233
|
+
if (currentTaskSteps) {
|
|
234
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
235
|
+
currentTaskSteps = null;
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (trimmed.startsWith('**Trade-offs:**') || trimmed.toLowerCase().startsWith('trade-offs:')) {
|
|
240
|
+
currentSection = 'tradeOffs';
|
|
241
|
+
if (currentTaskSteps) {
|
|
242
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
243
|
+
currentTaskSteps = null;
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (trimmed.startsWith('**Irreversible Actions:**') || trimmed.toLowerCase().startsWith('irreversible actions:')) {
|
|
248
|
+
currentSection = 'irreversibleActions';
|
|
249
|
+
if (currentTaskSteps) {
|
|
250
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
251
|
+
currentTaskSteps = null;
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (trimmed.startsWith('**Edge Cases:**') || trimmed.toLowerCase().startsWith('edge cases:')) {
|
|
256
|
+
currentSection = 'edgeCases';
|
|
257
|
+
if (currentTaskSteps) {
|
|
258
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
259
|
+
currentTaskSteps = null;
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (trimmed.startsWith('**Rollback:**') || trimmed.toLowerCase().startsWith('rollback:')) {
|
|
264
|
+
currentSection = 'rollback';
|
|
265
|
+
plan.rollback = { steps: [], reversibility: 'full' };
|
|
266
|
+
if (currentTaskSteps) {
|
|
267
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
268
|
+
currentTaskSteps = null;
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
// Parse numbered list items (e.g., "1. Task name")
|
|
273
|
+
const numberedMatch = trimmed.match(/^\d+\.\s+(.+)/);
|
|
274
|
+
if (numberedMatch && currentSection === 'taskSteps') {
|
|
275
|
+
// Save previous task if exists
|
|
276
|
+
if (currentTaskSteps) {
|
|
277
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
278
|
+
}
|
|
279
|
+
currentTaskSteps = {
|
|
280
|
+
task: numberedMatch[1].trim(),
|
|
281
|
+
steps: [],
|
|
282
|
+
};
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
// Parse list items
|
|
286
|
+
if (trimmed.startsWith('- ')) {
|
|
287
|
+
const item = trimmed.substring(2).trim();
|
|
288
|
+
if (currentSection === 'files') {
|
|
289
|
+
plan.files.push(item);
|
|
290
|
+
}
|
|
291
|
+
else if (currentSection === 'taskSteps') {
|
|
292
|
+
// Check if it's a new task (starts with "task:")
|
|
293
|
+
if (item.toLowerCase().startsWith('task:')) {
|
|
294
|
+
// Save previous task if exists
|
|
295
|
+
if (currentTaskSteps) {
|
|
296
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
297
|
+
}
|
|
298
|
+
currentTaskSteps = {
|
|
299
|
+
task: item.substring(5).trim(),
|
|
300
|
+
steps: [],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
else if (currentTaskSteps) {
|
|
304
|
+
// It's a step for the current task
|
|
305
|
+
currentTaskSteps.steps.push(item);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else if (currentSection === 'dodVerification') {
|
|
309
|
+
// Parse "dod → verify", "dod: X | verify: Y", or plain text
|
|
310
|
+
let dod = item;
|
|
311
|
+
let verify = '';
|
|
312
|
+
const arrowParts = item.split(/\s*(?:→|->)\s*/);
|
|
313
|
+
if (arrowParts.length >= 2) {
|
|
314
|
+
dod = arrowParts[0].trim();
|
|
315
|
+
verify = arrowParts.slice(1).join(' → ').trim();
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const pipeParts = item.split('|').map(p => p.trim());
|
|
319
|
+
for (const part of pipeParts) {
|
|
320
|
+
if (part.toLowerCase().startsWith('dod:')) {
|
|
321
|
+
dod = part.substring(4).trim();
|
|
322
|
+
}
|
|
323
|
+
else if (part.toLowerCase().startsWith('verify:')) {
|
|
324
|
+
verify = part.substring(7).trim();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
plan.dodVerification.push({ dod, verify });
|
|
329
|
+
}
|
|
330
|
+
else if (currentSection === 'decisions') {
|
|
331
|
+
// Parse "choice → reason", "choice: X | reason: Y", or plain text
|
|
332
|
+
let choice = item;
|
|
333
|
+
let reason = '';
|
|
334
|
+
const arrowParts = item.split(/\s*(?:→|->)\s*/);
|
|
335
|
+
if (arrowParts.length >= 2) {
|
|
336
|
+
choice = arrowParts[0].trim();
|
|
337
|
+
reason = arrowParts.slice(1).join(' → ').trim();
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
const pipeParts = item.split('|').map(p => p.trim());
|
|
341
|
+
for (const part of pipeParts) {
|
|
342
|
+
if (part.toLowerCase().startsWith('choice:')) {
|
|
343
|
+
choice = part.substring(7).trim();
|
|
344
|
+
}
|
|
345
|
+
else if (part.toLowerCase().startsWith('reason:')) {
|
|
346
|
+
reason = part.substring(7).trim();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
plan.decisions.push({ choice, reason });
|
|
351
|
+
}
|
|
352
|
+
else if (currentSection === 'tradeOffs') {
|
|
353
|
+
// Parse "considered: X | rejected: Y" or "limitation: Z" format
|
|
354
|
+
const parts = item.split('|').map(p => p.trim());
|
|
355
|
+
let considered = item;
|
|
356
|
+
let rejected = '';
|
|
357
|
+
for (const part of parts) {
|
|
358
|
+
if (part.toLowerCase().startsWith('considered:')) {
|
|
359
|
+
considered = part.substring(11).trim();
|
|
360
|
+
}
|
|
361
|
+
else if (part.toLowerCase().startsWith('rejected:')) {
|
|
362
|
+
rejected = part.substring(9).trim();
|
|
363
|
+
}
|
|
364
|
+
else if (part.toLowerCase().startsWith('limitation:')) {
|
|
365
|
+
considered = part.substring(11).trim();
|
|
366
|
+
rejected = 'Scale/performance limitation';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
plan.tradeOffs.push({ considered, rejected });
|
|
370
|
+
}
|
|
371
|
+
else if (currentSection === 'irreversibleActions') {
|
|
372
|
+
plan.irreversibleActions.push(item);
|
|
373
|
+
}
|
|
374
|
+
else if (currentSection === 'edgeCases') {
|
|
375
|
+
plan.edgeCases.push(item);
|
|
376
|
+
}
|
|
377
|
+
else if (currentSection === 'rollback' && plan.rollback) {
|
|
378
|
+
// Check for "Reversibility: full|partial|none" line
|
|
379
|
+
if (item.toLowerCase().startsWith('reversibility:')) {
|
|
380
|
+
const rev = item.substring(14).trim().toLowerCase();
|
|
381
|
+
if (rev === 'full' || rev === 'partial' || rev === 'none') {
|
|
382
|
+
plan.rollback.reversibility = rev;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
plan.rollback.steps.push(item);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else if (currentSection === 'taskSteps' && currentTaskSteps && line.match(/^\s{2,}- /)) {
|
|
391
|
+
// Indented step for current task (e.g., " - step text")
|
|
392
|
+
const step = line.replace(/^\s*-\s*/, '').trim();
|
|
393
|
+
currentTaskSteps.steps.push(step);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Don't forget to save the last task
|
|
397
|
+
if (currentTaskSteps) {
|
|
398
|
+
plan.taskSteps.push(currentTaskSteps);
|
|
399
|
+
}
|
|
400
|
+
return plan;
|
|
401
|
+
}
|
|
402
|
+
// Generate knowledge extraction proposals from a completed ticket
|
|
403
|
+
export function generateExtractProposals(ticket, namespace) {
|
|
404
|
+
const suggestions = [];
|
|
405
|
+
const ticketType = ticket.type; // Pass to all suggestions
|
|
406
|
+
// Pattern from intent + context
|
|
407
|
+
if (ticket.intent && ticket.context) {
|
|
408
|
+
suggestions.push({
|
|
409
|
+
namespace,
|
|
410
|
+
title: ticket.intent.slice(0, 100),
|
|
411
|
+
content: `Why:\n${ticket.context}\n\nWhen:\n[AI: Describe when to apply this pattern]\n\nPattern:\n${ticket.intent}`,
|
|
412
|
+
category: 'pattern',
|
|
413
|
+
source: 'ticket',
|
|
414
|
+
originTicketId: ticket.id,
|
|
415
|
+
originTicketType: ticketType,
|
|
416
|
+
confidence: 0.75,
|
|
417
|
+
decisionScope: 'new-only',
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// Truths from validated assumptions
|
|
421
|
+
if (ticket.assumptions && ticket.assumptions.length > 0) {
|
|
422
|
+
for (const assumption of ticket.assumptions) {
|
|
423
|
+
suggestions.push({
|
|
424
|
+
namespace,
|
|
425
|
+
title: `Validated: ${assumption.slice(0, 80)}`,
|
|
426
|
+
content: `Fact:\n${assumption}\n\nVerified:\nValidated during ticket ${ticket.id}`,
|
|
427
|
+
category: 'truth',
|
|
428
|
+
source: 'ticket',
|
|
429
|
+
originTicketId: ticket.id,
|
|
430
|
+
originTicketType: ticketType,
|
|
431
|
+
confidence: 0.9,
|
|
432
|
+
decisionScope: 'global',
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Principles from constraints
|
|
437
|
+
if (ticket.constraints_use && ticket.constraints_use.length > 0) {
|
|
438
|
+
for (const constraint of ticket.constraints_use) {
|
|
439
|
+
suggestions.push({
|
|
440
|
+
namespace,
|
|
441
|
+
title: `Use: ${constraint.slice(0, 80)}`,
|
|
442
|
+
content: `Rule:\n${constraint}\n\nWhy:\n[AI: Explain rationale]\n\nApplies:\nNew code only`,
|
|
443
|
+
category: 'principle',
|
|
444
|
+
source: 'ticket',
|
|
445
|
+
originTicketId: ticket.id,
|
|
446
|
+
originTicketType: ticketType,
|
|
447
|
+
confidence: 0.7,
|
|
448
|
+
decisionScope: 'new-only',
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (ticket.constraints_avoid && ticket.constraints_avoid.length > 0) {
|
|
453
|
+
for (const constraint of ticket.constraints_avoid) {
|
|
454
|
+
suggestions.push({
|
|
455
|
+
namespace,
|
|
456
|
+
title: `Avoid: ${constraint.slice(0, 80)}`,
|
|
457
|
+
content: `Avoid:\n${constraint}\n\nWhy:\n[AI: Explain why this is problematic]\n\nApplies:\nNew code only`,
|
|
458
|
+
category: 'principle',
|
|
459
|
+
source: 'ticket',
|
|
460
|
+
originTicketId: ticket.id,
|
|
461
|
+
originTicketType: ticketType,
|
|
462
|
+
confidence: 0.7,
|
|
463
|
+
decisionScope: 'new-only',
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Decisions from plan (high-value knowledge)
|
|
468
|
+
if (ticket.plan?.decisions && ticket.plan.decisions.length > 0) {
|
|
469
|
+
for (const decision of ticket.plan.decisions) {
|
|
470
|
+
if (!decision.choice)
|
|
471
|
+
continue;
|
|
472
|
+
suggestions.push({
|
|
473
|
+
namespace,
|
|
474
|
+
title: `Decision: ${decision.choice.slice(0, 70)}`,
|
|
475
|
+
content: `Rule:\n${decision.choice}\n\nWhy:\n${decision.reason || '[AI: Explain rationale]'}\n\nApplies:\nSimilar contexts`,
|
|
476
|
+
category: 'principle',
|
|
477
|
+
source: 'ticket',
|
|
478
|
+
originTicketId: ticket.id,
|
|
479
|
+
originTicketType: ticketType,
|
|
480
|
+
confidence: 0.85, // Decisions are deliberate choices, higher confidence
|
|
481
|
+
decisionScope: 'new-only',
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Trade-offs from plan (what we didn't choose and why)
|
|
486
|
+
if (ticket.plan?.tradeOffs && ticket.plan.tradeOffs.length > 0) {
|
|
487
|
+
for (const tradeOff of ticket.plan.tradeOffs) {
|
|
488
|
+
if (!tradeOff.considered)
|
|
489
|
+
continue;
|
|
490
|
+
suggestions.push({
|
|
491
|
+
namespace,
|
|
492
|
+
title: `Avoid: ${tradeOff.considered.slice(0, 70)}`,
|
|
493
|
+
content: `Avoid:\n${tradeOff.considered}\n\nWhy rejected:\n${tradeOff.rejected || '[AI: Explain why this was rejected]'}\n\nContext:\nTicket ${ticket.id}`,
|
|
494
|
+
category: 'principle',
|
|
495
|
+
source: 'ticket',
|
|
496
|
+
originTicketId: ticket.id,
|
|
497
|
+
originTicketType: ticketType,
|
|
498
|
+
confidence: 0.8, // Trade-offs are deliberate rejections
|
|
499
|
+
decisionScope: 'new-only',
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// DoD → Verification patterns (validated criteria)
|
|
504
|
+
if (ticket.plan?.dodVerification && ticket.plan.dodVerification.length > 0) {
|
|
505
|
+
for (const dv of ticket.plan.dodVerification) {
|
|
506
|
+
if (!dv.dod || !dv.verify)
|
|
507
|
+
continue;
|
|
508
|
+
suggestions.push({
|
|
509
|
+
namespace,
|
|
510
|
+
title: `Verify: ${dv.dod.slice(0, 70)}`,
|
|
511
|
+
content: `Criterion:\n${dv.dod}\n\nVerification:\n${dv.verify}\n\nValidated:\nTicket ${ticket.id}`,
|
|
512
|
+
category: 'pattern',
|
|
513
|
+
source: 'ticket',
|
|
514
|
+
originTicketId: ticket.id,
|
|
515
|
+
originTicketType: ticketType,
|
|
516
|
+
confidence: 0.8, // Verified criteria are reliable patterns
|
|
517
|
+
decisionScope: 'new-only',
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return suggestions;
|
|
522
|
+
}
|
|
523
|
+
export const ticketCommand = new Command('ticket')
|
|
524
|
+
.description('Manage tickets');
|
|
525
|
+
// Create subcommand
|
|
526
|
+
ticketCommand
|
|
527
|
+
.command('create')
|
|
528
|
+
.description('Create a new ticket from stdin, file, or options')
|
|
529
|
+
.option('--stdin', 'Read ticket markdown from stdin')
|
|
530
|
+
.option('--file <path>', 'Read ticket from markdown file')
|
|
531
|
+
.option('--intent <intent>', 'What user wants to achieve')
|
|
532
|
+
.option('--context <context>', 'Relevant files, patterns, background')
|
|
533
|
+
.option('--use <constraints...>', 'Constraints: things to use')
|
|
534
|
+
.option('--avoid <constraints...>', 'Constraints: things to avoid')
|
|
535
|
+
.option('--assumptions <assumptions...>', 'AI assumptions to validate')
|
|
536
|
+
.option('--class <class>', 'Change class: A, B, or C', 'A')
|
|
537
|
+
.option('--class-reason <reason>', 'Reason for change class')
|
|
538
|
+
.option('--spec <spec-id>', 'Origin spec ID')
|
|
539
|
+
.action(async (options) => {
|
|
540
|
+
try {
|
|
541
|
+
let id;
|
|
542
|
+
let type = null;
|
|
543
|
+
let title = null;
|
|
544
|
+
let intent;
|
|
545
|
+
let context = null;
|
|
546
|
+
let constraintsUse = null;
|
|
547
|
+
let constraintsAvoid = null;
|
|
548
|
+
let assumptions = null;
|
|
549
|
+
let tasks;
|
|
550
|
+
let dod;
|
|
551
|
+
let changeClass = null;
|
|
552
|
+
let changeClassReason = null;
|
|
553
|
+
let originSpecId = null;
|
|
554
|
+
let plan = null;
|
|
555
|
+
// Read from stdin or file if provided
|
|
556
|
+
if (options.stdin || options.file) {
|
|
557
|
+
let content;
|
|
558
|
+
if (options.stdin) {
|
|
559
|
+
content = await readStdin();
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
if (!existsSync(options.file)) {
|
|
563
|
+
throw new Error(`File not found: ${options.file}`);
|
|
564
|
+
}
|
|
565
|
+
content = readFileSync(options.file, 'utf-8');
|
|
566
|
+
}
|
|
567
|
+
const parsed = parseMarkdownTicket(content);
|
|
568
|
+
const issues = validateParsedTicket(parsed);
|
|
569
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
570
|
+
if (errors.length > 0) {
|
|
571
|
+
throw new Error(errors.map(e => `${e.field}: ${e.message}`).join('; '));
|
|
572
|
+
}
|
|
573
|
+
id = generateId('TICKET');
|
|
574
|
+
title = parsed.title || null;
|
|
575
|
+
intent = parsed.intent;
|
|
576
|
+
type = parsed.type || inferTicketType(intent);
|
|
577
|
+
context = parsed.context || null;
|
|
578
|
+
constraintsUse = parsed.constraintsUse || null;
|
|
579
|
+
constraintsAvoid = parsed.constraintsAvoid || null;
|
|
580
|
+
assumptions = parsed.assumptions || null;
|
|
581
|
+
changeClass = parsed.changeClass || null;
|
|
582
|
+
changeClassReason = parsed.changeClassReason || null;
|
|
583
|
+
originSpecId = options.spec || null;
|
|
584
|
+
// Parse plan and extract tasks/DoD from it
|
|
585
|
+
if (parsed.planContent) {
|
|
586
|
+
plan = parsePlanMarkdown(parsed.planContent);
|
|
587
|
+
if (plan.taskSteps.length > 0) {
|
|
588
|
+
tasks = plan.taskSteps.map(ts => ({ text: ts.task, done: false }));
|
|
589
|
+
}
|
|
590
|
+
if (plan.dodVerification.length > 0) {
|
|
591
|
+
dod = plan.dodVerification.map(dv => ({ text: dv.dod, done: false }));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
// Use CLI options
|
|
597
|
+
if (!options.intent) {
|
|
598
|
+
throw new Error('Either --file/--stdin or --intent is required');
|
|
599
|
+
}
|
|
600
|
+
id = generateId('TICKET');
|
|
601
|
+
intent = options.intent;
|
|
602
|
+
type = inferTicketType(intent);
|
|
603
|
+
context = options.context || null;
|
|
604
|
+
constraintsUse = options.use || null;
|
|
605
|
+
constraintsAvoid = options.avoid || null;
|
|
606
|
+
assumptions = options.assumptions || null;
|
|
607
|
+
changeClass = options.class || null;
|
|
608
|
+
changeClassReason = options.classReason || null;
|
|
609
|
+
originSpecId = options.spec || null;
|
|
610
|
+
}
|
|
611
|
+
const client = await getClient();
|
|
612
|
+
await client.execute({
|
|
613
|
+
sql: `INSERT INTO tickets (
|
|
614
|
+
id, type, title, status, intent, context,
|
|
615
|
+
constraints_use, constraints_avoid, assumptions,
|
|
616
|
+
tasks, definition_of_done, change_class, change_class_reason, plan, origin_spec_id
|
|
617
|
+
) VALUES (?, ?, ?, 'Backlog', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
618
|
+
args: [
|
|
619
|
+
id,
|
|
620
|
+
type,
|
|
621
|
+
title,
|
|
622
|
+
intent,
|
|
623
|
+
context,
|
|
624
|
+
constraintsUse ? JSON.stringify(constraintsUse) : null,
|
|
625
|
+
constraintsAvoid ? JSON.stringify(constraintsAvoid) : null,
|
|
626
|
+
assumptions ? JSON.stringify(assumptions) : null,
|
|
627
|
+
tasks ? JSON.stringify(tasks) : null,
|
|
628
|
+
dod ? JSON.stringify(dod) : null,
|
|
629
|
+
changeClass,
|
|
630
|
+
changeClassReason,
|
|
631
|
+
plan ? JSON.stringify(plan) : null,
|
|
632
|
+
originSpecId,
|
|
633
|
+
],
|
|
634
|
+
});
|
|
635
|
+
closeClient();
|
|
636
|
+
const response = {
|
|
637
|
+
success: true,
|
|
638
|
+
data: { id, status: 'created' },
|
|
639
|
+
};
|
|
640
|
+
console.log(JSON.stringify(response));
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
const response = {
|
|
644
|
+
success: false,
|
|
645
|
+
error: `Failed to create ticket: ${error.message}`,
|
|
646
|
+
};
|
|
647
|
+
console.log(JSON.stringify(response));
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
// Get subcommand
|
|
652
|
+
ticketCommand
|
|
653
|
+
.command('get')
|
|
654
|
+
.description('Get a ticket by ID')
|
|
655
|
+
.argument('<id>', 'Ticket ID')
|
|
656
|
+
.action(async (id) => {
|
|
657
|
+
try {
|
|
658
|
+
const client = await getClient();
|
|
659
|
+
const result = await client.execute({
|
|
660
|
+
sql: 'SELECT * FROM tickets WHERE id = ?',
|
|
661
|
+
args: [id],
|
|
662
|
+
});
|
|
663
|
+
closeClient();
|
|
664
|
+
if (result.rows.length === 0) {
|
|
665
|
+
const response = {
|
|
666
|
+
success: false,
|
|
667
|
+
error: `Ticket ${id} not found`,
|
|
668
|
+
};
|
|
669
|
+
console.log(JSON.stringify(response));
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
const ticket = parseTicketRow(result.rows[0]);
|
|
673
|
+
const response = {
|
|
674
|
+
success: true,
|
|
675
|
+
data: ticket,
|
|
676
|
+
};
|
|
677
|
+
console.log(JSON.stringify(response));
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
const response = {
|
|
681
|
+
success: false,
|
|
682
|
+
error: `Failed to get ticket: ${error.message}`,
|
|
683
|
+
};
|
|
684
|
+
console.log(JSON.stringify(response));
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
// Update subcommand
|
|
689
|
+
ticketCommand
|
|
690
|
+
.command('update')
|
|
691
|
+
.description('Update a ticket')
|
|
692
|
+
.argument('<id>', 'Ticket ID')
|
|
693
|
+
.option('--status <status>', 'New status (Backlog|In Progress|In Review|Done)')
|
|
694
|
+
.option('--context <context>', 'Update context')
|
|
695
|
+
.option('--comment <comment>', 'Add a comment (appended to context)')
|
|
696
|
+
.option('--tasks <tasks...>', 'Replace tasks')
|
|
697
|
+
.option('--dod <criteria...>', 'Replace definition of done')
|
|
698
|
+
.option('--complete-task <indices>', 'Mark tasks as done (comma-separated indices, e.g., 0,1,2)')
|
|
699
|
+
.option('--complete-dod <indices>', 'Mark DoD items as done (comma-separated indices, e.g., 0,1,2)')
|
|
700
|
+
.option('--complete-all', 'Mark all tasks and DoD items as complete')
|
|
701
|
+
.option('--plan-stdin', 'Read plan from stdin (markdown format)')
|
|
702
|
+
.option('--spec <spec-id>', 'Set origin spec ID')
|
|
703
|
+
.action(async (id, options) => {
|
|
704
|
+
try {
|
|
705
|
+
const client = await getClient();
|
|
706
|
+
// Check ticket exists
|
|
707
|
+
const existing = await client.execute({
|
|
708
|
+
sql: 'SELECT * FROM tickets WHERE id = ?',
|
|
709
|
+
args: [id],
|
|
710
|
+
});
|
|
711
|
+
if (existing.rows.length === 0) {
|
|
712
|
+
closeClient();
|
|
713
|
+
const response = {
|
|
714
|
+
success: false,
|
|
715
|
+
error: `Ticket ${id} not found`,
|
|
716
|
+
};
|
|
717
|
+
console.log(JSON.stringify(response));
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
const currentTicket = parseTicketRow(existing.rows[0]);
|
|
721
|
+
const updates = [];
|
|
722
|
+
const args = [];
|
|
723
|
+
// Track if tasks/dod have been modified to avoid duplicate updates
|
|
724
|
+
let tasksModified = false;
|
|
725
|
+
let dodModified = false;
|
|
726
|
+
let tasks = currentTicket.tasks ? [...currentTicket.tasks] : [];
|
|
727
|
+
let dod = currentTicket.definition_of_done ? [...currentTicket.definition_of_done] : [];
|
|
728
|
+
// Handle --complete-all flag (highest priority for completion)
|
|
729
|
+
if (options.completeAll) {
|
|
730
|
+
if (tasks.length > 0) {
|
|
731
|
+
tasks = tasks.map(t => ({ ...t, done: true }));
|
|
732
|
+
tasksModified = true;
|
|
733
|
+
}
|
|
734
|
+
if (dod.length > 0) {
|
|
735
|
+
dod = dod.map(d => ({ ...d, done: true }));
|
|
736
|
+
dodModified = true;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// Handle --complete-task with comma-separated indices
|
|
740
|
+
if (options.completeTask !== undefined && !options.completeAll) {
|
|
741
|
+
const indices = String(options.completeTask).split(',').map(s => parseInt(s.trim(), 10));
|
|
742
|
+
for (const idx of indices) {
|
|
743
|
+
if (tasks[idx]) {
|
|
744
|
+
tasks[idx].done = true;
|
|
745
|
+
tasksModified = true;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// Handle --complete-dod with comma-separated indices
|
|
750
|
+
if (options.completeDod !== undefined && !options.completeAll) {
|
|
751
|
+
const indices = String(options.completeDod).split(',').map(s => parseInt(s.trim(), 10));
|
|
752
|
+
for (const idx of indices) {
|
|
753
|
+
if (dod[idx]) {
|
|
754
|
+
dod[idx].done = true;
|
|
755
|
+
dodModified = true;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (options.status) {
|
|
760
|
+
updates.push('status = ?');
|
|
761
|
+
args.push(options.status);
|
|
762
|
+
// Auto-complete all tasks and DoD when status is "Done" (unless already handled by --complete-all)
|
|
763
|
+
if (options.status === 'Done' && !options.completeAll) {
|
|
764
|
+
if (tasks.length > 0 && !tasksModified) {
|
|
765
|
+
tasks = tasks.map(t => ({ ...t, done: true }));
|
|
766
|
+
tasksModified = true;
|
|
767
|
+
}
|
|
768
|
+
if (dod.length > 0 && !dodModified) {
|
|
769
|
+
dod = dod.map(d => ({ ...d, done: true }));
|
|
770
|
+
dodModified = true;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (options.context) {
|
|
775
|
+
updates.push('context = ?');
|
|
776
|
+
args.push(options.context);
|
|
777
|
+
}
|
|
778
|
+
if (options.comment) {
|
|
779
|
+
const currentComments = currentTicket.comments || [];
|
|
780
|
+
const newComment = {
|
|
781
|
+
text: options.comment,
|
|
782
|
+
timestamp: new Date().toISOString(),
|
|
783
|
+
};
|
|
784
|
+
currentComments.push(newComment);
|
|
785
|
+
updates.push('comments = ?');
|
|
786
|
+
args.push(JSON.stringify(currentComments));
|
|
787
|
+
}
|
|
788
|
+
if (options.tasks) {
|
|
789
|
+
tasks = options.tasks.map((text) => ({ text, done: false }));
|
|
790
|
+
tasksModified = true;
|
|
791
|
+
}
|
|
792
|
+
if (options.dod) {
|
|
793
|
+
dod = options.dod.map((text) => ({ text, done: false }));
|
|
794
|
+
dodModified = true;
|
|
795
|
+
}
|
|
796
|
+
if (options.spec) {
|
|
797
|
+
updates.push('origin_spec_id = ?');
|
|
798
|
+
args.push(options.spec);
|
|
799
|
+
}
|
|
800
|
+
// Handle --plan-stdin: read and parse plan from stdin
|
|
801
|
+
if (options.planStdin) {
|
|
802
|
+
const planContent = await readStdin();
|
|
803
|
+
const plan = parsePlanMarkdown(planContent);
|
|
804
|
+
updates.push('plan = ?');
|
|
805
|
+
args.push(JSON.stringify(plan));
|
|
806
|
+
}
|
|
807
|
+
// Apply task modifications
|
|
808
|
+
if (tasksModified) {
|
|
809
|
+
updates.push('tasks = ?');
|
|
810
|
+
args.push(JSON.stringify(tasks));
|
|
811
|
+
}
|
|
812
|
+
// Apply dod modifications
|
|
813
|
+
if (dodModified) {
|
|
814
|
+
updates.push('definition_of_done = ?');
|
|
815
|
+
args.push(JSON.stringify(dod));
|
|
816
|
+
}
|
|
817
|
+
if (updates.length === 0) {
|
|
818
|
+
closeClient();
|
|
819
|
+
const response = {
|
|
820
|
+
success: false,
|
|
821
|
+
error: 'No updates provided',
|
|
822
|
+
};
|
|
823
|
+
console.log(JSON.stringify(response));
|
|
824
|
+
process.exit(1);
|
|
825
|
+
}
|
|
826
|
+
updates.push("updated_at = datetime('now')");
|
|
827
|
+
args.push(id);
|
|
828
|
+
await client.execute({
|
|
829
|
+
sql: `UPDATE tickets SET ${updates.join(', ')} WHERE id = ?`,
|
|
830
|
+
args,
|
|
831
|
+
});
|
|
832
|
+
// Auto-extract: generate knowledge proposals when status is "Done"
|
|
833
|
+
let extractProposals;
|
|
834
|
+
if (options.status === 'Done') {
|
|
835
|
+
// Fetch updated ticket for extraction
|
|
836
|
+
const updatedResult = await client.execute({
|
|
837
|
+
sql: 'SELECT * FROM tickets WHERE id = ?',
|
|
838
|
+
args: [id],
|
|
839
|
+
});
|
|
840
|
+
if (updatedResult.rows.length > 0) {
|
|
841
|
+
const updatedTicket = parseTicketRow(updatedResult.rows[0]);
|
|
842
|
+
const namespace = getProjectNamespace();
|
|
843
|
+
extractProposals = generateExtractProposals(updatedTicket, namespace);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
closeClient();
|
|
847
|
+
const response = {
|
|
848
|
+
success: true,
|
|
849
|
+
data: {
|
|
850
|
+
id,
|
|
851
|
+
status: 'updated',
|
|
852
|
+
...(extractProposals && extractProposals.length > 0 && { extractProposals }),
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
console.log(JSON.stringify(response));
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
const response = {
|
|
859
|
+
success: false,
|
|
860
|
+
error: `Failed to update ticket: ${error.message}`,
|
|
861
|
+
};
|
|
862
|
+
console.log(JSON.stringify(response));
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
// List subcommand
|
|
867
|
+
ticketCommand
|
|
868
|
+
.command('list')
|
|
869
|
+
.description('List tickets')
|
|
870
|
+
.option('--status <status>', 'Filter by status')
|
|
871
|
+
.option('--limit <n>', 'Limit results', '20')
|
|
872
|
+
.action(async (options) => {
|
|
873
|
+
try {
|
|
874
|
+
const client = await getClient();
|
|
875
|
+
let sql = 'SELECT * FROM tickets';
|
|
876
|
+
const args = [];
|
|
877
|
+
if (options.status) {
|
|
878
|
+
sql += ' WHERE status = ?';
|
|
879
|
+
args.push(options.status);
|
|
880
|
+
}
|
|
881
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
882
|
+
args.push(parseInt(options.limit, 10));
|
|
883
|
+
const result = await client.execute({ sql, args });
|
|
884
|
+
closeClient();
|
|
885
|
+
const tickets = result.rows.map((row) => parseTicketRow(row));
|
|
886
|
+
const response = {
|
|
887
|
+
success: true,
|
|
888
|
+
data: tickets,
|
|
889
|
+
};
|
|
890
|
+
console.log(JSON.stringify(response));
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
const response = {
|
|
894
|
+
success: false,
|
|
895
|
+
error: `Failed to list tickets: ${error.message}`,
|
|
896
|
+
};
|
|
897
|
+
console.log(JSON.stringify(response));
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
// Delete subcommand
|
|
902
|
+
ticketCommand
|
|
903
|
+
.command('delete')
|
|
904
|
+
.description('Delete a ticket by ID')
|
|
905
|
+
.argument('<id>', 'Ticket ID')
|
|
906
|
+
.action(async (id) => {
|
|
907
|
+
try {
|
|
908
|
+
const client = await getClient();
|
|
909
|
+
// Check ticket exists
|
|
910
|
+
const existing = await client.execute({
|
|
911
|
+
sql: 'SELECT id FROM tickets WHERE id = ?',
|
|
912
|
+
args: [id],
|
|
913
|
+
});
|
|
914
|
+
if (existing.rows.length === 0) {
|
|
915
|
+
closeClient();
|
|
916
|
+
const response = {
|
|
917
|
+
success: false,
|
|
918
|
+
error: `Ticket ${id} not found`,
|
|
919
|
+
};
|
|
920
|
+
console.log(JSON.stringify(response));
|
|
921
|
+
process.exit(1);
|
|
922
|
+
}
|
|
923
|
+
await client.execute({
|
|
924
|
+
sql: 'DELETE FROM tickets WHERE id = ?',
|
|
925
|
+
args: [id],
|
|
926
|
+
});
|
|
927
|
+
closeClient();
|
|
928
|
+
const response = {
|
|
929
|
+
success: true,
|
|
930
|
+
data: { id, status: 'deleted' },
|
|
931
|
+
};
|
|
932
|
+
console.log(JSON.stringify(response));
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
const response = {
|
|
936
|
+
success: false,
|
|
937
|
+
error: `Failed to delete ticket: ${error.message}`,
|
|
938
|
+
};
|
|
939
|
+
console.log(JSON.stringify(response));
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
});
|