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
@@ -0,0 +1,473 @@
1
+ import { describe, it, beforeEach, afterEach, expect } from "bun:test";
2
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ loadTasks,
7
+ updateTaskStatus,
8
+ updateTaskAssignee,
9
+ createTask,
10
+ groupTasks,
11
+ nextStatus,
12
+ flattenTaskList,
13
+ isBlocked,
14
+ ensureTasksDir,
15
+ getTasksDir,
16
+ type Task,
17
+ } from "./task-model.ts";
18
+ import {
19
+ saveTask as cliSaveTask,
20
+ ensureTasksDir as cliEnsure,
21
+ loadTask as cliLoadTask,
22
+ } from "../../lib/task-store.ts";
23
+
24
+ let tmpDir: string;
25
+
26
+ function writeTask(task: Partial<Task> & { id: string; title: string; status: string }) {
27
+ const dir = join(tmpDir, ".tasks", "tasks");
28
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
29
+ const full: Task = {
30
+ description: "",
31
+ goal: null,
32
+ assignee: null,
33
+ priority: 2,
34
+ created: "2026-01-01T00:00:00Z",
35
+ updated: "2026-01-01T00:00:00Z",
36
+
37
+ tags: [],
38
+ proof: null,
39
+ retryCount: 0,
40
+ maxRetries: 5,
41
+ lastError: null,
42
+ nextRetryAt: null,
43
+ depends_on: [],
44
+ ...task,
45
+ } as Task;
46
+ writeFileSync(join(dir, `${task.id}-test.json`), JSON.stringify(full, null, 2) + "\n");
47
+ }
48
+
49
+ // Write raw JSON without new fields to simulate legacy task files
50
+ function writeLegacyTask(task: { id: string; title: string; status: string }) {
51
+ const dir = join(tmpDir, ".tasks", "tasks");
52
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
53
+ const legacy = {
54
+ id: task.id,
55
+ title: task.title,
56
+ description: "",
57
+ status: task.status,
58
+ assignee: null,
59
+ priority: 2,
60
+ created: "2026-01-01T00:00:00Z",
61
+ updated: "2026-01-01T00:00:00Z",
62
+
63
+ tags: [],
64
+ proof: null,
65
+ };
66
+ writeFileSync(join(dir, `${task.id}-test.json`), JSON.stringify(legacy, null, 2) + "\n");
67
+ }
68
+
69
+ beforeEach(() => {
70
+ tmpDir = mkdtempSync(join(tmpdir(), "tmux-ide-tasks-test-"));
71
+ });
72
+
73
+ afterEach(() => {
74
+ rmSync(tmpDir, { recursive: true, force: true });
75
+ });
76
+
77
+ describe("loadTasks", () => {
78
+ it("returns empty array when .tasks/ does not exist", () => {
79
+ expect(loadTasks(tmpDir)).toEqual([]);
80
+ });
81
+
82
+ it("loads tasks from .tasks/tasks/ directory", () => {
83
+ writeTask({ id: "001", title: "First task", status: "todo" });
84
+ writeTask({ id: "002", title: "Second task", status: "in-progress" });
85
+ const tasks = loadTasks(tmpDir);
86
+ expect(tasks.length).toBe(2);
87
+ expect(tasks[0]!.id).toBe("001");
88
+ expect(tasks[1]!.id).toBe("002");
89
+ });
90
+
91
+ it("includes goal field from loaded tasks", () => {
92
+ writeTask({ id: "001", title: "With goal", status: "todo", goal: "01" });
93
+ const tasks = loadTasks(tmpDir);
94
+ expect(tasks[0]!.goal).toBe("01");
95
+ });
96
+
97
+ it("defaults goal to null for legacy tasks", () => {
98
+ writeLegacyTask({ id: "001", title: "Legacy", status: "todo" });
99
+ const tasks = loadTasks(tmpDir);
100
+ // goal is not in legacy JSON, normalizeTask in task-store should handle it
101
+ expect(tasks[0]!.goal === null || tasks[0]!.goal === undefined).toBeTruthy();
102
+ });
103
+ });
104
+
105
+ describe("updateTaskStatus", () => {
106
+ it("updates status and updated timestamp", () => {
107
+ writeTask({ id: "001", title: "Test", status: "todo" });
108
+ updateTaskStatus(tmpDir, "001", "in-progress");
109
+ const tasks = loadTasks(tmpDir);
110
+ expect(tasks[0]!.status).toBe("in-progress");
111
+ expect(tasks[0]!.updated).not.toBe("2026-01-01T00:00:00Z");
112
+ });
113
+
114
+ it("does nothing for non-existent task ID", () => {
115
+ writeTask({ id: "001", title: "Test", status: "todo" });
116
+ updateTaskStatus(tmpDir, "999", "done");
117
+ const tasks = loadTasks(tmpDir);
118
+ expect(tasks[0]!.status).toBe("todo");
119
+ });
120
+ });
121
+
122
+ describe("updateTaskAssignee", () => {
123
+ it("sets assignee", () => {
124
+ writeTask({ id: "001", title: "Test", status: "todo" });
125
+ updateTaskAssignee(tmpDir, "001", "Agent 1");
126
+ const tasks = loadTasks(tmpDir);
127
+ expect(tasks[0]!.assignee).toBe("Agent 1");
128
+ });
129
+
130
+ it("clears assignee with null", () => {
131
+ writeTask({ id: "001", title: "Test", status: "todo", assignee: "Agent 1" });
132
+ updateTaskAssignee(tmpDir, "001", null);
133
+ const tasks = loadTasks(tmpDir);
134
+ expect(tasks[0]!.assignee).toBe(null);
135
+ });
136
+ });
137
+
138
+ describe("createTask", () => {
139
+ it("creates a new task file with incremented ID", () => {
140
+ writeTask({ id: "001", title: "Existing", status: "todo" });
141
+ const existing = loadTasks(tmpDir);
142
+ const task = createTask(tmpDir, existing);
143
+ expect(task.id).toBe("002");
144
+ expect(task.status).toBe("todo");
145
+ expect(task.goal).toBe(null);
146
+ const all = loadTasks(tmpDir);
147
+ expect(all.length).toBe(2);
148
+ });
149
+
150
+ it("creates .tasks/ directory if it does not exist", () => {
151
+ const task = createTask(tmpDir, []);
152
+ expect(task.id).toBe("001");
153
+ expect(existsSync(getTasksDir(tmpDir))).toBeTruthy();
154
+ });
155
+
156
+ it("created task is loadable via loadTasks", () => {
157
+ const task = createTask(tmpDir, []);
158
+ const loaded = loadTasks(tmpDir);
159
+ expect(loaded.length).toBe(1);
160
+ expect(loaded[0]!.id).toBe(task.id);
161
+ expect(loaded[0]!.goal).toBe(null);
162
+ });
163
+ });
164
+
165
+ describe("groupTasks", () => {
166
+ const taskBase = {
167
+ description: "",
168
+ goal: null,
169
+ assignee: null,
170
+ created: "",
171
+ updated: "",
172
+
173
+ tags: [],
174
+ proof: null,
175
+ retryCount: 0,
176
+ maxRetries: 5,
177
+ lastError: null,
178
+ nextRetryAt: null,
179
+ depends_on: [],
180
+ } as const;
181
+
182
+ it("groups tasks by status in display order", () => {
183
+ const tasks: Task[] = [
184
+ { ...taskBase, id: "1", title: "A", status: "done", priority: 1 },
185
+ { ...taskBase, id: "2", title: "B", status: "todo", priority: 2 },
186
+ { ...taskBase, id: "3", title: "C", status: "in-progress", priority: 1 },
187
+ ];
188
+ const groups = groupTasks(tasks);
189
+ expect(groups[0]!.status).toBe("in-progress");
190
+ expect(groups[0]!.tasks.length).toBe(1);
191
+ expect(groups[1]!.status).toBe("todo");
192
+ expect(groups[1]!.tasks.length).toBe(1);
193
+ expect(groups[3]!.status).toBe("done");
194
+ expect(groups[3]!.tasks.length).toBe(1);
195
+ });
196
+
197
+ it("sorts by priority within each group", () => {
198
+ const tasks: Task[] = [
199
+ { ...taskBase, id: "1", title: "Low", status: "todo", priority: 3 },
200
+ { ...taskBase, id: "2", title: "High", status: "todo", priority: 1 },
201
+ ];
202
+ const groups = groupTasks(tasks);
203
+ const todo = groups.find((g) => g.status === "todo")!;
204
+ expect(todo.tasks[0]!.title).toBe("High");
205
+ expect(todo.tasks[1]!.title).toBe("Low");
206
+ });
207
+ });
208
+
209
+ describe("nextStatus", () => {
210
+ it("cycles through statuses", () => {
211
+ expect(nextStatus("todo")).toBe("in-progress");
212
+ expect(nextStatus("in-progress")).toBe("review");
213
+ expect(nextStatus("review")).toBe("done");
214
+ expect(nextStatus("done")).toBe("todo");
215
+ });
216
+ });
217
+
218
+ describe("flattenTaskList", () => {
219
+ it("produces flat list with headers and tasks interleaved", () => {
220
+ const groups = [
221
+ { status: "in-progress" as const, tasks: [{ id: "1", title: "A" } as Task] },
222
+ {
223
+ status: "todo" as const,
224
+ tasks: [{ id: "2", title: "B" } as Task, { id: "3", title: "C" } as Task],
225
+ },
226
+ { status: "review" as const, tasks: [] },
227
+ { status: "done" as const, tasks: [] },
228
+ ];
229
+ const flat = flattenTaskList(groups);
230
+ expect(flat.length).toBe(5);
231
+ expect(flat[0]!.kind).toBe("header");
232
+ expect(flat[1]!.kind).toBe("task");
233
+ if (flat[1]!.kind === "task") expect(flat[1]!.task.id).toBe("1");
234
+ expect(flat[2]!.kind).toBe("header");
235
+ expect(flat[3]!.kind).toBe("task");
236
+ if (flat[3]!.kind === "task") expect(flat[3]!.task.id).toBe("2");
237
+ expect(flat[4]!.kind).toBe("task");
238
+ if (flat[4]!.kind === "task") expect(flat[4]!.task.id).toBe("3");
239
+ });
240
+ });
241
+
242
+ describe("ensureTasksDir", () => {
243
+ it("creates .tasks/ directory", () => {
244
+ ensureTasksDir(tmpDir);
245
+ expect(existsSync(getTasksDir(tmpDir))).toBeTruthy();
246
+ });
247
+
248
+ it("does nothing if already exists", () => {
249
+ ensureTasksDir(tmpDir);
250
+ ensureTasksDir(tmpDir);
251
+ expect(existsSync(getTasksDir(tmpDir))).toBeTruthy();
252
+ });
253
+ });
254
+
255
+ describe("retry field defaults", () => {
256
+ it("loadTasks applies retry defaults to legacy JSON without retry fields", () => {
257
+ writeLegacyTask({ id: "001", title: "Legacy", status: "todo" });
258
+ const tasks = loadTasks(tmpDir);
259
+ expect(tasks.length).toBe(1);
260
+ const t = tasks[0]!;
261
+ expect(t.retryCount).toBe(0);
262
+ expect(t.maxRetries).toBe(5);
263
+ expect(t.lastError).toBe(null);
264
+ expect(t.nextRetryAt).toBe(null);
265
+ });
266
+
267
+ it("preserves explicit retry values from JSON", () => {
268
+ writeTask({
269
+ id: "001",
270
+ title: "Retried",
271
+ status: "in-progress",
272
+ retryCount: 3,
273
+ maxRetries: 10,
274
+ lastError: "timeout",
275
+ nextRetryAt: "2026-01-01T01:00:00Z",
276
+ });
277
+ const tasks = loadTasks(tmpDir);
278
+ const t = tasks[0]!;
279
+ expect(t.retryCount).toBe(3);
280
+ expect(t.maxRetries).toBe(10);
281
+ expect(t.lastError).toBe("timeout");
282
+ expect(t.nextRetryAt).toBe("2026-01-01T01:00:00Z");
283
+ });
284
+
285
+ it("createTask includes retry defaults", () => {
286
+ const task = createTask(tmpDir, []);
287
+ expect(task.retryCount).toBe(0);
288
+ expect(task.maxRetries).toBe(5);
289
+ expect(task.lastError).toBe(null);
290
+ expect(task.nextRetryAt).toBe(null);
291
+ });
292
+ });
293
+
294
+ describe("depends_on", () => {
295
+ it("defaults depends_on to empty array for legacy tasks", () => {
296
+ writeLegacyTask({ id: "001", title: "Legacy", status: "todo" });
297
+ const tasks = loadTasks(tmpDir);
298
+ expect(tasks[0]!.depends_on).toEqual([]);
299
+ });
300
+
301
+ it("preserves depends_on when present in JSON", () => {
302
+ const dir = join(tmpDir, ".tasks", "tasks");
303
+ mkdirSync(dir, { recursive: true });
304
+ writeFileSync(
305
+ join(dir, "001-with-deps.json"),
306
+ JSON.stringify({
307
+ id: "001",
308
+ title: "Has deps",
309
+ status: "todo",
310
+ description: "",
311
+ goal: null,
312
+ assignee: null,
313
+ priority: 1,
314
+ created: "2026-01-01T00:00:00Z",
315
+ updated: "2026-01-01T00:00:00Z",
316
+
317
+ tags: [],
318
+ proof: null,
319
+ depends_on: ["002", "003"],
320
+ }) + "\n",
321
+ );
322
+ const tasks = loadTasks(tmpDir);
323
+ expect(tasks[0]!.depends_on).toEqual(["002", "003"]);
324
+ });
325
+
326
+ it("createTask sets depends_on to empty array", () => {
327
+ const task = createTask(tmpDir, []);
328
+ expect(task.depends_on).toEqual([]);
329
+ });
330
+ });
331
+
332
+ describe("isBlocked", () => {
333
+ const base = {
334
+ description: "",
335
+ goal: null,
336
+ assignee: null,
337
+ created: "",
338
+ updated: "",
339
+
340
+ tags: [],
341
+ proof: null,
342
+ retryCount: 0,
343
+ maxRetries: 5,
344
+ lastError: null,
345
+ nextRetryAt: null,
346
+ } as const;
347
+
348
+ it("returns false when depends_on is empty", () => {
349
+ const task: Task = {
350
+ ...base,
351
+ id: "001",
352
+ title: "A",
353
+ status: "todo",
354
+ priority: 1,
355
+ depends_on: [],
356
+ };
357
+ expect(isBlocked(task, [])).toBe(false);
358
+ });
359
+
360
+ it("returns true when a dependency is not done", () => {
361
+ const dep: Task = {
362
+ ...base,
363
+ id: "002",
364
+ title: "Dep",
365
+ status: "in-progress",
366
+ priority: 1,
367
+ depends_on: [],
368
+ };
369
+ const task: Task = {
370
+ ...base,
371
+ id: "001",
372
+ title: "A",
373
+ status: "todo",
374
+ priority: 1,
375
+ depends_on: ["002"],
376
+ };
377
+ expect(isBlocked(task, [task, dep])).toBe(true);
378
+ });
379
+
380
+ it("returns false when all dependencies are done", () => {
381
+ const dep: Task = {
382
+ ...base,
383
+ id: "002",
384
+ title: "Dep",
385
+ status: "done",
386
+ priority: 1,
387
+ depends_on: [],
388
+ };
389
+ const task: Task = {
390
+ ...base,
391
+ id: "001",
392
+ title: "A",
393
+ status: "todo",
394
+ priority: 1,
395
+ depends_on: ["002"],
396
+ };
397
+ expect(isBlocked(task, [task, dep])).toBe(false);
398
+ });
399
+
400
+ it("returns true when dependency ID is missing from task list", () => {
401
+ const task: Task = {
402
+ ...base,
403
+ id: "001",
404
+ title: "A",
405
+ status: "todo",
406
+ priority: 1,
407
+ depends_on: ["999"],
408
+ };
409
+ expect(isBlocked(task, [task])).toBe(true);
410
+ });
411
+ });
412
+
413
+ describe("CLI interop", () => {
414
+ it("widget loadTasks reads tasks created by CLI saveTask", () => {
415
+ cliEnsure(tmpDir);
416
+ cliSaveTask(tmpDir, {
417
+ id: "001",
418
+ title: "CLI task",
419
+ description: "created by CLI",
420
+ goal: "01",
421
+ status: "todo",
422
+ assignee: null,
423
+ priority: 1,
424
+ created: "2026-01-01T00:00:00Z",
425
+ updated: "2026-01-01T00:00:00Z",
426
+
427
+ tags: ["test"],
428
+ proof: null,
429
+ retryCount: 0,
430
+ maxRetries: 5,
431
+ lastError: null,
432
+ nextRetryAt: null,
433
+ depends_on: [],
434
+ });
435
+
436
+ // Widget loadTasks should see it
437
+ const tasks = loadTasks(tmpDir);
438
+ expect(tasks.length).toBe(1);
439
+ expect(tasks[0]!.id).toBe("001");
440
+ expect(tasks[0]!.title).toBe("CLI task");
441
+ expect(tasks[0]!.goal).toBe("01");
442
+ });
443
+
444
+ it("widget updateTaskStatus changes are visible to CLI loadTask", () => {
445
+ cliEnsure(tmpDir);
446
+ cliSaveTask(tmpDir, {
447
+ id: "001",
448
+ title: "Test",
449
+ description: "",
450
+ goal: null,
451
+ status: "todo",
452
+ assignee: null,
453
+ priority: 1,
454
+ created: "2026-01-01T00:00:00Z",
455
+ updated: "2026-01-01T00:00:00Z",
456
+
457
+ tags: [],
458
+ proof: null,
459
+ retryCount: 0,
460
+ maxRetries: 5,
461
+ lastError: null,
462
+ nextRetryAt: null,
463
+ depends_on: [],
464
+ });
465
+
466
+ // Widget updates the task
467
+ updateTaskStatus(tmpDir, "001", "in-progress");
468
+
469
+ // CLI should see the change
470
+ const loaded = cliLoadTask(tmpDir, "001");
471
+ expect(loaded?.status).toBe("in-progress");
472
+ });
473
+ });
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Widget-specific task utilities.
3
+ *
4
+ * Re-exports core Task type and CRUD functions from the canonical task-store,
5
+ * and adds widget-only helpers for grouping, sorting, and display logic.
6
+ */
7
+ import {
8
+ loadTasks as storeLoadTasks,
9
+ loadTask as storeLoadTask,
10
+ saveTask,
11
+ ensureTasksDir as storeEnsureTasksDir,
12
+ getTasksRoot,
13
+ type Task,
14
+ } from "../../lib/task-store.ts";
15
+
16
+ // Re-export the canonical Task type and CRUD functions
17
+ export type { Task };
18
+ export type TaskStatus = Task["status"];
19
+
20
+ export { storeEnsureTasksDir as ensureTasksDir };
21
+
22
+ export function getTasksDir(projectDir: string): string {
23
+ return getTasksRoot(projectDir);
24
+ }
25
+
26
+ export function loadTasks(projectDir: string): Task[] {
27
+ return storeLoadTasks(projectDir);
28
+ }
29
+
30
+ export function updateTaskStatus(projectDir: string, taskId: string, status: TaskStatus): void {
31
+ const task = storeLoadTask(projectDir, taskId);
32
+ if (!task) return;
33
+ task.status = status;
34
+ task.updated = new Date().toISOString();
35
+ saveTask(projectDir, task);
36
+ }
37
+
38
+ export function updateTaskAssignee(
39
+ projectDir: string,
40
+ taskId: string,
41
+ assignee: string | null,
42
+ ): void {
43
+ const task = storeLoadTask(projectDir, taskId);
44
+ if (!task) return;
45
+ task.assignee = assignee;
46
+ task.updated = new Date().toISOString();
47
+ saveTask(projectDir, task);
48
+ }
49
+
50
+ export function updateTaskField(
51
+ projectDir: string,
52
+ taskId: string,
53
+ fields: Partial<Pick<Task, "title" | "description" | "tags" | "priority">>,
54
+ ): void {
55
+ const task = storeLoadTask(projectDir, taskId);
56
+ if (!task) return;
57
+ if (fields.title !== undefined) task.title = fields.title;
58
+ if (fields.description !== undefined) task.description = fields.description;
59
+ if (fields.tags !== undefined) task.tags = fields.tags;
60
+ if (fields.priority !== undefined) task.priority = fields.priority;
61
+ task.updated = new Date().toISOString();
62
+ saveTask(projectDir, task);
63
+ }
64
+
65
+ export function createTask(projectDir: string, tasks: Task[]): Task {
66
+ storeEnsureTasksDir(projectDir);
67
+
68
+ // Find next ID
69
+ let maxId = 0;
70
+ for (const t of tasks) {
71
+ const num = parseInt(t.id, 10);
72
+ if (num > maxId) maxId = num;
73
+ }
74
+ const nextId = String(maxId + 1).padStart(3, "0");
75
+
76
+ const task: Task = {
77
+ id: nextId,
78
+ title: "New task",
79
+ description: "",
80
+ goal: null,
81
+ status: "todo",
82
+ assignee: null,
83
+ priority: 2,
84
+ created: new Date().toISOString(),
85
+ updated: new Date().toISOString(),
86
+ tags: [],
87
+ proof: null,
88
+ retryCount: 0,
89
+ maxRetries: 5,
90
+ lastError: null,
91
+ nextRetryAt: null,
92
+ depends_on: [],
93
+ milestone: null,
94
+ specialty: null,
95
+ fulfills: [],
96
+ discoveredIssues: [],
97
+ salientSummary: null,
98
+ };
99
+
100
+ saveTask(projectDir, task);
101
+ return task;
102
+ }
103
+
104
+ // --- Widget-specific helpers ---
105
+
106
+ const STATUS_ORDER: TaskStatus[] = ["in-progress", "todo", "review", "done"];
107
+
108
+ export function groupTasks(tasks: Task[]): { status: TaskStatus; tasks: Task[] }[] {
109
+ const groups: Record<TaskStatus, Task[]> = {
110
+ "in-progress": [],
111
+ todo: [],
112
+ review: [],
113
+ done: [],
114
+ };
115
+
116
+ for (const task of tasks) {
117
+ const group = groups[task.status];
118
+ if (group) group.push(task);
119
+ }
120
+
121
+ // Sort each group by priority (1 = highest)
122
+ for (const group of Object.values(groups)) {
123
+ group.sort((a, b) => a.priority - b.priority);
124
+ }
125
+
126
+ return STATUS_ORDER.map((status) => ({ status, tasks: groups[status] }));
127
+ }
128
+
129
+ export function nextStatus(current: TaskStatus): TaskStatus {
130
+ const cycle: TaskStatus[] = ["todo", "in-progress", "review", "done"];
131
+ const idx = cycle.indexOf(current);
132
+ return cycle[(idx + 1) % cycle.length]!;
133
+ }
134
+
135
+ export function isBlocked(task: Task, allTasks: Task[]): boolean {
136
+ if (task.depends_on.length === 0) return false;
137
+ const statusMap = new Map(allTasks.map((t) => [t.id, t.status]));
138
+ return task.depends_on.some((depId) => statusMap.get(depId) !== "done");
139
+ }
140
+
141
+ export type FlatItem =
142
+ | { kind: "header"; status: TaskStatus; count: number }
143
+ | { kind: "task"; task: Task; taskIndex: number };
144
+
145
+ // Build a flat list with headers interleaved — indices match DOM order
146
+ export function flattenTaskList(groups: { status: TaskStatus; tasks: Task[] }[]): FlatItem[] {
147
+ const result: FlatItem[] = [];
148
+ let taskIdx = 0;
149
+ for (const group of groups) {
150
+ if (group.tasks.length === 0) continue;
151
+ result.push({ kind: "header", status: group.status, count: group.tasks.length });
152
+ for (const task of group.tasks) {
153
+ result.push({ kind: "task", task, taskIndex: taskIdx++ });
154
+ }
155
+ }
156
+ return result;
157
+ }
@@ -0,0 +1,20 @@
1
+ # Agent Guidelines
2
+
3
+ ## Project: {{name}}
4
+
5
+ ## Boundaries
6
+
7
+ - Stay within the scope of your assigned task
8
+ - Report out-of-scope issues via --discovered-issues
9
+ - Do not modify files outside your task scope without coordination
10
+
11
+ ## Conventions
12
+
13
+ - Run tests before marking tasks done
14
+ - Use the project existing code style
15
+ - Commit messages should reference the task ID
16
+
17
+ ## Completion Protocol
18
+
19
+ - tmux-ide task done <ID> --proof "summary" --summary "key learnings"
20
+ - Report issues: include --discovered-issues for anything out of scope
@@ -0,0 +1,41 @@
1
+ name: project-name
2
+
3
+ team:
4
+ name: missions
5
+
6
+ rows:
7
+ - size: 70%
8
+ panes:
9
+ - title: Lead
10
+ command: claude
11
+ role: lead
12
+ focus: true
13
+ - title: Frontend
14
+ command: claude
15
+ role: teammate
16
+ specialty: frontend
17
+ - title: Backend
18
+ command: claude
19
+ role: teammate
20
+ specialty: backend
21
+
22
+ - size: 30%
23
+ panes:
24
+ - title: Validator
25
+ command: claude
26
+ role: validator
27
+ skill: reviewer
28
+ - title: Researcher
29
+ command: claude
30
+ role: researcher
31
+ specialty: researcher
32
+ skill: researcher
33
+ - title: Shell
34
+
35
+ orchestrator:
36
+ enabled: true
37
+ auto_dispatch: true
38
+ dispatch_mode: missions
39
+ poll_interval: 5000
40
+ stall_timeout: 300000
41
+ master_pane: Lead