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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/bin/superintent.js +2 -0
  4. package/dist/commands/extract.d.ts +2 -0
  5. package/dist/commands/extract.js +66 -0
  6. package/dist/commands/init.d.ts +2 -0
  7. package/dist/commands/init.js +56 -0
  8. package/dist/commands/knowledge.d.ts +2 -0
  9. package/dist/commands/knowledge.js +647 -0
  10. package/dist/commands/search.d.ts +2 -0
  11. package/dist/commands/search.js +153 -0
  12. package/dist/commands/spec.d.ts +2 -0
  13. package/dist/commands/spec.js +283 -0
  14. package/dist/commands/status.d.ts +2 -0
  15. package/dist/commands/status.js +43 -0
  16. package/dist/commands/ticket.d.ts +4 -0
  17. package/dist/commands/ticket.js +942 -0
  18. package/dist/commands/ui.d.ts +2 -0
  19. package/dist/commands/ui.js +954 -0
  20. package/dist/db/client.d.ts +4 -0
  21. package/dist/db/client.js +26 -0
  22. package/dist/db/init-schema.d.ts +2 -0
  23. package/dist/db/init-schema.js +28 -0
  24. package/dist/db/parsers.d.ts +24 -0
  25. package/dist/db/parsers.js +79 -0
  26. package/dist/db/schema.d.ts +7 -0
  27. package/dist/db/schema.js +64 -0
  28. package/dist/db/usage.d.ts +8 -0
  29. package/dist/db/usage.js +24 -0
  30. package/dist/embed/model.d.ts +5 -0
  31. package/dist/embed/model.js +34 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +31 -0
  34. package/dist/types.d.ts +120 -0
  35. package/dist/types.js +1 -0
  36. package/dist/ui/components/index.d.ts +6 -0
  37. package/dist/ui/components/index.js +13 -0
  38. package/dist/ui/components/knowledge.d.ts +33 -0
  39. package/dist/ui/components/knowledge.js +238 -0
  40. package/dist/ui/components/layout.d.ts +1 -0
  41. package/dist/ui/components/layout.js +323 -0
  42. package/dist/ui/components/search.d.ts +15 -0
  43. package/dist/ui/components/search.js +114 -0
  44. package/dist/ui/components/spec.d.ts +11 -0
  45. package/dist/ui/components/spec.js +253 -0
  46. package/dist/ui/components/ticket.d.ts +90 -0
  47. package/dist/ui/components/ticket.js +604 -0
  48. package/dist/ui/components/utils.d.ts +26 -0
  49. package/dist/ui/components/utils.js +34 -0
  50. package/dist/ui/styles.css +2 -0
  51. package/dist/utils/cli.d.ts +21 -0
  52. package/dist/utils/cli.js +31 -0
  53. package/dist/utils/config.d.ts +12 -0
  54. package/dist/utils/config.js +116 -0
  55. package/dist/utils/id.d.ts +6 -0
  56. package/dist/utils/id.js +13 -0
  57. package/dist/utils/io.d.ts +8 -0
  58. package/dist/utils/io.js +15 -0
  59. 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
+ });