tmux-ide 1.3.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 (236) hide show
  1. package/bin/cli.js +4 -216
  2. package/bin/cli.ts +536 -0
  3. package/package.json +27 -14
  4. package/src/__tests__/support.ts +186 -0
  5. package/src/attach.ts +20 -0
  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.ts +248 -0
  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.ts +212 -0
  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.ts +76 -0
  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.ts +100 -0
  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.ts +36 -0
  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.ts +27 -0
  103. package/src/ls.test.ts +76 -0
  104. package/src/ls.ts +40 -0
  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/dist/attach.d.ts +0 -3
  194. package/dist/attach.js +0 -14
  195. package/dist/config.d.ts +0 -5
  196. package/dist/config.js +0 -269
  197. package/dist/detect.d.ts +0 -15
  198. package/dist/detect.js +0 -228
  199. package/dist/doctor.d.ts +0 -3
  200. package/dist/doctor.js +0 -66
  201. package/dist/init.d.ts +0 -4
  202. package/dist/init.js +0 -67
  203. package/dist/inspect.d.ts +0 -54
  204. package/dist/inspect.js +0 -118
  205. package/dist/launch.d.ts +0 -22
  206. package/dist/launch.js +0 -174
  207. package/dist/lib/dot-path.d.ts +0 -2
  208. package/dist/lib/dot-path.js +0 -17
  209. package/dist/lib/errors.d.ts +0 -29
  210. package/dist/lib/errors.js +0 -37
  211. package/dist/lib/launch-plan.d.ts +0 -6
  212. package/dist/lib/launch-plan.js +0 -40
  213. package/dist/lib/output.d.ts +0 -9
  214. package/dist/lib/output.js +0 -65
  215. package/dist/lib/session-monitor.d.ts +0 -12
  216. package/dist/lib/session-monitor.js +0 -160
  217. package/dist/lib/session-options.d.ts +0 -13
  218. package/dist/lib/session-options.js +0 -85
  219. package/dist/lib/sizes.d.ts +0 -16
  220. package/dist/lib/sizes.js +0 -36
  221. package/dist/lib/tmux.d.ts +0 -42
  222. package/dist/lib/tmux.js +0 -225
  223. package/dist/lib/yaml-io.d.ts +0 -10
  224. package/dist/lib/yaml-io.js +0 -24
  225. package/dist/ls.d.ts +0 -3
  226. package/dist/ls.js +0 -35
  227. package/dist/restart.d.ts +0 -4
  228. package/dist/restart.js +0 -13
  229. package/dist/status.d.ts +0 -3
  230. package/dist/status.js +0 -29
  231. package/dist/stop.d.ts +0 -3
  232. package/dist/stop.js +0 -21
  233. package/dist/types.d.ts +0 -41
  234. package/dist/types.js +0 -1
  235. package/dist/validate.d.ts +0 -4
  236. package/dist/validate.js +0 -172
