@cluesmith/codev 2.0.3 → 2.0.7

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 (271) hide show
  1. package/dashboard/dist/assets/index-BblS3DWL.js +135 -0
  2. package/dashboard/dist/assets/index-BblS3DWL.js.map +1 -0
  3. package/dashboard/dist/assets/index-Cr9PyjqX.css +32 -0
  4. package/dashboard/dist/index.html +2 -2
  5. package/dist/agent-farm/cli.d.ts.map +1 -1
  6. package/dist/agent-farm/cli.js +54 -61
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/architect.d.ts +5 -5
  9. package/dist/agent-farm/commands/architect.d.ts.map +1 -1
  10. package/dist/agent-farm/commands/architect.js +37 -20
  11. package/dist/agent-farm/commands/architect.js.map +1 -1
  12. package/dist/agent-farm/commands/attach.d.ts +19 -0
  13. package/dist/agent-farm/commands/attach.d.ts.map +1 -1
  14. package/dist/agent-farm/commands/attach.js +169 -29
  15. package/dist/agent-farm/commands/attach.js.map +1 -1
  16. package/dist/agent-farm/commands/cleanup.d.ts +12 -0
  17. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  18. package/dist/agent-farm/commands/cleanup.js +108 -7
  19. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  20. package/dist/agent-farm/commands/send.d.ts +22 -2
  21. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  22. package/dist/agent-farm/commands/send.js +97 -178
  23. package/dist/agent-farm/commands/send.js.map +1 -1
  24. package/dist/agent-farm/commands/spawn-roles.d.ts +3 -9
  25. package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -1
  26. package/dist/agent-farm/commands/spawn-roles.js +14 -53
  27. package/dist/agent-farm/commands/spawn-roles.js.map +1 -1
  28. package/dist/agent-farm/commands/spawn-worktree.d.ts +11 -18
  29. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  30. package/dist/agent-farm/commands/spawn-worktree.js +35 -22
  31. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  32. package/dist/agent-farm/commands/spawn.d.ts +8 -6
  33. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  34. package/dist/agent-farm/commands/spawn.js +207 -89
  35. package/dist/agent-farm/commands/spawn.js.map +1 -1
  36. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  37. package/dist/agent-farm/commands/start.js +2 -6
  38. package/dist/agent-farm/commands/start.js.map +1 -1
  39. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  40. package/dist/agent-farm/commands/status.js +5 -35
  41. package/dist/agent-farm/commands/status.js.map +1 -1
  42. package/dist/agent-farm/commands/stop.d.ts.map +1 -1
  43. package/dist/agent-farm/commands/stop.js +2 -6
  44. package/dist/agent-farm/commands/stop.js.map +1 -1
  45. package/dist/agent-farm/commands/tower-cloud.d.ts +2 -9
  46. package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -1
  47. package/dist/agent-farm/commands/tower-cloud.js +12 -47
  48. package/dist/agent-farm/commands/tower-cloud.js.map +1 -1
  49. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  50. package/dist/agent-farm/commands/tower.js +6 -23
  51. package/dist/agent-farm/commands/tower.js.map +1 -1
  52. package/dist/agent-farm/db/index.d.ts.map +1 -1
  53. package/dist/agent-farm/db/index.js +52 -2
  54. package/dist/agent-farm/db/index.js.map +1 -1
  55. package/dist/agent-farm/db/schema.d.ts +1 -1
  56. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  57. package/dist/agent-farm/db/schema.js +1 -1
  58. package/dist/agent-farm/lib/cloud-config.d.ts +1 -0
  59. package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -1
  60. package/dist/agent-farm/lib/cloud-config.js +2 -2
  61. package/dist/agent-farm/lib/cloud-config.js.map +1 -1
  62. package/dist/agent-farm/lib/tower-client.d.ts +65 -6
  63. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  64. package/dist/agent-farm/lib/tower-client.js +57 -2
  65. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  66. package/dist/agent-farm/servers/overview.d.ts +157 -0
  67. package/dist/agent-farm/servers/overview.d.ts.map +1 -0
  68. package/dist/agent-farm/servers/overview.js +625 -0
  69. package/dist/agent-farm/servers/overview.js.map +1 -0
  70. package/dist/agent-farm/servers/tower-instances.d.ts +1 -3
  71. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  72. package/dist/agent-farm/servers/tower-instances.js +12 -14
  73. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  74. package/dist/agent-farm/servers/tower-messages.d.ts +87 -0
  75. package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -0
  76. package/dist/agent-farm/servers/tower-messages.js +202 -0
  77. package/dist/agent-farm/servers/tower-messages.js.map +1 -0
  78. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  79. package/dist/agent-farm/servers/tower-routes.js +182 -34
  80. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  81. package/dist/agent-farm/servers/tower-server.js +30 -6
  82. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  83. package/dist/agent-farm/servers/tower-terminals.d.ts +9 -3
  84. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  85. package/dist/agent-farm/servers/tower-terminals.js +129 -84
  86. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  87. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  88. package/dist/agent-farm/servers/tower-tunnel.js +3 -19
  89. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  90. package/dist/agent-farm/servers/tower-types.d.ts +0 -2
  91. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
  92. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  93. package/dist/agent-farm/servers/tower-websocket.js +27 -5
  94. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  95. package/dist/agent-farm/types.d.ts +4 -5
  96. package/dist/agent-farm/types.d.ts.map +1 -1
  97. package/dist/agent-farm/utils/agent-names.d.ts +85 -0
  98. package/dist/agent-farm/utils/agent-names.d.ts.map +1 -0
  99. package/dist/agent-farm/utils/agent-names.js +140 -0
  100. package/dist/agent-farm/utils/agent-names.js.map +1 -0
  101. package/dist/agent-farm/utils/display.d.ts +8 -0
  102. package/dist/agent-farm/utils/display.d.ts.map +1 -0
  103. package/dist/agent-farm/utils/display.js +26 -0
  104. package/dist/agent-farm/utils/display.js.map +1 -0
  105. package/dist/agent-farm/utils/message-format.d.ts +17 -0
  106. package/dist/agent-farm/utils/message-format.d.ts.map +1 -0
  107. package/dist/agent-farm/utils/message-format.js +41 -0
  108. package/dist/agent-farm/utils/message-format.js.map +1 -0
  109. package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
  110. package/dist/agent-farm/utils/notifications.js +7 -16
  111. package/dist/agent-farm/utils/notifications.js.map +1 -1
  112. package/dist/agent-farm/utils/server-utils.d.ts +4 -0
  113. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  114. package/dist/agent-farm/utils/server-utils.js +20 -0
  115. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  116. package/dist/agent-farm/utils/shell.d.ts +5 -0
  117. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  118. package/dist/agent-farm/utils/shell.js +15 -11
  119. package/dist/agent-farm/utils/shell.js.map +1 -1
  120. package/dist/cli.d.ts.map +1 -1
  121. package/dist/cli.js +46 -15
  122. package/dist/cli.js.map +1 -1
  123. package/dist/commands/adopt.d.ts.map +1 -1
  124. package/dist/commands/adopt.js +1 -13
  125. package/dist/commands/adopt.js.map +1 -1
  126. package/dist/commands/consult/index.d.ts +34 -9
  127. package/dist/commands/consult/index.d.ts.map +1 -1
  128. package/dist/commands/consult/index.js +617 -263
  129. package/dist/commands/consult/index.js.map +1 -1
  130. package/dist/commands/consult/metrics.d.ts +90 -0
  131. package/dist/commands/consult/metrics.d.ts.map +1 -0
  132. package/dist/commands/consult/metrics.js +203 -0
  133. package/dist/commands/consult/metrics.js.map +1 -0
  134. package/dist/commands/consult/stats.d.ts +18 -0
  135. package/dist/commands/consult/stats.d.ts.map +1 -0
  136. package/dist/commands/consult/stats.js +150 -0
  137. package/dist/commands/consult/stats.js.map +1 -0
  138. package/dist/commands/consult/usage-extractor.d.ts +41 -0
  139. package/dist/commands/consult/usage-extractor.d.ts.map +1 -0
  140. package/dist/commands/consult/usage-extractor.js +122 -0
  141. package/dist/commands/consult/usage-extractor.js.map +1 -0
  142. package/dist/commands/doctor.d.ts.map +1 -1
  143. package/dist/commands/doctor.js +5 -3
  144. package/dist/commands/doctor.js.map +1 -1
  145. package/dist/commands/init.d.ts.map +1 -1
  146. package/dist/commands/init.js +1 -13
  147. package/dist/commands/init.js.map +1 -1
  148. package/dist/commands/porch/index.d.ts.map +1 -1
  149. package/dist/commands/porch/index.js +13 -12
  150. package/dist/commands/porch/index.js.map +1 -1
  151. package/dist/commands/porch/next.d.ts.map +1 -1
  152. package/dist/commands/porch/next.js +57 -77
  153. package/dist/commands/porch/next.js.map +1 -1
  154. package/dist/commands/porch/plan.d.ts.map +1 -1
  155. package/dist/commands/porch/plan.js +17 -2
  156. package/dist/commands/porch/plan.js.map +1 -1
  157. package/dist/commands/porch/prompts.d.ts +10 -1
  158. package/dist/commands/porch/prompts.d.ts.map +1 -1
  159. package/dist/commands/porch/prompts.js +56 -29
  160. package/dist/commands/porch/prompts.js.map +1 -1
  161. package/dist/commands/porch/protocol.js +2 -2
  162. package/dist/commands/porch/state.d.ts +13 -0
  163. package/dist/commands/porch/state.d.ts.map +1 -1
  164. package/dist/commands/porch/state.js +49 -2
  165. package/dist/commands/porch/state.js.map +1 -1
  166. package/dist/commands/update.d.ts.map +1 -1
  167. package/dist/commands/update.js +0 -10
  168. package/dist/commands/update.js.map +1 -1
  169. package/dist/lib/github.d.ts +82 -0
  170. package/dist/lib/github.d.ts.map +1 -0
  171. package/dist/lib/github.js +181 -0
  172. package/dist/lib/github.js.map +1 -0
  173. package/dist/lib/scaffold.d.ts +0 -21
  174. package/dist/lib/scaffold.d.ts.map +1 -1
  175. package/dist/lib/scaffold.js +0 -57
  176. package/dist/lib/scaffold.js.map +1 -1
  177. package/dist/terminal/index.d.ts +16 -0
  178. package/dist/terminal/index.d.ts.map +1 -1
  179. package/dist/terminal/index.js +14 -0
  180. package/dist/terminal/index.js.map +1 -1
  181. package/dist/terminal/pty-manager.d.ts.map +1 -1
  182. package/dist/terminal/pty-manager.js +8 -5
  183. package/dist/terminal/pty-manager.js.map +1 -1
  184. package/dist/terminal/pty-session.js +4 -4
  185. package/dist/terminal/pty-session.js.map +1 -1
  186. package/dist/terminal/session-manager.d.ts +64 -0
  187. package/dist/terminal/session-manager.d.ts.map +1 -1
  188. package/dist/terminal/session-manager.js +299 -10
  189. package/dist/terminal/session-manager.js.map +1 -1
  190. package/dist/terminal/shellper-client.d.ts +2 -1
  191. package/dist/terminal/shellper-client.d.ts.map +1 -1
  192. package/dist/terminal/shellper-client.js +4 -2
  193. package/dist/terminal/shellper-client.js.map +1 -1
  194. package/dist/terminal/shellper-main.js +33 -4
  195. package/dist/terminal/shellper-main.js.map +1 -1
  196. package/dist/terminal/shellper-process.d.ts +24 -7
  197. package/dist/terminal/shellper-process.d.ts.map +1 -1
  198. package/dist/terminal/shellper-process.js +139 -36
  199. package/dist/terminal/shellper-process.js.map +1 -1
  200. package/dist/terminal/shellper-protocol.d.ts +1 -0
  201. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  202. package/dist/terminal/shellper-protocol.js.map +1 -1
  203. package/package.json +4 -1
  204. package/skeleton/.claude/skills/af/SKILL.md +10 -10
  205. package/skeleton/.claude/skills/consult/SKILL.md +55 -38
  206. package/skeleton/.claude/skills/porch/SKILL.md +53 -0
  207. package/skeleton/DEPENDENCIES.md +2 -2
  208. package/skeleton/builders.md +8 -19
  209. package/skeleton/maintain/.gitkeep +1 -1
  210. package/skeleton/porch/prompts/specify.md +1 -1
  211. package/skeleton/protocol-schema.json +1 -1
  212. package/skeleton/protocols/bugfix/prompts/pr.md +18 -7
  213. package/skeleton/protocols/bugfix/protocol.json +1 -1
  214. package/skeleton/protocols/experiment/protocol.md +17 -17
  215. package/skeleton/protocols/maintain/consult-types/impl-review.md +72 -0
  216. package/skeleton/protocols/maintain/consult-types/pr-review.md +72 -0
  217. package/skeleton/protocols/maintain/prompts/audit.md +2 -2
  218. package/skeleton/protocols/maintain/prompts/sync.md +1 -1
  219. package/skeleton/protocols/maintain/prompts/verify.md +1 -1
  220. package/skeleton/protocols/maintain/protocol.json +4 -4
  221. package/skeleton/protocols/maintain/protocol.md +11 -12
  222. package/skeleton/protocols/maintain/templates/maintenance-run.md +2 -2
  223. package/skeleton/protocols/protocol-schema.json +1 -1
  224. package/skeleton/protocols/spir/consult-types/impl-review.md +72 -0
  225. package/skeleton/protocols/spir/consult-types/phase-review.md +72 -0
  226. package/skeleton/protocols/spir/consult-types/pr-review.md +72 -0
  227. package/skeleton/protocols/spir/prompts/plan.md +4 -4
  228. package/skeleton/protocols/spir/prompts/review.md +8 -8
  229. package/skeleton/protocols/spir/prompts/specify.md +6 -6
  230. package/skeleton/protocols/spir/protocol.json +16 -16
  231. package/skeleton/protocols/spir/protocol.md +8 -8
  232. package/skeleton/protocols/spir/templates/review.md +2 -2
  233. package/skeleton/protocols/tick/consult-types/impl-review.md +72 -0
  234. package/skeleton/protocols/tick/consult-types/plan-review.md +59 -0
  235. package/skeleton/protocols/tick/consult-types/pr-review.md +72 -0
  236. package/skeleton/protocols/tick/consult-types/spec-review.md +55 -0
  237. package/skeleton/protocols/tick/protocol.json +2 -7
  238. package/skeleton/protocols/tick/protocol.md +31 -31
  239. package/skeleton/resources/commands/agent-farm.md +21 -19
  240. package/skeleton/resources/commands/codev.md +0 -36
  241. package/skeleton/resources/commands/consult.md +88 -234
  242. package/skeleton/resources/commands/overview.md +6 -7
  243. package/skeleton/resources/spikes.md +3 -3
  244. package/skeleton/resources/workflow-reference.md +28 -28
  245. package/skeleton/roles/architect.md +34 -38
  246. package/skeleton/roles/builder.md +14 -14
  247. package/skeleton/roles/consultant.md +6 -0
  248. package/skeleton/templates/AGENTS.md +6 -6
  249. package/skeleton/templates/CLAUDE.md +6 -6
  250. package/skeleton/templates/cheatsheet.md +22 -18
  251. package/skeleton/templates/lifecycle.md +9 -9
  252. package/skeleton/templates/pr-overview.md +5 -5
  253. package/templates/open.html +6 -3
  254. package/templates/tower.html +1 -41
  255. package/dashboard/dist/assets/index-4n9zpWLY.css +0 -32
  256. package/dashboard/dist/assets/index-UsH9ixz1.js +0 -136
  257. package/dashboard/dist/assets/index-UsH9ixz1.js.map +0 -1
  258. package/dist/agent-farm/commands/consult.d.ts +0 -15
  259. package/dist/agent-farm/commands/consult.d.ts.map +0 -1
  260. package/dist/agent-farm/commands/consult.js +0 -39
  261. package/dist/agent-farm/commands/consult.js.map +0 -1
  262. package/dist/agent-farm/utils/gate-status.d.ts +0 -16
  263. package/dist/agent-farm/utils/gate-status.d.ts.map +0 -1
  264. package/dist/agent-farm/utils/gate-status.js +0 -79
  265. package/dist/agent-farm/utils/gate-status.js.map +0 -1
  266. package/skeleton/templates/projectlist-archive.md +0 -21
  267. package/skeleton/templates/projectlist.md +0 -147
  268. /package/skeleton/{consult-types → protocols/bugfix/consult-types}/impl-review.md +0 -0
  269. /package/skeleton/{consult-types/pr-ready.md → protocols/bugfix/consult-types/pr-review.md} +0 -0
  270. /package/skeleton/{consult-types → protocols/spir/consult-types}/plan-review.md +0 -0
  271. /package/skeleton/{consult-types → protocols/spir/consult-types}/spec-review.md +0 -0
