@kynetic-ai/spec 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.
Files changed (278) hide show
  1. package/README.md +263 -0
  2. package/dist/acp/client.d.ts +159 -0
  3. package/dist/acp/client.d.ts.map +1 -0
  4. package/dist/acp/client.js +255 -0
  5. package/dist/acp/client.js.map +1 -0
  6. package/dist/acp/framing.d.ts +119 -0
  7. package/dist/acp/framing.d.ts.map +1 -0
  8. package/dist/acp/framing.js +302 -0
  9. package/dist/acp/framing.js.map +1 -0
  10. package/dist/acp/index.d.ts +14 -0
  11. package/dist/acp/index.d.ts.map +1 -0
  12. package/dist/acp/index.js +13 -0
  13. package/dist/acp/index.js.map +1 -0
  14. package/dist/acp/types.d.ts +89 -0
  15. package/dist/acp/types.d.ts.map +1 -0
  16. package/dist/acp/types.js +99 -0
  17. package/dist/acp/types.js.map +1 -0
  18. package/dist/agents/adapters.d.ts +55 -0
  19. package/dist/agents/adapters.d.ts.map +1 -0
  20. package/dist/agents/adapters.js +84 -0
  21. package/dist/agents/adapters.js.map +1 -0
  22. package/dist/agents/index.d.ts +8 -0
  23. package/dist/agents/index.d.ts.map +1 -0
  24. package/dist/agents/index.js +10 -0
  25. package/dist/agents/index.js.map +1 -0
  26. package/dist/agents/spawner.d.ts +53 -0
  27. package/dist/agents/spawner.d.ts.map +1 -0
  28. package/dist/agents/spawner.js +83 -0
  29. package/dist/agents/spawner.js.map +1 -0
  30. package/dist/cli/batch.d.ts +82 -0
  31. package/dist/cli/batch.d.ts.map +1 -0
  32. package/dist/cli/batch.js +162 -0
  33. package/dist/cli/batch.js.map +1 -0
  34. package/dist/cli/commands/clone-for-testing.d.ts +6 -0
  35. package/dist/cli/commands/clone-for-testing.d.ts.map +1 -0
  36. package/dist/cli/commands/clone-for-testing.js +176 -0
  37. package/dist/cli/commands/clone-for-testing.js.map +1 -0
  38. package/dist/cli/commands/derive.d.ts +6 -0
  39. package/dist/cli/commands/derive.d.ts.map +1 -0
  40. package/dist/cli/commands/derive.js +450 -0
  41. package/dist/cli/commands/derive.js.map +1 -0
  42. package/dist/cli/commands/help.d.ts +6 -0
  43. package/dist/cli/commands/help.d.ts.map +1 -0
  44. package/dist/cli/commands/help.js +196 -0
  45. package/dist/cli/commands/help.js.map +1 -0
  46. package/dist/cli/commands/inbox.d.ts +6 -0
  47. package/dist/cli/commands/inbox.d.ts.map +1 -0
  48. package/dist/cli/commands/inbox.js +235 -0
  49. package/dist/cli/commands/inbox.js.map +1 -0
  50. package/dist/cli/commands/index.d.ts +20 -0
  51. package/dist/cli/commands/index.d.ts.map +1 -0
  52. package/dist/cli/commands/index.js +21 -0
  53. package/dist/cli/commands/index.js.map +1 -0
  54. package/dist/cli/commands/init.d.ts +6 -0
  55. package/dist/cli/commands/init.d.ts.map +1 -0
  56. package/dist/cli/commands/init.js +245 -0
  57. package/dist/cli/commands/init.js.map +1 -0
  58. package/dist/cli/commands/item.d.ts +6 -0
  59. package/dist/cli/commands/item.d.ts.map +1 -0
  60. package/dist/cli/commands/item.js +1311 -0
  61. package/dist/cli/commands/item.js.map +1 -0
  62. package/dist/cli/commands/link.d.ts +6 -0
  63. package/dist/cli/commands/link.d.ts.map +1 -0
  64. package/dist/cli/commands/link.js +288 -0
  65. package/dist/cli/commands/link.js.map +1 -0
  66. package/dist/cli/commands/log.d.ts +16 -0
  67. package/dist/cli/commands/log.d.ts.map +1 -0
  68. package/dist/cli/commands/log.js +291 -0
  69. package/dist/cli/commands/log.js.map +1 -0
  70. package/dist/cli/commands/meta.d.ts +15 -0
  71. package/dist/cli/commands/meta.d.ts.map +1 -0
  72. package/dist/cli/commands/meta.js +1378 -0
  73. package/dist/cli/commands/meta.js.map +1 -0
  74. package/dist/cli/commands/module.d.ts +6 -0
  75. package/dist/cli/commands/module.d.ts.map +1 -0
  76. package/dist/cli/commands/module.js +102 -0
  77. package/dist/cli/commands/module.js.map +1 -0
  78. package/dist/cli/commands/ralph.d.ts +9 -0
  79. package/dist/cli/commands/ralph.d.ts.map +1 -0
  80. package/dist/cli/commands/ralph.js +465 -0
  81. package/dist/cli/commands/ralph.js.map +1 -0
  82. package/dist/cli/commands/search.d.ts +6 -0
  83. package/dist/cli/commands/search.d.ts.map +1 -0
  84. package/dist/cli/commands/search.js +134 -0
  85. package/dist/cli/commands/search.js.map +1 -0
  86. package/dist/cli/commands/session.d.ts +164 -0
  87. package/dist/cli/commands/session.d.ts.map +1 -0
  88. package/dist/cli/commands/session.js +745 -0
  89. package/dist/cli/commands/session.js.map +1 -0
  90. package/dist/cli/commands/setup.d.ts +26 -0
  91. package/dist/cli/commands/setup.d.ts.map +1 -0
  92. package/dist/cli/commands/setup.js +586 -0
  93. package/dist/cli/commands/setup.js.map +1 -0
  94. package/dist/cli/commands/shadow.d.ts +6 -0
  95. package/dist/cli/commands/shadow.d.ts.map +1 -0
  96. package/dist/cli/commands/shadow.js +299 -0
  97. package/dist/cli/commands/shadow.js.map +1 -0
  98. package/dist/cli/commands/task.d.ts +6 -0
  99. package/dist/cli/commands/task.d.ts.map +1 -0
  100. package/dist/cli/commands/task.js +1514 -0
  101. package/dist/cli/commands/task.js.map +1 -0
  102. package/dist/cli/commands/tasks.d.ts +6 -0
  103. package/dist/cli/commands/tasks.d.ts.map +1 -0
  104. package/dist/cli/commands/tasks.js +347 -0
  105. package/dist/cli/commands/tasks.js.map +1 -0
  106. package/dist/cli/commands/trait.d.ts +10 -0
  107. package/dist/cli/commands/trait.d.ts.map +1 -0
  108. package/dist/cli/commands/trait.js +295 -0
  109. package/dist/cli/commands/trait.js.map +1 -0
  110. package/dist/cli/commands/validate.d.ts +6 -0
  111. package/dist/cli/commands/validate.d.ts.map +1 -0
  112. package/dist/cli/commands/validate.js +626 -0
  113. package/dist/cli/commands/validate.js.map +1 -0
  114. package/dist/cli/exit-codes.d.ts +62 -0
  115. package/dist/cli/exit-codes.d.ts.map +1 -0
  116. package/dist/cli/exit-codes.js +65 -0
  117. package/dist/cli/exit-codes.js.map +1 -0
  118. package/dist/cli/help/content.d.ts +35 -0
  119. package/dist/cli/help/content.d.ts.map +1 -0
  120. package/dist/cli/help/content.js +312 -0
  121. package/dist/cli/help/content.js.map +1 -0
  122. package/dist/cli/index.d.ts +5 -0
  123. package/dist/cli/index.d.ts.map +1 -0
  124. package/dist/cli/index.js +85 -0
  125. package/dist/cli/index.js.map +1 -0
  126. package/dist/cli/introspection.d.ts +87 -0
  127. package/dist/cli/introspection.d.ts.map +1 -0
  128. package/dist/cli/introspection.js +127 -0
  129. package/dist/cli/introspection.js.map +1 -0
  130. package/dist/cli/output.d.ts +56 -0
  131. package/dist/cli/output.d.ts.map +1 -0
  132. package/dist/cli/output.js +467 -0
  133. package/dist/cli/output.js.map +1 -0
  134. package/dist/cli/suggest.d.ts +16 -0
  135. package/dist/cli/suggest.d.ts.map +1 -0
  136. package/dist/cli/suggest.js +72 -0
  137. package/dist/cli/suggest.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +5 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/parser/alignment.d.ts +113 -0
  143. package/dist/parser/alignment.d.ts.map +1 -0
  144. package/dist/parser/alignment.js +261 -0
  145. package/dist/parser/alignment.js.map +1 -0
  146. package/dist/parser/assess.d.ts +81 -0
  147. package/dist/parser/assess.d.ts.map +1 -0
  148. package/dist/parser/assess.js +197 -0
  149. package/dist/parser/assess.js.map +1 -0
  150. package/dist/parser/convention-validation.d.ts +48 -0
  151. package/dist/parser/convention-validation.d.ts.map +1 -0
  152. package/dist/parser/convention-validation.js +167 -0
  153. package/dist/parser/convention-validation.js.map +1 -0
  154. package/dist/parser/fix.d.ts +38 -0
  155. package/dist/parser/fix.d.ts.map +1 -0
  156. package/dist/parser/fix.js +185 -0
  157. package/dist/parser/fix.js.map +1 -0
  158. package/dist/parser/index.d.ts +12 -0
  159. package/dist/parser/index.d.ts.map +1 -0
  160. package/dist/parser/index.js +13 -0
  161. package/dist/parser/index.js.map +1 -0
  162. package/dist/parser/items.d.ts +138 -0
  163. package/dist/parser/items.d.ts.map +1 -0
  164. package/dist/parser/items.js +321 -0
  165. package/dist/parser/items.js.map +1 -0
  166. package/dist/parser/meta.d.ts +120 -0
  167. package/dist/parser/meta.d.ts.map +1 -0
  168. package/dist/parser/meta.js +441 -0
  169. package/dist/parser/meta.js.map +1 -0
  170. package/dist/parser/refs.d.ts +185 -0
  171. package/dist/parser/refs.d.ts.map +1 -0
  172. package/dist/parser/refs.js +404 -0
  173. package/dist/parser/refs.js.map +1 -0
  174. package/dist/parser/shadow.d.ts +253 -0
  175. package/dist/parser/shadow.d.ts.map +1 -0
  176. package/dist/parser/shadow.js +1053 -0
  177. package/dist/parser/shadow.js.map +1 -0
  178. package/dist/parser/traits.d.ts +72 -0
  179. package/dist/parser/traits.d.ts.map +1 -0
  180. package/dist/parser/traits.js +120 -0
  181. package/dist/parser/traits.js.map +1 -0
  182. package/dist/parser/validate.d.ts +89 -0
  183. package/dist/parser/validate.d.ts.map +1 -0
  184. package/dist/parser/validate.js +817 -0
  185. package/dist/parser/validate.js.map +1 -0
  186. package/dist/parser/yaml.d.ts +326 -0
  187. package/dist/parser/yaml.d.ts.map +1 -0
  188. package/dist/parser/yaml.js +1383 -0
  189. package/dist/parser/yaml.js.map +1 -0
  190. package/dist/ralph/cli-renderer.d.ts +20 -0
  191. package/dist/ralph/cli-renderer.d.ts.map +1 -0
  192. package/dist/ralph/cli-renderer.js +179 -0
  193. package/dist/ralph/cli-renderer.js.map +1 -0
  194. package/dist/ralph/events.d.ts +65 -0
  195. package/dist/ralph/events.d.ts.map +1 -0
  196. package/dist/ralph/events.js +397 -0
  197. package/dist/ralph/events.js.map +1 -0
  198. package/dist/ralph/index.d.ts +8 -0
  199. package/dist/ralph/index.d.ts.map +1 -0
  200. package/dist/ralph/index.js +10 -0
  201. package/dist/ralph/index.js.map +1 -0
  202. package/dist/schema/common.d.ts +46 -0
  203. package/dist/schema/common.d.ts.map +1 -0
  204. package/dist/schema/common.js +71 -0
  205. package/dist/schema/common.js.map +1 -0
  206. package/dist/schema/inbox.d.ts +90 -0
  207. package/dist/schema/inbox.d.ts.map +1 -0
  208. package/dist/schema/inbox.js +30 -0
  209. package/dist/schema/inbox.js.map +1 -0
  210. package/dist/schema/index.d.ts +6 -0
  211. package/dist/schema/index.d.ts.map +1 -0
  212. package/dist/schema/index.js +7 -0
  213. package/dist/schema/index.js.map +1 -0
  214. package/dist/schema/meta.d.ts +762 -0
  215. package/dist/schema/meta.d.ts.map +1 -0
  216. package/dist/schema/meta.js +144 -0
  217. package/dist/schema/meta.js.map +1 -0
  218. package/dist/schema/spec.d.ts +912 -0
  219. package/dist/schema/spec.d.ts.map +1 -0
  220. package/dist/schema/spec.js +104 -0
  221. package/dist/schema/spec.js.map +1 -0
  222. package/dist/schema/task.d.ts +664 -0
  223. package/dist/schema/task.d.ts.map +1 -0
  224. package/dist/schema/task.js +130 -0
  225. package/dist/schema/task.js.map +1 -0
  226. package/dist/sessions/index.d.ts +11 -0
  227. package/dist/sessions/index.d.ts.map +1 -0
  228. package/dist/sessions/index.js +13 -0
  229. package/dist/sessions/index.js.map +1 -0
  230. package/dist/sessions/store.d.ts +144 -0
  231. package/dist/sessions/store.d.ts.map +1 -0
  232. package/dist/sessions/store.js +325 -0
  233. package/dist/sessions/store.js.map +1 -0
  234. package/dist/sessions/types.d.ts +157 -0
  235. package/dist/sessions/types.d.ts.map +1 -0
  236. package/dist/sessions/types.js +90 -0
  237. package/dist/sessions/types.js.map +1 -0
  238. package/dist/strings/errors.d.ts +420 -0
  239. package/dist/strings/errors.d.ts.map +1 -0
  240. package/dist/strings/errors.js +282 -0
  241. package/dist/strings/errors.js.map +1 -0
  242. package/dist/strings/guidance.d.ts +65 -0
  243. package/dist/strings/guidance.d.ts.map +1 -0
  244. package/dist/strings/guidance.js +66 -0
  245. package/dist/strings/guidance.js.map +1 -0
  246. package/dist/strings/index.d.ts +12 -0
  247. package/dist/strings/index.d.ts.map +1 -0
  248. package/dist/strings/index.js +12 -0
  249. package/dist/strings/index.js.map +1 -0
  250. package/dist/strings/labels.d.ts +74 -0
  251. package/dist/strings/labels.d.ts.map +1 -0
  252. package/dist/strings/labels.js +75 -0
  253. package/dist/strings/labels.js.map +1 -0
  254. package/dist/strings/validation.d.ts +126 -0
  255. package/dist/strings/validation.d.ts.map +1 -0
  256. package/dist/strings/validation.js +135 -0
  257. package/dist/strings/validation.js.map +1 -0
  258. package/dist/utils/commit.d.ts +23 -0
  259. package/dist/utils/commit.d.ts.map +1 -0
  260. package/dist/utils/commit.js +67 -0
  261. package/dist/utils/commit.js.map +1 -0
  262. package/dist/utils/git.d.ts +57 -0
  263. package/dist/utils/git.d.ts.map +1 -0
  264. package/dist/utils/git.js +192 -0
  265. package/dist/utils/git.js.map +1 -0
  266. package/dist/utils/grep.d.ts +28 -0
  267. package/dist/utils/grep.d.ts.map +1 -0
  268. package/dist/utils/grep.js +86 -0
  269. package/dist/utils/grep.js.map +1 -0
  270. package/dist/utils/index.d.ts +8 -0
  271. package/dist/utils/index.d.ts.map +1 -0
  272. package/dist/utils/index.js +6 -0
  273. package/dist/utils/index.js.map +1 -0
  274. package/dist/utils/time.d.ts +18 -0
  275. package/dist/utils/time.d.ts.map +1 -0
  276. package/dist/utils/time.js +61 -0
  277. package/dist/utils/time.js.map +1 -0
  278. package/package.json +62 -0