@@ -0,0 +1,265 @@
1
+ import { describe, it, beforeEach, afterEach, expect } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { agentIdentifier } from "../lib/orchestrator.ts";
6
+ import { ensureTasksDir, saveMission, saveTask, loadTask, type Goal } from "../lib/task-store.ts";
7
+ import { _setExecutor, type PaneInfo } from "../widgets/lib/pane-comms.ts";
8
+ import {
9
+ _setTmuxRunner,
10
+ discoverSessions,
11
+ computeStats,
12
+ computeGoalProgress,
13
+ buildOverviews,
14
+ buildProjectDetail,
15
+ updateTask,
16
+ type SessionInfo,
17
+ } from "./discovery.ts";
18
+ import { makeTask, makePane } from "../__tests__/support.ts";
19
+
20
+ let tmpDir: string;
21
+ let restoreTmux: () => void;
22
+ let restoreDiscoveryTmux: () => void;
23
+ let mockPanes: PaneInfo[];
24
+
25
+ function makeSessionInfo(overrides: Partial<SessionInfo> = {}): SessionInfo {
26
+ return {
27
+ name: "test-session",
28
+ dir: tmpDir,
29
+ mission: null,
30
+ goals: [],
31
+ tasks: [],
32
+ panes: [],
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ beforeEach(() => {
38
+ tmpDir = mkdtempSync(join(tmpdir(), "tmux-ide-cc-test-"));
39
+ ensureTasksDir(tmpDir);
40
+ mockPanes = [];
41
+
42
+ restoreTmux = _setExecutor((_cmd: string, args: string[]) => {
43
+ if (args[0] === "list-panes") {
44
+ return mockPanes
45
+ .map(
46
+ (p) =>
47
+ `${p.id}\t${p.index}\t${p.title}\t${p.currentCommand}\t${p.width}\t${p.height}\t${p.active ? "1" : "0"}\t${p.role ?? ""}\t${p.name ?? ""}\t${p.type ?? ""}`,
48
+ )
49
+ .join("\n");
50
+ }
51
+ return "";
52
+ });
53
+
54
+ restoreDiscoveryTmux = _setTmuxRunner((args: string[]) => {
55
+ if (args[0] === "list-sessions") {
56
+ return "test-session";
57
+ }
58
+ if (args[0] === "display-message") {
59
+ return tmpDir;
60
+ }
61
+ return "";
62
+ });
63
+ });
64
+
65
+ afterEach(() => {
66
+ restoreTmux();
67
+ restoreDiscoveryTmux();
68
+ rmSync(tmpDir, { recursive: true, force: true });
69
+ });
70
+
71
+ describe("discoverSessions", () => {
72
+ it("discovers sessions with tasks data", () => {
73
+ saveMission(tmpDir, {
74
+ title: "Test mission",
75
+ description: "desc",
76
+ status: "active",
77
+ branch: null,
78
+ milestones: [],
79
+ created: "2026-01-01T00:00:00Z",
80
+ updated: "2026-01-01T00:00:00Z",
81
+ });
82
+ saveTask(tmpDir, makeTask());
83
+
84
+ const sessions = discoverSessions();
85
+ expect(sessions.length).toBe(1);
86
+ expect(sessions[0]!.name).toBe("test-session");
87
+ expect(sessions[0]!.mission?.title).toBe("Test mission");
88
+ expect(sessions[0]!.tasks.length).toBe(1);
89
+ });
90
+
91
+ it("returns empty when no sessions exist", () => {
92
+ restoreDiscoveryTmux();
93
+ restoreDiscoveryTmux = _setTmuxRunner(() => "");
94
+
95
+ const sessions = discoverSessions();
96
+ expect(sessions.length).toBe(0);
97
+ });
98
+ });
99
+
100
+ describe("computeStats", () => {
101
+ it("counts tasks and agents", () => {
102
+ const info = makeSessionInfo({
103
+ tasks: [
104
+ makeTask({ id: "001", status: "done" }),
105
+ makeTask({ id: "002", status: "in-progress" }),
106
+ makeTask({ id: "003", status: "todo" }),
107
+ ],
108
+ panes: [
109
+ makePane({ id: "%1", title: "Agent 1", currentCommand: "claude" }),
110
+ makePane({ id: "%2", title: "Shell", currentCommand: "zsh" }),
111
+ ],
112
+ });
113
+
114
+ const stats = computeStats(info);
115
+ expect(stats.totalTasks).toBe(3);
116
+ expect(stats.doneTasks).toBe(1);
117
+ expect(stats.agents).toBe(1); // only claude pane
118
+ expect(stats.activeAgents).toBe(0); // no spinner
119
+ });
120
+
121
+ it("detects active agents with spinner", () => {
122
+ const info = makeSessionInfo({
123
+ panes: [makePane({ id: "%1", title: "⠙ Working...", currentCommand: "claude" })],
124
+ });
125
+
126
+ const stats = computeStats(info);
127
+ expect(stats.agents).toBe(1);
128
+ expect(stats.activeAgents).toBe(1);
129
+ });
130
+ });
131
+
132
+ describe("computeGoalProgress", () => {
133
+ it("computes progress per goal", () => {
134
+ const goals: Goal[] = [
135
+ {
136
+ id: "01",
137
+ title: "Goal A",
138
+ description: "",
139
+ status: "in-progress",
140
+ acceptance: "",
141
+ priority: 1,
142
+ created: "2026-01-01T00:00:00Z",
143
+ updated: "2026-01-01T00:00:00Z",
144
+ assignee: null,
145
+ specialty: null,
146
+ milestone: null,
147
+ },
148
+ ];
149
+ const tasks = [
150
+ makeTask({ id: "001", goal: "01", status: "done" }),
151
+ makeTask({ id: "002", goal: "01", status: "todo" }),
152
+ makeTask({ id: "003", goal: "01", status: "done" }),
153
+ makeTask({ id: "004", goal: null, status: "todo" }),
154
+ ];
155
+
156
+ const progress = computeGoalProgress(goals, tasks);
157
+ expect(progress.length).toBe(1);
158
+ expect(progress[0]!.id).toBe("01");
159
+ expect(progress[0]!.progress).toBe(67); // 2/3
160
+ });
161
+
162
+ it("returns 0 progress for goals with no tasks", () => {
163
+ const goals: Goal[] = [
164
+ {
165
+ id: "01",
166
+ title: "Empty",
167
+ description: "",
168
+ status: "todo",
169
+ acceptance: "",
170
+ priority: 1,
171
+ created: "2026-01-01T00:00:00Z",
172
+ updated: "2026-01-01T00:00:00Z",
173
+ assignee: null,
174
+ specialty: null,
175
+ milestone: null,
176
+ },
177
+ ];
178
+
179
+ const progress = computeGoalProgress(goals, []);
180
+ expect(progress[0]!.progress).toBe(0);
181
+ });
182
+ });
183
+
184
+ describe("buildOverviews", () => {
185
+ it("builds overview with stats for each session", () => {
186
+ const sessions: SessionInfo[] = [
187
+ makeSessionInfo({
188
+ mission: {
189
+ title: "Ship it",
190
+ description: "fast",
191
+ created: "2026-01-01T00:00:00Z",
192
+ updated: "2026-01-01T00:00:00Z",
193
+ },
194
+ tasks: [makeTask({ id: "001", status: "done" }), makeTask({ id: "002", status: "todo" })],
195
+ }),
196
+ ];
197
+
198
+ const overviews = buildOverviews(sessions);
199
+ expect(overviews.length).toBe(1);
200
+ expect(overviews[0]!.mission?.title).toBe("Ship it");
201
+ expect(overviews[0]!.stats.totalTasks).toBe(2);
202
+ expect(overviews[0]!.stats.doneTasks).toBe(1);
203
+ });
204
+ });
205
+
206
+ describe("buildProjectDetail", () => {
207
+ it("builds detail with agents matched to tasks", () => {
208
+ const pane = makePane({ id: "%1", index: 0, title: "Agent 1", currentCommand: "claude" });
209
+ const name = agentIdentifier(pane);
210
+ const info = makeSessionInfo({
211
+ tasks: [
212
+ makeTask({
213
+ id: "001",
214
+ status: "in-progress",
215
+ assignee: name,
216
+ updated: new Date().toISOString(),
217
+ }),
218
+ ],
219
+ panes: [pane],
220
+ });
221
+
222
+ const detail = buildProjectDetail(info);
223
+ expect(detail.agents.length).toBe(1);
224
+ expect(detail.agents[0]!.paneTitle).toBe(name);
225
+ expect(detail.agents[0]!.taskTitle).toBe("Test task");
226
+ expect(detail.agents[0]!.taskId).toBe("001");
227
+ });
228
+
229
+ it("shows idle agent with no task", () => {
230
+ const info = makeSessionInfo({
231
+ tasks: [],
232
+ panes: [makePane({ id: "%1", title: "Agent 1", currentCommand: "claude" })],
233
+ });
234
+
235
+ const detail = buildProjectDetail(info);
236
+ expect(detail.agents[0]!.taskTitle).toBe(null);
237
+ expect(detail.agents[0]!.isBusy).toBe(false);
238
+ });
239
+ });
240
+
241
+ describe("updateTask", () => {
242
+ it("updates task status and returns updated task", () => {
243
+ saveTask(tmpDir, makeTask({ id: "001", status: "todo" }));
244
+
245
+ const result = updateTask(tmpDir, "001", { status: "in-progress" });
246
+ expect(result).toBeTruthy();
247
+ expect(result.status).toBe("in-progress");
248
+
249
+ const loaded = loadTask(tmpDir, "001");
250
+ expect(loaded?.status).toBe("in-progress");
251
+ });
252
+
253
+ it("updates assignee", () => {
254
+ saveTask(tmpDir, makeTask({ id: "001" }));
255
+
256
+ const result = updateTask(tmpDir, "001", { assignee: "Agent 1" });
257
+ expect(result).toBeTruthy();
258
+ expect(result.assignee).toBe("Agent 1");
259
+ });
260
+
261
+ it("returns null for non-existent task", () => {
262
+ const result = updateTask(tmpDir, "999", { status: "done" });
263
+ expect(result).toBe(null);
264
+ });
265
+ });
@@ -0,0 +1,296 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ loadMission,
6
+ loadGoals,
7
+ loadTasks,
8
+ saveTask,
9
+ loadTask,
10
+ type Mission,
11
+ type Goal,
12
+ type Task,
13
+ } from "../lib/task-store.ts";
14
+ import { listSessionPanes, type PaneInfo } from "../widgets/lib/pane-comms.ts";
15
+ import { isAgentPane, isAgentBusy, agentIdentifier } from "../lib/orchestrator.ts";
16
+ import { loadValidationState } from "../lib/validation.ts";
17
+ import { loadSkills } from "../lib/skill-registry.ts";
18
+
19
+ export interface SessionInfo {
20
+ name: string;
21
+ dir: string;
22
+ mission: Mission | null;
23
+ goals: Goal[];
24
+ tasks: Task[];
25
+ panes: PaneInfo[];
26
+ }
27
+
28
+ export interface SessionStats {
29
+ totalTasks: number;
30
+ doneTasks: number;
31
+ agents: number;
32
+ activeAgents: number;
33
+ }
34
+
35
+ export interface SessionOverview {
36
+ name: string;
37
+ dir: string;
38
+ mission: Mission | null;
39
+ stats: SessionStats;
40
+ goals: { id: string; title: string; progress: number }[];
41
+ }
42
+
43
+ export interface AgentDetail {
44
+ paneTitle: string;
45
+ paneId: string;
46
+ isBusy: boolean;
47
+ taskTitle: string | null;
48
+ taskId: string | null;
49
+ elapsed: string;
50
+ }
51
+
52
+ export interface ValidationSummary {
53
+ total: number;
54
+ passing: number;
55
+ failing: number;
56
+ pending: number;
57
+ blocked: number;
58
+ }
59
+
60
+ export interface SkillSummary {
61
+ name: string;
62
+ specialties: string[];
63
+ }
64
+
65
+ export interface ProjectDetail {
66
+ session: string;
67
+ dir: string;
68
+ mission: Mission | null;
69
+ goals: Goal[];
70
+ tasks: Task[];
71
+ agents: AgentDetail[];
72
+ milestones: Mission["milestones"];
73
+ validationSummary: ValidationSummary;
74
+ skills: SkillSummary[];
75
+ }
76
+
77
+ type TmuxRunner = (args: string[]) => string;
78
+
79
+ let _tmuxRunner: TmuxRunner = (args) =>
80
+ execFileSync("tmux", args, {
81
+ encoding: "utf-8",
82
+ stdio: ["ignore", "pipe", "ignore"],
83
+ }).trim();
84
+
85
+ export function _setTmuxRunner(fn: TmuxRunner): () => void {
86
+ const prev = _tmuxRunner;
87
+ _tmuxRunner = fn;
88
+ return () => {
89
+ _tmuxRunner = prev;
90
+ };
91
+ }
92
+
93
+ function tmuxSilent(args: string[]): string {
94
+ try {
95
+ return _tmuxRunner(args);
96
+ } catch {
97
+ return "";
98
+ }
99
+ }
100
+
101
+ export function listTmuxSessions(): string[] {
102
+ const raw = tmuxSilent(["list-sessions", "-F", "#{session_name}"]);
103
+ if (!raw) return [];
104
+ return raw.split("\n").filter(Boolean);
105
+ }
106
+
107
+ export function getSessionCwd(session: string): string {
108
+ return tmuxSilent(["display-message", "-t", session, "-p", "#{pane_current_path}"]);
109
+ }
110
+
111
+ function formatElapsed(isoDate: string): string {
112
+ const ms = Date.now() - new Date(isoDate).getTime();
113
+ const mins = Math.floor(ms / 60000);
114
+ if (mins < 1) return "<1m";
115
+ if (mins < 60) return `${mins}m`;
116
+ return `${Math.floor(mins / 60)}h ${mins % 60}m`;
117
+ }
118
+
119
+ export function discoverSessions(): SessionInfo[] {
120
+ const sessionNames = listTmuxSessions();
121
+ const results: SessionInfo[] = [];
122
+
123
+ for (const name of sessionNames) {
124
+ const dir = getSessionCwd(name);
125
+ if (!dir) continue;
126
+
127
+ const tasksDir = join(dir, ".tasks");
128
+ const hasTasks = existsSync(tasksDir);
129
+
130
+ const mission = hasTasks ? loadMission(dir) : null;
131
+ const goals = hasTasks ? loadGoals(dir) : [];
132
+ const tasks = hasTasks ? loadTasks(dir) : [];
133
+
134
+ let panes: PaneInfo[] = [];
135
+ try {
136
+ panes = listSessionPanes(name);
137
+ } catch {
138
+ // session may have vanished
139
+ }
140
+
141
+ results.push({ name, dir, mission, goals, tasks, panes });
142
+ }
143
+
144
+ return results;
145
+ }
146
+
147
+ export function computeStats(info: SessionInfo): SessionStats {
148
+ const agentPanes = info.panes.filter((p) => isAgentPane(p));
149
+ const activeAgents = agentPanes.filter((p) => isAgentBusy(p)).length;
150
+
151
+ return {
152
+ totalTasks: info.tasks.length,
153
+ doneTasks: info.tasks.filter((t) => t.status === "done").length,
154
+ agents: agentPanes.length,
155
+ activeAgents,
156
+ };
157
+ }
158
+
159
+ export function computeGoalProgress(
160
+ goals: Goal[],
161
+ tasks: Task[],
162
+ ): { id: string; title: string; progress: number }[] {
163
+ return goals.map((g) => {
164
+ const goalTasks = tasks.filter((t) => t.goal === g.id);
165
+ const done = goalTasks.filter((t) => t.status === "done").length;
166
+ const progress = goalTasks.length > 0 ? Math.round((done / goalTasks.length) * 100) : 0;
167
+ return { id: g.id, title: g.title, progress };
168
+ });
169
+ }
170
+
171
+ export function buildOverviews(sessions: SessionInfo[]): SessionOverview[] {
172
+ return sessions.map((s) => ({
173
+ name: s.name,
174
+ dir: s.dir,
175
+ mission: s.mission,
176
+ stats: computeStats(s),
177
+ goals: computeGoalProgress(s.goals, s.tasks),
178
+ }));
179
+ }
180
+
181
+ export function buildProjectDetail(info: SessionInfo): ProjectDetail {
182
+ const agents: AgentDetail[] = info.panes
183
+ .filter((p) => isAgentPane(p))
184
+ .map((pane) => {
185
+ const name = agentIdentifier(pane);
186
+ const assignedTask = info.tasks.find(
187
+ (t) => t.assignee === name && t.status === "in-progress",
188
+ );
189
+ return {
190
+ paneTitle: name,
191
+ paneId: pane.id,
192
+ isBusy: isAgentBusy(pane),
193
+ taskTitle: assignedTask?.title ?? null,
194
+ taskId: assignedTask?.id ?? null,
195
+ elapsed: assignedTask ? formatElapsed(assignedTask.updated) : "",
196
+ };
197
+ });
198
+
199
+ const valState = loadValidationState(info.dir);
200
+ const assertions = valState ? Object.values(valState.assertions) : [];
201
+ const validationSummary: ValidationSummary = {
202
+ total: assertions.length,
203
+ passing: assertions.filter((a) => a.status === "passing").length,
204
+ failing: assertions.filter((a) => a.status === "failing").length,
205
+ pending: assertions.filter((a) => a.status === "pending").length,
206
+ blocked: assertions.filter((a) => a.status === "blocked").length,
207
+ };
208
+
209
+ const skills = loadSkills(info.dir).map((s) => ({
210
+ name: s.name,
211
+ specialties: s.specialties,
212
+ }));
213
+
214
+ return {
215
+ session: info.name,
216
+ dir: info.dir,
217
+ mission: info.mission,
218
+ goals: info.goals,
219
+ tasks: info.tasks,
220
+ agents,
221
+ milestones: info.mission?.milestones ?? [],
222
+ validationSummary,
223
+ skills,
224
+ };
225
+ }
226
+
227
+ export interface OrchestratorSnapshot {
228
+ running: boolean;
229
+ claimedCount: number;
230
+ agents: AgentDetail[];
231
+ inProgressCount: number;
232
+ pendingCount: number;
233
+ }
234
+
235
+ export function buildOrchestratorSnapshot(info: SessionInfo): OrchestratorSnapshot {
236
+ const agents: AgentDetail[] = info.panes
237
+ .filter((p) => isAgentPane(p))
238
+ .map((pane) => {
239
+ const name = agentIdentifier(pane);
240
+ const assignedTask = info.tasks.find(
241
+ (t) => t.assignee === name && t.status === "in-progress",
242
+ );
243
+ return {
244
+ paneTitle: name,
245
+ paneId: pane.id,
246
+ isBusy: isAgentBusy(pane),
247
+ taskTitle: assignedTask?.title ?? null,
248
+ taskId: assignedTask?.id ?? null,
249
+ elapsed: assignedTask ? formatElapsed(assignedTask.updated) : "",
250
+ };
251
+ });
252
+
253
+ return {
254
+ running: agents.length > 0,
255
+ claimedCount: info.tasks.filter((t) => t.status === "in-progress").length,
256
+ agents,
257
+ inProgressCount: info.tasks.filter((t) => t.status === "in-progress").length,
258
+ pendingCount: info.tasks.filter((t) => t.status === "todo").length,
259
+ };
260
+ }
261
+
262
+ export function updateTask(
263
+ dir: string,
264
+ taskId: string,
265
+ fields: {
266
+ status?: string;
267
+ assignee?: string;
268
+ title?: string;
269
+ description?: string;
270
+ priority?: number;
271
+ },
272
+ ): Task | null {
273
+ const task = loadTask(dir, taskId);
274
+ if (!task) return null;
275
+
276
+ if (fields.status) {
277
+ const validStatuses = ["todo", "in-progress", "review", "done"];
278
+ if (!validStatuses.includes(fields.status)) return null;
279
+ task.status = fields.status as Task["status"];
280
+ }
281
+ if (fields.assignee !== undefined) {
282
+ task.assignee = fields.assignee || null;
283
+ }
284
+ if (fields.title !== undefined) {
285
+ task.title = fields.title;
286
+ }
287
+ if (fields.description !== undefined) {
288
+ task.description = fields.description;
289
+ }
290
+ if (fields.priority !== undefined) {
291
+ task.priority = fields.priority;
292
+ }
293
+ task.updated = new Date().toISOString();
294
+ saveTask(dir, task);
295
+ return task;
296
+ }
@@ -0,0 +1,31 @@
1
+ import { createServer, type Server } from "node:http";
2
+ import { getRequestListener } from "@hono/node-server";
3
+ import { createApp, type CreateAppOptions } from "./server.ts";
4
+ import { AuthService } from "../lib/auth/auth-service.ts";
5
+ import type { AuthConfig } from "../lib/auth/types.ts";
6
+
7
+ export interface CommandCenterOptions {
8
+ port?: number;
9
+ hostname?: string;
10
+ authService?: AuthService;
11
+ authConfig?: AuthConfig;
12
+ }
13
+
14
+ export async function startCommandCenter(options: CommandCenterOptions = {}): Promise<Server> {
15
+ const port = options.port ?? 6060;
16
+ const hostname = options.hostname ?? "0.0.0.0";
17
+ const appOpts: CreateAppOptions = {};
18
+ if (options.authService) appOpts.authService = options.authService;
19
+ if (options.authConfig) appOpts.authConfig = options.authConfig;
20
+ const app = createApp(appOpts);
21
+
22
+ const listener = getRequestListener(app.fetch);
23
+ const server = createServer(listener);
24
+
25
+ return new Promise((resolve) => {
26
+ server.listen(port, hostname, () => {
27
+ console.log(`Command Center API on http://${hostname}:${port}`);
28
+ resolve(server);
29
+ });
30
+ });
31
+ }
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+
3
+ export const updateTaskSchema = z.object({
4
+ status: z.enum(["todo", "in-progress", "review", "done"]).optional(),
5
+ assignee: z.string().optional(),
6
+ title: z.string().optional(),
7
+ description: z.string().optional(),
8
+ priority: z.number().optional(),
9
+ });
10
+
11
+ export const createTaskSchema = z.object({
12
+ title: z.string().trim().min(1, "Title is required"),
13
+ description: z.string().optional(),
14
+ priority: z.number().optional(),
15
+ goal: z.string().optional(),
16
+ tags: z.array(z.string()).optional(),
17
+ });
18
+
19
+ export const savePlanSchema = z.object({
20
+ content: z.string(),
21
+ });
22
+
23
+ export const sendCommandSchema = z.object({
24
+ target: z.string().min(1, "Target pane is required"),
25
+ message: z.string().min(1, "Message is required"),
26
+ noEnter: z.boolean().optional(),
27
+ });
28
+
29
+ export const createMilestoneSchema = z.object({
30
+ title: z.string().trim().min(1, "Title is required"),
31
+ sequence: z.number().int().positive(),
32
+ description: z.string().optional(),
33
+ });
34
+
35
+ export const updateMilestoneSchema = z.object({
36
+ status: z.enum(["locked", "active", "done", "validating"]).optional(),
37
+ title: z.string().optional(),
38
+ description: z.string().optional(),
39
+ });
40
+
41
+ export const updateAssertionSchema = z.object({
42
+ status: z.enum(["pending", "passing", "failing", "blocked"]),
43
+ evidence: z.string().optional(),
44
+ verifiedBy: z.string().optional(),
45
+ });
46
+
47
+ export const triggerResearchSchema = z.object({
48
+ type: z.string().trim().min(1, "Research type is required"),
49
+ });
50
+
51
+ export const launchSchema = z
52
+ .object({
53
+ attach: z.boolean().optional(),
54
+ })
55
+ .optional();
56
+
57
+ export const stopSchema = z.object({}).optional();