tmux-ide 1.2.1 → 2.0.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 (205) hide show
  1. package/bin/cli.js +4 -216
  2. package/bin/cli.ts +536 -0
  3. package/package.json +33 -14
  4. package/src/__tests__/support.ts +186 -0
  5. package/src/{attach.js → attach.ts} +7 -4
  6. package/src/cli.test.ts +314 -0
  7. package/src/command-center/discovery.test.ts +265 -0
  8. package/src/command-center/discovery.ts +296 -0
  9. package/src/command-center/index.ts +31 -0
  10. package/src/command-center/schemas.ts +57 -0
  11. package/src/command-center/server.test.ts +818 -0
  12. package/src/command-center/server.ts +1426 -0
  13. package/src/config-cli.test.ts +86 -0
  14. package/src/config.test.ts +205 -0
  15. package/src/config.ts +313 -0
  16. package/src/detect.test.ts +142 -0
  17. package/src/{detect.js → detect.ts} +31 -15
  18. package/src/dispatch.ts +102 -0
  19. package/src/doctor.test.ts +119 -0
  20. package/src/doctor.ts +167 -0
  21. package/src/init.test.ts +101 -0
  22. package/src/init.ts +144 -0
  23. package/src/inspect.test.ts +67 -0
  24. package/src/{inspect.js → inspect.ts} +95 -10
  25. package/src/integration.test.ts.skip +276 -0
  26. package/src/js-yaml.d.ts +10 -0
  27. package/src/launch.test.ts +222 -0
  28. package/src/launch.ts +494 -0
  29. package/src/lib/auth/auth-service.test.ts +178 -0
  30. package/src/lib/auth/auth-service.ts +227 -0
  31. package/src/lib/auth/middleware.test.ts +119 -0
  32. package/src/lib/auth/middleware.ts +56 -0
  33. package/src/lib/auth/types.ts +13 -0
  34. package/src/lib/authorship.test.ts +242 -0
  35. package/src/lib/authorship.ts +280 -0
  36. package/src/lib/cast/recorder.test.ts +127 -0
  37. package/src/lib/cast/recorder.ts +148 -0
  38. package/src/lib/daemon-watchdog.ts +102 -0
  39. package/src/lib/daemon.ts +417 -0
  40. package/src/lib/dot-path.test.ts +66 -0
  41. package/src/lib/dot-path.ts +17 -0
  42. package/src/lib/errors.ts +44 -0
  43. package/src/lib/event-log.test.ts +189 -0
  44. package/src/lib/event-log.ts +237 -0
  45. package/src/lib/github-pr.ts +182 -0
  46. package/src/lib/hq/client.test.ts +154 -0
  47. package/src/lib/hq/client.ts +142 -0
  48. package/src/lib/hq/mdns.test.ts +54 -0
  49. package/src/lib/hq/mdns.ts +88 -0
  50. package/src/lib/hq/registry.test.ts +237 -0
  51. package/src/lib/hq/registry.ts +145 -0
  52. package/src/lib/hq/types.ts +44 -0
  53. package/src/lib/ipc/socket-protocol.test.ts +126 -0
  54. package/src/lib/ipc/socket-protocol.ts +324 -0
  55. package/src/lib/launch-plan.test.ts +119 -0
  56. package/src/lib/launch-plan.ts +90 -0
  57. package/src/lib/log.test.ts +39 -0
  58. package/src/lib/log.ts +71 -0
  59. package/src/lib/metrics.test.ts +213 -0
  60. package/src/lib/metrics.ts +345 -0
  61. package/src/lib/orchestrator.test.ts +2358 -0
  62. package/src/lib/orchestrator.ts +1792 -0
  63. package/src/lib/{output.js → output.ts} +18 -10
  64. package/src/lib/plan-store.test.ts +148 -0
  65. package/src/lib/plan-store.ts +131 -0
  66. package/src/lib/research.test.ts +208 -0
  67. package/src/lib/research.ts +475 -0
  68. package/src/lib/round-trip.test.ts +341 -0
  69. package/src/lib/session-monitor.test.ts +152 -0
  70. package/src/lib/session-monitor.ts +282 -0
  71. package/src/lib/session-options.test.ts +146 -0
  72. package/src/lib/{session-options.js → session-options.ts} +11 -6
  73. package/src/lib/shell.test.ts +20 -0
  74. package/src/lib/shell.ts +8 -0
  75. package/src/lib/sizes.test.ts +73 -0
  76. package/src/lib/{sizes.js → sizes.ts} +4 -4
  77. package/src/lib/skill-registry.test.ts +134 -0
  78. package/src/lib/skill-registry.ts +86 -0
  79. package/src/lib/slugify.test.ts +35 -0
  80. package/src/lib/slugify.ts +10 -0
  81. package/src/lib/task-store.ts +352 -0
  82. package/src/lib/tmux.test.ts +500 -0
  83. package/src/lib/tmux.ts +372 -0
  84. package/src/lib/token-tracker.test.ts +102 -0
  85. package/src/lib/token-tracker.ts +70 -0
  86. package/src/lib/tunnels/cloudflare.test.ts +73 -0
  87. package/src/lib/tunnels/cloudflare.ts +254 -0
  88. package/src/lib/tunnels/manager.test.ts +63 -0
  89. package/src/lib/tunnels/manager.ts +132 -0
  90. package/src/lib/tunnels/ngrok.test.ts +94 -0
  91. package/src/lib/tunnels/ngrok.ts +261 -0
  92. package/src/lib/tunnels/tailscale.test.ts +103 -0
  93. package/src/lib/tunnels/tailscale.ts +1048 -0
  94. package/src/lib/tunnels/types.ts +44 -0
  95. package/src/lib/validation.test.ts +289 -0
  96. package/src/lib/validation.ts +123 -0
  97. package/src/lib/webhook.ts +51 -0
  98. package/src/lib/workflow-store.ts +209 -0
  99. package/src/lib/ws-v3/protocol.test.ts +127 -0
  100. package/src/lib/ws-v3/protocol.ts +160 -0
  101. package/src/lib/yaml-io.test.ts +125 -0
  102. package/src/lib/{yaml-io.js → yaml-io.ts} +5 -4
  103. package/src/ls.test.ts +76 -0
  104. package/src/{ls.js → ls.ts} +4 -4
  105. package/src/metrics-cli.ts +177 -0
  106. package/src/notify.ts +85 -0
  107. package/src/orchestrator-status.ts +228 -0
  108. package/src/plan.ts +117 -0
  109. package/src/postinstall.test.ts +79 -0
  110. package/src/remote.test.ts +115 -0
  111. package/src/remote.ts +148 -0
  112. package/src/restart.ts +24 -0
  113. package/src/schemas/domain.ts +422 -0
  114. package/src/schemas/ide-config.ts +148 -0
  115. package/src/schemas/index.ts +53 -0
  116. package/src/send.test.ts +70 -0
  117. package/src/send.ts +181 -0
  118. package/src/skill.test.ts +114 -0
  119. package/src/skill.ts +138 -0
  120. package/src/status.ts +66 -0
  121. package/src/stop.test.ts +176 -0
  122. package/src/stop.ts +47 -0
  123. package/src/task.test.ts +1466 -0
  124. package/src/task.ts +1143 -0
  125. package/src/tunnel.test.ts +80 -0
  126. package/src/tunnel.ts +133 -0
  127. package/src/types.ts +15 -0
  128. package/src/ui/index.ts +32 -0
  129. package/src/ui/terminal/index.ts +9 -0
  130. package/src/ui/types.ts +91 -0
  131. package/src/ui/web/base.css +80 -0
  132. package/src/ui/web/components/Box.tsx +59 -0
  133. package/src/ui/web/components/Input.tsx +32 -0
  134. package/src/ui/web/components/ScrollBox.tsx +60 -0
  135. package/src/ui/web/components/Text.tsx +28 -0
  136. package/src/ui/web/hooks.ts +106 -0
  137. package/src/ui/web/index.ts +27 -0
  138. package/src/ui/web/render.ts +77 -0
  139. package/src/ui/web/utils/color.test.ts +93 -0
  140. package/src/ui/web/utils/color.ts +27 -0
  141. package/src/validate.test.ts +385 -0
  142. package/src/validate.ts +217 -0
  143. package/src/widgets/changes/index.tsx +576 -0
  144. package/src/widgets/config/index.tsx +440 -0
  145. package/src/widgets/costs/index.tsx +216 -0
  146. package/src/widgets/explorer/breadcrumbs.tsx +77 -0
  147. package/src/widgets/explorer/footer.tsx +20 -0
  148. package/src/widgets/explorer/header.tsx +23 -0
  149. package/src/widgets/explorer/index.tsx +408 -0
  150. package/src/widgets/explorer/tree-model.test.ts +214 -0
  151. package/src/widgets/explorer/tree-model.ts +103 -0
  152. package/src/widgets/explorer/tree.tsx +156 -0
  153. package/src/widgets/lib/config-model.ts +116 -0
  154. package/src/widgets/lib/files.test.ts +103 -0
  155. package/src/widgets/lib/files.ts +88 -0
  156. package/src/widgets/lib/git.test.ts +151 -0
  157. package/src/widgets/lib/git.ts +88 -0
  158. package/src/widgets/lib/pane-comms.test.ts +178 -0
  159. package/src/widgets/lib/pane-comms.ts +192 -0
  160. package/src/widgets/lib/theme.ts +151 -0
  161. package/src/widgets/lib/watcher.ts +50 -0
  162. package/src/widgets/mission-control/activity-feed.tsx +59 -0
  163. package/src/widgets/mission-control/agent-panel.tsx +132 -0
  164. package/src/widgets/mission-control/command-bar.tsx +63 -0
  165. package/src/widgets/mission-control/index.tsx +751 -0
  166. package/src/widgets/mission-control/task-panel.tsx +238 -0
  167. package/src/widgets/preview/index.tsx +415 -0
  168. package/src/widgets/resolve.ts +40 -0
  169. package/src/widgets/setup/agent-naming.tsx +112 -0
  170. package/src/widgets/setup/config-tree.tsx +238 -0
  171. package/src/widgets/setup/detect-panel.tsx +72 -0
  172. package/src/widgets/setup/field-editor.tsx +265 -0
  173. package/src/widgets/setup/footer.tsx +107 -0
  174. package/src/widgets/setup/index.tsx +340 -0
  175. package/src/widgets/setup/layout-picker.tsx +96 -0
  176. package/src/widgets/setup/orchestrator-panel.tsx +200 -0
  177. package/src/widgets/setup/review-panel.tsx +140 -0
  178. package/src/widgets/setup/setup-model.test.ts +303 -0
  179. package/src/widgets/setup/setup-model.ts +188 -0
  180. package/src/widgets/tasks/index.tsx +129 -0
  181. package/src/widgets/tasks/task-detail.tsx +309 -0
  182. package/src/widgets/tasks/task-form.tsx +193 -0
  183. package/src/widgets/tasks/task-list.tsx +205 -0
  184. package/src/widgets/tasks/task-model.test.ts +473 -0
  185. package/src/widgets/tasks/task-model.ts +157 -0
  186. package/templates/AGENTS.md +20 -0
  187. package/templates/missions.yml +41 -0
  188. package/templates/skills/backend.md +30 -0
  189. package/templates/skills/frontend.md +30 -0
  190. package/templates/skills/general-worker.md +30 -0
  191. package/templates/skills/researcher.md +69 -0
  192. package/templates/skills/reviewer.md +30 -0
  193. package/src/config.js +0 -341
  194. package/src/doctor.js +0 -91
  195. package/src/init.js +0 -73
  196. package/src/launch.js +0 -245
  197. package/src/lib/dot-path.js +0 -16
  198. package/src/lib/errors.js +0 -35
  199. package/src/lib/launch-plan.js +0 -49
  200. package/src/lib/session-monitor.js +0 -179
  201. package/src/lib/tmux.js +0 -237
  202. package/src/restart.js +0 -16
  203. package/src/status.js +0 -35
  204. package/src/stop.js +0 -25
  205. package/src/validate.js +0 -154
