@stoneforge/quarry 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 (330) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +160 -0
  3. package/dist/api/index.d.ts +8 -0
  4. package/dist/api/index.d.ts.map +1 -0
  5. package/dist/api/index.js +8 -0
  6. package/dist/api/index.js.map +1 -0
  7. package/dist/api/quarry-api.d.ts +268 -0
  8. package/dist/api/quarry-api.d.ts.map +1 -0
  9. package/dist/api/quarry-api.js +3905 -0
  10. package/dist/api/quarry-api.js.map +1 -0
  11. package/dist/api/types.d.ts +1359 -0
  12. package/dist/api/types.d.ts.map +1 -0
  13. package/dist/api/types.js +204 -0
  14. package/dist/api/types.js.map +1 -0
  15. package/dist/bin/sf.d.ts +3 -0
  16. package/dist/bin/sf.d.ts.map +1 -0
  17. package/dist/bin/sf.js +9 -0
  18. package/dist/bin/sf.js.map +1 -0
  19. package/dist/cli/commands/admin.d.ts +11 -0
  20. package/dist/cli/commands/admin.d.ts.map +1 -0
  21. package/dist/cli/commands/admin.js +465 -0
  22. package/dist/cli/commands/admin.js.map +1 -0
  23. package/dist/cli/commands/alias.d.ts +8 -0
  24. package/dist/cli/commands/alias.d.ts.map +1 -0
  25. package/dist/cli/commands/alias.js +70 -0
  26. package/dist/cli/commands/alias.js.map +1 -0
  27. package/dist/cli/commands/channel.d.ts +13 -0
  28. package/dist/cli/commands/channel.d.ts.map +1 -0
  29. package/dist/cli/commands/channel.js +680 -0
  30. package/dist/cli/commands/channel.js.map +1 -0
  31. package/dist/cli/commands/completion.d.ts +8 -0
  32. package/dist/cli/commands/completion.d.ts.map +1 -0
  33. package/dist/cli/commands/completion.js +87 -0
  34. package/dist/cli/commands/completion.js.map +1 -0
  35. package/dist/cli/commands/config.d.ts +12 -0
  36. package/dist/cli/commands/config.d.ts.map +1 -0
  37. package/dist/cli/commands/config.js +242 -0
  38. package/dist/cli/commands/config.js.map +1 -0
  39. package/dist/cli/commands/crud.d.ts +64 -0
  40. package/dist/cli/commands/crud.d.ts.map +1 -0
  41. package/dist/cli/commands/crud.js +805 -0
  42. package/dist/cli/commands/crud.js.map +1 -0
  43. package/dist/cli/commands/dep.d.ts +16 -0
  44. package/dist/cli/commands/dep.d.ts.map +1 -0
  45. package/dist/cli/commands/dep.js +499 -0
  46. package/dist/cli/commands/dep.js.map +1 -0
  47. package/dist/cli/commands/document.d.ts +12 -0
  48. package/dist/cli/commands/document.d.ts.map +1 -0
  49. package/dist/cli/commands/document.js +1039 -0
  50. package/dist/cli/commands/document.js.map +1 -0
  51. package/dist/cli/commands/embeddings.d.ts +12 -0
  52. package/dist/cli/commands/embeddings.d.ts.map +1 -0
  53. package/dist/cli/commands/embeddings.js +273 -0
  54. package/dist/cli/commands/embeddings.js.map +1 -0
  55. package/dist/cli/commands/entity.d.ts +16 -0
  56. package/dist/cli/commands/entity.d.ts.map +1 -0
  57. package/dist/cli/commands/entity.js +522 -0
  58. package/dist/cli/commands/entity.js.map +1 -0
  59. package/dist/cli/commands/gc.d.ts +10 -0
  60. package/dist/cli/commands/gc.d.ts.map +1 -0
  61. package/dist/cli/commands/gc.js +257 -0
  62. package/dist/cli/commands/gc.js.map +1 -0
  63. package/dist/cli/commands/help.d.ts +11 -0
  64. package/dist/cli/commands/help.d.ts.map +1 -0
  65. package/dist/cli/commands/help.js +169 -0
  66. package/dist/cli/commands/help.js.map +1 -0
  67. package/dist/cli/commands/history.d.ts +9 -0
  68. package/dist/cli/commands/history.d.ts.map +1 -0
  69. package/dist/cli/commands/history.js +160 -0
  70. package/dist/cli/commands/history.js.map +1 -0
  71. package/dist/cli/commands/identity.d.ts +18 -0
  72. package/dist/cli/commands/identity.d.ts.map +1 -0
  73. package/dist/cli/commands/identity.js +698 -0
  74. package/dist/cli/commands/identity.js.map +1 -0
  75. package/dist/cli/commands/inbox.d.ts +20 -0
  76. package/dist/cli/commands/inbox.d.ts.map +1 -0
  77. package/dist/cli/commands/inbox.js +493 -0
  78. package/dist/cli/commands/inbox.js.map +1 -0
  79. package/dist/cli/commands/init.d.ts +20 -0
  80. package/dist/cli/commands/init.d.ts.map +1 -0
  81. package/dist/cli/commands/init.js +144 -0
  82. package/dist/cli/commands/init.js.map +1 -0
  83. package/dist/cli/commands/install.d.ts +9 -0
  84. package/dist/cli/commands/install.d.ts.map +1 -0
  85. package/dist/cli/commands/install.js +200 -0
  86. package/dist/cli/commands/install.js.map +1 -0
  87. package/dist/cli/commands/library.d.ts +12 -0
  88. package/dist/cli/commands/library.d.ts.map +1 -0
  89. package/dist/cli/commands/library.js +665 -0
  90. package/dist/cli/commands/library.js.map +1 -0
  91. package/dist/cli/commands/message.d.ts +11 -0
  92. package/dist/cli/commands/message.d.ts.map +1 -0
  93. package/dist/cli/commands/message.js +608 -0
  94. package/dist/cli/commands/message.js.map +1 -0
  95. package/dist/cli/commands/plan.d.ts +17 -0
  96. package/dist/cli/commands/plan.d.ts.map +1 -0
  97. package/dist/cli/commands/plan.js +698 -0
  98. package/dist/cli/commands/plan.js.map +1 -0
  99. package/dist/cli/commands/playbook.d.ts +12 -0
  100. package/dist/cli/commands/playbook.d.ts.map +1 -0
  101. package/dist/cli/commands/playbook.js +730 -0
  102. package/dist/cli/commands/playbook.js.map +1 -0
  103. package/dist/cli/commands/reset.d.ts +12 -0
  104. package/dist/cli/commands/reset.d.ts.map +1 -0
  105. package/dist/cli/commands/reset.js +306 -0
  106. package/dist/cli/commands/reset.js.map +1 -0
  107. package/dist/cli/commands/serve.d.ts +11 -0
  108. package/dist/cli/commands/serve.d.ts.map +1 -0
  109. package/dist/cli/commands/serve.js +106 -0
  110. package/dist/cli/commands/serve.js.map +1 -0
  111. package/dist/cli/commands/stats.d.ts +8 -0
  112. package/dist/cli/commands/stats.d.ts.map +1 -0
  113. package/dist/cli/commands/stats.js +82 -0
  114. package/dist/cli/commands/stats.js.map +1 -0
  115. package/dist/cli/commands/sync.d.ts +14 -0
  116. package/dist/cli/commands/sync.d.ts.map +1 -0
  117. package/dist/cli/commands/sync.js +370 -0
  118. package/dist/cli/commands/sync.js.map +1 -0
  119. package/dist/cli/commands/task.d.ts +25 -0
  120. package/dist/cli/commands/task.d.ts.map +1 -0
  121. package/dist/cli/commands/task.js +1153 -0
  122. package/dist/cli/commands/task.js.map +1 -0
  123. package/dist/cli/commands/team.d.ts +13 -0
  124. package/dist/cli/commands/team.d.ts.map +1 -0
  125. package/dist/cli/commands/team.js +471 -0
  126. package/dist/cli/commands/team.js.map +1 -0
  127. package/dist/cli/commands/workflow.d.ts +16 -0
  128. package/dist/cli/commands/workflow.d.ts.map +1 -0
  129. package/dist/cli/commands/workflow.js +753 -0
  130. package/dist/cli/commands/workflow.js.map +1 -0
  131. package/dist/cli/completion.d.ts +28 -0
  132. package/dist/cli/completion.d.ts.map +1 -0
  133. package/dist/cli/completion.js +295 -0
  134. package/dist/cli/completion.js.map +1 -0
  135. package/dist/cli/db.d.ts +38 -0
  136. package/dist/cli/db.d.ts.map +1 -0
  137. package/dist/cli/db.js +90 -0
  138. package/dist/cli/db.js.map +1 -0
  139. package/dist/cli/formatter.d.ts +87 -0
  140. package/dist/cli/formatter.d.ts.map +1 -0
  141. package/dist/cli/formatter.js +464 -0
  142. package/dist/cli/formatter.js.map +1 -0
  143. package/dist/cli/index.d.ts +33 -0
  144. package/dist/cli/index.d.ts.map +1 -0
  145. package/dist/cli/index.js +38 -0
  146. package/dist/cli/index.js.map +1 -0
  147. package/dist/cli/parser.d.ts +45 -0
  148. package/dist/cli/parser.d.ts.map +1 -0
  149. package/dist/cli/parser.js +256 -0
  150. package/dist/cli/parser.js.map +1 -0
  151. package/dist/cli/plugin-loader.d.ts +39 -0
  152. package/dist/cli/plugin-loader.d.ts.map +1 -0
  153. package/dist/cli/plugin-loader.js +165 -0
  154. package/dist/cli/plugin-loader.js.map +1 -0
  155. package/dist/cli/plugin-registry.d.ts +50 -0
  156. package/dist/cli/plugin-registry.d.ts.map +1 -0
  157. package/dist/cli/plugin-registry.js +206 -0
  158. package/dist/cli/plugin-registry.js.map +1 -0
  159. package/dist/cli/plugin-types.d.ts +106 -0
  160. package/dist/cli/plugin-types.d.ts.map +1 -0
  161. package/dist/cli/plugin-types.js +103 -0
  162. package/dist/cli/plugin-types.js.map +1 -0
  163. package/dist/cli/runner.d.ts +35 -0
  164. package/dist/cli/runner.d.ts.map +1 -0
  165. package/dist/cli/runner.js +340 -0
  166. package/dist/cli/runner.js.map +1 -0
  167. package/dist/cli/suggest.d.ts +15 -0
  168. package/dist/cli/suggest.d.ts.map +1 -0
  169. package/dist/cli/suggest.js +49 -0
  170. package/dist/cli/suggest.js.map +1 -0
  171. package/dist/cli/types.d.ts +138 -0
  172. package/dist/cli/types.d.ts.map +1 -0
  173. package/dist/cli/types.js +63 -0
  174. package/dist/cli/types.js.map +1 -0
  175. package/dist/config/config.d.ts +86 -0
  176. package/dist/config/config.d.ts.map +1 -0
  177. package/dist/config/config.js +348 -0
  178. package/dist/config/config.js.map +1 -0
  179. package/dist/config/defaults.d.ts +66 -0
  180. package/dist/config/defaults.d.ts.map +1 -0
  181. package/dist/config/defaults.js +114 -0
  182. package/dist/config/defaults.js.map +1 -0
  183. package/dist/config/duration.d.ts +75 -0
  184. package/dist/config/duration.d.ts.map +1 -0
  185. package/dist/config/duration.js +190 -0
  186. package/dist/config/duration.js.map +1 -0
  187. package/dist/config/env.d.ts +67 -0
  188. package/dist/config/env.d.ts.map +1 -0
  189. package/dist/config/env.js +207 -0
  190. package/dist/config/env.js.map +1 -0
  191. package/dist/config/file.d.ts +97 -0
  192. package/dist/config/file.d.ts.map +1 -0
  193. package/dist/config/file.js +365 -0
  194. package/dist/config/file.js.map +1 -0
  195. package/dist/config/index.d.ts +35 -0
  196. package/dist/config/index.d.ts.map +1 -0
  197. package/dist/config/index.js +41 -0
  198. package/dist/config/index.js.map +1 -0
  199. package/dist/config/merge.d.ts +53 -0
  200. package/dist/config/merge.d.ts.map +1 -0
  201. package/dist/config/merge.js +226 -0
  202. package/dist/config/merge.js.map +1 -0
  203. package/dist/config/types.d.ts +257 -0
  204. package/dist/config/types.d.ts.map +1 -0
  205. package/dist/config/types.js +72 -0
  206. package/dist/config/types.js.map +1 -0
  207. package/dist/config/validation.d.ts +55 -0
  208. package/dist/config/validation.d.ts.map +1 -0
  209. package/dist/config/validation.js +251 -0
  210. package/dist/config/validation.js.map +1 -0
  211. package/dist/http/index.d.ts +8 -0
  212. package/dist/http/index.d.ts.map +1 -0
  213. package/dist/http/index.js +12 -0
  214. package/dist/http/index.js.map +1 -0
  215. package/dist/http/sync-handlers.d.ts +162 -0
  216. package/dist/http/sync-handlers.d.ts.map +1 -0
  217. package/dist/http/sync-handlers.js +271 -0
  218. package/dist/http/sync-handlers.js.map +1 -0
  219. package/dist/index.d.ts +25 -0
  220. package/dist/index.d.ts.map +1 -0
  221. package/dist/index.js +69 -0
  222. package/dist/index.js.map +1 -0
  223. package/dist/server/index.d.ts +34 -0
  224. package/dist/server/index.d.ts.map +1 -0
  225. package/dist/server/index.js +3329 -0
  226. package/dist/server/index.js.map +1 -0
  227. package/dist/server/static.d.ts +18 -0
  228. package/dist/server/static.d.ts.map +1 -0
  229. package/dist/server/static.js +71 -0
  230. package/dist/server/static.js.map +1 -0
  231. package/dist/server/ws/broadcaster.d.ts +8 -0
  232. package/dist/server/ws/broadcaster.d.ts.map +1 -0
  233. package/dist/server/ws/broadcaster.js +7 -0
  234. package/dist/server/ws/broadcaster.js.map +1 -0
  235. package/dist/server/ws/handler.d.ts +55 -0
  236. package/dist/server/ws/handler.d.ts.map +1 -0
  237. package/dist/server/ws/handler.js +160 -0
  238. package/dist/server/ws/handler.js.map +1 -0
  239. package/dist/services/blocked-cache.d.ts +297 -0
  240. package/dist/services/blocked-cache.d.ts.map +1 -0
  241. package/dist/services/blocked-cache.js +755 -0
  242. package/dist/services/blocked-cache.js.map +1 -0
  243. package/dist/services/dependency.d.ts +205 -0
  244. package/dist/services/dependency.d.ts.map +1 -0
  245. package/dist/services/dependency.js +566 -0
  246. package/dist/services/dependency.js.map +1 -0
  247. package/dist/services/embeddings/fusion.d.ts +33 -0
  248. package/dist/services/embeddings/fusion.d.ts.map +1 -0
  249. package/dist/services/embeddings/fusion.js +34 -0
  250. package/dist/services/embeddings/fusion.js.map +1 -0
  251. package/dist/services/embeddings/index.d.ts +12 -0
  252. package/dist/services/embeddings/index.d.ts.map +1 -0
  253. package/dist/services/embeddings/index.js +10 -0
  254. package/dist/services/embeddings/index.js.map +1 -0
  255. package/dist/services/embeddings/local-provider.d.ts +31 -0
  256. package/dist/services/embeddings/local-provider.d.ts.map +1 -0
  257. package/dist/services/embeddings/local-provider.js +80 -0
  258. package/dist/services/embeddings/local-provider.js.map +1 -0
  259. package/dist/services/embeddings/service.d.ts +76 -0
  260. package/dist/services/embeddings/service.d.ts.map +1 -0
  261. package/dist/services/embeddings/service.js +153 -0
  262. package/dist/services/embeddings/service.js.map +1 -0
  263. package/dist/services/embeddings/types.d.ts +70 -0
  264. package/dist/services/embeddings/types.d.ts.map +1 -0
  265. package/dist/services/embeddings/types.js +8 -0
  266. package/dist/services/embeddings/types.js.map +1 -0
  267. package/dist/services/id-length-cache.d.ts +156 -0
  268. package/dist/services/id-length-cache.d.ts.map +1 -0
  269. package/dist/services/id-length-cache.js +197 -0
  270. package/dist/services/id-length-cache.js.map +1 -0
  271. package/dist/services/inbox.d.ts +147 -0
  272. package/dist/services/inbox.d.ts.map +1 -0
  273. package/dist/services/inbox.js +428 -0
  274. package/dist/services/inbox.js.map +1 -0
  275. package/dist/services/index.d.ts +10 -0
  276. package/dist/services/index.d.ts.map +1 -0
  277. package/dist/services/index.js +10 -0
  278. package/dist/services/index.js.map +1 -0
  279. package/dist/services/priority-service.d.ts +145 -0
  280. package/dist/services/priority-service.d.ts.map +1 -0
  281. package/dist/services/priority-service.js +272 -0
  282. package/dist/services/priority-service.js.map +1 -0
  283. package/dist/services/search-utils.d.ts +47 -0
  284. package/dist/services/search-utils.d.ts.map +1 -0
  285. package/dist/services/search-utils.js +83 -0
  286. package/dist/services/search-utils.js.map +1 -0
  287. package/dist/sync/hash.d.ts +48 -0
  288. package/dist/sync/hash.d.ts.map +1 -0
  289. package/dist/sync/hash.js +136 -0
  290. package/dist/sync/hash.js.map +1 -0
  291. package/dist/sync/index.d.ts +11 -0
  292. package/dist/sync/index.d.ts.map +1 -0
  293. package/dist/sync/index.js +16 -0
  294. package/dist/sync/index.js.map +1 -0
  295. package/dist/sync/merge.d.ts +80 -0
  296. package/dist/sync/merge.d.ts.map +1 -0
  297. package/dist/sync/merge.js +310 -0
  298. package/dist/sync/merge.js.map +1 -0
  299. package/dist/sync/serialization.d.ts +132 -0
  300. package/dist/sync/serialization.d.ts.map +1 -0
  301. package/dist/sync/serialization.js +306 -0
  302. package/dist/sync/serialization.js.map +1 -0
  303. package/dist/sync/service.d.ts +102 -0
  304. package/dist/sync/service.d.ts.map +1 -0
  305. package/dist/sync/service.js +493 -0
  306. package/dist/sync/service.js.map +1 -0
  307. package/dist/sync/types.d.ts +275 -0
  308. package/dist/sync/types.d.ts.map +1 -0
  309. package/dist/sync/types.js +76 -0
  310. package/dist/sync/types.js.map +1 -0
  311. package/dist/systems/identity.d.ts +479 -0
  312. package/dist/systems/identity.d.ts.map +1 -0
  313. package/dist/systems/identity.js +817 -0
  314. package/dist/systems/identity.js.map +1 -0
  315. package/dist/systems/index.d.ts +8 -0
  316. package/dist/systems/index.d.ts.map +1 -0
  317. package/dist/systems/index.js +29 -0
  318. package/dist/systems/index.js.map +1 -0
  319. package/package.json +121 -0
  320. package/web/assets/charts-vendor-D1YcbGux.js +55 -0
  321. package/web/assets/dnd-vendor-DmxE-_ZH.js +5 -0
  322. package/web/assets/editor-vendor-BxraAWts.js +279 -0
  323. package/web/assets/index-B77vv208.js +341 -0
  324. package/web/assets/index-CF_XnVLh.css +1 -0
  325. package/web/assets/router-vendor-BCKpRBrB.js +41 -0
  326. package/web/assets/ui-vendor-DUahGnbT.js +45 -0
  327. package/web/assets/utils-vendor-CfYKiENT.js +813 -0
  328. package/web/favicon.ico +0 -0
  329. package/web/index.html +23 -0
  330. package/web/logo.png +0 -0