@@ -14,38 +14,40 @@
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import crypto from 'node:crypto';
17
- import { execSync } from 'node:child_process';
17
+ import { exec } from 'node:child_process';
18
+ import { promisify } from 'node:util';
18
19
  import { homedir, tmpdir } from 'node:os';
20
+ import { encodeWorkspacePath, decodeWorkspacePath } from '../lib/tower-client.js';
19
21
  import { fileURLToPath } from 'node:url';
22
+ const execAsync = promisify(exec);
23
+ import { DEFAULT_COLS, defaultSessionOptions } from '../../terminal/index.js';
20
24
  import { parseJsonBody, isRequestAllowed } from '../utils/server-utils.js';
21
25
  import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
22
26
  import { handleTunnelEndpoint } from './tower-tunnel.js';
27
+ import { resolveTarget, broadcastMessage, isResolveError } from './tower-messages.js';
28
+ import { formatArchitectMessage, formatBuilderMessage } from '../utils/message-format.js';
23
29
  import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, } from './tower-instances.js';
24
- import { getWorkspaceTerminals, getTerminalManager, getWorkspaceTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, deleteWorkspaceTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForWorkspace, } from './tower-terminals.js';
30
+ import { OverviewCache } from './overview.js';
31
+ import { getWorkspaceTerminals, getTerminalManager, getWorkspaceTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, removeTerminalFromRegistry, deleteWorkspaceTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForWorkspace, } from './tower-terminals.js';
25
32
  const __filename = fileURLToPath(import.meta.url);
