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
package/src/ls.test.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { describe, it, beforeEach, afterEach, expect } from "bun:test";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const cli = join(__dirname, "..", "bin", "cli.js");
8
+
9
+ // ls.js uses execSync directly (not our tmux.js layer), so we test via CLI
10
+ // to avoid needing to mock execSync at the module level.
11
+
12
+ let origLog;
13
+ let logged;
14
+
15
+ beforeEach(() => {
16
+ logged = [];
17
+ origLog = console.log;
18
+ console.log = (...a) => logged.push(a.join(" "));
19
+ });
20
+
21
+ afterEach(() => {
22
+ console.log = origLog;
23
+ });
24
+
25
+ describe("ls", () => {
26
+ it("returns sessions array via CLI --json", () => {
27
+ const result = spawnSync("node", [cli, "ls", "--json"], {
28
+ encoding: "utf-8",
29
+ timeout: 10000,
30
+ });
31
+ expect(result.status).toBe(0);
32
+ const output = JSON.parse(result.stdout);
33
+ expect(Array.isArray(output.sessions)).toBeTruthy();
34
+ });
35
+
36
+ it("parses session list with expected fields", () => {
37
+ const result = spawnSync("node", [cli, "ls", "--json"], {
38
+ encoding: "utf-8",
39
+ timeout: 10000,
40
+ });
41
+ const output = JSON.parse(result.stdout);
42
+ // If there are sessions, each should have name, created, attached fields
43
+ for (const session of output.sessions) {
44
+ expect(typeof session.name === "string").toBeTruthy();
45
+ expect(typeof session.created === "string").toBeTruthy();
46
+ expect(typeof session.attached === "boolean").toBeTruthy();
47
+ }
48
+ });
49
+
50
+ it("returns valid JSON structure even if sessions exist", () => {
51
+ const result = spawnSync("node", [cli, "ls", "--json"], {
52
+ encoding: "utf-8",
53
+ timeout: 10000,
54
+ });
55
+ expect(result.status).toBe(0);
56
+ const output = JSON.parse(result.stdout);
57
+ expect(Object.hasOwn(output, "sessions")).toBeTruthy();
58
+ expect(Array.isArray(output.sessions)).toBeTruthy();
59
+ // Each session must have the expected shape
60
+ for (const s of output.sessions) {
61
+ expect(typeof s.name === "string" && s.name.length > 0).toBeTruthy();
62
+ expect(typeof s.created === "string").toBeTruthy();
63
+ expect(typeof s.attached === "boolean").toBeTruthy();
64
+ }
65
+ });
66
+
67
+ it("prints human-readable output without --json", () => {
68
+ // Just verify it doesn't crash and produces output
69
+ const result = spawnSync("node", [cli, "ls"], {
70
+ encoding: "utf-8",
71
+ timeout: 10000,
72
+ });
73
+ expect(result.status).toBe(0);
74
+ expect(result.stdout.length > 0).toBeTruthy();
75
+ });
76
+ });
package/src/ls.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { execSync } from "node:child_process";
2
+
3
+ export async function ls({ json }: { json?: boolean } = {}): Promise<void> {
4
+ let raw: string;
5
+ try {
6
+ raw = execSync(
7
+ 'tmux list-sessions -F "#{session_name}|#{session_created}|#{session_attached}"',
8
+ { encoding: "utf-8" },
9
+ ).trim();
10
+ } catch {
11
+ if (json) {
12
+ console.log(JSON.stringify({ sessions: [] }));
13
+ } else {
14
+ console.log("No tmux sessions running.");
15
+ }
16
+ return;
17
+ }
18
+
19
+ const sessions = raw.split("\n").map((line) => {
20
+ const [name, created, attached] = line.split("|");
21
+ return {
22
+ name,
23
+ created: new Date(parseInt(created!) * 1000).toISOString(),
24
+ attached: attached !== "0",
25
+ };
26
+ });
27
+
28
+ if (json) {
29
+ console.log(JSON.stringify({ sessions }, null, 2));
30
+ return;
31
+ }
32
+
33
+ // Table output
34
+ console.log("SESSION".padEnd(24) + "CREATED".padEnd(22) + "ATTACHED");
35
+ console.log("─".repeat(54));
36
+ for (const s of sessions) {
37
+ const date = new Date(s.created).toLocaleString();
38
+ console.log(s.name!.padEnd(24) + date.padEnd(22) + (s.attached ? "yes" : "no"));
39
+ }
40
+ }
@@ -0,0 +1,177 @@
1
+ import { resolve } from "node:path";
2
+ import { computeMetrics, loadMissionHistory } from "./lib/metrics.ts";
3
+
4
+ function fmtDuration(ms: number): string {
5
+ if (ms < 60000) return `${Math.round(ms / 1000)}s`;
6
+ if (ms < 3600000) return `${Math.round(ms / 60000)}m`;
7
+ const h = Math.floor(ms / 3600000);
8
+ const m = Math.round((ms % 3600000) / 60000);
9
+ return `${h}h ${m}m`;
10
+ }
11
+
12
+ function fmtPct(n: number): string {
13
+ return `${Math.round(n * 100)}%`;
14
+ }
15
+
16
+ export async function metricsCommand(
17
+ targetDir: string | undefined,
18
+ {
19
+ json = false,
20
+ sub,
21
+ }: {
22
+ json?: boolean;
23
+ sub?: string;
24
+ },
25
+ ): Promise<void> {
26
+ const dir = resolve(targetDir ?? ".");
27
+
28
+ switch (sub) {
29
+ case "agents": {
30
+ const metrics = computeMetrics(dir);
31
+ if (json) {
32
+ console.log(JSON.stringify(metrics.agents, null, 2));
33
+ return;
34
+ }
35
+ if (metrics.agents.length === 0) {
36
+ console.log("No agent data yet.");
37
+ return;
38
+ }
39
+ console.log("Agent Performance:");
40
+ for (const a of metrics.agents) {
41
+ const specs = a.specialties.length > 0 ? ` [${a.specialties.join(", ")}]` : "";
42
+ console.log(` ${a.name}${specs}`);
43
+ console.log(
44
+ ` Tasks: ${a.taskCount} Retries: ${a.retryCount} Utilization: ${fmtPct(a.utilization)}`,
45
+ );
46
+ console.log(
47
+ ` Active: ${fmtDuration(a.activeTimeMs)} Idle: ${fmtDuration(a.idleTimeMs)}`,
48
+ );
49
+ }
50
+ break;
51
+ }
52
+ case "timeline": {
53
+ const metrics = computeMetrics(dir);
54
+ if (json) {
55
+ console.log(JSON.stringify(metrics.timeline, null, 2));
56
+ return;
57
+ }
58
+ if (metrics.timeline.length === 0) {
59
+ console.log("No timeline data yet.");
60
+ return;
61
+ }
62
+ console.log("Activity Timeline:");
63
+ for (const t of metrics.timeline.slice(-20)) {
64
+ const ts = new Date(t.timestamp).toLocaleTimeString();
65
+ console.log(
66
+ ` ${ts} done:${t.completedTasks} active:${t.activeTasks} busy:${t.busyAgents} idle:${t.idleAgents}`,
67
+ );
68
+ }
69
+ break;
70
+ }
71
+ case "eval": {
72
+ const metrics = computeMetrics(dir);
73
+ if (json) {
74
+ const bottlenecks = [...metrics.tasks.byMilestone]
75
+ .sort((a, b) => b.durationMs - a.durationMs)
76
+ .slice(0, 3);
77
+ const highRetryAgents = [...metrics.agents]
78
+ .filter((a) => a.retryCount > 0)
79
+ .sort((a, b) => b.retryCount - a.retryCount);
80
+ const ranking = [...metrics.agents].sort((a, b) => b.utilization - a.utilization);
81
+ console.log(JSON.stringify({ bottlenecks, highRetryAgents, ranking }, null, 2));
82
+ return;
83
+ }
84
+ console.log("Evaluation:");
85
+ // Bottleneck milestones
86
+ const slowest = [...metrics.tasks.byMilestone]
87
+ .filter((m) => m.durationMs > 0)
88
+ .sort((a, b) => b.durationMs - a.durationMs);
89
+ if (slowest.length > 0) {
90
+ console.log(" Slowest milestones:");
91
+ for (const m of slowest.slice(0, 3)) {
92
+ console.log(
93
+ ` ${m.id} ${m.title}: ${fmtDuration(m.durationMs)} (${m.completedCount}/${m.taskCount} tasks)`,
94
+ );
95
+ }
96
+ }
97
+ // High-retry agents
98
+ const retryAgents = metrics.agents.filter((a) => a.retryCount > 0);
99
+ if (retryAgents.length > 0) {
100
+ console.log(" High-retry agents:");
101
+ for (const a of retryAgents.sort((x, y) => y.retryCount - x.retryCount)) {
102
+ console.log(` ${a.name}: ${a.retryCount} retries across ${a.taskCount} tasks`);
103
+ }
104
+ }
105
+ // Agent ranking by utilization
106
+ if (metrics.agents.length > 0) {
107
+ console.log(" Agent ranking (by utilization):");
108
+ for (const a of [...metrics.agents].sort((x, y) => y.utilization - x.utilization)) {
109
+ console.log(` ${a.name}: ${fmtPct(a.utilization)} util, ${a.taskCount} tasks`);
110
+ }
111
+ }
112
+ break;
113
+ }
114
+ case "history": {
115
+ const history = loadMissionHistory(dir);
116
+ if (json) {
117
+ console.log(JSON.stringify(history, null, 2));
118
+ return;
119
+ }
120
+ if (history.length === 0) {
121
+ console.log("No mission history yet.");
122
+ return;
123
+ }
124
+ console.log("Mission History:");
125
+ for (const m of history) {
126
+ console.log(` ${m.title} [${m.status}]`);
127
+ console.log(` Completed: ${m.completedAt} Duration: ${fmtDuration(m.wallClockMs)}`);
128
+ console.log(
129
+ ` Tasks: ${m.taskCount} (${fmtPct(m.completionRate)} done) Agents: ${m.agentCount} Milestones: ${m.milestonesCompleted}`,
130
+ );
131
+ }
132
+ break;
133
+ }
134
+ case "help":
135
+ console.log(`Usage: tmux-ide metrics [subcommand] [--json]
136
+
137
+ (default) Session summary — duration, task rates, agent utilization
138
+ agents Per-agent table — tasks, time, utilization, specialties
139
+ timeline Activity timeline — sampled every 5 minutes
140
+ eval Evaluation — bottleneck milestones, high-retry tasks, agent ranking
141
+ history Cross-mission comparison from history`);
142
+ break;
143
+ default: {
144
+ // Default: session summary
145
+ const metrics = computeMetrics(dir);
146
+ if (json) {
147
+ console.log(JSON.stringify(metrics, null, 2));
148
+ return;
149
+ }
150
+ console.log("Session Metrics:");
151
+ console.log(` Status: ${metrics.session.status}`);
152
+ console.log(` Duration: ${fmtDuration(metrics.session.durationMs)}`);
153
+ console.log(` Agents: ${metrics.session.agentCount}`);
154
+ console.log();
155
+ console.log("Tasks:");
156
+ console.log(
157
+ ` Total: ${metrics.tasks.total} Done: ${metrics.tasks.completed} Failed: ${metrics.tasks.failed}`,
158
+ );
159
+ console.log(
160
+ ` Completion rate: ${fmtPct(metrics.tasks.completionRate)} Retry rate: ${fmtPct(metrics.tasks.retryRate)}`,
161
+ );
162
+ if (metrics.tasks.avgDurationMs > 0) {
163
+ console.log(
164
+ ` Avg duration: ${fmtDuration(metrics.tasks.avgDurationMs)} Median: ${fmtDuration(metrics.tasks.medianDurationMs)} P90: ${fmtDuration(metrics.tasks.p90DurationMs)}`,
165
+ );
166
+ }
167
+ if (metrics.mission.title) {
168
+ console.log();
169
+ console.log(`Mission: ${metrics.mission.title} [${metrics.mission.status}]`);
170
+ console.log(` Milestones: ${metrics.mission.milestonesCompleted} completed`);
171
+ console.log(` Validation: ${fmtPct(metrics.mission.validationPassRate)} pass rate`);
172
+ console.log(` Wall clock: ${fmtDuration(metrics.mission.wallClockMs)}`);
173
+ }
174
+ break;
175
+ }
176
+ }
177
+ }
package/src/notify.ts ADDED
@@ -0,0 +1,85 @@
1
+ import { resolve } from "node:path";
2
+ import { getSessionName } from "./lib/yaml-io.ts";
3
+ import { getSessionState } from "./lib/tmux.ts";
4
+ import { listSessionPanes, sendCommand } from "./widgets/lib/pane-comms.ts";
5
+ import { appendEvent } from "./lib/event-log.ts";
6
+ import { IdeError } from "./lib/errors.ts";
7
+
8
+ interface NotifyOptions {
9
+ message?: string;
10
+ json?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Send a short notification message to the lead/master pane.
15
+ *
16
+ * This is the counterpart to `tmux-ide dispatch` — dispatch sends tasks
17
+ * TO agents, notify sends completion messages BACK to the master.
18
+ *
19
+ * The message is always kept under 200 chars to avoid triggering the
20
+ * paste preview in Claude Code's TUI.
21
+ */
22
+ export async function notify(
23
+ targetDir: string | null | undefined,
24
+ opts: NotifyOptions,
25
+ ): Promise<void> {
26
+ const dir = resolve(targetDir ?? ".");
27
+ const { message, json } = opts;
28
+ const { name: session } = getSessionName(dir);
29
+
30
+ if (!message) {
31
+ throw new IdeError("Missing message. Usage: tmux-ide notify <message>", {
32
+ code: "USAGE",
33
+ });
34
+ }
35
+
36
+ const state = getSessionState(session);
37
+ if (!state.running) {
38
+ throw new IdeError(`Session "${session}" is not running`, {
39
+ code: "SESSION_NOT_FOUND",
40
+ });
41
+ }
42
+
43
+ const panes = listSessionPanes(session);
44
+ const leadPane = panes.find((p) => p.role === "lead") ?? panes[0];
45
+
46
+ if (!leadPane) {
47
+ throw new IdeError("No panes found in session", {
48
+ code: "PANE_NOT_FOUND",
49
+ });
50
+ }
51
+
52
+ // Keep message under 200 chars to avoid paste preview
53
+ const short = message.length > 180 ? message.slice(0, 177) + "..." : message;
54
+
55
+ sendCommand(session, leadPane.id, short);
56
+
57
+ // Log notify event
58
+ appendEvent(dir, {
59
+ timestamp: new Date().toISOString(),
60
+ type: "notify",
61
+ target: leadPane.name ?? leadPane.title,
62
+ paneId: leadPane.id,
63
+ message: short,
64
+ });
65
+
66
+ if (json) {
67
+ console.log(
68
+ JSON.stringify({
69
+ ok: true,
70
+ session,
71
+ target: {
72
+ paneId: leadPane.id,
73
+ name: leadPane.name,
74
+ title: leadPane.title,
75
+ role: leadPane.role,
76
+ },
77
+ message: short,
78
+ }),
79
+ );
80
+ return;
81
+ }
82
+
83
+ const label = leadPane.name ?? leadPane.title;
84
+ console.log(`Notified "${label}" (${leadPane.id}): ${short}`);
85
+ }
@@ -0,0 +1,228 @@
1
+ import { resolve } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { getSessionName, readConfig } from "./lib/yaml-io.ts";
5
+ import { getSessionState } from "./lib/tmux.ts";
6
+ import { loadTasks, loadMission } from "./lib/task-store.ts";
7
+ import { readEvents } from "./lib/event-log.ts";
8
+ import { listSessionPanes } from "./widgets/lib/pane-comms.ts";
9
+ import { isAgentPane, isAgentBusy, agentIdentifier } from "./lib/orchestrator.ts";
10
+
11
+ function formatElapsed(ms: number): string {
12
+ if (ms < 60000) return `${Math.round(ms / 1000)}s`;
13
+ if (ms < 3600000) return `${Math.round(ms / 60000)}m`;
14
+ return `${(ms / 3600000).toFixed(1)}h`;
15
+ }
16
+
17
+ function formatRelative(iso: string): string {
18
+ const ms = Date.now() - new Date(iso).getTime();
19
+ if (ms < 60000) return `${Math.round(ms / 1000)}s ago`;
20
+ if (ms < 3600000) return `${Math.round(ms / 60000)}m ago`;
21
+ return `${(ms / 3600000).toFixed(1)}h ago`;
22
+ }
23
+
24
+ export async function orchestratorStatus(
25
+ targetDir: string | undefined,
26
+ { json }: { json?: boolean } = {},
27
+ ): Promise<void> {
28
+ const dir = resolve(targetDir ?? ".");
29
+ const { name: session } = getSessionName(dir);
30
+
31
+ // Read config
32
+ let orchConfig: {
33
+ autoDispatch: boolean;
34
+ dispatchMode: string;
35
+ pollInterval: number;
36
+ stallTimeout: number;
37
+ maxConcurrentAgents: number;
38
+ masterPane: string | null;
39
+ } | null = null;
40
+
41
+ try {
42
+ const { config } = readConfig(dir);
43
+ const orch = config.orchestrator;
44
+ if (orch) {
45
+ orchConfig = {
46
+ autoDispatch: orch.auto_dispatch ?? true,
47
+ dispatchMode: orch.dispatch_mode ?? "tasks",
48
+ pollInterval: orch.poll_interval ?? 5000,
49
+ stallTimeout: orch.stall_timeout ?? 300000,
50
+ maxConcurrentAgents: orch.max_concurrent_agents ?? 10,
51
+ masterPane: orch.master_pane ?? null,
52
+ };
53
+ }
54
+ } catch {
55
+ // No config or unreadable
56
+ }
57
+
58
+ // Check if session is running
59
+ const state = getSessionState(session);
60
+ const running = state.running && orchConfig?.autoDispatch !== undefined;
61
+
62
+ // Read persisted orchestrator state
63
+ const statePath = join(dir, ".tasks", "orchestrator-state.json");
64
+ let claimedTasks: string[] = [];
65
+ let taskClaimTimes: Record<string, number> = {};
66
+
67
+ if (existsSync(statePath)) {
68
+ try {
69
+ const data = JSON.parse(readFileSync(statePath, "utf-8"));
70
+ claimedTasks = data.claimedTasks ?? [];
71
+ taskClaimTimes = data.taskClaimTimes ?? {};
72
+ } catch {
73
+ // Corrupted state
74
+ }
75
+ }
76
+
77
+ // Load tasks, panes
78
+ const tasks = loadTasks(dir);
79
+ const mission = loadMission(dir);
80
+
81
+ // Agent info from tmux panes
82
+ const agents: Array<{
83
+ name: string;
84
+ paneId: string;
85
+ state: "busy" | "idle";
86
+ currentTask: { id: string; title: string; elapsed: string } | null;
87
+ }> = [];
88
+
89
+ if (state.running) {
90
+ const panes = listSessionPanes(session);
91
+ const agentPanes = panes.filter((p) => isAgentPane(p));
92
+
93
+ for (const pane of agentPanes) {
94
+ const name = agentIdentifier(pane);
95
+ const busy = isAgentBusy(pane);
96
+ const assignedTask = tasks.find((t) => t.status === "in-progress" && t.assignee === name);
97
+
98
+ let currentTask: { id: string; title: string; elapsed: string } | null = null;
99
+ if (assignedTask) {
100
+ const claimTime = taskClaimTimes[assignedTask.id];
101
+ const elapsed = claimTime
102
+ ? formatElapsed(Date.now() - claimTime)
103
+ : formatElapsed(Date.now() - new Date(assignedTask.updated).getTime());
104
+ currentTask = { id: assignedTask.id, title: assignedTask.title, elapsed };
105
+ }
106
+
107
+ agents.push({
108
+ name,
109
+ paneId: pane.id,
110
+ state: busy ? "busy" : "idle",
111
+ currentTask,
112
+ });
113
+ }
114
+ }
115
+
116
+ // Categorize tasks
117
+ const inProgressTasks = tasks
118
+ .filter((t) => t.status === "in-progress")
119
+ .map((t) => {
120
+ const claimTime = taskClaimTimes[t.id];
121
+ const elapsed = claimTime
122
+ ? formatElapsed(Date.now() - claimTime)
123
+ : formatElapsed(Date.now() - new Date(t.updated).getTime());
124
+ return { id: t.id, title: t.title, assignee: t.assignee ?? "unassigned", elapsed };
125
+ });
126
+
127
+ const doneTaskIds = new Set(tasks.filter((t) => t.status === "done").map((t) => t.id));
128
+ const pendingTasks = tasks
129
+ .filter((t) => t.status === "todo")
130
+ .sort((a, b) => a.priority - b.priority)
131
+ .map((t) => {
132
+ const blockedBy = t.depends_on.filter((dep) => !doneTaskIds.has(dep));
133
+ return {
134
+ id: t.id,
135
+ title: t.title,
136
+ priority: t.priority,
137
+ blocked: blockedBy.length > 0,
138
+ blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
139
+ };
140
+ });
141
+
142
+ const recentlyDone = tasks
143
+ .filter((t) => t.status === "done")
144
+ .sort((a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime())
145
+ .slice(0, 5)
146
+ .map((t) => ({ id: t.id, title: t.title, completedAt: t.updated }));
147
+
148
+ // Recent events
149
+ const allEvents = readEvents(dir);
150
+ const recentEvents = allEvents.slice(-10).reverse();
151
+
152
+ const data = {
153
+ running,
154
+ config: orchConfig,
155
+ agents,
156
+ tasks: { inProgress: inProgressTasks, pending: pendingTasks, recentlyDone },
157
+ claims: { count: claimedTasks.length, taskIds: claimedTasks },
158
+ recentEvents,
159
+ };
160
+
161
+ if (json) {
162
+ console.log(JSON.stringify(data, null, 2));
163
+ return;
164
+ }
165
+
166
+ // Human-readable output
167
+ const noColor = "NO_COLOR" in process.env;
168
+ const bold = (s: string) => (noColor ? s : `\x1b[1m${s}\x1b[22m`);
169
+ const dim = (s: string) => (noColor ? s : `\x1b[2m${s}\x1b[22m`);
170
+ const green = (s: string) => (noColor ? s : `\x1b[32m${s}\x1b[39m`);
171
+ const yellow = (s: string) => (noColor ? s : `\x1b[33m${s}\x1b[39m`);
172
+
173
+ console.log(`${bold("Orchestrator:")} ${running ? green("running") : dim("stopped")}`);
174
+
175
+ if (orchConfig) {
176
+ const stallMin = Math.round(orchConfig.stallTimeout / 60000);
177
+ console.log(
178
+ ` Poll: ${orchConfig.pollInterval}ms | Stall: ${stallMin}m | Mode: ${orchConfig.dispatchMode} | Auto-dispatch: ${orchConfig.autoDispatch ? "on" : "off"}`,
179
+ );
180
+ console.log(` Max concurrent: ${orchConfig.maxConcurrentAgents}`);
181
+ }
182
+
183
+ if (mission) {
184
+ console.log(`\n${bold("Mission:")} ${mission.title}`);
185
+ }
186
+
187
+ // Agents
188
+ const busyCount = agents.filter((a) => a.state === "busy").length;
189
+ console.log(`\n${bold("Agents")} (${busyCount} busy / ${agents.length} total):`);
190
+ if (agents.length === 0) {
191
+ console.log(` ${dim("No agents detected")}`);
192
+ }
193
+ for (const agent of agents) {
194
+ const stateLabel = agent.state === "busy" ? yellow("busy") : green("idle");
195
+ const taskLabel = agent.currentTask
196
+ ? ` -> ${agent.currentTask.id} "${agent.currentTask.title}" (${agent.currentTask.elapsed})`
197
+ : "";
198
+ console.log(` ${agent.name.padEnd(12)} ${agent.paneId.padEnd(5)} ${stateLabel}${taskLabel}`);
199
+ }
200
+
201
+ // In-progress
202
+ if (inProgressTasks.length > 0) {
203
+ console.log(`\n${bold("In-progress")} (${inProgressTasks.length}):`);
204
+ for (const t of inProgressTasks) {
205
+ console.log(` ${t.id} "${t.title}" @${t.assignee} ${dim(t.elapsed)}`);
206
+ }
207
+ }
208
+
209
+ // Pending
210
+ if (pendingTasks.length > 0) {
211
+ console.log(`\n${bold("Pending")} (${pendingTasks.length}):`);
212
+ for (const t of pendingTasks.slice(0, 10)) {
213
+ const blocked = t.blocked ? dim(` (blocked by ${t.blockedBy!.join(", ")})`) : "";
214
+ console.log(` ${t.id} "${t.title}" P${t.priority}${blocked}`);
215
+ }
216
+ if (pendingTasks.length > 10) {
217
+ console.log(` ${dim(`... and ${pendingTasks.length - 10} more`)}`);
218
+ }
219
+ }
220
+
221
+ // Recent events
222
+ if (recentEvents.length > 0) {
223
+ console.log(`\n${bold("Recent Events")}:`);
224
+ for (const evt of recentEvents) {
225
+ console.log(` ${dim(formatRelative(evt.timestamp))} ${evt.type.padEnd(15)} ${evt.message}`);
226
+ }
227
+ }
228
+ }