@@ -0,0 +1,1153 @@
1
+ /**
2
+ * Task Commands - Task-specific CLI operations
3
+ *
4
+ * Provides CLI commands for task management:
5
+ * - ready: List tasks ready for work
6
+ * - blocked: List blocked tasks with reasons
7
+ * - close: Close a task
8
+ * - reopen: Reopen a closed task
9
+ * - assign: Assign a task to an entity
10
+ * - defer: Defer a task
11
+ * - undefer: Remove deferral from a task
12
+ */
13
+ import { success, failure, ExitCode } from '../types.js';
14
+ import { getFormatter, getOutputMode } from '../formatter.js';
15
+ import { TaskStatus, TaskTypeValue, updateTaskStatus, isValidStatusTransition, createDocument, ContentType, } from '@stoneforge/core';
16
+ import { existsSync as fileExists, readFileSync } from 'node:fs';
17
+ import { resolve } from 'node:path';
18
+ import { createHandler, createOptions, listHandler, listOptions, showHandler, showOptions, updateHandler, updateOptions, deleteHandler, deleteOptions, } from './crud.js';
19
+ import { suggestCommands } from '../suggest.js';
20
+ import { createAPI } from '../db.js';
21
+ const readyOptions = [
22
+ {
23
+ name: 'assignee',
24
+ short: 'a',
25
+ description: 'Filter by assignee',
26
+ hasValue: true,
27
+ },
28
+ {
29
+ name: 'priority',
30
+ short: 'p',
31
+ description: 'Filter by priority (1-5)',
32
+ hasValue: true,
33
+ },
34
+ {
35
+ name: 'type',
36
+ short: 't',
37
+ description: 'Filter by task type (bug, feature, task, chore)',
38
+ hasValue: true,
39
+ },
40
+ {
41
+ name: 'limit',
42
+ short: 'l',
43
+ description: 'Maximum number of results',
44
+ hasValue: true,
45
+ },
46
+ ];
47
+ async function readyHandler(_args, options) {
48
+ const { api, error } = createAPI(options);
49
+ if (error) {
50
+ return failure(error, ExitCode.GENERAL_ERROR);
51
+ }
52
+ try {
53
+ // Build filter from options
54
+ const filter = {};
55
+ if (options.assignee) {
56
+ filter.assignee = options.assignee;
57
+ }
58
+ if (options.priority) {
59
+ const priority = parseInt(options.priority, 10);
60
+ if (isNaN(priority) || priority < 1 || priority > 5) {
61
+ return failure('Priority must be a number from 1 to 5', ExitCode.VALIDATION);
62
+ }
63
+ filter.priority = priority;
64
+ }
65
+ if (options.type) {
66
+ const validTypes = Object.values(TaskTypeValue);
67
+ if (!validTypes.includes(options.type)) {
68
+ return failure(`Invalid task type: ${options.type}. Must be one of: ${validTypes.join(', ')}`, ExitCode.VALIDATION);
69
+ }
70
+ filter.taskType = options.type;
71
+ }
72
+ if (options.limit) {
73
+ const limit = parseInt(options.limit, 10);
74
+ if (isNaN(limit) || limit < 1) {
75
+ return failure('Limit must be a positive number', ExitCode.VALIDATION);
76
+ }
77
+ filter.limit = limit;
78
+ }
79
+ // Get ready tasks
80
+ const tasks = await api.ready(filter);
81
+ // Format output based on mode
82
+ const mode = getOutputMode(options);
83
+ const formatter = getFormatter(mode);
84
+ if (mode === 'json') {
85
+ return success(tasks);
86
+ }
87
+ if (mode === 'quiet') {
88
+ return success(tasks.map((t) => t.id).join('\n'));
89
+ }
90
+ // Human-readable output
91
+ if (tasks.length === 0) {
92
+ return success(null, 'No ready tasks found');
93
+ }
94
+ // Build table data
95
+ const headers = ['ID', 'TITLE', 'PRIORITY', 'ASSIGNEE', 'TYPE'];
96
+ const rows = tasks.map((task) => [
97
+ task.id,
98
+ task.title.length > 40 ? task.title.substring(0, 37) + '...' : task.title,
99
+ `P${task.priority}`,
100
+ task.assignee ?? '-',
101
+ task.taskType,
102
+ ]);
103
+ const table = formatter.table(headers, rows);
104
+ const summary = `\n${tasks.length} ready task(s)`;
105
+ return success(tasks, table + summary);
106
+ }
107
+ catch (err) {
108
+ const message = err instanceof Error ? err.message : String(err);
109
+ return failure(`Failed to get ready tasks: ${message}`, ExitCode.GENERAL_ERROR);
110
+ }
111
+ }
112
+ export const readyCommand = {
113
+ name: 'ready',
114
+ description: 'List tasks ready for work',
115
+ usage: 'sf ready [options]',
116
+ help: `List tasks that are ready for work.
117
+
118
+ Ready tasks are:
119
+ - Status is 'open' or 'in_progress'
120
+ - Not blocked by any dependency
121
+ - scheduledFor is null or in the past
122
+
123
+ Options:
124
+ -a, --assignee <id> Filter by assignee entity ID
125
+ -p, --priority <1-5> Filter by priority
126
+ -t, --type <type> Filter by task type (bug, feature, task, chore)
127
+ -l, --limit <n> Maximum number of results
128
+
129
+ Examples:
130
+ sf ready
131
+ sf ready --assignee alice
132
+ sf ready --priority 1
133
+ sf ready -a alice -p 1 -l 10`,
134
+ options: readyOptions,
135
+ handler: readyHandler,
136
+ };
137
+ const backlogOptions = [
138
+ {
139
+ name: 'priority',
140
+ short: 'p',
141
+ description: 'Filter by priority (1-5)',
142
+ hasValue: true,
143
+ },
144
+ {
145
+ name: 'limit',
146
+ short: 'l',
147
+ description: 'Maximum number of results',
148
+ hasValue: true,
149
+ },
150
+ ];
151
+ async function backlogHandler(_args, options) {
152
+ const { api, error } = createAPI(options);
153
+ if (error) {
154
+ return failure(error, ExitCode.GENERAL_ERROR);
155
+ }
156
+ try {
157
+ const filter = {};
158
+ if (options.priority) {
159
+ const priority = parseInt(options.priority, 10);
160
+ if (isNaN(priority) || priority < 1 || priority > 5) {
161
+ return failure('Priority must be a number from 1 to 5', ExitCode.VALIDATION);
162
+ }
163
+ filter.priority = priority;
164
+ }
165
+ if (options.limit) {
166
+ const limit = parseInt(options.limit, 10);
167
+ if (isNaN(limit) || limit < 1) {
168
+ return failure('Limit must be a positive number', ExitCode.VALIDATION);
169
+ }
170
+ filter.limit = limit;
171
+ }
172
+ const tasks = await api.backlog(filter);
173
+ const mode = getOutputMode(options);
174
+ const formatter = getFormatter(mode);
175
+ if (mode === 'json') {
176
+ return success(tasks);
177
+ }
178
+ if (mode === 'quiet') {
179
+ return success(tasks.map((t) => t.id).join('\n'));
180
+ }
181
+ if (tasks.length === 0) {
182
+ return success(null, 'No backlog tasks found');
183
+ }
184
+ const headers = ['ID', 'TITLE', 'PRIORITY', 'TYPE', 'CREATED'];
185
+ const rows = tasks.map((task) => [
186
+ task.id,
187
+ task.title.length > 40 ? task.title.substring(0, 37) + '...' : task.title,
188
+ `P${task.priority}`,
189
+ task.taskType,
190
+ new Date(task.createdAt).toLocaleDateString(),
191
+ ]);
192
+ const table = formatter.table(headers, rows);
193
+ const summary = `\n${tasks.length} backlog task(s)`;
194
+ return success(tasks, table + summary);
195
+ }
196
+ catch (err) {
197
+ const message = err instanceof Error ? err.message : String(err);
198
+ return failure(`Failed to get backlog tasks: ${message}`, ExitCode.GENERAL_ERROR);
199
+ }
200
+ }
201
+ export const backlogCommand = {
202
+ name: 'backlog',
203
+ description: 'List tasks in backlog',
204
+ usage: 'sf backlog [options]',
205
+ help: `List tasks in backlog (not ready for work, needs triage).
206
+
207
+ Backlog tasks are excluded from ready() and won't be auto-dispatched.
208
+
209
+ Options:
210
+ -p, --priority <1-5> Filter by priority
211
+ -l, --limit <n> Maximum number of results
212
+
213
+ Examples:
214
+ sf backlog
215
+ sf backlog --priority 1
216
+ sf backlog -l 10`,
217
+ options: backlogOptions,
218
+ handler: backlogHandler,
219
+ };
220
+ const blockedOptions = [
221
+ {
222
+ name: 'assignee',
223
+ short: 'a',
224
+ description: 'Filter by assignee',
225
+ hasValue: true,
226
+ },
227
+ {
228
+ name: 'priority',
229
+ short: 'p',
230
+ description: 'Filter by priority (1-5)',
231
+ hasValue: true,
232
+ },
233
+ {
234
+ name: 'limit',
235
+ short: 'l',
236
+ description: 'Maximum number of results',
237
+ hasValue: true,
238
+ },
239
+ ];
240
+ async function blockedHandler(_args, options) {
241
+ const { api, error } = createAPI(options);
242
+ if (error) {
243
+ return failure(error, ExitCode.GENERAL_ERROR);
244
+ }
245
+ try {
246
+ // Build filter from options
247
+ const filter = {};
248
+ if (options.assignee) {
249
+ filter.assignee = options.assignee;
250
+ }
251
+ if (options.priority) {
252
+ const priority = parseInt(options.priority, 10);
253
+ if (isNaN(priority) || priority < 1 || priority > 5) {
254
+ return failure('Priority must be a number from 1 to 5', ExitCode.VALIDATION);
255
+ }
256
+ filter.priority = priority;
257
+ }
258
+ if (options.limit) {
259
+ const limit = parseInt(options.limit, 10);
260
+ if (isNaN(limit) || limit < 1) {
261
+ return failure('Limit must be a positive number', ExitCode.VALIDATION);
262
+ }
263
+ filter.limit = limit;
264
+ }
265
+ // Get blocked tasks
266
+ const tasks = await api.blocked(filter);
267
+ // Format output based on mode
268
+ const mode = getOutputMode(options);
269
+ const formatter = getFormatter(mode);
270
+ if (mode === 'json') {
271
+ return success(tasks);
272
+ }
273
+ if (mode === 'quiet') {
274
+ return success(tasks.map((t) => t.id).join('\n'));
275
+ }
276
+ // Human-readable output
277
+ if (tasks.length === 0) {
278
+ return success(null, 'No blocked tasks found');
279
+ }
280
+ // Build table data
281
+ const headers = ['ID', 'TITLE', 'BLOCKED BY', 'REASON'];
282
+ const rows = tasks.map((task) => [
283
+ task.id,
284
+ task.title.length > 30 ? task.title.substring(0, 27) + '...' : task.title,
285
+ task.blockedBy,
286
+ task.blockReason.length > 30 ? task.blockReason.substring(0, 27) + '...' : task.blockReason,
287
+ ]);
288
+ const table = formatter.table(headers, rows);
289
+ const summary = `\n${tasks.length} blocked task(s)`;
290
+ return success(tasks, table + summary);
291
+ }
292
+ catch (err) {
293
+ const message = err instanceof Error ? err.message : String(err);
294
+ return failure(`Failed to get blocked tasks: ${message}`, ExitCode.GENERAL_ERROR);
295
+ }
296
+ }
297
+ export const blockedCommand = {
298
+ name: 'blocked',
299
+ description: 'List blocked tasks with reasons',
300
+ usage: 'sf blocked [options]',
301
+ help: `List tasks that are blocked with blocking details.
302
+
303
+ Options:
304
+ -a, --assignee <id> Filter by assignee entity ID
305
+ -p, --priority <1-5> Filter by priority
306
+ -l, --limit <n> Maximum number of results
307
+
308
+ Examples:
309
+ sf blocked
310
+ sf blocked --assignee alice
311
+ sf blocked --json`,
312
+ options: blockedOptions,
313
+ handler: blockedHandler,
314
+ };
315
+ const closeOptions = [
316
+ {
317
+ name: 'reason',
318
+ short: 'r',
319
+ description: 'Close reason',
320
+ hasValue: true,
321
+ },
322
+ ];
323
+ async function closeHandler(args, options) {
324
+ const [id] = args;
325
+ if (!id) {
326
+ return failure('Usage: sf task close <id> [--reason "reason"]', ExitCode.INVALID_ARGUMENTS);
327
+ }
328
+ const { api, error } = createAPI(options);
329
+ if (error) {
330
+ return failure(error, ExitCode.GENERAL_ERROR);
331
+ }
332
+ try {
333
+ // Get the task
334
+ const task = await api.get(id);
335
+ if (!task) {
336
+ return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
337
+ }
338
+ if (task.type !== 'task') {
339
+ return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
340
+ }
341
+ // Check if already closed
342
+ if (task.status === TaskStatus.CLOSED) {
343
+ return failure(`Task is already closed: ${id}`, ExitCode.VALIDATION);
344
+ }
345
+ // Check if transition is valid
346
+ if (!isValidStatusTransition(task.status, TaskStatus.CLOSED)) {
347
+ return failure(`Cannot close task with status '${task.status}'`, ExitCode.VALIDATION);
348
+ }
349
+ // Update the task
350
+ const updated = updateTaskStatus(task, {
351
+ status: TaskStatus.CLOSED,
352
+ closeReason: options.reason,
353
+ });
354
+ // Save the update with optimistic concurrency control
355
+ await api.update(id, updated, {
356
+ expectedUpdatedAt: task.updatedAt,
357
+ });
358
+ // Format output based on mode
359
+ const mode = getOutputMode(options);
360
+ if (mode === 'json') {
361
+ return success(updated);
362
+ }
363
+ if (mode === 'quiet') {
364
+ return success(updated.id);
365
+ }
366
+ return success(updated, `Closed task ${id}`);
367
+ }
368
+ catch (err) {
369
+ const message = err instanceof Error ? err.message : String(err);
370
+ return failure(`Failed to close task: ${message}`, ExitCode.GENERAL_ERROR);
371
+ }
372
+ }
373
+ export const closeCommand = {
374
+ name: 'close',
375
+ description: 'Close a task',
376
+ usage: 'sf close <id> [options]',
377
+ help: `Close a task, marking it as completed.
378
+
379
+ Arguments:
380
+ id Task identifier (e.g., el-abc123)
381
+
382
+ Options:
383
+ -r, --reason <text> Close reason
384
+
385
+ Examples:
386
+ sf close el-abc123
387
+ sf close el-abc123 --reason "Fixed in PR #42"`,
388
+ options: closeOptions,
389
+ handler: closeHandler,
390
+ };
391
+ const reopenOptions = [
392
+ {
393
+ name: 'message',
394
+ short: 'm',
395
+ description: 'Message to append to the task description explaining why it was reopened',
396
+ hasValue: true,
397
+ },
398
+ ];
399
+ async function reopenHandler(args, options) {
400
+ const [id] = args;
401
+ if (!id) {
402
+ return failure('Usage: sf task reopen <id> [--message "reason"]', ExitCode.INVALID_ARGUMENTS);
403
+ }
404
+ const { api, error } = createAPI(options);
405
+ if (error) {
406
+ return failure(error, ExitCode.GENERAL_ERROR);
407
+ }
408
+ try {
409
+ // Get the task
410
+ const task = await api.get(id);
411
+ if (!task) {
412
+ return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
413
+ }
414
+ if (task.type !== 'task') {
415
+ return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
416
+ }
417
+ // Check if not closed
418
+ if (task.status !== TaskStatus.CLOSED) {
419
+ return failure(`Task is not closed (status: ${task.status})`, ExitCode.VALIDATION);
420
+ }
421
+ // Update status to OPEN (clears closedAt)
422
+ const updated = updateTaskStatus(task, {
423
+ status: TaskStatus.OPEN,
424
+ });
425
+ // Clear assignee and closeReason
426
+ updated.assignee = undefined;
427
+ updated.closeReason = undefined;
428
+ // Clear orchestrator metadata fields while preserving branch/worktree/handoff info
429
+ const orchestratorMeta = updated.metadata?.orchestrator;
430
+ if (orchestratorMeta) {
431
+ const reconciliationCount = orchestratorMeta.reconciliationCount ?? 0;
432
+ const clearedMeta = {
433
+ ...orchestratorMeta,
434
+ mergeStatus: undefined,
435
+ mergedAt: undefined,
436
+ mergeFailureReason: undefined,
437
+ assignedAgent: undefined,
438
+ sessionId: undefined,
439
+ startedAt: undefined,
440
+ completedAt: undefined,
441
+ completionSummary: undefined,
442
+ lastCommitHash: undefined,
443
+ testRunCount: undefined,
444
+ lastTestResult: undefined,
445
+ lastSyncResult: undefined,
446
+ reconciliationCount: reconciliationCount + 1,
447
+ };
448
+ updated.metadata = {
449
+ ...updated.metadata,
450
+ orchestrator: clearedMeta,
451
+ };
452
+ }
453
+ // Save the update with optimistic concurrency control
454
+ await api.update(id, updated, {
455
+ expectedUpdatedAt: task.updatedAt,
456
+ });
457
+ // If message provided, append to or create description document
458
+ if (options.message) {
459
+ const reopenLine = `**Re-opened** — Task was closed but incomplete. Message: ${options.message}`;
460
+ if (task.descriptionRef) {
461
+ try {
462
+ const doc = await api.get(task.descriptionRef);
463
+ if (doc) {
464
+ await api.update(task.descriptionRef, {
465
+ content: doc.content + '\n\n' + reopenLine,
466
+ });
467
+ }
468
+ }
469
+ catch {
470
+ // Non-fatal: message is still shown in output
471
+ }
472
+ }
473
+ else {
474
+ const actor = options.actor;
475
+ const newDoc = await createDocument({
476
+ content: reopenLine,
477
+ contentType: ContentType.MARKDOWN,
478
+ createdBy: actor ?? 'operator',
479
+ });
480
+ const created = await api.create(newDoc);
481
+ await api.update(id, { descriptionRef: created.id }, { actor });
482
+ }
483
+ }
484
+ // Re-fetch task to get latest state (including any descriptionRef changes)
485
+ const finalTask = await api.get(id);
486
+ // Format output based on mode
487
+ const mode = getOutputMode(options);
488
+ if (mode === 'json') {
489
+ return success(finalTask ?? updated);
490
+ }
491
+ if (mode === 'quiet') {
492
+ return success(updated.id);
493
+ }
494
+ return success(finalTask ?? updated, `Reopened task ${id}`);
495
+ }
496
+ catch (err) {
497
+ const message = err instanceof Error ? err.message : String(err);
498
+ return failure(`Failed to reopen task: ${message}`, ExitCode.GENERAL_ERROR);
499
+ }
500
+ }
501
+ export const reopenCommand = {
502
+ name: 'reopen',
503
+ description: 'Reopen a closed task',
504
+ usage: 'sf reopen <id> [--message "reason"]',
505
+ help: `Reopen a previously closed task, clearing assignment and merge metadata.
506
+
507
+ Arguments:
508
+ id Task identifier (e.g., el-abc123)
509
+
510
+ Options:
511
+ -m, --message Message to append to the task description
512
+
513
+ Examples:
514
+ sf reopen el-abc123
515
+ sf reopen el-abc123 --message "Work was incomplete, needs fixes"`,
516
+ options: reopenOptions,
517
+ handler: reopenHandler,
518
+ };
519
+ const assignOptions = [
520
+ {
521
+ name: 'unassign',
522
+ short: 'u',
523
+ description: 'Remove assignment',
524
+ },
525
+ ];
526
+ async function assignHandler(args, options) {
527
+ const [id, assignee] = args;
528
+ if (!id) {
529
+ return failure('Usage: sf task assign <id> [assignee] [--unassign]', ExitCode.INVALID_ARGUMENTS);
530
+ }
531
+ if (!assignee && !options.unassign) {
532
+ return failure('Specify an assignee or use --unassign to remove assignment', ExitCode.INVALID_ARGUMENTS);
533
+ }
534
+ const { api, error } = createAPI(options);
535
+ if (error) {
536
+ return failure(error, ExitCode.GENERAL_ERROR);
537
+ }
538
+ try {
539
+ // Get the task
540
+ const task = await api.get(id);
541
+ if (!task) {
542
+ return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
543
+ }
544
+ if (task.type !== 'task') {
545
+ return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
546
+ }
547
+ // Update assignment
548
+ const updates = {
549
+ assignee: options.unassign ? undefined : assignee,
550
+ };
551
+ // Save the update with optimistic concurrency control
552
+ const updated = await api.update(id, updates, {
553
+ expectedUpdatedAt: task.updatedAt,
554
+ });
555
+ // Format output based on mode
556
+ const mode = getOutputMode(options);
557
+ if (mode === 'json') {
558
+ return success(updated);
559
+ }
560
+ if (mode === 'quiet') {
561
+ return success(updated.id);
562
+ }
563
+ const message = options.unassign
564
+ ? `Unassigned task ${id}`
565
+ : `Assigned task ${id} to ${assignee}`;
566
+ return success(updated, message);
567
+ }
568
+ catch (err) {
569
+ const message = err instanceof Error ? err.message : String(err);
570
+ return failure(`Failed to assign task: ${message}`, ExitCode.GENERAL_ERROR);
571
+ }
572
+ }
573
+ export const assignCommand = {
574
+ name: 'assign',
575
+ description: 'Assign a task to an entity',
576
+ usage: 'sf assign <id> [assignee]',
577
+ help: `Assign a task to an entity.
578
+
579
+ Arguments:
580
+ id Task identifier (e.g., el-abc123)
581
+ assignee Entity to assign to
582
+
583
+ Options:
584
+ -u, --unassign Remove assignment
585
+
586
+ Examples:
587
+ sf assign el-abc123 alice
588
+ sf assign el-abc123 --unassign`,
589
+ options: assignOptions,
590
+ handler: assignHandler,
591
+ };
592
+ const deferOptions = [
593
+ {
594
+ name: 'until',
595
+ description: 'Schedule for date (ISO format)',
596
+ hasValue: true,
597
+ },
598
+ ];
599
+ async function deferHandler(args, options) {
600
+ const [id] = args;
601
+ if (!id) {
602
+ return failure('Usage: sf task defer <id> [--until date]', ExitCode.INVALID_ARGUMENTS);
603
+ }
604
+ const { api, error } = createAPI(options);
605
+ if (error) {
606
+ return failure(error, ExitCode.GENERAL_ERROR);
607
+ }
608
+ try {
609
+ // Get the task
610
+ const task = await api.get(id);
611
+ if (!task) {
612
+ return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
613
+ }
614
+ if (task.type !== 'task') {
615
+ return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
616
+ }
617
+ // Check if transition is valid
618
+ if (!isValidStatusTransition(task.status, TaskStatus.DEFERRED)) {
619
+ return failure(`Cannot defer task with status '${task.status}'`, ExitCode.VALIDATION);
620
+ }
621
+ // Parse until date if provided
622
+ let scheduledFor;
623
+ if (options.until) {
624
+ const date = new Date(options.until);
625
+ if (isNaN(date.getTime())) {
626
+ return failure(`Invalid date format: ${options.until}`, ExitCode.VALIDATION);
627
+ }
628
+ scheduledFor = date.toISOString();
629
+ }
630
+ // Update the task
631
+ const updated = updateTaskStatus(task, {
632
+ status: TaskStatus.DEFERRED,
633
+ });
634
+ // Add scheduledFor if provided
635
+ if (scheduledFor) {
636
+ updated.scheduledFor = scheduledFor;
637
+ }
638
+ // Save the update with optimistic concurrency control
639
+ await api.update(id, updated, {
640
+ expectedUpdatedAt: task.updatedAt,
641
+ });
642
+ // Format output based on mode
643
+ const mode = getOutputMode(options);
644
+ if (mode === 'json') {
645
+ return success(updated);
646
+ }
647
+ if (mode === 'quiet') {
648
+ return success(updated.id);
649
+ }
650
+ const message = scheduledFor
651
+ ? `Deferred task ${id} until ${new Date(scheduledFor).toLocaleDateString()}`
652
+ : `Deferred task ${id}`;
653
+ return success(updated, message);
654
+ }
655
+ catch (err) {
656
+ const message = err instanceof Error ? err.message : String(err);
657
+ return failure(`Failed to defer task: ${message}`, ExitCode.GENERAL_ERROR);
658
+ }
659
+ }
660
+ export const deferCommand = {
661
+ name: 'defer',
662
+ description: 'Defer a task',
663
+ usage: 'sf defer <id> [options]',
664
+ help: `Defer a task, putting it on hold.
665
+
666
+ Arguments:
667
+ id Task identifier (e.g., el-abc123)
668
+
669
+ Options:
670
+ --until <date> Schedule for a specific date (ISO format)
671
+
672
+ Examples:
673
+ sf defer el-abc123
674
+ sf defer el-abc123 --until 2024-03-01`,
675
+ options: deferOptions,
676
+ handler: deferHandler,
677
+ };
678
+ // ============================================================================
679
+ // Undefer Command
680
+ // ============================================================================
681
+ async function undeferHandler(args, options) {
682
+ const [id] = args;
683
+ if (!id) {
684
+ return failure('Usage: sf task undefer <id>', ExitCode.INVALID_ARGUMENTS);
685
+ }
686
+ const { api, error } = createAPI(options);
687
+ if (error) {
688
+ return failure(error, ExitCode.GENERAL_ERROR);
689
+ }
690
+ try {
691
+ // Get the task
692
+ const task = await api.get(id);
693
+ if (!task) {
694
+ return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
695
+ }
696
+ if (task.type !== 'task') {
697
+ return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
698
+ }
699
+ // Check if deferred
700
+ if (task.status !== TaskStatus.DEFERRED) {
701
+ return failure(`Task is not deferred (status: ${task.status})`, ExitCode.VALIDATION);
702
+ }
703
+ // Update the task - reopen it
704
+ const updated = updateTaskStatus(task, {
705
+ status: TaskStatus.OPEN,
706
+ });
707
+ // Clear scheduledFor
708
+ updated.scheduledFor = undefined;
709
+ // Save the update with optimistic concurrency control
710
+ await api.update(id, updated, {
711
+ expectedUpdatedAt: task.updatedAt,
712
+ });
713
+ // Format output based on mode
714
+ const mode = getOutputMode(options);
715
+ if (mode === 'json') {
716
+ return success(updated);
717
+ }
718
+ if (mode === 'quiet') {
719
+ return success(updated.id);
720
+ }
721
+ return success(updated, `Undeferred task ${id}`);
722
+ }
723
+ catch (err) {
724
+ const message = err instanceof Error ? err.message : String(err);
725
+ return failure(`Failed to undefer task: ${message}`, ExitCode.GENERAL_ERROR);
726
+ }
727
+ }
728
+ export const undeferCommand = {
729
+ name: 'undefer',
730
+ description: 'Remove deferral from a task',
731
+ usage: 'sf undefer <id>',
732
+ help: `Remove deferral from a task, making it ready for work again.
733
+
734
+ Arguments:
735
+ id Task identifier (e.g., el-abc123)
736
+
737
+ Examples:
738
+ sf undefer el-abc123`,
739
+ options: [],
740
+ handler: undeferHandler,
741
+ };
742
+ const describeOptions = [
743
+ {
744
+ name: 'content',
745
+ short: 'c',
746
+ description: 'Description content (text)',
747
+ hasValue: true,
748
+ },
749
+ {
750
+ name: 'file',
751
+ short: 'f',
752
+ description: 'Read description from file',
753
+ hasValue: true,
754
+ },
755
+ {
756
+ name: 'show',
757
+ short: 's',
758
+ description: 'Show current description instead of setting it',
759
+ },
760
+ {
761
+ name: 'append',
762
+ description: 'Append to existing description instead of replacing',
763
+ },
764
+ ];
765
+ async function describeHandler(args, options) {
766
+ const [id] = args;
767
+ if (!id) {
768
+ return failure('Usage: sf task describe <id> --content <text> | --file <path> | --show', ExitCode.INVALID_ARGUMENTS);
769
+ }
770
+ const { api, error } = createAPI(options);
771
+ if (error) {
772
+ return failure(error, ExitCode.GENERAL_ERROR);
773
+ }
774
+ try {
775
+ // Get the task
776
+ const task = await api.get(id);
777
+ if (!task) {
778
+ return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
779
+ }
780
+ if (task.type !== 'task') {
781
+ return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
782
+ }
783
+ // Show mode - display current description
784
+ if (options.show) {
785
+ const mode = getOutputMode(options);
786
+ if (!task.descriptionRef) {
787
+ if (mode === 'json') {
788
+ return success({ taskId: id, description: null });
789
+ }
790
+ return success(null, `Task ${id} has no description`);
791
+ }
792
+ // Get the description document
793
+ const doc = await api.get(task.descriptionRef);
794
+ if (!doc) {
795
+ return failure(`Description document not found: ${task.descriptionRef}`, ExitCode.NOT_FOUND);
796
+ }
797
+ if (mode === 'json') {
798
+ return success({ taskId: id, descriptionRef: task.descriptionRef, content: doc.content });
799
+ }
800
+ if (mode === 'quiet') {
801
+ return success(doc.content);
802
+ }
803
+ return success(doc, `Description for task ${id}:\n\n${doc.content}`);
804
+ }
805
+ // Set mode - must specify either --content or --file
806
+ if (!options.content && !options.file) {
807
+ return failure('Either --content, --file, or --show is required', ExitCode.INVALID_ARGUMENTS);
808
+ }
809
+ if (options.content && options.file) {
810
+ return failure('Cannot specify both --content and --file', ExitCode.INVALID_ARGUMENTS);
811
+ }
812
+ // Get new content
813
+ let content;
814
+ if (options.content) {
815
+ content = options.content;
816
+ }
817
+ else {
818
+ const filePath = resolve(options.file);
819
+ if (!fileExists(filePath)) {
820
+ return failure(`File not found: ${filePath}`, ExitCode.NOT_FOUND);
821
+ }
822
+ content = readFileSync(filePath, 'utf-8');
823
+ }
824
+ const actor = options.actor;
825
+ // Check if task already has a description document
826
+ if (task.descriptionRef) {
827
+ let finalContent = content;
828
+ // If appending, fetch existing content and combine
829
+ if (options.append) {
830
+ const existingDoc = await api.get(task.descriptionRef);
831
+ if (existingDoc) {
832
+ finalContent = existingDoc.content + '\n\n' + content;
833
+ }
834
+ }
835
+ // Update existing document
836
+ const updated = await api.update(task.descriptionRef, { content: finalContent }, { actor });
837
+ const mode = getOutputMode(options);
838
+ if (mode === 'json') {
839
+ return success({ taskId: id, descriptionRef: task.descriptionRef, document: updated, appended: options.append ?? false });
840
+ }
841
+ if (mode === 'quiet') {
842
+ return success(task.descriptionRef);
843
+ }
844
+ const action = options.append ? 'Appended to' : 'Updated';
845
+ return success(updated, `${action} description for task ${id} (document ${task.descriptionRef}, version ${updated.version})`);
846
+ }
847
+ else {
848
+ // Create new description document
849
+ const docInput = {
850
+ content,
851
+ contentType: ContentType.MARKDOWN,
852
+ createdBy: actor ?? 'operator',
853
+ };
854
+ const newDoc = await createDocument(docInput);
855
+ const created = await api.create(newDoc);
856
+ // Update task with description reference
857
+ await api.update(id, { descriptionRef: created.id }, { actor, expectedUpdatedAt: task.updatedAt });
858
+ const mode = getOutputMode(options);
859
+ if (mode === 'json') {
860
+ return success({ taskId: id, descriptionRef: created.id, document: created });
861
+ }
862
+ if (mode === 'quiet') {
863
+ return success(created.id);
864
+ }
865
+ return success(created, `Created description for task ${id} (document ${created.id})`);
866
+ }
867
+ }
868
+ catch (err) {
869
+ const message = err instanceof Error ? err.message : String(err);
870
+ return failure(`Failed to update task description: ${message}`, ExitCode.GENERAL_ERROR);
871
+ }
872
+ }
873
+ export const describeCommand = {
874
+ name: 'describe',
875
+ description: 'Set or show task description',
876
+ usage: 'sf task describe <id> --content <text> | --file <path> | --show',
877
+ help: `Set or show the description for a task.
878
+
879
+ Task descriptions are stored as separate versioned documents. If the task
880
+ already has a description, it will be updated (creating a new version).
881
+ If not, a new document will be created and linked to the task.
882
+
883
+ Arguments:
884
+ id Task identifier (e.g., el-abc123)
885
+
886
+ Options:
887
+ -c, --content <text> Description content (inline)
888
+ -f, --file <path> Read description from file
889
+ -s, --show Show current description instead of setting it
890
+ --append Append to existing description instead of replacing
891
+
892
+ Examples:
893
+ sf task describe el-abc123 --content "Implement the login feature"
894
+ sf task describe el-abc123 --file description.md
895
+ sf task describe el-abc123 --show
896
+ sf task describe el-abc123 --append --content "Additional notes"`,
897
+ options: describeOptions,
898
+ handler: describeHandler,
899
+ };
900
+ // ============================================================================
901
+ // Activate Command
902
+ // ============================================================================
903
+ async function activateHandler(args, options) {
904
+ const [id] = args;
905
+ if (!id) {
906
+ return failure('Usage: sf task activate <id>', ExitCode.INVALID_ARGUMENTS);
907
+ }
908
+ const { api, error } = createAPI(options);
909
+ if (error) {
910
+ return failure(error, ExitCode.GENERAL_ERROR);
911
+ }
912
+ try {
913
+ const task = await api.get(id);
914
+ if (!task) {
915
+ return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
916
+ }
917
+ if (task.type !== 'task') {
918
+ return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
919
+ }
920
+ if (task.status !== TaskStatus.BACKLOG) {
921
+ return failure(`Task is not in backlog (status: ${task.status})`, ExitCode.VALIDATION);
922
+ }
923
+ const updated = updateTaskStatus(task, {
924
+ status: TaskStatus.OPEN,
925
+ });
926
+ await api.update(id, updated, {
927
+ expectedUpdatedAt: task.updatedAt,
928
+ });
929
+ const mode = getOutputMode(options);
930
+ if (mode === 'json') {
931
+ return success(updated);
932
+ }
933
+ if (mode === 'quiet') {
934
+ return success(updated.id);
935
+ }
936
+ return success(updated, `Activated task ${id} (moved from backlog to open)`);
937
+ }
938
+ catch (err) {
939
+ const message = err instanceof Error ? err.message : String(err);
940
+ return failure(`Failed to activate task: ${message}`, ExitCode.GENERAL_ERROR);
941
+ }
942
+ }
943
+ export const activateCommand = {
944
+ name: 'activate',
945
+ description: 'Move a task from backlog to open',
946
+ usage: 'sf task activate <id>',
947
+ help: `Move a task from backlog status to open status.
948
+
949
+ Arguments:
950
+ id Task identifier
951
+
952
+ Examples:
953
+ sf task activate el-abc123`,
954
+ options: [],
955
+ handler: activateHandler,
956
+ };
957
+ // ============================================================================
958
+ // CRUD Wrapper Commands (delegate to crud.ts handlers with 'task' pre-filled)
959
+ // ============================================================================
960
+ const taskCreateCommand = {
961
+ name: 'create',
962
+ description: 'Create a new task',
963
+ usage: 'sf task create [options]',
964
+ help: `Create a new task.
965
+
966
+ Options:
967
+ -t, --title <text> Task title (required)
968
+ -d, --description <text> Task description (creates a linked document)
969
+ -p, --priority <1-5> Priority (1=critical, 5=minimal, default=3)
970
+ -c, --complexity <1-5> Complexity (1=trivial, 5=very complex, default=3)
971
+ --type <type> Task type: bug, feature, task, chore
972
+ -a, --assignee <id> Assignee entity ID
973
+ --tag <tag> Add a tag (can be repeated)
974
+ --plan <id|name> Plan to attach this task to
975
+
976
+ Examples:
977
+ sf task create --title "Fix login bug" --priority 1 --type bug
978
+ sf task create -t "Add dark mode" --tag ui --tag feature
979
+ sf task create -t "Implement feature X" --plan "My Plan"`,
980
+ options: createOptions,
981
+ handler: ((args, options) => createHandler(['task', ...args], options)),
982
+ };
983
+ const taskListCommand = {
984
+ name: 'list',
985
+ description: 'List tasks',
986
+ usage: 'sf task list [options]',
987
+ help: `List tasks with optional filtering.
988
+
989
+ Options:
990
+ -s, --status <status> Filter by status
991
+ -p, --priority <1-5> Filter by priority
992
+ -a, --assignee <id> Filter by assignee
993
+ --tag <tag> Filter by tag (can be repeated)
994
+ -l, --limit <n> Maximum results (default: 50)
995
+ -o, --offset <n> Skip first n results
996
+
997
+ Examples:
998
+ sf task list
999
+ sf task list --status open
1000
+ sf task list --priority 1 --status in_progress`,
1001
+ options: listOptions,
1002
+ handler: ((args, options) => listHandler(['task', ...args], options)),
1003
+ };
1004
+ const taskShowCommand = {
1005
+ name: 'show',
1006
+ description: 'Show task details',
1007
+ usage: 'sf task show <id> [options]',
1008
+ help: `Show detailed information about a task.
1009
+
1010
+ Arguments:
1011
+ id Task identifier (e.g., el-abc123)
1012
+
1013
+ Options:
1014
+ -e, --events Include recent events/history
1015
+ --events-limit <n> Maximum events to show (default: 10)
1016
+
1017
+ Examples:
1018
+ sf task show el-abc123
1019
+ sf task show el-abc123 --events`,
1020
+ options: showOptions,
1021
+ handler: showHandler,
1022
+ };
1023
+ const taskUpdateCommand = {
1024
+ name: 'update',
1025
+ description: 'Update a task',
1026
+ usage: 'sf task update <id> [options]',
1027
+ help: `Update fields on an existing task.
1028
+
1029
+ Arguments:
1030
+ id Task identifier (e.g., el-abc123)
1031
+
1032
+ Options:
1033
+ -t, --title <text> New title
1034
+ -p, --priority <1-5> New priority
1035
+ -c, --complexity <1-5> New complexity
1036
+ -s, --status <status> New status (open, in_progress, closed, deferred)
1037
+ -a, --assignee <id> New assignee (empty string to unassign)
1038
+ --tag <tag> Replace all tags
1039
+ --add-tag <tag> Add a tag
1040
+ --remove-tag <tag> Remove a tag
1041
+
1042
+ Examples:
1043
+ sf task update el-abc123 --title "New Title"
1044
+ sf task update el-abc123 --priority 1 --status in_progress`,
1045
+ options: updateOptions,
1046
+ handler: updateHandler,
1047
+ };
1048
+ const taskDeleteCommand = {
1049
+ name: 'delete',
1050
+ description: 'Delete a task',
1051
+ usage: 'sf task delete <id> [options]',
1052
+ help: `Soft-delete a task.
1053
+
1054
+ Arguments:
1055
+ id Task identifier (e.g., el-abc123)
1056
+
1057
+ Options:
1058
+ -r, --reason <text> Deletion reason
1059
+ -f, --force Skip confirmation
1060
+
1061
+ Examples:
1062
+ sf task delete el-abc123
1063
+ sf task delete el-abc123 --reason "Duplicate entry"`,
1064
+ options: deleteOptions,
1065
+ handler: deleteHandler,
1066
+ };
1067
+ // ============================================================================
1068
+ // Task Root Command
1069
+ // ============================================================================
1070
+ const allTaskSubcommands = {
1071
+ // CRUD
1072
+ create: taskCreateCommand,
1073
+ list: taskListCommand,
1074
+ show: taskShowCommand,
1075
+ update: taskUpdateCommand,
1076
+ delete: taskDeleteCommand,
1077
+ // Status
1078
+ ready: readyCommand,
1079
+ blocked: blockedCommand,
1080
+ backlog: backlogCommand,
1081
+ close: closeCommand,
1082
+ reopen: reopenCommand,
1083
+ // Assignment
1084
+ assign: assignCommand,
1085
+ // Scheduling
1086
+ defer: deferCommand,
1087
+ undefer: undeferCommand,
1088
+ // Description
1089
+ describe: describeCommand,
1090
+ activate: activateCommand,
1091
+ // Aliases (hidden from --help via dedup in getCommandHelp)
1092
+ new: taskCreateCommand,
1093
+ add: taskCreateCommand,
1094
+ ls: taskListCommand,
1095
+ rm: taskDeleteCommand,
1096
+ get: taskShowCommand,
1097
+ view: taskShowCommand,
1098
+ edit: taskUpdateCommand,
1099
+ };
1100
+ export const taskCommand = {
1101
+ name: 'task',
1102
+ description: 'Task management',
1103
+ usage: 'sf task <subcommand> [options]',
1104
+ help: `Task management - create, list, and manage tasks.
1105
+
1106
+ CRUD:
1107
+ create Create a new task
1108
+ list List tasks
1109
+ show Show task details
1110
+ update Update a task
1111
+ delete Delete a task
1112
+
1113
+ Status:
1114
+ ready List tasks ready for work
1115
+ blocked List blocked tasks with reasons
1116
+ backlog List backlog tasks
1117
+ close Close a task
1118
+ reopen Reopen a closed task
1119
+ activate Move a task from backlog to open
1120
+
1121
+ Assignment:
1122
+ assign Assign a task to an entity
1123
+
1124
+ Scheduling:
1125
+ defer Defer a task
1126
+ undefer Remove deferral from a task
1127
+
1128
+ Description:
1129
+ describe Set or show task description
1130
+
1131
+ Examples:
1132
+ sf task create --title "Fix login bug" --priority 1
1133
+ sf task list --status open
1134
+ sf task ready
1135
+ sf task close el-abc123
1136
+ sf task describe el-abc123 --show`,
1137
+ subcommands: allTaskSubcommands,
1138
+ handler: async (args, _options) => {
1139
+ if (args.length === 0) {
1140
+ return failure('Usage: sf task <subcommand>. Use "sf task --help" for available subcommands.', ExitCode.INVALID_ARGUMENTS);
1141
+ }
1142
+ // Show "did you mean?" for unknown subcommands
1143
+ const subNames = Object.keys(allTaskSubcommands);
1144
+ const suggestions = suggestCommands(args[0], subNames);
1145
+ let msg = `Unknown subcommand: ${args[0]}`;
1146
+ if (suggestions.length > 0) {
1147
+ msg += `\n\nDid you mean?\n${suggestions.map(s => ` ${s}`).join('\n')}`;
1148
+ }
1149
+ msg += '\n\nRun "sf task --help" to see available subcommands.';
1150
+ return failure(msg, ExitCode.INVALID_ARGUMENTS);
1151
+ },
1152
+ };
1153
+ //# sourceMappingURL=task.js.map