package/src/task.ts ADDED
@@ -0,0 +1,1143 @@
1
+ import { resolve } from "node:path";
2
+ import { outputError } from "./lib/output.ts";
3
+ import type { ProofSchema } from "./types.ts";
4
+ import {
5
+ ensureTasksDir,
6
+ loadMission,
7
+ saveMission,
8
+ clearMission,
9
+ loadGoals,
10
+ loadGoal,
11
+ saveGoal,
12
+ deleteGoal,
13
+ nextGoalId,
14
+ loadTasks,
15
+ loadTask,
16
+ saveTask,
17
+ deleteTask,
18
+ nextTaskId,
19
+ loadTasksForGoal,
20
+ detectCycle,
21
+ type Mission,
22
+ type Goal,
23
+ type Task,
24
+ } from "./lib/task-store.ts";
25
+ import { isGhAvailable, createTaskPr } from "./lib/github-pr.ts";
26
+ import {
27
+ loadValidationContract,
28
+ loadValidationState,
29
+ saveValidationState,
30
+ checkCoverage,
31
+ parseAssertionIds,
32
+ } from "./lib/validation.ts";
33
+ import { getSessionName, readConfig } from "./lib/yaml-io.ts";
34
+ import { getSessionState } from "./lib/tmux.ts";
35
+ import { listSessionPanes } from "./widgets/lib/pane-comms.ts";
36
+ import { dispatchResearch, loadResearchState, type ResearchTrigger } from "./lib/research.ts";
37
+ import { type OrchestratorState } from "./lib/orchestrator.ts";
38
+
39
+ export function parseProof(raw: string, existing: ProofSchema | null): ProofSchema {
40
+ // Try parsing as JSON first
41
+ if (raw.startsWith("{")) {
42
+ try {
43
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
44
+ const proof: ProofSchema = { ...existing };
45
+ if (parsed.tests && typeof parsed.tests === "object") {
46
+ const t = parsed.tests as Record<string, unknown>;
47
+ if (typeof t.passed === "number" && typeof t.total === "number") {
48
+ proof.tests = { passed: t.passed, total: t.total };
49
+ }
50
+ }
51
+ if (parsed.pr && typeof parsed.pr === "object") {
52
+ const p = parsed.pr as Record<string, unknown>;
53
+ if (typeof p.number === "number") {
54
+ proof.pr = { number: p.number };
55
+ if (typeof p.url === "string") proof.pr.url = p.url;
56
+ if (typeof p.status === "string") proof.pr.status = p.status;
57
+ }
58
+ }
59
+ if (parsed.ci && typeof parsed.ci === "object") {
60
+ const c = parsed.ci as Record<string, unknown>;
61
+ if (typeof c.status === "string") {
62
+ proof.ci = { status: c.status };
63
+ if (typeof c.url === "string") proof.ci.url = c.url;
64
+ }
65
+ }
66
+ if (typeof parsed.notes === "string") proof.notes = parsed.notes;
67
+ return proof;
68
+ } catch {
69
+ // Not valid JSON, fall through to treat as plain string
70
+ }
71
+ }
72
+ // Plain string → proof.notes (backward compat)
73
+ return { ...existing, notes: raw };
74
+ }
75
+
76
+ interface TaskCommandValues {
77
+ title?: string;
78
+ description?: string;
79
+ acceptance?: string;
80
+ priority?: string;
81
+ status?: string;
82
+ assign?: string;
83
+ goal?: string;
84
+ tags?: string;
85
+ proof?: string;
86
+ depends?: string;
87
+ pr?: boolean;
88
+ specialty?: string;
89
+ milestone?: string;
90
+ fulfills?: string;
91
+ summary?: string;
92
+ sequence?: string;
93
+ evidence?: string;
94
+ }
95
+
96
+ export async function taskCommand(
97
+ targetDir: string | undefined,
98
+ {
99
+ json = false,
100
+ action,
101
+ sub,
102
+ args = [],
103
+ values = {},
104
+ }: {
105
+ json?: boolean;
106
+ action: string;
107
+ sub?: string;
108
+ args: string[];
109
+ values?: TaskCommandValues;
110
+ },
111
+ ): Promise<void> {
112
+ const dir = resolve(targetDir ?? ".");
113
+
114
+ switch (action) {
115
+ case "mission":
116
+ return handleMission(dir, sub, args, values, json);
117
+ case "milestone":
118
+ return handleMilestone(dir, sub, args, values, json);
119
+ case "validate":
120
+ return handleValidation(dir, sub, args, values, json);
121
+ case "research":
122
+ return handleResearch(dir, sub, args, json);
123
+ case "goal":
124
+ return handleGoal(dir, sub, args, values, json);
125
+ case "task":
126
+ return handleTask(dir, sub, args, values, json);
127
+ default:
128
+ outputError(`Unknown action: ${action}`, "USAGE");
129
+ }
130
+ }
131
+
132
+ function emptyOrchestratorState(): OrchestratorState {
133
+ return {
134
+ lastActivity: new Map(),
135
+ previousTasks: new Map(),
136
+ claimedTasks: new Set(),
137
+ taskClaimTimes: new Map(),
138
+ };
139
+ }
140
+
141
+ function handleResearch(dir: string, sub: string | undefined, args: string[], json: boolean): void {
142
+ switch (sub) {
143
+ case "status": {
144
+ const researchState = loadResearchState(dir);
145
+ const tasks = loadTasks(dir);
146
+ const activeTask =
147
+ researchState.activeResearchTaskId != null
148
+ ? (tasks.find((task) => task.id === researchState.activeResearchTaskId) ?? null)
149
+ : null;
150
+ const recentFindings = tasks
151
+ .filter((task) => task.tags.includes("research") && task.status === "done")
152
+ .sort((a, b) => Date.parse(b.updated) - Date.parse(a.updated))
153
+ .slice(0, 5)
154
+ .map((task) => ({
155
+ id: task.id,
156
+ title: task.title,
157
+ updated: task.updated,
158
+ type: task.tags.find((tag) => tag !== "research") ?? "research",
159
+ summary: task.salientSummary,
160
+ }));
161
+
162
+ const payload = {
163
+ activeTask,
164
+ recentFindings,
165
+ cooldowns: researchState.lastResearchAt,
166
+ missionStartAnalyzed: researchState.missionStartAnalyzed,
167
+ retryWindow: researchState.retryWindow,
168
+ };
169
+
170
+ if (json) {
171
+ console.log(JSON.stringify(payload, null, 2));
172
+ } else {
173
+ console.log(
174
+ `Active research task: ${activeTask ? `${activeTask.id} — ${activeTask.title}` : "none"}`,
175
+ );
176
+ console.log(`Mission start analyzed: ${researchState.missionStartAnalyzed ? "yes" : "no"}`);
177
+ console.log(`Recent findings: ${recentFindings.length}`);
178
+ }
179
+ break;
180
+ }
181
+
182
+ case "trigger": {
183
+ const type = args[0];
184
+ if (!type) outputError("Usage: tmux-ide research trigger <type>", "USAGE");
185
+
186
+ const { name: session } = getSessionName(dir);
187
+ const sessionState = getSessionState(session);
188
+ if (!sessionState.running) {
189
+ outputError(`Session "${session}" is not running`, "SESSION_NOT_FOUND");
190
+ }
191
+
192
+ const panes = listSessionPanes(session);
193
+ const tasks = loadTasks(dir);
194
+ const researchState = loadResearchState(dir);
195
+ let maxConcurrentAgents = 10;
196
+ let masterPane: string | null = null;
197
+ let researchEnabled = true;
198
+
199
+ try {
200
+ const { config } = readConfig(dir);
201
+ maxConcurrentAgents = config.orchestrator?.max_concurrent_agents ?? 10;
202
+ masterPane = config.orchestrator?.master_pane ?? null;
203
+ researchEnabled = config.orchestrator?.research?.enabled ?? true;
204
+ } catch {
205
+ // Fall back to defaults when ide.yml is unreadable
206
+ }
207
+
208
+ const task = dispatchResearch(
209
+ {
210
+ session,
211
+ dir,
212
+ masterPane,
213
+ maxConcurrentAgents,
214
+ research: { enabled: researchEnabled },
215
+ },
216
+ emptyOrchestratorState(),
217
+ researchState,
218
+ tasks,
219
+ panes,
220
+ {
221
+ type,
222
+ reason: `Manual research trigger: ${type}`,
223
+ } satisfies ResearchTrigger,
224
+ );
225
+
226
+ if (!task) {
227
+ outputError(`Unable to dispatch research trigger "${type}"`, "CONFLICT");
228
+ }
229
+
230
+ if (json) {
231
+ console.log(JSON.stringify(task, null, 2));
232
+ } else {
233
+ console.log(`Dispatched research task ${task.id}: ${task.title}`);
234
+ }
235
+ break;
236
+ }
237
+
238
+ case "help":
239
+ case undefined:
240
+ console.log(`Usage: tmux-ide research <status|trigger>
241
+
242
+ status Show research agent state
243
+ trigger <type> Manually dispatch a research task`);
244
+ break;
245
+ default:
246
+ outputError(
247
+ "Usage: tmux-ide research <status|trigger>\nRun: tmux-ide research help",
248
+ "USAGE",
249
+ );
250
+ }
251
+ }
252
+
253
+ // --- Mission ---
254
+
255
+ function handleMission(
256
+ dir: string,
257
+ sub: string | undefined,
258
+ args: string[],
259
+ values: TaskCommandValues,
260
+ json: boolean,
261
+ ): void {
262
+ switch (sub) {
263
+ case "set": {
264
+ const title = args[0];
265
+ if (!title) outputError('Usage: tmux-ide mission set "title"', "USAGE");
266
+ ensureTasksDir(dir);
267
+ const now = new Date().toISOString();
268
+ const mission: Mission = {
269
+ title,
270
+ description: values.description ?? "",
271
+ status: "active",
272
+ branch: null,
273
+ milestones: [],
274
+ created: now,
275
+ updated: now,
276
+ };
277
+ saveMission(dir, mission);
278
+ if (json) {
279
+ console.log(JSON.stringify(mission, null, 2));
280
+ } else {
281
+ console.log(`Mission set: ${title}`);
282
+ }
283
+ break;
284
+ }
285
+ case "create": {
286
+ const title = args[0];
287
+ if (!title) outputError('Usage: tmux-ide mission create "title" [-d "description"]', "USAGE");
288
+ ensureTasksDir(dir);
289
+ const now = new Date().toISOString();
290
+ const mission: Mission = {
291
+ title,
292
+ description: values.description ?? "",
293
+ status: "planning",
294
+ branch: null,
295
+ milestones: [],
296
+ created: now,
297
+ updated: now,
298
+ };
299
+ saveMission(dir, mission);
300
+ if (json) {
301
+ console.log(JSON.stringify(mission, null, 2));
302
+ } else {
303
+ console.log(`Mission created (planning): ${title}`);
304
+ }
305
+ break;
306
+ }
307
+ case "plan-complete": {
308
+ const mission = loadMission(dir);
309
+ if (!mission) outputError("No mission set", "NOT_FOUND");
310
+ if (mission.status !== "planning") {
311
+ outputError(`Mission is "${mission.status}", expected "planning"`, "CONFLICT");
312
+ }
313
+ mission.status = "active";
314
+ // Normalize milestone activation so plan completion always starts with only
315
+ // the earliest milestone active, even if planning left bad statuses behind.
316
+ const sorted = [...mission.milestones].sort((a, b) => a.order - b.order);
317
+ const first = sorted[0] ?? null;
318
+ const now = new Date().toISOString();
319
+ for (const milestone of sorted) {
320
+ milestone.status = first && milestone.id === first.id ? "active" : "locked";
321
+ milestone.updated = now;
322
+ }
323
+ mission.updated = now;
324
+ saveMission(dir, mission);
325
+ // Check coverage invariant
326
+ const { unclaimed } = checkCoverage(dir);
327
+ if (json) {
328
+ console.log(JSON.stringify({ ...mission, coverageGaps: unclaimed }, null, 2));
329
+ } else {
330
+ console.log(`Mission "${mission.title}" planning complete — now active`);
331
+ if (first) console.log(` Activated milestone: ${first.id} — ${first.title}`);
332
+ if (unclaimed.length > 0) {
333
+ console.log(
334
+ ` Warning: ${unclaimed.length} assertion(s) not claimed by any task: ${unclaimed.join(", ")}`,
335
+ );
336
+ }
337
+ }
338
+ break;
339
+ }
340
+ case "show": {
341
+ const mission = loadMission(dir);
342
+ if (!mission) outputError('No mission set. Run: tmux-ide mission set "..."', "NOT_FOUND");
343
+ if (json) {
344
+ console.log(JSON.stringify(mission, null, 2));
345
+ } else {
346
+ console.log(`Mission: ${mission.title}`);
347
+ if (mission.description) console.log(` ${mission.description}`);
348
+ }
349
+ break;
350
+ }
351
+ case "status": {
352
+ const mission = loadMission(dir);
353
+ if (!mission) outputError("No mission set", "NOT_FOUND");
354
+ const tasks = loadTasks(dir);
355
+ const doneMilestones = mission.milestones.filter((m) => m.status === "done").length;
356
+ const totalMilestones = mission.milestones.length;
357
+ const doneTasks = tasks.filter((t) => t.status === "done").length;
358
+ const totalTasks = tasks.length;
359
+ const valState = loadValidationState(dir);
360
+ const assertions = valState ? Object.values(valState.assertions) : [];
361
+ const passingCount = assertions.filter((a) => a.status === "passing").length;
362
+ const failingCount = assertions.filter((a) => a.status === "failing").length;
363
+ const pendingCount = assertions.filter((a) => a.status === "pending").length;
364
+ if (json) {
365
+ console.log(
366
+ JSON.stringify(
367
+ {
368
+ title: mission.title,
369
+ status: mission.status,
370
+ milestones: { done: doneMilestones, total: totalMilestones },
371
+ tasks: { done: doneTasks, total: totalTasks },
372
+ validation: { passing: passingCount, failing: failingCount, pending: pendingCount },
373
+ },
374
+ null,
375
+ 2,
376
+ ),
377
+ );
378
+ } else {
379
+ console.log(`Mission: ${mission.title} [${mission.status}]`);
380
+ console.log(` Milestones: ${doneMilestones}/${totalMilestones} done`);
381
+ console.log(` Tasks: ${doneTasks}/${totalTasks} done`);
382
+ if (assertions.length > 0) {
383
+ console.log(
384
+ ` Validation: ${passingCount} passing, ${failingCount} failing, ${pendingCount} pending`,
385
+ );
386
+ }
387
+ }
388
+ break;
389
+ }
390
+ case "clear": {
391
+ clearMission(dir);
392
+ if (json) {
393
+ console.log(JSON.stringify({ cleared: true }));
394
+ } else {
395
+ console.log("Mission cleared");
396
+ }
397
+ break;
398
+ }
399
+ case "help":
400
+ case undefined:
401
+ console.log(`Usage: tmux-ide mission <set|create|show|status|plan-complete|clear>
402
+
403
+ set "title" [-d "description"] Set mission (status: active)
404
+ create "title" [-d "description"] Create mission (status: planning)
405
+ show Show current mission
406
+ status Show mission progress
407
+ plan-complete Signal planning is done
408
+ clear Clear the mission`);
409
+ break;
410
+ default:
411
+ outputError(
412
+ "Usage: tmux-ide mission <set|create|show|status|plan-complete|clear>\nRun: tmux-ide mission help",
413
+ "USAGE",
414
+ );
415
+ }
416
+ }
417
+
418
+ // --- Milestone ---
419
+
420
+ function handleMilestone(
421
+ dir: string,
422
+ sub: string | undefined,
423
+ args: string[],
424
+ values: TaskCommandValues,
425
+ json: boolean,
426
+ ): void {
427
+ switch (sub) {
428
+ case "create": {
429
+ const title = args[0];
430
+ if (!title) outputError('Usage: tmux-ide milestone create "title" --sequence N', "USAGE");
431
+ const mission = loadMission(dir);
432
+ if (!mission)
433
+ outputError('No mission set. Run: tmux-ide mission set "..." first', "NOT_FOUND");
434
+ const sequence = parseInt(values.sequence ?? "0", 10) || mission.milestones.length + 1;
435
+ const id = `M${sequence}`;
436
+ // Check for duplicate ID
437
+ if (mission.milestones.find((m) => m.id === id)) {
438
+ outputError(`Milestone ${id} already exists`, "CONFLICT");
439
+ }
440
+ const now = new Date().toISOString();
441
+ const hasActive = mission.milestones.some(
442
+ (m) => m.status === "active" || m.status === "done",
443
+ );
444
+ const milestone: Mission["milestones"][number] = {
445
+ id,
446
+ title,
447
+ description: values.description ?? "",
448
+ status: hasActive ? "locked" : "active",
449
+ order: sequence,
450
+ created: now,
451
+ updated: now,
452
+ };
453
+ mission.milestones.push(milestone);
454
+ mission.milestones.sort((a, b) => a.order - b.order);
455
+ mission.updated = now;
456
+ saveMission(dir, mission);
457
+ if (json) {
458
+ console.log(JSON.stringify(milestone, null, 2));
459
+ } else {
460
+ console.log(`Created milestone ${id}: ${title} [${milestone.status}]`);
461
+ }
462
+ break;
463
+ }
464
+ case "list": {
465
+ const mission = loadMission(dir);
466
+ if (!mission) outputError("No mission set", "NOT_FOUND");
467
+ const tasks = loadTasks(dir);
468
+ const milestones = [...mission.milestones].sort((a, b) => a.order - b.order);
469
+ if (json) {
470
+ const data = milestones.map((m) => {
471
+ const mTasks = tasks.filter((t) => t.milestone === m.id);
472
+ const done = mTasks.filter((t) => t.status === "done").length;
473
+ return { ...m, taskCount: mTasks.length, tasksDone: done };
474
+ });
475
+ console.log(JSON.stringify(data, null, 2));
476
+ } else if (milestones.length === 0) {
477
+ console.log('No milestones. Run: tmux-ide milestone create "title" --sequence N');
478
+ } else {
479
+ for (const m of milestones) {
480
+ const mTasks = tasks.filter((t) => t.milestone === m.id);
481
+ const done = mTasks.filter((t) => t.status === "done").length;
482
+ console.log(` ${m.id} [${m.status}] ${m.title} (${done}/${mTasks.length} tasks)`);
483
+ }
484
+ }
485
+ break;
486
+ }
487
+ case "show": {
488
+ const id = args[0];
489
+ if (!id) outputError("Usage: tmux-ide milestone show <id>", "USAGE");
490
+ const mission = loadMission(dir);
491
+ if (!mission) outputError("No mission set", "NOT_FOUND");
492
+ const milestone = mission.milestones.find((m) => m.id === id);
493
+ if (!milestone) outputError(`Milestone ${id} not found`, "NOT_FOUND");
494
+ const tasks = loadTasks(dir).filter((t) => t.milestone === id);
495
+ if (json) {
496
+ console.log(JSON.stringify({ milestone, tasks }, null, 2));
497
+ } else {
498
+ console.log(`Milestone ${milestone.id}: ${milestone.title}`);
499
+ console.log(` Status: ${milestone.status}`);
500
+ console.log(` Order: ${milestone.order}`);
501
+ if (milestone.description) console.log(` Description: ${milestone.description}`);
502
+ if (tasks.length > 0) {
503
+ console.log(` Tasks:`);
504
+ for (const t of tasks) {
505
+ console.log(` ${t.id} [${t.status}] ${t.title}`);
506
+ }
507
+ }
508
+ }
509
+ break;
510
+ }
511
+ case "update": {
512
+ const id = args[0];
513
+ if (!id) outputError("Usage: tmux-ide milestone update <id> --status <status>", "USAGE");
514
+ const mission = loadMission(dir);
515
+ if (!mission) outputError("No mission set", "NOT_FOUND");
516
+ const milestone = mission.milestones.find((m) => m.id === id);
517
+ if (!milestone) outputError(`Milestone ${id} not found`, "NOT_FOUND");
518
+ if (values.status) {
519
+ milestone.status = values.status as typeof milestone.status;
520
+ }
521
+ if (values.description) milestone.description = values.description;
522
+ milestone.updated = new Date().toISOString();
523
+ mission.updated = milestone.updated;
524
+ saveMission(dir, mission);
525
+ if (json) {
526
+ console.log(JSON.stringify(milestone, null, 2));
527
+ } else {
528
+ console.log(`Updated milestone ${id}`);
529
+ }
530
+ break;
531
+ }
532
+ case "help":
533
+ case undefined:
534
+ console.log(`Usage: tmux-ide milestone <create|list|show|update>
535
+
536
+ create "title" [--sequence N] [-d "desc"] Create a milestone
537
+ list List milestones
538
+ show <id> Show milestone with tasks
539
+ update <id> [--status active|done|locked] Update milestone`);
540
+ break;
541
+ default:
542
+ outputError(
543
+ "Usage: tmux-ide milestone <create|list|show|update>\nRun: tmux-ide milestone help",
544
+ "USAGE",
545
+ );
546
+ }
547
+ }
548
+
549
+ // --- Validation ---
550
+
551
+ function handleValidation(
552
+ dir: string,
553
+ sub: string | undefined,
554
+ args: string[],
555
+ values: TaskCommandValues,
556
+ json: boolean,
557
+ ): void {
558
+ switch (sub) {
559
+ case "show": {
560
+ const contract = loadValidationContract(dir);
561
+ const state = loadValidationState(dir);
562
+ if (json) {
563
+ console.log(JSON.stringify({ contract: contract ?? null, state: state ?? null }, null, 2));
564
+ } else {
565
+ if (contract) {
566
+ console.log("Validation Contract:");
567
+ console.log(contract);
568
+ console.log();
569
+ } else {
570
+ console.log("No validation contract found (.tasks/validation-contract.md)");
571
+ }
572
+ if (state && Object.keys(state.assertions).length > 0) {
573
+ console.log("Assertion Status:");
574
+ for (const [id, entry] of Object.entries(state.assertions)) {
575
+ const evidence = entry.evidence ? ` — ${entry.evidence}` : "";
576
+ console.log(` ${id} [${entry.status}]${evidence}`);
577
+ }
578
+ } else {
579
+ console.log("No assertion state yet.");
580
+ }
581
+ }
582
+ break;
583
+ }
584
+ case "assert": {
585
+ const assertionId = args[0];
586
+ if (!assertionId || !values.status) {
587
+ outputError(
588
+ 'Usage: tmux-ide validate assert <ASSERT_ID> --status passing|failing [--evidence "..."]',
589
+ "USAGE",
590
+ );
591
+ }
592
+ const status = values.status as "passing" | "failing" | "blocked";
593
+ if (status !== "passing" && status !== "failing" && status !== "blocked") {
594
+ outputError("Status must be 'passing', 'failing', or 'blocked'", "USAGE");
595
+ }
596
+ ensureTasksDir(dir);
597
+ const state = loadValidationState(dir) ?? { assertions: {}, lastVerified: null };
598
+ state.assertions[assertionId] = {
599
+ status,
600
+ verifiedBy: values.assign ?? null,
601
+ verifiedAt: new Date().toISOString(),
602
+ evidence: values.evidence ?? null,
603
+ blockedBy: status === "blocked" ? (values.evidence ?? null) : null,
604
+ };
605
+ state.lastVerified = new Date().toISOString();
606
+ saveValidationState(dir, state);
607
+ if (json) {
608
+ console.log(JSON.stringify({ assertionId, ...state.assertions[assertionId] }, null, 2));
609
+ } else {
610
+ console.log(
611
+ `Assertion ${assertionId}: ${status}${values.evidence ? ` — ${values.evidence}` : ""}`,
612
+ );
613
+ }
614
+ break;
615
+ }
616
+ case "report": {
617
+ const state = loadValidationState(dir);
618
+ if (!state || Object.keys(state.assertions).length === 0) {
619
+ if (json) {
620
+ console.log(JSON.stringify({ total: 0, passing: 0, failing: 0, pending: 0, blocked: 0 }));
621
+ } else {
622
+ console.log("No assertions to report.");
623
+ }
624
+ break;
625
+ }
626
+ const entries = Object.values(state.assertions);
627
+ const report = {
628
+ total: entries.length,
629
+ passing: entries.filter((e) => e.status === "passing").length,
630
+ failing: entries.filter((e) => e.status === "failing").length,
631
+ pending: entries.filter((e) => e.status === "pending").length,
632
+ blocked: entries.filter((e) => e.status === "blocked").length,
633
+ };
634
+ if (json) {
635
+ console.log(JSON.stringify(report, null, 2));
636
+ } else {
637
+ console.log(`Validation Report: ${report.total} assertions`);
638
+ console.log(` Passing: ${report.passing}`);
639
+ console.log(` Failing: ${report.failing}`);
640
+ console.log(` Pending: ${report.pending}`);
641
+ if (report.blocked > 0) console.log(` Blocked: ${report.blocked}`);
642
+ }
643
+ break;
644
+ }
645
+ case "coverage": {
646
+ const result = checkCoverage(dir);
647
+ const contract = loadValidationContract(dir);
648
+ const allIds = contract ? parseAssertionIds(contract) : [];
649
+ const claimed = allIds.length - result.unclaimed.length;
650
+ if (json) {
651
+ console.log(
652
+ JSON.stringify(
653
+ {
654
+ total: allIds.length,
655
+ claimed,
656
+ unclaimed: result.unclaimed,
657
+ duplicates: result.duplicates,
658
+ },
659
+ null,
660
+ 2,
661
+ ),
662
+ );
663
+ } else {
664
+ console.log(`Assertion Coverage: ${claimed}/${allIds.length} claimed`);
665
+ if (result.unclaimed.length > 0) {
666
+ console.log(` Unclaimed: ${result.unclaimed.join(", ")}`);
667
+ }
668
+ if (Object.keys(result.duplicates).length > 0) {
669
+ console.log(` Duplicate claims:`);
670
+ for (const [id, taskIds] of Object.entries(result.duplicates)) {
671
+ console.log(` ${id}: tasks ${taskIds.join(", ")}`);
672
+ }
673
+ }
674
+ if (result.unclaimed.length === 0 && Object.keys(result.duplicates).length === 0) {
675
+ console.log(" Full coverage — all assertions claimed.");
676
+ }
677
+ }
678
+ break;
679
+ }
680
+ case "help":
681
+ case undefined:
682
+ console.log(`Usage: tmux-ide validate <show|assert|report|coverage>
683
+
684
+ show Show contract + assertion state
685
+ assert <ID> --status passing|failing|blocked [--evidence] Update assertion status
686
+ report Summary of assertion results
687
+ coverage Check assertion coverage`);
688
+ break;
689
+ default:
690
+ outputError(
691
+ "Usage: tmux-ide validate <show|assert|report|coverage>\nRun: tmux-ide validate help",
692
+ "USAGE",
693
+ );
694
+ }
695
+ }
696
+
697
+ // --- Goal ---
698
+
699
+ function handleGoal(
700
+ dir: string,
701
+ sub: string | undefined,
702
+ args: string[],
703
+ values: TaskCommandValues,
704
+ json: boolean,
705
+ ): void {
706
+ switch (sub) {
707
+ case "list": {
708
+ const goals = loadGoals(dir);
709
+ if (json) {
710
+ console.log(JSON.stringify(goals, null, 2));
711
+ } else if (goals.length === 0) {
712
+ console.log('No goals. Run: tmux-ide goal create "..."');
713
+ } else {
714
+ for (const g of goals) {
715
+ const tasks = loadTasksForGoal(dir, g.id);
716
+ const done = tasks.filter((t) => t.status === "done").length;
717
+ console.log(` ${g.id} [${g.status}] ${g.title} (${done}/${tasks.length} tasks)`);
718
+ }
719
+ }
720
+ break;
721
+ }
722
+ case "create": {
723
+ const title = args[0];
724
+ if (!title) outputError('Usage: tmux-ide goal create "title"', "USAGE");
725
+ ensureTasksDir(dir);
726
+ const id = nextGoalId(dir);
727
+ const now = new Date().toISOString();
728
+ const goal: Goal = {
729
+ id,
730
+ title,
731
+ description: values.description ?? "",
732
+ status: "todo",
733
+ acceptance: values.acceptance ?? "",
734
+ priority: parseInt(values.priority ?? "2", 10),
735
+ created: now,
736
+ updated: now,
737
+ assignee: null,
738
+ specialty: values.specialty ?? null,
739
+ milestone: values.milestone ?? null,
740
+ };
741
+ saveGoal(dir, goal);
742
+ if (json) {
743
+ console.log(JSON.stringify(goal, null, 2));
744
+ } else {
745
+ console.log(`Created goal ${id}: ${title}`);
746
+ }
747
+ break;
748
+ }
749
+ case "update": {
750
+ const id = args[0];
751
+ if (!id) outputError("Usage: tmux-ide goal update <id>", "USAGE");
752
+ const goal = loadGoal(dir, id);
753
+ if (!goal) outputError(`Goal ${id} not found`, "NOT_FOUND");
754
+ if (values.status) goal.status = values.status as Goal["status"];
755
+ if (values.description) goal.description = values.description;
756
+ if (values.acceptance) goal.acceptance = values.acceptance;
757
+ if (values.priority) goal.priority = parseInt(values.priority, 10);
758
+ if (values.milestone) goal.milestone = values.milestone;
759
+ goal.updated = new Date().toISOString();
760
+ saveGoal(dir, goal);
761
+ if (json) {
762
+ console.log(JSON.stringify(goal, null, 2));
763
+ } else {
764
+ console.log(`Updated goal ${id}`);
765
+ }
766
+ break;
767
+ }
768
+ case "done": {
769
+ const id = args[0];
770
+ if (!id) outputError("Usage: tmux-ide goal done <id>", "USAGE");
771
+ const goal = loadGoal(dir, id);
772
+ if (!goal) outputError(`Goal ${id} not found`, "NOT_FOUND");
773
+ goal.status = "done";
774
+ goal.updated = new Date().toISOString();
775
+ saveGoal(dir, goal);
776
+ if (json) {
777
+ console.log(JSON.stringify(goal, null, 2));
778
+ } else {
779
+ console.log(`Goal ${id} marked done: ${goal.title}`);
780
+ }
781
+ break;
782
+ }
783
+ case "show": {
784
+ const id = args[0];
785
+ if (!id) outputError("Usage: tmux-ide goal show <id>", "USAGE");
786
+ const goal = loadGoal(dir, id);
787
+ if (!goal) outputError(`Goal ${id} not found`, "NOT_FOUND");
788
+ const tasks = loadTasksForGoal(dir, id);
789
+ if (json) {
790
+ console.log(JSON.stringify({ goal, tasks }, null, 2));
791
+ } else {
792
+ console.log(`Goal ${goal.id}: ${goal.title}`);
793
+ console.log(` Status: ${goal.status}`);
794
+ if (goal.acceptance) console.log(` Acceptance: ${goal.acceptance}`);
795
+ if (tasks.length > 0) {
796
+ console.log(` Tasks:`);
797
+ for (const t of tasks) {
798
+ console.log(` ${t.id} [${t.status}] ${t.title}`);
799
+ }
800
+ }
801
+ }
802
+ break;
803
+ }
804
+ case "delete": {
805
+ const id = args[0];
806
+ if (!id) outputError("Usage: tmux-ide goal delete <id>", "USAGE");
807
+ if (!deleteGoal(dir, id)) outputError(`Goal ${id} not found`, "NOT_FOUND");
808
+ if (json) {
809
+ console.log(JSON.stringify({ deleted: id }));
810
+ } else {
811
+ console.log(`Deleted goal ${id}`);
812
+ }
813
+ break;
814
+ }
815
+ case "help":
816
+ case undefined:
817
+ console.log(`Usage: tmux-ide goal <list|create|update|done|show|delete>
818
+
819
+ list List all goals
820
+ create "title" [-p N] [-d "desc"] Create a goal
821
+ update <id> [-s status] [-p N] Update a goal
822
+ done <id> Mark goal complete
823
+ show <id> Show goal with tasks
824
+ delete <id> Delete a goal`);
825
+ break;
826
+ default:
827
+ outputError(
828
+ "Usage: tmux-ide goal <list|create|update|done|show|delete>\nRun: tmux-ide goal help",
829
+ "USAGE",
830
+ );
831
+ }
832
+ }
833
+
834
+ // --- Task ---
835
+
836
+ function handleTask(
837
+ dir: string,
838
+ sub: string | undefined,
839
+ args: string[],
840
+ values: TaskCommandValues,
841
+ json: boolean,
842
+ ): void {
843
+ switch (sub) {
844
+ case "init": {
845
+ ensureTasksDir(dir);
846
+ if (json) {
847
+ console.log(JSON.stringify({ initialized: true }));
848
+ } else {
849
+ console.log("Initialized .tasks/ directory");
850
+ }
851
+ break;
852
+ }
853
+ case "list": {
854
+ let tasks = loadTasks(dir);
855
+ const allTasks = tasks; // keep full list for dependency checks
856
+ if (values.status) tasks = tasks.filter((t) => t.status === values.status);
857
+ if (values.goal) tasks = tasks.filter((t) => t.goal === values.goal);
858
+ tasks.sort((a, b) => a.priority - b.priority);
859
+ if (json) {
860
+ console.log(JSON.stringify(tasks, null, 2));
861
+ } else if (tasks.length === 0) {
862
+ console.log('No tasks. Run: tmux-ide task create "title"');
863
+ } else {
864
+ // Build goal name map for display
865
+ const goals = loadGoals(dir);
866
+ const goalNames = new Map(goals.map((g) => [g.id, g.title]));
867
+
868
+ for (const t of tasks) {
869
+ // Check if blocked by unfinished dependencies
870
+ const blocked =
871
+ t.depends_on.length > 0 &&
872
+ t.depends_on.some((depId) => {
873
+ const dep = allTasks.find((d) => d.id === depId);
874
+ return !dep || dep.status !== "done";
875
+ });
876
+
877
+ const status = blocked ? "blocked" : t.status;
878
+ const assignee = t.assignee ? ` @${t.assignee}` : "";
879
+ const goalLabel = t.goal ? ` (${goalNames.get(t.goal) ?? `goal ${t.goal}`})` : "";
880
+ const deps = blocked ? ` (depends: ${t.depends_on.join(", ")})` : "";
881
+ console.log(` ${t.id} [${status}] ${t.title}${assignee}${goalLabel}${deps}`);
882
+ }
883
+ }
884
+ break;
885
+ }
886
+ case "create": {
887
+ const title = args[0];
888
+ if (!title)
889
+ outputError(
890
+ 'Missing title. Usage:\n tmux-ide task create "Fix the auth bug" -g 01 -p 1 -d "Token storage issue"',
891
+ "USAGE",
892
+ );
893
+ ensureTasksDir(dir);
894
+ const id = nextTaskId(dir);
895
+ const now = new Date().toISOString();
896
+ const task: Task = {
897
+ id,
898
+ title,
899
+ description: values.description ?? "",
900
+ goal: values.goal ?? null,
901
+ status: "todo",
902
+ assignee: values.assign ?? null,
903
+ priority: parseInt(values.priority ?? "2", 10),
904
+ created: now,
905
+ updated: now,
906
+ tags: values.tags ? values.tags.split(",").map((t) => t.trim()) : [],
907
+ proof: null,
908
+ depends_on: values.depends ? values.depends.split(",").map((d) => d.trim()) : [],
909
+ retryCount: 0,
910
+ maxRetries: 5,
911
+ lastError: null,
912
+ nextRetryAt: null,
913
+ milestone: values.milestone ?? null,
914
+ specialty: values.specialty ?? null,
915
+ fulfills: values.fulfills ? values.fulfills.split(",").map((f) => f.trim()) : [],
916
+ discoveredIssues: [],
917
+ salientSummary: null,
918
+ };
919
+ if (task.depends_on.length > 0) {
920
+ const cycle = detectCycle(dir, task.id, task.depends_on);
921
+ if (cycle) {
922
+ outputError(`Dependency cycle detected: ${cycle.join(" -> ")}`, "CYCLE");
923
+ return;
924
+ }
925
+ }
926
+ saveTask(dir, task);
927
+ if (json) {
928
+ console.log(JSON.stringify(task, null, 2));
929
+ } else {
930
+ console.log(`Created task ${id}: ${title}`);
931
+ }
932
+ break;
933
+ }
934
+ case "update": {
935
+ const id = args[0];
936
+ if (!id) outputError("Usage: tmux-ide task update <id>", "USAGE");
937
+ const task = loadTask(dir, id);
938
+ if (!task) outputError(`Task ${id} not found`, "NOT_FOUND");
939
+ if (values.status) task.status = values.status as Task["status"];
940
+ if (values.assign) task.assignee = values.assign;
941
+ if (values.description) task.description = values.description;
942
+ if (values.priority) task.priority = parseInt(values.priority, 10);
943
+ if (values.tags) task.tags = values.tags.split(",").map((t) => t.trim());
944
+ if (values.goal) task.goal = values.goal;
945
+ if (values.depends) {
946
+ const newDeps = values.depends.split(",").map((d) => d.trim());
947
+ const cycle = detectCycle(dir, task.id, newDeps);
948
+ if (cycle) {
949
+ outputError(`Dependency cycle detected: ${cycle.join(" -> ")}`, "CYCLE");
950
+ return;
951
+ }
952
+ task.depends_on = newDeps;
953
+ }
954
+ if (values.proof) task.proof = parseProof(values.proof, task.proof);
955
+ if (values.milestone) task.milestone = values.milestone;
956
+ if (values.specialty) task.specialty = values.specialty;
957
+ if (values.fulfills) task.fulfills = values.fulfills.split(",").map((f) => f.trim());
958
+ task.updated = new Date().toISOString();
959
+ saveTask(dir, task);
960
+ if (json) {
961
+ console.log(JSON.stringify(task, null, 2));
962
+ } else {
963
+ console.log(`Updated task ${id}`);
964
+ }
965
+ break;
966
+ }
967
+ case "done": {
968
+ const id = args[0];
969
+ if (!id) outputError("Usage: tmux-ide task done <id>", "USAGE");
970
+ const task = loadTask(dir, id);
971
+ if (!task) outputError(`Task ${id} not found`, "NOT_FOUND");
972
+ task.status = "done";
973
+ if (values.proof) task.proof = parseProof(values.proof, task.proof);
974
+ if (values.summary) task.salientSummary = values.summary;
975
+ task.updated = new Date().toISOString();
976
+
977
+ // Auto-create GitHub PR if --pr flag is set
978
+ let prWarning: string | undefined;
979
+ if (values.pr) {
980
+ if (isGhAvailable()) {
981
+ const pr = createTaskPr(task, dir);
982
+ if (pr) {
983
+ if (!task.proof) task.proof = {};
984
+ (task.proof as ProofSchema).pr = {
985
+ number: pr.number,
986
+ url: pr.url,
987
+ status: "open",
988
+ };
989
+ if (!json) console.log(`PR created: ${pr.url}`);
990
+ } else {
991
+ prWarning = "PR creation failed (gh error)";
992
+ console.error(`Warning: ${prWarning}`);
993
+ }
994
+ } else {
995
+ prWarning = "gh CLI not found, skipping PR creation";
996
+ console.error(`Warning: ${prWarning}`);
997
+ }
998
+ }
999
+
1000
+ saveTask(dir, task);
1001
+ if (json) {
1002
+ const output: Record<string, unknown> = { ...task };
1003
+ if (prWarning) output.prError = prWarning;
1004
+ console.log(JSON.stringify(output, null, 2));
1005
+ } else {
1006
+ console.log(`Task ${id} done: ${task.title}`);
1007
+ }
1008
+ // Check if all tasks for this goal are done
1009
+ if (task.goal) {
1010
+ const goalTasks = loadTasksForGoal(dir, task.goal);
1011
+ if (goalTasks.every((t) => t.status === "done")) {
1012
+ if (!json) {
1013
+ console.log(
1014
+ `All tasks for goal ${task.goal} are done. Run: tmux-ide goal done ${task.goal}`,
1015
+ );
1016
+ }
1017
+ }
1018
+ }
1019
+ break;
1020
+ }
1021
+ case "claim": {
1022
+ const id = args[0];
1023
+ if (!id) outputError("Usage: tmux-ide task claim <id>", "USAGE");
1024
+ const task = loadTask(dir, id);
1025
+ if (!task) outputError(`Task ${id} not found`, "NOT_FOUND");
1026
+ task.assignee = values.assign ?? null;
1027
+ task.status = "in-progress";
1028
+ task.updated = new Date().toISOString();
1029
+ saveTask(dir, task);
1030
+ if (json) {
1031
+ console.log(JSON.stringify(task, null, 2));
1032
+ } else {
1033
+ const who = task.assignee ? ` by ${task.assignee}` : "";
1034
+ console.log(`Task ${id} claimed${who}: ${task.title}`);
1035
+ }
1036
+ break;
1037
+ }
1038
+ case "show": {
1039
+ const id = args[0];
1040
+ if (!id) outputError("Usage: tmux-ide task show <id>", "USAGE");
1041
+ const task = loadTask(dir, id);
1042
+ if (!task) outputError(`Task ${id} not found. Run: tmux-ide task list`, "NOT_FOUND");
1043
+ const mission = loadMission(dir);
1044
+ const goal = task.goal ? loadGoal(dir, task.goal) : null;
1045
+ if (json) {
1046
+ console.log(
1047
+ JSON.stringify({ mission: mission ?? null, goal: goal ?? null, task }, null, 2),
1048
+ );
1049
+ } else {
1050
+ if (mission) console.log(`Mission: ${mission.title}`);
1051
+ if (goal) {
1052
+ console.log(`Goal ${goal.id}: ${goal.title}`);
1053
+ if (goal.acceptance) console.log(` Acceptance: ${goal.acceptance}`);
1054
+ }
1055
+ console.log(`Task ${task.id}: ${task.title}`);
1056
+ console.log(` Status: ${task.status}`);
1057
+ if (task.description) console.log(` Description: ${task.description}`);
1058
+ if (task.assignee) console.log(` Assignee: ${task.assignee}`);
1059
+ if (task.tags.length > 0) console.log(` Tags: ${task.tags.join(", ")}`);
1060
+ if (task.depends_on.length > 0) console.log(` Depends on: ${task.depends_on.join(", ")}`);
1061
+ if (task.proof) console.log(` Proof: ${JSON.stringify(task.proof)}`);
1062
+ }
1063
+ break;
1064
+ }
1065
+ case "edit": {
1066
+ const id = args[0];
1067
+ if (!id)
1068
+ outputError("Usage: tmux-ide task edit <id> --title 'new title' -d 'new desc'", "USAGE");
1069
+ const task = loadTask(dir, id);
1070
+ if (!task) outputError(`Task ${id} not found. Run: tmux-ide task list`, "NOT_FOUND");
1071
+ if (values.title) task.title = values.title;
1072
+ if (values.description) task.description = values.description;
1073
+ if (values.priority) task.priority = parseInt(values.priority, 10);
1074
+ if (values.tags) task.tags = values.tags.split(",").map((t) => t.trim());
1075
+ if (values.goal) task.goal = values.goal;
1076
+ if (values.depends) {
1077
+ const newDeps = values.depends.split(",").map((d) => d.trim());
1078
+ const cycle = detectCycle(dir, task.id, newDeps);
1079
+ if (cycle) {
1080
+ outputError(`Dependency cycle detected: ${cycle.join(" -> ")}`, "CYCLE");
1081
+ return;
1082
+ }
1083
+ task.depends_on = newDeps;
1084
+ }
1085
+ task.updated = new Date().toISOString();
1086
+ saveTask(dir, task);
1087
+ if (json) {
1088
+ console.log(JSON.stringify(task, null, 2));
1089
+ } else {
1090
+ console.log(`Edited task ${id}: ${task.title}`);
1091
+ }
1092
+ break;
1093
+ }
1094
+ case "unassign": {
1095
+ const id = args[0];
1096
+ if (!id) outputError("Usage: tmux-ide task unassign <id>", "USAGE");
1097
+ const task = loadTask(dir, id);
1098
+ if (!task) outputError(`Task ${id} not found. Run: tmux-ide task list`, "NOT_FOUND");
1099
+ task.assignee = null;
1100
+ task.status = "todo";
1101
+ task.updated = new Date().toISOString();
1102
+ saveTask(dir, task);
1103
+ if (json) {
1104
+ console.log(JSON.stringify(task, null, 2));
1105
+ } else {
1106
+ console.log(`Task ${id} unassigned, status reset to todo: ${task.title}`);
1107
+ }
1108
+ break;
1109
+ }
1110
+ case "delete": {
1111
+ const id = args[0];
1112
+ if (!id) outputError("Usage: tmux-ide task delete <id>", "USAGE");
1113
+ if (!deleteTask(dir, id))
1114
+ outputError(`Task ${id} not found. Run: tmux-ide task list`, "NOT_FOUND");
1115
+ if (json) {
1116
+ console.log(JSON.stringify({ deleted: id }));
1117
+ } else {
1118
+ console.log(`Deleted task ${id}`);
1119
+ }
1120
+ break;
1121
+ }
1122
+ case "help":
1123
+ case undefined:
1124
+ console.log(`Usage: tmux-ide task <subcommand>
1125
+
1126
+ init Initialize .tasks/ directory
1127
+ list [-s status] [-g goalId] [--json] List tasks
1128
+ create "title" [-g id] [-p N] [-d ...] Create a task
1129
+ edit <id> [--title ...] [-d ...] [-p N] Edit task fields
1130
+ update <id> [-s status] [-a name] Update task status/assignee
1131
+ done <id> [--proof "..."] Mark task complete
1132
+ claim <id> [-a name] Claim and start a task
1133
+ unassign <id> Unassign and reset to todo
1134
+ show <id> [--json] Show task with full context
1135
+ delete <id> Delete a task
1136
+
1137
+ Short flags: -p priority, -g goal, -d description, -a assign,
1138
+ -s status, -t tags, -b branch`);
1139
+ break;
1140
+ default:
1141
+ outputError(`Unknown subcommand: ${sub}\nRun: tmux-ide task help`, "USAGE");
1142
+ }
1143
+ }