26
33
  const __dirname = path.dirname(__filename);
27
- // ============================================================================
28
- // Helper: read raw request body
29
- // ============================================================================
30
- async function readBody(req) {
31
- return new Promise((resolve) => {
32
- let data = '';
33
- req.on('data', (chunk) => data += chunk.toString());
34
- req.on('end', () => resolve(data));
35
- });
36
- }
34
+ // Singleton cache for overview endpoint (Spec 0126 Phase 4)
35
+ const overviewCache = new OverviewCache();
37
36
  const ROUTES = {
38
37
  'GET /health': (_req, res) => handleHealthCheck(res),
39
38
  'GET /api/workspaces': (_req, res) => handleListWorkspaces(res),
40
39
  'POST /api/terminals': (req, res, _url, ctx) => handleTerminalCreate(req, res, ctx),
41
40
  'GET /api/terminals': (_req, res) => handleTerminalList(res),
42
41
  'GET /api/status': (_req, res) => handleStatus(res),
42
+ 'GET /api/overview': (_req, res, url) => handleOverview(res, url),
43
+ 'POST /api/overview/refresh': (_req, res, _url, ctx) => handleOverviewRefresh(res, ctx),
43
44
  'GET /api/events': (req, res, _url, ctx) => handleSSEEvents(req, res, ctx),
44
45
  'POST /api/notify': (req, res, _url, ctx) => handleNotify(req, res, ctx),
45
46
  'GET /api/browse': (_req, res, url) => handleBrowse(res, url),
46
47
  'POST /api/create': (req, res, _url, ctx) => handleCreateWorkspace(req, res, ctx),
47
48
  'POST /api/launch': (req, res) => handleLaunchInstance(req, res),
48
49
  'POST /api/stop': (req, res) => handleStopInstance(req, res),
50
+ 'POST /api/send': (req, res, _url, ctx) => handleSend(req, res, ctx),
49
51
  'GET /': (_req, res, _url, ctx) => handleDashboard(res, ctx),
50
52
  'GET /index.html': (_req, res, _url, ctx) => handleDashboard(res, ctx),
51
53
  };