@@ -0,0 +1,1311 @@
1
+ import chalk from 'chalk';
2
+ import { initContext, buildIndexes, createSpecItem, deleteSpecItem, updateSpecItem, addChildItem, loadAllItems, loadAllTasks, ReferenceIndex, AlignmentIndex, checkSlugUniqueness, patchSpecItems, findChildItems, findTraitImplementors, createNote, } from '../../parser/index.js';
3
+ import { commitIfShadow } from '../../parser/shadow.js';
4
+ import { SpecItemPatchSchema } from '../../schema/index.js';
5
+ import { output, error, success, warn, isJsonMode } from '../output.js';
6
+ import { grepItem, formatMatchedFields } from '../../utils/grep.js';
7
+ import { errors } from '../../strings/errors.js';
8
+ import { fieldLabels, sectionHeaders } from '../../strings/labels.js';
9
+ import { EXIT_CODES } from '../exit-codes.js';
10
+ /**
11
+ * Format a spec item for display
12
+ */
13
+ function formatItem(item, verbose = false, grepPattern) {
14
+ const shortId = item._ulid.slice(0, 8);
15
+ const slugStr = item.slugs.length > 0 ? chalk.cyan(`@${item.slugs[0]}`) : '';
16
+ const typeStr = chalk.gray(`[${item.type}]`);
17
+ let status = '';
18
+ if (item.status && typeof item.status === 'object') {
19
+ const s = item.status;
20
+ if (s.implementation) {
21
+ const implColor = s.implementation === 'verified' ? chalk.green
22
+ : s.implementation === 'implemented' ? chalk.cyan
23
+ : s.implementation === 'in_progress' ? chalk.yellow
24
+ : chalk.gray;
25
+ status = implColor(s.implementation);
26
+ }
27
+ else if (s.maturity) {
28
+ status = chalk.gray(s.maturity);
29
+ }
30
+ }
31
+ let line = `${chalk.gray(shortId)} ${typeStr} ${item.title}`;
32
+ if (slugStr)
33
+ line += ` ${slugStr}`;
34
+ if (status)
35
+ line += ` ${status}`;
36
+ if (verbose) {
37
+ const tags = 'tags' in item && Array.isArray(item.tags) ? item.tags : [];
38
+ if (tags.length > 0) {
39
+ line += chalk.blue(` #${tags.join(' #')}`);
40
+ }
41
+ }
42
+ // Show matched fields if grep pattern provided
43
+ if (grepPattern) {
44
+ const match = grepItem(item, grepPattern);
45
+ if (match && match.matchedFields.length > 0) {
46
+ line += '\n ' + chalk.gray(`matched: ${formatMatchedFields(match.matchedFields)}`);
47
+ }
48
+ }
49
+ return line;
50
+ }
51
+ /**
52
+ * Format item list for display
53
+ */
54
+ function formatItemList(items, verbose = false, grepPattern) {
55
+ if (items.length === 0) {
56
+ console.log(chalk.gray('No items found'));
57
+ return;
58
+ }
59
+ for (const item of items) {
60
+ console.log(formatItem(item, verbose, grepPattern));
61
+ }
62
+ console.log(chalk.gray(`\n${items.length} item(s)`));
63
+ }
64
+ /**
65
+ * Format item list as a tree showing parent/child hierarchy
66
+ */
67
+ function formatItemTree(items, verbose = false, grepPattern) {
68
+ if (items.length === 0) {
69
+ console.log(chalk.gray('No items found'));
70
+ return;
71
+ }
72
+ // Build parent-child map
73
+ const childrenMap = new Map();
74
+ const rootItems = [];
75
+ for (const item of items) {
76
+ const path = item._path || '';
77
+ // Determine parent path
78
+ let parentPath = '';
79
+ if (path) {
80
+ // Extract parent path from current path
81
+ // e.g., "features[0].requirements[1]" -> "features[0]"
82
+ const lastDotIndex = path.lastIndexOf('.');
83
+ if (lastDotIndex !== -1) {
84
+ parentPath = path.substring(0, lastDotIndex);
85
+ }
86
+ }
87
+ if (parentPath === '') {
88
+ // Root level item
89
+ rootItems.push(item);
90
+ }
91
+ else {
92
+ // Find parent by path
93
+ const parent = items.find(i => i._path === parentPath);
94
+ if (parent) {
95
+ const parentUlid = parent._ulid;
96
+ if (!childrenMap.has(parentUlid)) {
97
+ childrenMap.set(parentUlid, []);
98
+ }
99
+ childrenMap.get(parentUlid).push(item);
100
+ }
101
+ else {
102
+ // Parent not in filtered list, show at root
103
+ rootItems.push(item);
104
+ }
105
+ }
106
+ }
107
+ // Recursive function to print tree
108
+ function printTree(item, prefix = '', isLast = true) {
109
+ // Print current item with tree prefix
110
+ const connector = isLast ? '└── ' : '├── ';
111
+ const itemLine = formatItem(item, verbose, grepPattern);
112
+ console.log(prefix + connector + itemLine);
113
+ // Print children
114
+ const children = childrenMap.get(item._ulid) || [];
115
+ const childPrefix = prefix + (isLast ? ' ' : '│ ');
116
+ children.forEach((child, index) => {
117
+ const isLastChild = index === children.length - 1;
118
+ printTree(child, childPrefix, isLastChild);
119
+ });
120
+ }
121
+ // Print all root items
122
+ rootItems.forEach((item, index) => {
123
+ const isLast = index === rootItems.length - 1;
124
+ printTree(item, '', isLast);
125
+ });
126
+ console.log(chalk.gray(`\n${items.length} item(s)`));
127
+ }
128
+ /**
129
+ * Handle cascading status updates to child items
130
+ * Returns array of updated child items
131
+ */
132
+ async function handleStatusCascade(ctx, parent, newStatus, allItems, refIndex) {
133
+ // Find direct children
134
+ const children = findChildItems(parent, allItems);
135
+ if (children.length === 0) {
136
+ return [];
137
+ }
138
+ // Skip prompt in JSON mode
139
+ if (isJsonMode()) {
140
+ return [];
141
+ }
142
+ // Prompt user for cascade
143
+ const readline = await import('readline');
144
+ const rl = readline.createInterface({
145
+ input: process.stdin,
146
+ output: process.stdout,
147
+ });
148
+ const answer = await new Promise((resolve) => {
149
+ rl.question(`Update ${children.length} child item(s) to ${newStatus}? [y/n] `, resolve);
150
+ });
151
+ rl.close();
152
+ if (answer.toLowerCase() !== 'y') {
153
+ return [];
154
+ }
155
+ // Update children
156
+ const updatedChildren = [];
157
+ for (const child of children) {
158
+ const currentStatus = child.status && typeof child.status === 'object'
159
+ ? child.status
160
+ : { maturity: 'draft', implementation: 'not_started' };
161
+ const updates = {
162
+ status: {
163
+ maturity: currentStatus.maturity || 'draft',
164
+ implementation: newStatus,
165
+ },
166
+ };
167
+ const updated = await updateSpecItem(ctx, child, updates);
168
+ updatedChildren.push(updated);
169
+ // Log each child update (non-JSON mode only)
170
+ const childRef = child.slugs[0] || refIndex.shortUlid(child._ulid);
171
+ console.log(chalk.gray(` ✓ Updated @${childRef}`));
172
+ }
173
+ return updatedChildren;
174
+ }
175
+ /**
176
+ * Register item commands
177
+ */
178
+ export function registerItemCommands(program) {
179
+ const item = program
180
+ .command('item')
181
+ .description('Spec item commands');
182
+ // kspec item list
183
+ item
184
+ .command('list')
185
+ .description('List spec items with optional filters')
186
+ .option('-t, --type <type>', 'Filter by item type (module, feature, requirement, constraint, decision)')
187
+ .option('-s, --status <status>', 'Filter by implementation status (not_started, in_progress, implemented, verified)')
188
+ .option('-m, --maturity <maturity>', 'Filter by maturity (draft, proposed, stable, deferred, deprecated)')
189
+ .option('--tag <tag>', 'Filter by tag (can specify multiple)', (val, prev) => [...prev, val], [])
190
+ .option('--has <field>', 'Filter items that have field present', (val, prev) => [...prev, val], [])
191
+ .option('-q, --search <text>', 'Search in title')
192
+ .option('-g, --grep <pattern>', 'Search content with regex pattern')
193
+ .option('-v, --verbose', 'Show more details')
194
+ .option('--tree', 'Show parent/child hierarchy')
195
+ .option('--limit <n>', 'Limit results', '50')
196
+ .action(async (options) => {
197
+ try {
198
+ const ctx = await initContext();
199
+ const { itemIndex, items } = await buildIndexes(ctx);
200
+ // Build filter from options
201
+ const filter = {
202
+ specItemsOnly: true, // Only spec items, not tasks
203
+ };
204
+ if (options.type) {
205
+ filter.type = options.type;
206
+ }
207
+ if (options.status) {
208
+ filter.implementation = options.status;
209
+ }
210
+ if (options.maturity) {
211
+ filter.maturity = options.maturity;
212
+ }
213
+ if (options.tag && options.tag.length > 0) {
214
+ filter.tags = options.tag;
215
+ }
216
+ if (options.has && options.has.length > 0) {
217
+ filter.hasFields = options.has;
218
+ }
219
+ if (options.search) {
220
+ filter.titleContains = options.search;
221
+ }
222
+ if (options.grep) {
223
+ filter.grepSearch = options.grep;
224
+ }
225
+ const limit = parseInt(options.limit, 10) || 50;
226
+ const result = itemIndex.queryPaginated(filter, 0, limit);
227
+ // Filter to only LoadedSpecItem (not tasks)
228
+ const specItems = result.items.filter((item) => !('status' in item && typeof item.status === 'string'));
229
+ output({
230
+ items: specItems,
231
+ total: result.total,
232
+ showing: specItems.length,
233
+ grepPattern: options.grep,
234
+ tree: options.tree,
235
+ }, () => {
236
+ if (options.tree) {
237
+ formatItemTree(specItems, options.verbose, options.grep);
238
+ }
239
+ else {
240
+ formatItemList(specItems, options.verbose, options.grep);
241
+ }
242
+ });
243
+ }
244
+ catch (err) {
245
+ error(errors.failures.listItems, err);
246
+ process.exit(EXIT_CODES.ERROR);
247
+ }
248
+ });
249
+ // kspec item get <ref>
250
+ item
251
+ .command('get <ref>')
252
+ .description('Get details for a specific item')
253
+ .action(async (ref) => {
254
+ try {
255
+ const ctx = await initContext();
256
+ const { refIndex, traitIndex, items } = await buildIndexes(ctx);
257
+ const result = refIndex.resolve(ref);
258
+ if (!result.ok) {
259
+ error(errors.reference.itemNotFound(ref));
260
+ process.exit(EXIT_CODES.ERROR);
261
+ }
262
+ const item = result.item;
263
+ // AC: @trait-display ac-2 - JSON mode includes inherited_traits array
264
+ const inheritedTraits = traitIndex.getInheritedAC(item._ulid);
265
+ const traitsByTrait = new Map();
266
+ for (const { trait, ac } of inheritedTraits) {
267
+ if (!traitsByTrait.has(trait.ulid)) {
268
+ traitsByTrait.set(trait.ulid, { trait, acs: [] });
269
+ }
270
+ traitsByTrait.get(trait.ulid).acs.push(ac);
271
+ }
272
+ // Build JSON output with inherited traits
273
+ const jsonOutput = {
274
+ ...item,
275
+ inherited_traits: Array.from(traitsByTrait.values()).map(({ trait, acs }) => ({
276
+ ref: `@${trait.slug}`,
277
+ title: trait.title,
278
+ acceptance_criteria: acs,
279
+ })),
280
+ };
281
+ output(jsonOutput, () => {
282
+ console.log(chalk.bold(item.title));
283
+ console.log(chalk.gray('─'.repeat(40)));
284
+ console.log(`${fieldLabels.ulid} ${item._ulid}`);
285
+ if (item.slugs.length > 0) {
286
+ console.log(`${fieldLabels.slugs} ${item.slugs.join(', ')}`);
287
+ }
288
+ console.log(`${fieldLabels.type} ${item.type}`);
289
+ if (item.status && typeof item.status === 'object') {
290
+ const s = item.status;
291
+ if (s.maturity)
292
+ console.log(`${fieldLabels.maturity} ${s.maturity}`);
293
+ if (s.implementation)
294
+ console.log(`${fieldLabels.implementation}${s.implementation}`);
295
+ }
296
+ if ('tags' in item && Array.isArray(item.tags) && item.tags.length > 0) {
297
+ console.log(`${fieldLabels.tags} ${item.tags.join(', ')}`);
298
+ }
299
+ if (item.description) {
300
+ console.log('\n' + sectionHeaders.description);
301
+ console.log(item.description);
302
+ }
303
+ // AC: @trait-display ac-1 - Show own AC first
304
+ if ('acceptance_criteria' in item && Array.isArray(item.acceptance_criteria) && item.acceptance_criteria.length > 0) {
305
+ console.log('\n' + sectionHeaders.acceptanceCriteria);
306
+ for (const ac of item.acceptance_criteria) {
307
+ if (ac && typeof ac === 'object' && 'id' in ac) {
308
+ const acObj = ac;
309
+ console.log(chalk.cyan(` [${acObj.id}]`));
310
+ if (acObj.given)
311
+ console.log(` Given: ${acObj.given}`);
312
+ if (acObj.when)
313
+ console.log(` When: ${acObj.when}`);
314
+ if (acObj.then)
315
+ console.log(` Then: ${acObj.then}`);
316
+ }
317
+ }
318
+ }
319
+ // AC: @trait-display ac-1, ac-4, ac-5 - Show inherited AC per trait in labeled sections
320
+ if (traitsByTrait.size > 0) {
321
+ for (const { trait, acs } of traitsByTrait.values()) {
322
+ console.log(chalk.gray(`\n─── Inherited from @${trait.slug} ───`));
323
+ for (const ac of acs) {
324
+ console.log(chalk.cyan(` [${ac.id}]`) + chalk.gray(` (from @${trait.slug})`));
325
+ if (ac.given)
326
+ console.log(` Given: ${ac.given}`);
327
+ if (ac.when)
328
+ console.log(` When: ${ac.when}`);
329
+ if (ac.then)
330
+ console.log(` Then: ${ac.then}`);
331
+ }
332
+ }
333
+ }
334
+ });
335
+ }
336
+ catch (err) {
337
+ error(errors.failures.getItem, err);
338
+ process.exit(EXIT_CODES.ERROR);
339
+ }
340
+ });
341
+ // kspec item types - show available types and counts
342
+ item
343
+ .command('types')
344
+ .description('Show item types and counts')
345
+ .action(async () => {
346
+ try {
347
+ const ctx = await initContext();
348
+ const { itemIndex } = await buildIndexes(ctx);
349
+ const typeCounts = itemIndex.getTypeCounts();
350
+ output(Object.fromEntries(typeCounts), () => {
351
+ console.log(chalk.bold('Item Types'));
352
+ console.log(chalk.gray('─'.repeat(30)));
353
+ for (const [type, count] of typeCounts) {
354
+ console.log(` ${type}: ${count}`);
355
+ }
356
+ console.log(chalk.gray(`\nTotal: ${itemIndex.size} items`));
357
+ });
358
+ }
359
+ catch (err) {
360
+ error(errors.failures.getTypes, err);
361
+ process.exit(EXIT_CODES.ERROR);
362
+ }
363
+ });
364
+ // kspec item tags - show available tags and counts
365
+ item
366
+ .command('tags')
367
+ .description('Show tags and counts')
368
+ .action(async () => {
369
+ try {
370
+ const ctx = await initContext();
371
+ const { itemIndex } = await buildIndexes(ctx);
372
+ const tagCounts = itemIndex.getTagCounts();
373
+ output(Object.fromEntries(tagCounts), () => {
374
+ console.log(chalk.bold('Tags'));
375
+ console.log(chalk.gray('─'.repeat(30)));
376
+ for (const [tag, count] of tagCounts) {
377
+ console.log(` #${tag}: ${count}`);
378
+ }
379
+ });
380
+ }
381
+ catch (err) {
382
+ error(errors.failures.getTags, err);
383
+ process.exit(EXIT_CODES.ERROR);
384
+ }
385
+ });
386
+ // kspec item add - create a new spec item under a parent
387
+ item
388
+ .command('add')
389
+ .description('Create a new spec item under a parent')
390
+ .requiredOption('--under <ref>', 'Parent item reference (e.g., @core-primitives)')
391
+ .requiredOption('--title <title>', 'Item title')
392
+ .option('--type <type>', 'Item type (feature, requirement, constraint, decision)', 'feature')
393
+ .option('--slug <slug>', 'Human-friendly slug')
394
+ .option('--priority <priority>', 'Priority (high, medium, low)')
395
+ .option('--tag <tag...>', 'Tags')
396
+ .option('--description <desc>', 'Description')
397
+ .option('--as <field>', 'Child field override (e.g., requirements, constraints)')
398
+ .action(async (options) => {
399
+ try {
400
+ const ctx = await initContext();
401
+ const { refIndex, items } = await buildIndexes(ctx);
402
+ // Find the parent item
403
+ const parentResult = refIndex.resolve(options.under);
404
+ if (!parentResult.ok) {
405
+ error(errors.reference.itemNotFound(options.under));
406
+ process.exit(EXIT_CODES.ERROR);
407
+ }
408
+ const parent = parentResult.item;
409
+ // Check it's not a task
410
+ if ('status' in parent && typeof parent.status === 'string') {
411
+ error(errors.reference.parentIsTask(options.under));
412
+ process.exit(EXIT_CODES.ERROR);
413
+ }
414
+ // Check slug uniqueness if provided
415
+ if (options.slug) {
416
+ const slugCheck = checkSlugUniqueness(refIndex, [options.slug]);
417
+ if (!slugCheck.ok) {
418
+ error(errors.slug.alreadyExists(slugCheck.slug, slugCheck.existingUlid));
419
+ process.exit(EXIT_CODES.CONFLICT);
420
+ }
421
+ }
422
+ const input = {
423
+ title: options.title,
424
+ type: options.type,
425
+ slugs: options.slug ? [options.slug] : [],
426
+ priority: options.priority,
427
+ tags: options.tag || [],
428
+ description: options.description,
429
+ depends_on: [],
430
+ implements: [],
431
+ relates_to: [],
432
+ tests: [],
433
+ traits: [],
434
+ notes: [],
435
+ };
436
+ const newItem = createSpecItem(input);
437
+ const result = await addChildItem(ctx, parent, newItem, options.as);
438
+ // Build index including the new item for accurate short ULID
439
+ const index = new ReferenceIndex([], [...items, result.item]);
440
+ const itemSlug = result.item.slugs?.[0] || index.shortUlid(result.item._ulid);
441
+ await commitIfShadow(ctx.shadow, 'item-add', itemSlug);
442
+ success(`Created item: ${index.shortUlid(result.item._ulid)} under @${parent.slugs[0] || parent._ulid.slice(0, 8)}`, {
443
+ item: result.item,
444
+ path: result.path,
445
+ });
446
+ // Derive hint
447
+ if (!isJsonMode()) {
448
+ const refSlug = result.item.slugs?.[0] || index.shortUlid(result.item._ulid);
449
+ console.log(chalk.gray(`\nDerive implementation task? kspec derive @${refSlug}`));
450
+ }
451
+ }
452
+ catch (err) {
453
+ error(errors.failures.createItem, err);
454
+ process.exit(EXIT_CODES.ERROR);
455
+ }
456
+ });
457
+ // kspec item set - update a spec item field
458
+ item
459
+ .command('set <ref>')
460
+ .description('Update a spec item field')
461
+ .option('--title <title>', 'Set title')
462
+ .option('--type <type>', 'Set type')
463
+ .option('--slug <slug>', 'Add a slug')
464
+ .option('--remove-slug <slug>', 'Remove a slug')
465
+ .option('--priority <priority>', 'Set priority')
466
+ .option('--tag <tag...>', 'Set tags (replaces existing)')
467
+ .option('--description <desc>', 'Set description')
468
+ .option('--status <status>', 'Set implementation status (not_started, in_progress, implemented, verified)')
469
+ .option('--maturity <maturity>', 'Set maturity (draft, proposed, stable, deferred, deprecated)')
470
+ .action(async (ref, options) => {
471
+ try {
472
+ const ctx = await initContext();
473
+ const { refIndex, items } = await buildIndexes(ctx);
474
+ const result = refIndex.resolve(ref);
475
+ if (!result.ok) {
476
+ error(errors.reference.itemNotFound(ref));
477
+ process.exit(EXIT_CODES.ERROR);
478
+ }
479
+ const foundItem = result.item;
480
+ // Check if it's a task (tasks should use task commands)
481
+ if ('status' in foundItem && typeof foundItem.status === 'string') {
482
+ error(errors.reference.taskUseTaskCommands(ref));
483
+ process.exit(EXIT_CODES.ERROR);
484
+ }
485
+ // Check slug uniqueness if adding a new slug
486
+ if (options.slug) {
487
+ const slugCheck = checkSlugUniqueness(refIndex, [options.slug], foundItem._ulid);
488
+ if (!slugCheck.ok) {
489
+ error(errors.slug.alreadyExists(slugCheck.slug, slugCheck.existingUlid));
490
+ process.exit(EXIT_CODES.CONFLICT);
491
+ }
492
+ }
493
+ // Validate --remove-slug
494
+ if (options.removeSlug) {
495
+ const currentSlugs = foundItem.slugs || [];
496
+ if (!currentSlugs.includes(options.removeSlug)) {
497
+ error(errors.slug.notFound(options.removeSlug));
498
+ process.exit(EXIT_CODES.ERROR);
499
+ }
500
+ if (currentSlugs.length === 1) {
501
+ error(errors.slug.cannotRemoveLast(options.removeSlug));
502
+ process.exit(EXIT_CODES.ERROR);
503
+ }
504
+ }
505
+ // Build updates object
506
+ const updates = {};
507
+ if (options.title)
508
+ updates.title = options.title;
509
+ if (options.type)
510
+ updates.type = options.type;
511
+ if (options.slug || options.removeSlug) {
512
+ let slugs = [...(foundItem.slugs || [])];
513
+ if (options.removeSlug) {
514
+ slugs = slugs.filter(s => s !== options.removeSlug);
515
+ }
516
+ if (options.slug) {
517
+ slugs.push(options.slug);
518
+ }
519
+ updates.slugs = slugs;
520
+ }
521
+ if (options.priority)
522
+ updates.priority = options.priority;
523
+ if (options.tag)
524
+ updates.tags = options.tag;
525
+ if (options.description)
526
+ updates.description = options.description;
527
+ // Handle status updates
528
+ if (options.status || options.maturity) {
529
+ const currentStatus = foundItem.status && typeof foundItem.status === 'object'
530
+ ? foundItem.status
531
+ : {};
532
+ updates.status = {
533
+ ...currentStatus,
534
+ ...(options.status && { implementation: options.status }),
535
+ ...(options.maturity && { maturity: options.maturity }),
536
+ };
537
+ }
538
+ if (Object.keys(updates).length === 0) {
539
+ warn('No updates specified');
540
+ return;
541
+ }
542
+ const updated = await updateSpecItem(ctx, foundItem, updates);
543
+ const itemSlug = foundItem.slugs[0] || refIndex.shortUlid(foundItem._ulid);
544
+ // Handle cascade for implementation status updates
545
+ const updatedItems = [updated];
546
+ if (options.status) {
547
+ const cascadeResult = await handleStatusCascade(ctx, updated, options.status, items, refIndex);
548
+ updatedItems.push(...cascadeResult);
549
+ }
550
+ await commitIfShadow(ctx.shadow, 'item-set', itemSlug);
551
+ success(`Updated item: ${refIndex.shortUlid(updated._ulid)}`, { item: updated });
552
+ // Derive hint
553
+ if (!isJsonMode()) {
554
+ const refSlug = updated.slugs?.[0] || refIndex.shortUlid(updated._ulid);
555
+ console.log(chalk.gray(`\nDerive implementation task? kspec derive @${refSlug}`));
556
+ }
557
+ }
558
+ catch (err) {
559
+ error(errors.failures.updateItem, err);
560
+ process.exit(EXIT_CODES.ERROR);
561
+ }
562
+ });
563
+ // kspec item delete - delete a spec item
564
+ item
565
+ .command('delete <ref>')
566
+ .description('Delete a spec item (including nested items)')
567
+ .option('--force', 'Skip confirmation')
568
+ .option('--cascade', 'Delete item and all descendants')
569
+ .action(async (ref, options) => {
570
+ try {
571
+ const ctx = await initContext();
572
+ const { refIndex, items } = await buildIndexes(ctx);
573
+ const result = refIndex.resolve(ref);
574
+ if (!result.ok) {
575
+ error(errors.reference.itemNotFound(ref));
576
+ process.exit(EXIT_CODES.ERROR);
577
+ }
578
+ const foundItem = result.item;
579
+ // Check if it's a task
580
+ if ('status' in foundItem && typeof foundItem.status === 'string') {
581
+ error(errors.reference.itemUseTaskCancel(ref));
582
+ process.exit(EXIT_CODES.ERROR);
583
+ }
584
+ if (!foundItem._sourceFile) {
585
+ error(errors.operation.cannotDeleteNoSource);
586
+ process.exit(EXIT_CODES.ERROR);
587
+ }
588
+ // AC-7: Check if this is a trait with implementors
589
+ const implementors = findTraitImplementors(foundItem, items);
590
+ if (implementors.length > 0) {
591
+ const implementorRefs = implementors.map(i => `@${i.slugs[0] || i._ulid.slice(0, 8)}`).join(', ');
592
+ const errorMsg = `Cannot delete: trait is used by ${implementors.length} specs. Remove trait from specs first: ${implementorRefs}`;
593
+ if (isJsonMode()) {
594
+ error(errorMsg, {
595
+ error: 'trait_in_use',
596
+ implementors: implementors.map(i => ({
597
+ ulid: i._ulid,
598
+ slug: i.slugs[0],
599
+ title: i.title,
600
+ })),
601
+ });
602
+ }
603
+ else {
604
+ error(errorMsg);
605
+ }
606
+ process.exit(EXIT_CODES.ERROR);
607
+ }
608
+ // AC-1/AC-8: Check for child items (nested YAML items, not relates_to refs)
609
+ const children = findChildItems(foundItem, items);
610
+ if (children.length > 0 && !options.cascade) {
611
+ // AC-1: Block deletion if children exist without --cascade
612
+ const errorMsg = `Cannot delete: item has ${children.length} children. Use --cascade to delete recursively`;
613
+ if (isJsonMode()) {
614
+ // AC-10: JSON error includes children array
615
+ error(errorMsg, {
616
+ error: 'has_children',
617
+ children: children.map(c => ({
618
+ ulid: c._ulid,
619
+ slug: c.slugs[0],
620
+ title: c.title,
621
+ ref: `@${c.slugs[0] || c._ulid.slice(0, 8)}`,
622
+ })),
623
+ });
624
+ }
625
+ else {
626
+ error(errorMsg);
627
+ }
628
+ process.exit(EXIT_CODES.ERROR);
629
+ }
630
+ // AC-9: Custom confirmation prompt for cascade
631
+ if (children.length > 0 && options.cascade && !options.force) {
632
+ const itemRef = `@${foundItem.slugs[0] || foundItem._ulid.slice(0, 8)}`;
633
+ // Check for JSON mode - requires --force
634
+ if (isJsonMode()) {
635
+ error('Confirmation required. Use --force with --json');
636
+ process.exit(EXIT_CODES.ERROR);
637
+ }
638
+ // Check for non-interactive environment
639
+ const isTTY = process.env.KSPEC_TEST_TTY === 'true' || process.stdin.isTTY;
640
+ if (!isTTY) {
641
+ error('Non-interactive environment. Use --force to proceed');
642
+ process.exit(EXIT_CODES.ERROR);
643
+ }
644
+ // Show confirmation prompt
645
+ const readline = await import('readline');
646
+ const rl = readline.createInterface({
647
+ input: process.stdin,
648
+ output: process.stdout,
649
+ });
650
+ const response = await new Promise(resolve => {
651
+ rl.question(chalk.yellow(`Delete ${itemRef} and ${children.length} descendant items? [y/N] `), answer => {
652
+ rl.close();
653
+ resolve(answer);
654
+ });
655
+ });
656
+ if (response.toLowerCase() !== 'y') {
657
+ console.log(chalk.gray('Operation cancelled'));
658
+ process.exit(EXIT_CODES.USAGE_ERROR);
659
+ }
660
+ }
661
+ // AC-2/AC-3: Delete item and all descendants with cascade
662
+ const itemsToDelete = options.cascade ? [foundItem, ...children] : [foundItem];
663
+ let deletedCount = 0;
664
+ // Delete in reverse order (deepest first) to avoid path issues
665
+ const sortedItems = [...itemsToDelete].sort((a, b) => {
666
+ const aDepth = a._path ? a._path.split('.').length : 0;
667
+ const bDepth = b._path ? b._path.split('.').length : 0;
668
+ return bDepth - aDepth;
669
+ });
670
+ for (const itemToDelete of sortedItems) {
671
+ const deleted = await deleteSpecItem(ctx, itemToDelete);
672
+ if (deleted) {
673
+ deletedCount++;
674
+ }
675
+ }
676
+ if (deletedCount > 0) {
677
+ // AC-6: Single shadow commit with all deletions
678
+ const itemSlug = foundItem.slugs[0] || refIndex.shortUlid(foundItem._ulid);
679
+ const commitMsg = deletedCount > 1 ? `${deletedCount} items` : itemSlug;
680
+ await commitIfShadow(ctx.shadow, 'item-delete', commitMsg);
681
+ if (deletedCount > 1) {
682
+ success(`Deleted ${deletedCount} items`, { deleted: deletedCount, root_ulid: foundItem._ulid });
683
+ }
684
+ else {
685
+ success(`Deleted item: ${foundItem.title}`, { deleted: true, ulid: foundItem._ulid });
686
+ }
687
+ }
688
+ else {
689
+ error(errors.failures.deleteItem);
690
+ console.log(chalk.gray('Edit the source file directly: ' + foundItem._sourceFile));
691
+ process.exit(EXIT_CODES.ERROR);
692
+ }
693
+ }
694
+ catch (err) {
695
+ error(errors.failures.deleteItem, err);
696
+ process.exit(EXIT_CODES.ERROR);
697
+ }
698
+ });
699
+ // kspec item patch - update item fields via JSON
700
+ item
701
+ .command('patch [ref]')
702
+ .description('Update spec item fields via JSON patch')
703
+ .option('--data <json>', 'JSON data to patch')
704
+ .option('--bulk', 'Read patches from stdin (JSONL or JSON array)')
705
+ .option('--allow-unknown', 'Allow fields not in schema')
706
+ .option('--dry-run', 'Preview changes without applying')
707
+ .option('--fail-fast', 'Stop on first error (bulk mode)')
708
+ .action(async (ref, options) => {
709
+ try {
710
+ const ctx = await initContext();
711
+ if (options.bulk) {
712
+ // Bulk mode: read from stdin
713
+ const stdin = await readStdinFully();
714
+ if (!stdin) {
715
+ error(errors.validation.noInputProvided);
716
+ process.exit(EXIT_CODES.ERROR);
717
+ }
718
+ let patches;
719
+ try {
720
+ patches = parseBulkInput(stdin);
721
+ }
722
+ catch (err) {
723
+ error(errors.validation.failedToParseBulk(err instanceof Error ? err.message : String(err)));
724
+ process.exit(EXIT_CODES.ERROR);
725
+ }
726
+ if (patches.length === 0) {
727
+ error(errors.validation.noPatchesProvided);
728
+ process.exit(EXIT_CODES.ERROR);
729
+ }
730
+ const { refIndex, items } = await buildIndexes(ctx);
731
+ const result = await patchSpecItems(ctx, refIndex, items, patches, {
732
+ allowUnknown: options.allowUnknown,
733
+ failFast: options.failFast,
734
+ dryRun: options.dryRun,
735
+ });
736
+ // Shadow commit if any updates
737
+ if (!options.dryRun && result.summary.updated > 0) {
738
+ await commitIfShadow(ctx.shadow, 'item-patch', `${result.summary.updated} items`);
739
+ }
740
+ output(result, () => formatBulkPatchResult(result, options.dryRun));
741
+ if (result.summary.failed > 0) {
742
+ process.exit(EXIT_CODES.ERROR);
743
+ }
744
+ }
745
+ else {
746
+ // Single item mode
747
+ if (!ref) {
748
+ error(errors.usage.patchNeedRef);
749
+ process.exit(EXIT_CODES.ERROR);
750
+ }
751
+ let data;
752
+ // Get data from --data option or stdin
753
+ if (options.data) {
754
+ try {
755
+ data = JSON.parse(options.data);
756
+ }
757
+ catch (err) {
758
+ error(errors.validation.invalidJsonInData(err instanceof Error ? err.message : ''));
759
+ process.exit(EXIT_CODES.ERROR);
760
+ }
761
+ }
762
+ else {
763
+ const stdin = await readStdinIfAvailable();
764
+ if (stdin) {
765
+ try {
766
+ data = JSON.parse(stdin.trim());
767
+ }
768
+ catch (err) {
769
+ error(errors.validation.invalidJsonFromStdin(err instanceof Error ? err.message : ''));
770
+ process.exit(EXIT_CODES.ERROR);
771
+ }
772
+ }
773
+ else {
774
+ error(errors.validation.noPatchData);
775
+ process.exit(EXIT_CODES.ERROR);
776
+ }
777
+ }
778
+ // Validate against schema (unless --allow-unknown)
779
+ if (!options.allowUnknown) {
780
+ // Use strict schema (no passthrough)
781
+ const strictSchema = SpecItemPatchSchema.strict();
782
+ const parseResult = strictSchema.safeParse(data);
783
+ if (!parseResult.success) {
784
+ const issues = parseResult.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ');
785
+ error(errors.validation.invalidPatchDataWithIssues(issues));
786
+ process.exit(EXIT_CODES.ERROR);
787
+ }
788
+ }
789
+ const { refIndex, items } = await buildIndexes(ctx);
790
+ // Resolve ref
791
+ const resolved = refIndex.resolve(ref);
792
+ if (!resolved.ok) {
793
+ error(errors.reference.itemNotFound(ref));
794
+ process.exit(EXIT_CODES.ERROR);
795
+ }
796
+ // Find the item
797
+ const foundItem = items.find(i => i._ulid === resolved.ulid);
798
+ if (!foundItem) {
799
+ error(errors.reference.notItem(ref));
800
+ process.exit(EXIT_CODES.ERROR);
801
+ }
802
+ if (options.dryRun) {
803
+ output({ ref, data, wouldApplyTo: foundItem.title, ulid: foundItem._ulid }, () => {
804
+ console.log(chalk.yellow('Would patch:'), foundItem.title);
805
+ console.log(chalk.gray('ULID:'), foundItem._ulid.slice(0, 8));
806
+ console.log(chalk.gray('Changes:'));
807
+ console.log(JSON.stringify(data, null, 2));
808
+ });
809
+ return;
810
+ }
811
+ const updated = await updateSpecItem(ctx, foundItem, data);
812
+ const itemSlug = foundItem.slugs[0] || refIndex.shortUlid(foundItem._ulid);
813
+ await commitIfShadow(ctx.shadow, 'item-patch', itemSlug);
814
+ success(`Patched item: ${itemSlug}`, { item: updated });
815
+ }
816
+ }
817
+ catch (err) {
818
+ error(errors.failures.patchItems, err);
819
+ process.exit(EXIT_CODES.ERROR);
820
+ }
821
+ });
822
+ // kspec item status - show implementation status with linked tasks
823
+ item
824
+ .command('status <ref>')
825
+ .description('Show implementation status and linked tasks for a spec item')
826
+ .action(async (ref) => {
827
+ try {
828
+ const ctx = await initContext();
829
+ const tasks = await loadAllTasks(ctx);
830
+ const items = await loadAllItems(ctx);
831
+ const refIndex = new ReferenceIndex(tasks, items);
832
+ const result = refIndex.resolve(ref);
833
+ if (!result.ok) {
834
+ error(errors.reference.itemNotFound(ref));
835
+ process.exit(EXIT_CODES.ERROR);
836
+ }
837
+ const foundItem = result.item;
838
+ // Check if it's a task
839
+ if ('status' in foundItem && typeof foundItem.status === 'string') {
840
+ error(errors.reference.notItem(ref));
841
+ process.exit(EXIT_CODES.ERROR);
842
+ }
843
+ // Build alignment index
844
+ const alignmentIndex = new AlignmentIndex(tasks, items);
845
+ alignmentIndex.buildLinks(refIndex);
846
+ const summary = alignmentIndex.getImplementationSummary(foundItem._ulid);
847
+ if (!summary) {
848
+ error(errors.project.couldNotGetImplSummary);
849
+ process.exit(EXIT_CODES.ERROR);
850
+ }
851
+ output(summary, () => {
852
+ console.log(chalk.bold(foundItem.title));
853
+ console.log(chalk.gray('─'.repeat(40)));
854
+ // Status
855
+ const currentColor = summary.currentStatus === 'implemented' ? chalk.green
856
+ : summary.currentStatus === 'in_progress' ? chalk.yellow
857
+ : chalk.gray;
858
+ const expectedColor = summary.expectedStatus === 'implemented' ? chalk.green
859
+ : summary.expectedStatus === 'in_progress' ? chalk.yellow
860
+ : chalk.gray;
861
+ console.log(`Current status: ${currentColor(summary.currentStatus)}`);
862
+ console.log(`Expected status: ${expectedColor(summary.expectedStatus)}`);
863
+ if (!summary.isAligned) {
864
+ console.log(chalk.yellow('\n⚠ Status mismatch - run task complete to sync'));
865
+ }
866
+ else {
867
+ console.log(chalk.green('\n✓ Aligned'));
868
+ }
869
+ // Linked tasks
870
+ console.log(chalk.bold('\nLinked Tasks:'));
871
+ if (summary.linkedTasks.length === 0) {
872
+ console.log(chalk.gray(' No tasks reference this spec item'));
873
+ }
874
+ else {
875
+ for (const task of summary.linkedTasks) {
876
+ const statusColor = task.taskStatus === 'completed' ? chalk.green
877
+ : task.taskStatus === 'in_progress' ? chalk.blue
878
+ : chalk.gray;
879
+ const shortId = task.taskUlid.slice(0, 8);
880
+ const notes = task.hasNotes ? chalk.gray(' (has notes)') : '';
881
+ console.log(` ${statusColor(`[${task.taskStatus}]`)} ${shortId} ${task.taskTitle}${notes}`);
882
+ }
883
+ }
884
+ });
885
+ }
886
+ catch (err) {
887
+ error(errors.failures.getItemStatus, err);
888
+ process.exit(EXIT_CODES.ERROR);
889
+ }
890
+ });
891
+ // kspec item note <ref> <message>
892
+ item
893
+ .command('note <ref> <message>')
894
+ .description('Add a note to a spec item')
895
+ .option('--author <author>', 'Note author')
896
+ .option('--supersedes <ulid>', 'ULID of note this supersedes')
897
+ .action(async (ref, message, options) => {
898
+ try {
899
+ const ctx = await initContext();
900
+ const items = await loadAllItems(ctx);
901
+ const tasks = await loadAllTasks(ctx);
902
+ const refIndex = new ReferenceIndex(tasks, items);
903
+ const result = refIndex.resolve(ref);
904
+ if (!result.ok) {
905
+ error(errors.reference.itemNotFound(ref));
906
+ process.exit(EXIT_CODES.ERROR);
907
+ }
908
+ const foundItem = items.find(i => i._ulid === result.ulid);
909
+ if (!foundItem) {
910
+ error(errors.reference.itemNotFound(ref));
911
+ process.exit(EXIT_CODES.ERROR);
912
+ }
913
+ const note = createNote(message, options.author, options.supersedes);
914
+ const updatedNotes = [...(foundItem.notes || []), note];
915
+ await updateSpecItem(ctx, foundItem, { notes: updatedNotes });
916
+ const itemSlug = foundItem.slugs[0] || refIndex.shortUlid(foundItem._ulid);
917
+ await commitIfShadow(ctx.shadow, 'item-note', itemSlug);
918
+ success(`Added note to spec item: ${refIndex.shortUlid(foundItem._ulid)}`, { note });
919
+ }
920
+ catch (err) {
921
+ error(errors.failures.addNote, err);
922
+ process.exit(EXIT_CODES.ERROR);
923
+ }
924
+ });
925
+ // kspec item notes <ref>
926
+ item
927
+ .command('notes <ref>')
928
+ .description('Show notes for a spec item')
929
+ .action(async (ref) => {
930
+ try {
931
+ const ctx = await initContext();
932
+ const items = await loadAllItems(ctx);
933
+ const tasks = await loadAllTasks(ctx);
934
+ const refIndex = new ReferenceIndex(tasks, items);
935
+ const result = refIndex.resolve(ref);
936
+ if (!result.ok) {
937
+ error(errors.reference.itemNotFound(ref));
938
+ process.exit(EXIT_CODES.ERROR);
939
+ }
940
+ const foundItem = items.find(i => i._ulid === result.ulid);
941
+ if (!foundItem) {
942
+ error(errors.reference.itemNotFound(ref));
943
+ process.exit(EXIT_CODES.ERROR);
944
+ }
945
+ const notes = foundItem.notes || [];
946
+ output(notes, () => {
947
+ if (notes.length === 0) {
948
+ console.log('No notes');
949
+ }
950
+ else {
951
+ for (const note of notes) {
952
+ const author = note.author || 'unknown';
953
+ console.log(`[${note.created_at}] ${author}:`);
954
+ console.log(note.content);
955
+ console.log('');
956
+ }
957
+ }
958
+ });
959
+ }
960
+ catch (err) {
961
+ error(errors.failures.getNotes, err);
962
+ process.exit(EXIT_CODES.ERROR);
963
+ }
964
+ });
965
+ // Create subcommand group for acceptance criteria operations
966
+ const acCmd = item
967
+ .command('ac')
968
+ .description('Manage acceptance criteria on spec items');
969
+ // Helper: Generate next AC ID based on existing AC
970
+ function generateNextAcId(existingAc) {
971
+ if (!existingAc || existingAc.length === 0)
972
+ return 'ac-1';
973
+ const numericIds = existingAc
974
+ .map(ac => ac.id.match(/^ac-(\d+)$/)?.[1])
975
+ .filter((id) => id !== null && id !== undefined)
976
+ .map(Number);
977
+ const maxId = numericIds.length > 0 ? Math.max(...numericIds) : 0;
978
+ return `ac-${maxId + 1}`;
979
+ }
980
+ // Helper: Resolve ref to spec item (not task)
981
+ async function resolveSpecItem(ref) {
982
+ const ctx = await initContext();
983
+ const { refIndex, items } = await buildIndexes(ctx);
984
+ const result = refIndex.resolve(ref);
985
+ if (!result.ok) {
986
+ error(errors.reference.itemNotFound(ref));
987
+ process.exit(EXIT_CODES.NOT_FOUND);
988
+ }
989
+ const foundItem = result.item;
990
+ // Check if it's a task
991
+ if ('status' in foundItem && typeof foundItem.status === 'string') {
992
+ error(errors.operation.tasksNoAcceptanceCriteria(ref));
993
+ process.exit(EXIT_CODES.NOT_FOUND);
994
+ }
995
+ return { ctx, item: foundItem, refIndex };
996
+ }
997
+ // kspec item ac list <ref>
998
+ acCmd
999
+ .command('list <ref>')
1000
+ .description('List acceptance criteria for a spec item')
1001
+ .action(async (ref) => {
1002
+ try {
1003
+ const { item, refIndex } = await resolveSpecItem(ref);
1004
+ const ac = item.acceptance_criteria || [];
1005
+ output(ac, () => {
1006
+ console.log(chalk.bold(`Acceptance Criteria for: ${item.title} (@${item.slugs[0] || refIndex.shortUlid(item._ulid)})`));
1007
+ console.log();
1008
+ if (ac.length === 0) {
1009
+ console.log(chalk.gray('No acceptance criteria'));
1010
+ }
1011
+ else {
1012
+ for (const criterion of ac) {
1013
+ console.log(chalk.cyan(` [${criterion.id}]`));
1014
+ console.log(chalk.gray(` Given: ${criterion.given}`));
1015
+ console.log(chalk.gray(` When: ${criterion.when}`));
1016
+ console.log(chalk.gray(` Then: ${criterion.then}`));
1017
+ console.log();
1018
+ }
1019
+ }
1020
+ console.log(chalk.gray(`${ac.length} acceptance criteria`));
1021
+ });
1022
+ }
1023
+ catch (err) {
1024
+ error(errors.failures.listAc, err);
1025
+ process.exit(EXIT_CODES.ERROR);
1026
+ }
1027
+ });
1028
+ // kspec item ac add <ref>
1029
+ acCmd
1030
+ .command('add <ref>')
1031
+ .description('Add an acceptance criterion to a spec item')
1032
+ .option('--id <id>', 'AC identifier (auto-generated if not provided)')
1033
+ .requiredOption('--given <text>', 'The precondition (Given...)')
1034
+ .requiredOption('--when <text>', 'The action/trigger (When...)')
1035
+ .requiredOption('--then <text>', 'The expected outcome (Then...)')
1036
+ .action(async (ref, options) => {
1037
+ try {
1038
+ const { ctx, item, refIndex } = await resolveSpecItem(ref);
1039
+ const existingAc = item.acceptance_criteria || [];
1040
+ // Determine ID
1041
+ const acId = options.id || generateNextAcId(existingAc);
1042
+ // Check for duplicate ID
1043
+ if (existingAc.some(ac => ac.id === acId)) {
1044
+ const itemRef = item.slugs[0] || refIndex.shortUlid(item._ulid);
1045
+ error(errors.conflict.acAlreadyExists(acId, itemRef));
1046
+ process.exit(EXIT_CODES.CONFLICT);
1047
+ }
1048
+ // Create new AC
1049
+ const newAc = {
1050
+ id: acId,
1051
+ given: options.given,
1052
+ when: options.when,
1053
+ then: options.then,
1054
+ };
1055
+ // Update item with new AC
1056
+ const updatedAc = [...existingAc, newAc];
1057
+ await updateSpecItem(ctx, item, { acceptance_criteria: updatedAc });
1058
+ const itemSlug = item.slugs[0] || refIndex.shortUlid(item._ulid);
1059
+ await commitIfShadow(ctx.shadow, 'item-ac-add', itemSlug);
1060
+ success(`Added acceptance criterion: ${acId} to @${itemSlug}`, { ac: newAc });
1061
+ }
1062
+ catch (err) {
1063
+ error(errors.failures.addAc, err);
1064
+ process.exit(EXIT_CODES.ERROR);
1065
+ }
1066
+ });
1067
+ // kspec item ac set <ref> <ac-id>
1068
+ acCmd
1069
+ .command('set <ref> <acId>')
1070
+ .description('Update an acceptance criterion')
1071
+ .option('--id <newId>', 'Rename the AC ID')
1072
+ .option('--given <text>', 'Update the precondition')
1073
+ .option('--when <text>', 'Update the action/trigger')
1074
+ .option('--then <text>', 'Update the expected outcome')
1075
+ .action(async (ref, acId, options) => {
1076
+ try {
1077
+ const { ctx, item, refIndex } = await resolveSpecItem(ref);
1078
+ const existingAc = item.acceptance_criteria || [];
1079
+ // Find the AC
1080
+ const acIndex = existingAc.findIndex(ac => ac.id === acId);
1081
+ if (acIndex === -1) {
1082
+ const itemRef = item.slugs[0] || refIndex.shortUlid(item._ulid);
1083
+ error(errors.reference.acNotFound(acId, itemRef));
1084
+ process.exit(EXIT_CODES.NOT_FOUND);
1085
+ }
1086
+ // Check for no updates
1087
+ if (!options.id && !options.given && !options.when && !options.then) {
1088
+ warn('No updates specified');
1089
+ return;
1090
+ }
1091
+ // Check for duplicate ID if renaming
1092
+ if (options.id && options.id !== acId && existingAc.some(ac => ac.id === options.id)) {
1093
+ error(errors.conflict.acIdAlreadyExists(options.id));
1094
+ process.exit(EXIT_CODES.CONFLICT);
1095
+ }
1096
+ // Build updated AC
1097
+ const updatedAc = [...existingAc];
1098
+ const updatedFields = [];
1099
+ updatedAc[acIndex] = {
1100
+ ...updatedAc[acIndex],
1101
+ ...(options.id && { id: options.id }),
1102
+ ...(options.given && { given: options.given }),
1103
+ ...(options.when && { when: options.when }),
1104
+ ...(options.then && { then: options.then }),
1105
+ };
1106
+ if (options.id)
1107
+ updatedFields.push('id');
1108
+ if (options.given)
1109
+ updatedFields.push('given');
1110
+ if (options.when)
1111
+ updatedFields.push('when');
1112
+ if (options.then)
1113
+ updatedFields.push('then');
1114
+ // Update item
1115
+ await updateSpecItem(ctx, item, { acceptance_criteria: updatedAc });
1116
+ const itemSlug = item.slugs[0] || refIndex.shortUlid(item._ulid);
1117
+ await commitIfShadow(ctx.shadow, 'item-ac-set', itemSlug);
1118
+ success(`Updated acceptance criterion: ${acId} on @${itemSlug} (${updatedFields.join(', ')})`, { ac: updatedAc[acIndex] });
1119
+ }
1120
+ catch (err) {
1121
+ error(errors.failures.updateAc, err);
1122
+ process.exit(EXIT_CODES.ERROR);
1123
+ }
1124
+ });
1125
+ // kspec item ac remove <ref> <ac-id>
1126
+ acCmd
1127
+ .command('remove <ref> <acId>')
1128
+ .description('Remove an acceptance criterion')
1129
+ .option('--force', 'Skip confirmation')
1130
+ .action(async (ref, acId, options) => {
1131
+ try {
1132
+ const { ctx, item, refIndex } = await resolveSpecItem(ref);
1133
+ const existingAc = item.acceptance_criteria || [];
1134
+ // Find the AC
1135
+ const acIndex = existingAc.findIndex(ac => ac.id === acId);
1136
+ if (acIndex === -1) {
1137
+ const itemRef = item.slugs[0] || refIndex.shortUlid(item._ulid);
1138
+ error(errors.reference.acNotFound(acId, itemRef));
1139
+ process.exit(EXIT_CODES.NOT_FOUND);
1140
+ }
1141
+ // Confirmation required unless --force
1142
+ if (!options.force) {
1143
+ // AC-5: JSON mode requires --force
1144
+ if (isJsonMode()) {
1145
+ error('Confirmation required. Use --force with --json');
1146
+ process.exit(EXIT_CODES.ERROR);
1147
+ }
1148
+ // AC-6: Non-interactive environment requires --force
1149
+ // Allow KSPEC_TEST_TTY for testing interactive prompts
1150
+ const isTTY = process.env.KSPEC_TEST_TTY === '1' || process.stdin.isTTY;
1151
+ if (!isTTY) {
1152
+ error('Non-interactive environment. Use --force to proceed');
1153
+ process.exit(EXIT_CODES.ERROR);
1154
+ }
1155
+ // AC-1: Prompt for confirmation
1156
+ const readline = await import('readline');
1157
+ const rl = readline.createInterface({
1158
+ input: process.stdin,
1159
+ output: process.stdout,
1160
+ });
1161
+ const answer = await new Promise((resolve) => {
1162
+ rl.question(`Remove acceptance criterion ${acId}? [y/N] `, resolve);
1163
+ });
1164
+ rl.close();
1165
+ // AC-3: User declines (n, N, or empty)
1166
+ if (answer.toLowerCase() !== 'y') {
1167
+ error('Operation cancelled');
1168
+ process.exit(EXIT_CODES.USAGE_ERROR);
1169
+ }
1170
+ }
1171
+ // AC-4: With --force, proceed immediately without prompt
1172
+ // AC-2: User confirmed, proceed with removal
1173
+ const updatedAc = existingAc.filter(ac => ac.id !== acId);
1174
+ await updateSpecItem(ctx, item, { acceptance_criteria: updatedAc });
1175
+ const itemSlug = item.slugs[0] || refIndex.shortUlid(item._ulid);
1176
+ await commitIfShadow(ctx.shadow, 'item-ac-remove', itemSlug);
1177
+ success(`Removed acceptance criterion: ${acId} from @${itemSlug}`, { removed: acId });
1178
+ }
1179
+ catch (err) {
1180
+ error(errors.failures.removeAc, err);
1181
+ process.exit(EXIT_CODES.ERROR);
1182
+ }
1183
+ });
1184
+ }
1185
+ // ─── Patch Helpers ───────────────────────────────────────────────────────────
1186
+ /**
1187
+ * Read stdin fully with timeout (for bulk input).
1188
+ * Returns null if stdin is a TTY or empty.
1189
+ */
1190
+ async function readStdinFully() {
1191
+ if (process.stdin.isTTY) {
1192
+ return null;
1193
+ }
1194
+ return new Promise((resolve) => {
1195
+ let data = '';
1196
+ const timeout = setTimeout(() => {
1197
+ process.stdin.removeAllListeners();
1198
+ resolve(data || null);
1199
+ }, 5000); // 5 second timeout for bulk input
1200
+ process.stdin.setEncoding('utf8');
1201
+ process.stdin.on('data', (chunk) => {
1202
+ data += chunk;
1203
+ });
1204
+ process.stdin.on('end', () => {
1205
+ clearTimeout(timeout);
1206
+ resolve(data || null);
1207
+ });
1208
+ process.stdin.on('error', () => {
1209
+ clearTimeout(timeout);
1210
+ resolve(null);
1211
+ });
1212
+ process.stdin.resume();
1213
+ });
1214
+ }
1215
+ /**
1216
+ * Read stdin if available (non-blocking for single item mode).
1217
+ * Returns null quickly if no data available.
1218
+ */
1219
+ async function readStdinIfAvailable() {
1220
+ if (process.stdin.isTTY) {
1221
+ return null;
1222
+ }
1223
+ return new Promise((resolve) => {
1224
+ let data = '';
1225
+ const timeout = setTimeout(() => {
1226
+ process.stdin.removeAllListeners();
1227
+ resolve(data || null);
1228
+ }, 100); // 100ms timeout for quick check
1229
+ process.stdin.setEncoding('utf8');
1230
+ process.stdin.on('data', (chunk) => {
1231
+ data += chunk;
1232
+ });
1233
+ process.stdin.on('end', () => {
1234
+ clearTimeout(timeout);
1235
+ resolve(data || null);
1236
+ });
1237
+ process.stdin.on('error', () => {
1238
+ clearTimeout(timeout);
1239
+ resolve(null);
1240
+ });
1241
+ process.stdin.resume();
1242
+ });
1243
+ }
1244
+ /**
1245
+ * Parse bulk input (JSONL or JSON array)
1246
+ */
1247
+ function parseBulkInput(input) {
1248
+ const trimmed = input.trim();
1249
+ // Try JSON array first
1250
+ if (trimmed.startsWith('[')) {
1251
+ const parsed = JSON.parse(trimmed);
1252
+ if (!Array.isArray(parsed)) {
1253
+ throw new Error(errors.validation.expectedJsonArray);
1254
+ }
1255
+ return parsed.map((item, i) => validatePatchOperation(item, i));
1256
+ }
1257
+ // Parse as JSONL (one JSON object per line)
1258
+ const lines = trimmed.split('\n').filter(line => line.trim());
1259
+ return lines.map((line, i) => {
1260
+ try {
1261
+ return validatePatchOperation(JSON.parse(line), i);
1262
+ }
1263
+ catch (err) {
1264
+ throw new Error(errors.validation.jsonLineError(i + 1, err instanceof Error ? err.message : 'Invalid JSON'));
1265
+ }
1266
+ });
1267
+ }
1268
+ /**
1269
+ * Validate a patch operation object
1270
+ */
1271
+ function validatePatchOperation(obj, index) {
1272
+ if (!obj || typeof obj !== 'object') {
1273
+ throw new Error(errors.validation.patchMustBeObject(index));
1274
+ }
1275
+ const op = obj;
1276
+ if (typeof op.ref !== 'string' || !op.ref) {
1277
+ throw new Error(errors.validation.patchMustHaveRef(index));
1278
+ }
1279
+ if (!op.data || typeof op.data !== 'object') {
1280
+ throw new Error(errors.validation.patchMustHaveData(index));
1281
+ }
1282
+ return { ref: op.ref, data: op.data };
1283
+ }
1284
+ /**
1285
+ * Format bulk patch result for human output
1286
+ */
1287
+ function formatBulkPatchResult(result, isDryRun = false) {
1288
+ const prefix = isDryRun ? 'Would patch' : 'Patched';
1289
+ for (const r of result.results) {
1290
+ if (r.status === 'updated') {
1291
+ console.log(chalk.green('OK'), `${prefix}: ${r.ref} (${r.ulid?.slice(0, 8)})`);
1292
+ }
1293
+ else if (r.status === 'error') {
1294
+ console.log(chalk.red('ERR'), `${r.ref}: ${r.error}`);
1295
+ }
1296
+ else {
1297
+ console.log(chalk.gray('SKIP'), r.ref);
1298
+ }
1299
+ }
1300
+ console.log('');
1301
+ console.log(chalk.bold('Summary:'));
1302
+ console.log(` Total: ${result.summary.total}`);
1303
+ console.log(chalk.green(` Updated: ${result.summary.updated}`));
1304
+ if (result.summary.failed > 0) {
1305
+ console.log(chalk.red(` Failed: ${result.summary.failed}`));
1306
+ }
1307
+ if (result.summary.skipped > 0) {
1308
+ console.log(chalk.gray(` Skipped: ${result.summary.skipped}`));
1309
+ }
1310
+ }
1311
+ //# sourceMappingURL=item.js.map