@@ -145,7 +147,7 @@ async function handleWorkspaceAction(req, res, ctx, match) {
145
147
  const [, encodedPath, action] = match;
146
148
  let workspacePath;
147
149
  try {
148
- workspacePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
150
+ workspacePath = decodeWorkspacePath(encodedPath);
149
151
  if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
150
152
  throw new Error('Invalid path');
151
153
  }
@@ -171,7 +173,6 @@ async function handleWorkspaceAction(req, res, ctx, match) {
171
173
  name: instance.workspaceName,
172
174
  active: instance.running,
173
175
  terminals: instance.terminals,
174
- gateStatus: instance.gateStatus,
175
176
  }));
176
177
  return;
177
178
  }
@@ -244,9 +245,8 @@ async function handleTerminalCreate(req, res, ctx) {
244
245
  args: args || [],
245
246
  cwd,
246
247
  env: sessionEnv,
247
- cols: cols || 200,
248
- rows: 50,
249
- restartOnExit: false,
248
+ ...defaultSessionOptions(),
249
+ cols: cols || DEFAULT_COLS,
250
250
  });
251
251
  const replayData = client.getReplayData() ?? Buffer.alloc(0);
252
252
  const shellperInfo = shellperManager.getSessionInfo(sessionId);
@@ -333,6 +333,9 @@ async function handleTerminalRoutes(req, res, url, match) {
333
333
  }
334
334
  // TICK-001: Delete from SQLite
335
335
  deleteTerminalSession(terminalId);
336
+ // Bugfix #290: Also remove from in-memory registry so dashboard
337
+ // stops showing tabs for cleaned-up builders
338
+ removeTerminalFromRegistry(terminalId);
336
339
  res.writeHead(204);
337
340
  res.end();
338
341
  return;
@@ -406,6 +409,41 @@ async function handleStatus(res) {
406
409
  res.writeHead(200, { 'Content-Type': 'application/json' });
407
410
  res.end(JSON.stringify({ instances }));
408
411
  }
412
+ async function handleOverview(res, url, workspaceOverride) {
413
+ // Accept workspace from: explicit override (workspace-scoped route), ?workspace= param, or first known path.
414
+ let workspaceRoot = workspaceOverride || url.searchParams.get('workspace');
415
+ if (!workspaceRoot) {
416
+ const knownPaths = getKnownWorkspacePaths();
417
+ workspaceRoot = knownPaths.find(p => !p.includes('/.builders/')) || null;
418
+ }
419
+ if (!workspaceRoot) {
420
+ res.writeHead(200, { 'Content-Type': 'application/json' });
421
+ res.end(JSON.stringify({ builders: [], pendingPRs: [], backlog: [] }));
422
+ return;
423
+ }
424
+ // Build set of active builder role_ids (lowercased) from live terminal sessions
425
+ const wsTerminals = getWorkspaceTerminals();
426
+ const entry = wsTerminals.get(normalizeWorkspacePath(workspaceRoot));
427
+ const activeBuilderRoleIds = new Set();
428
+ if (entry) {
429
+ for (const key of entry.builders.keys()) {
430
+ activeBuilderRoleIds.add(key.toLowerCase());
431
+ }
432
+ }
433
+ const data = await overviewCache.getOverview(workspaceRoot, activeBuilderRoleIds);
434
+ res.writeHead(200, { 'Content-Type': 'application/json' });
435
+ res.end(JSON.stringify(data));
436
+ }
437
+ function handleOverviewRefresh(res, ctx) {
438
+ overviewCache.invalidate();
439
+ // Bugfix #388: Broadcast SSE event so all connected dashboard clients
440
+ // immediately re-fetch instead of waiting for the next poll cycle.
441
+ if (ctx) {
442
+ ctx.broadcastNotification({ type: 'overview-changed', title: 'Overview updated', body: 'Cache invalidated' });
443
+ }
444
+ res.writeHead(200, { 'Content-Type': 'application/json' });
445
+ res.end(JSON.stringify({ ok: true }));
446
+ }
409
447
  function handleSSEEvents(req, res, ctx) {
410
448
  const clientId = crypto.randomBytes(8).toString('hex');
411
449
  res.writeHead(200, {
@@ -446,6 +484,107 @@ async function handleNotify(req, res, ctx) {
446
484
  res.writeHead(200, { 'Content-Type': 'application/json' });
447
485
  res.end(JSON.stringify({ success: true }));
448
486
  }
487
+ // ============================================================================
488
+ // POST /api/send — send a message to a resolved agent terminal
489
+ // ============================================================================
490
+ async function handleSend(req, res, ctx) {
491
+ const body = await parseJsonBody(req);
492
+ // Validate required fields
493
+ const to = typeof body.to === 'string' ? body.to.trim() : '';
494
+ const message = typeof body.message === 'string' ? body.message.trim() : '';
495
+ if (!to) {
496
+ res.writeHead(400, { 'Content-Type': 'application/json' });
497
+ res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Missing or empty "to" field' }));
498
+ return;
499
+ }
500
+ if (!message) {
501
+ res.writeHead(400, { 'Content-Type': 'application/json' });
502
+ res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Missing or empty "message" field' }));
503
+ return;
504
+ }
505
+ // Optional fields
506
+ const from = typeof body.from === 'string' ? body.from : undefined;
507
+ const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
508
+ const fromWorkspace = typeof body.fromWorkspace === 'string' ? body.fromWorkspace : undefined;
509
+ const options = typeof body.options === 'object' && body.options !== null
510
+ ? body.options
511
+ : {};
512
+ const raw = options.raw === true;
513
+ const noEnter = options.noEnter === true;
514
+ const interrupt = options.interrupt === true;
515
+ // Resolve the target address to a terminal ID
516
+ const result = resolveTarget(to, workspace);
517
+ if (isResolveError(result)) {
518
+ const statusCode = result.code === 'AMBIGUOUS' ? 409
519
+ : result.code === 'NO_CONTEXT' ? 400
520
+ : 404;
521
+ // Map NO_CONTEXT to INVALID_PARAMS per plan's error contract
522
+ const errorCode = result.code === 'NO_CONTEXT' ? 'INVALID_PARAMS' : result.code;
523
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
524
+ res.end(JSON.stringify({ error: errorCode, message: result.message }));
525
+ return;
526
+ }
527
+ // Get the terminal session
528
+ const manager = getTerminalManager();
529
+ const session = manager.getSession(result.terminalId);
530
+ if (!session) {
531
+ res.writeHead(404, { 'Content-Type': 'application/json' });
532
+ res.end(JSON.stringify({
533
+ error: 'NOT_FOUND',
534
+ message: `Terminal session ${result.terminalId} not found (agent '${result.agent}' resolved but terminal is gone).`,
535
+ }));
536
+ return;
537
+ }
538
+ // Format the message based on sender/target
539
+ const isArchitectTarget = result.agent === 'architect';
540
+ let formattedMessage;
541
+ if (isArchitectTarget && from) {
542
+ // Builder → Architect
543
+ formattedMessage = formatBuilderMessage(from, message, undefined, raw);
544
+ }
545
+ else if (!isArchitectTarget) {
546
+ // Architect → Builder (or any → builder)
547
+ formattedMessage = formatArchitectMessage(message, undefined, raw);
548
+ }
549
+ else {
550
+ // Unknown sender to architect — use raw
551
+ formattedMessage = raw ? message : formatArchitectMessage(message, undefined, false);
552
+ }
553
+ // Optionally interrupt first
554
+ if (interrupt) {
555
+ session.write('\x03'); // Ctrl+C
556
+ await new Promise(resolve => setTimeout(resolve, 100));
557
+ }
558
+ // Write the message to the terminal
559
+ session.write(formattedMessage);
560
+ // Send Enter to submit (unless noEnter)
561
+ if (!noEnter) {
562
+ session.write('\r');
563
+ }
564
+ // Broadcast structured message to WebSocket subscribers
565
+ const senderWorkspace = fromWorkspace ?? workspace ?? 'unknown';
566
+ broadcastMessage({
567
+ type: 'message',
568
+ from: {
569
+ project: path.basename(senderWorkspace),
570
+ agent: from ?? 'unknown',
571
+ },
572
+ to: {
573
+ project: path.basename(result.workspacePath),
574
+ agent: result.agent,
575
+ },
576
+ content: message,
577
+ metadata: { raw, source: 'api' },
578
+ timestamp: new Date().toISOString(),
579
+ });
580
+ ctx.log('INFO', `Message sent: ${from ?? 'unknown'} → ${result.agent} (terminal ${result.terminalId.slice(0, 8)}...)`);
581
+ res.writeHead(200, { 'Content-Type': 'application/json' });
582
+ res.end(JSON.stringify({
583
+ ok: true,
584
+ terminalId: result.terminalId,
585
+ resolvedTo: result.agent,
586
+ }));
587
+ }
449
588
  async function handleBrowse(res, url) {
450
589
  const inputPath = url.searchParams.get('path') || '';
451
590
  try {
@@ -493,9 +632,8 @@ async function handleCreateWorkspace(req, res, ctx) {
493
632
  }
494
633
  try {
495
634
  // Run codev init (it creates the directory)
496
- execSync(`codev init --yes "${workspaceName}"`, {
635
+ await execAsync(`codev init --yes "${workspaceName}"`, {
497
636
  cwd: expandedParent,
498
- stdio: 'pipe',
499
637
  timeout: 60000,
500
638
  });
501
639
  // Launch the instance
@@ -593,7 +731,7 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
593
731
  // Decode Base64URL (RFC 4648)
594
732
  let workspacePath;
595
733
  try {
596
- workspacePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
734
+ workspacePath = decodeWorkspacePath(encodedPath);
597
735
  // Support both POSIX (/) and Windows (C:\) paths
598
736
  if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
599
737
  throw new Error('Invalid workspace path');
@@ -618,7 +756,7 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
618
756
  await handleTunnelEndpoint(req, res, tunnelSub);
619
757
  return;
620
758
  }
621
- // GET /file?path=<relative-path> — Read workspace file by path (for StatusPanel workspace list)
759
+ // GET /file?path=<relative-path> — Read workspace file by path
622
760
  if (req.method === 'GET' && subPath === 'file' && url.searchParams.has('path')) {
623
761
  const relPath = url.searchParams.get('path');
624
762
  const fullPath = path.resolve(workspacePath, relPath);
@@ -771,6 +909,18 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
771
909
  });
772
910
  return;
773
911
  }
912
+ // GET /api/overview - Work view overview data (Spec 0126 Phase 4)
913
+ if (req.method === 'GET' && apiPath === 'overview') {
914
+ return handleOverview(res, url, workspacePath);
915
+ }
916
+ // POST /api/overview/refresh - Invalidate overview cache (Spec 0126 Phase 4)
917
+ if (req.method === 'POST' && apiPath === 'overview/refresh') {
918
+ return handleOverviewRefresh(res, ctx);
919
+ }
920
+ // GET /api/events - SSE push notifications (Bugfix #388)
921
+ if (req.method === 'GET' && apiPath === 'events') {
922
+ return handleSSEEvents(req, res, ctx);
923
+ }
774
924
  // Unhandled API route
775
925
  res.writeHead(404, { 'Content-Type': 'application/json' });
776
926
  res.end(JSON.stringify({ error: 'API endpoint not found', path: apiPath }));
@@ -799,9 +949,9 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
799
949
  async function handleWorkspaceState(res, workspacePath) {
800
950
  // Refresh cache via getTerminalsForWorkspace (handles SQLite sync
801
951
  // and shellper reconnection in one place)
802
- const encodedPath = Buffer.from(workspacePath).toString('base64url');
952
+ const encodedPath = encodeWorkspacePath(workspacePath);
803
953
  const proxyUrl = `/workspace/${encodedPath}/`;
804
- const { gateStatus } = await getTerminalsForWorkspace(workspacePath, proxyUrl);
954
+ await getTerminalsForWorkspace(workspacePath, proxyUrl);
805
955
  // Now read from the refreshed cache
806
956
  const entry = getWorkspaceTerminalsEntry(workspacePath);
807
957
  const manager = getTerminalManager();
@@ -811,7 +961,6 @@ async function handleWorkspaceState(res, workspacePath) {
811
961
  utils: [],
812
962
  annotations: [],
813
963
  workspaceName: path.basename(workspacePath),
814
- gateStatus,
815
964
  };
816
965
  // Add architect if exists
817
966
  if (entry.architect) {
@@ -845,7 +994,7 @@ async function handleWorkspaceState(res, workspacePath) {
845
994
  if (session) {
846
995
  state.builders.push({
847
996
  id: builderId,
848
- name: `Builder ${builderId}`,
997
+ name: builderId,
849
998
  port: 0,
850
999
  pid: session.pid || 0,
851
1000
  status: 'running',
@@ -891,9 +1040,7 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
891
1040
  args: shellArgs,
892
1041
  cwd: workspacePath,
893
1042
  env: shellEnv,
894
- cols: 200,
895
- rows: 50,
896
- restartOnExit: false,
1043
+ ...defaultSessionOptions(),
897
1044
  });
898
1045
  const replayData = client.getReplayData() ?? Buffer.alloc(0);
899
1046
  const shellperInfo = shellperManager.getSessionInfo(sessionId);
@@ -954,8 +1101,10 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
954
1101
  }
955
1102
  async function handleWorkspaceFileTabCreate(req, res, ctx, workspacePath) {
956
1103
  try {
957
- const body = await readBody(req);
958
- const { path: filePath, line, terminalId } = JSON.parse(body || '{}');
1104
+ const body = await parseJsonBody(req);
1105
+ const filePath = body.path;
1106
+ const line = body.line;
1107
+ const terminalId = body.terminalId;
959
1108
  if (!filePath || typeof filePath !== 'string') {
960
1109
  res.writeHead(400, { 'Content-Type': 'application/json' });
961
1110
  res.end(JSON.stringify({ error: 'Missing path parameter' }));
@@ -1116,8 +1265,7 @@ async function handleWorkspaceFileSave(req, res, ctx, workspacePath, tabId) {
1116
1265
  return;
1117
1266
  }
1118
1267
  try {
1119
- const body = await readBody(req);
1120
- const { content } = JSON.parse(body || '{}');
1268
+ const { content } = await parseJsonBody(req);
1121
1269
  if (typeof content !== 'string') {
1122
1270
  res.writeHead(400, { 'Content-Type': 'application/json' });
1123
1271
  res.end(JSON.stringify({ error: 'Missing content parameter' }));
@@ -1241,10 +1389,10 @@ function handleWorkspaceFiles(res, url, workspacePath) {
1241
1389
  res.writeHead(200, { 'Content-Type': 'application/json' });
1242
1390
  res.end(JSON.stringify(tree));
1243
1391
  }
1244
- function handleWorkspaceGitStatus(res, ctx, workspacePath) {
1392
+ async function handleWorkspaceGitStatus(res, ctx, workspacePath) {
1245
1393
  try {
1246
1394
  // Get git status in porcelain format for parsing
1247
- const result = execSync('git status --porcelain', {
1395
+ const { stdout: result } = await execAsync('git status --porcelain', {
1248
1396
  cwd: workspacePath,
1249
1397
  encoding: 'utf-8',
1250
1398
  timeout: 5000,