@cluesmith/codev 2.0.2 → 2.0.6

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 (312) hide show
  1. package/dashboard/dist/assets/index-B-s8BA2l.js +135 -0
  2. package/dashboard/dist/assets/index-B-s8BA2l.js.map +1 -0
  3. package/dashboard/dist/assets/index-DB2AxRP7.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 +32 -14
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/architect.d.ts +1 -1
  9. package/dist/agent-farm/commands/architect.js +3 -3
  10. package/dist/agent-farm/commands/architect.js.map +1 -1
  11. package/dist/agent-farm/commands/attach.d.ts +19 -0
  12. package/dist/agent-farm/commands/attach.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/attach.js +172 -12
  14. package/dist/agent-farm/commands/attach.js.map +1 -1
  15. package/dist/agent-farm/commands/cleanup.js +6 -6
  16. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  17. package/dist/agent-farm/commands/open.js +5 -5
  18. package/dist/agent-farm/commands/open.js.map +1 -1
  19. package/dist/agent-farm/commands/send.d.ts +22 -2
  20. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  21. package/dist/agent-farm/commands/send.js +100 -181
  22. package/dist/agent-farm/commands/send.js.map +1 -1
  23. package/dist/agent-farm/commands/shell.js +5 -5
  24. package/dist/agent-farm/commands/shell.js.map +1 -1
  25. package/dist/agent-farm/commands/spawn-roles.d.ts +3 -9
  26. package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -1
  27. package/dist/agent-farm/commands/spawn-roles.js +14 -53
  28. package/dist/agent-farm/commands/spawn-roles.js.map +1 -1
  29. package/dist/agent-farm/commands/spawn-worktree.d.ts +11 -17
  30. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  31. package/dist/agent-farm/commands/spawn-worktree.js +32 -13
  32. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  33. package/dist/agent-farm/commands/spawn.d.ts +8 -6
  34. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  35. package/dist/agent-farm/commands/spawn.js +183 -69
  36. package/dist/agent-farm/commands/spawn.js.map +1 -1
  37. package/dist/agent-farm/commands/start.d.ts +4 -4
  38. package/dist/agent-farm/commands/start.js +16 -16
  39. package/dist/agent-farm/commands/start.js.map +1 -1
  40. package/dist/agent-farm/commands/status.d.ts +1 -1
  41. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  42. package/dist/agent-farm/commands/status.js +15 -26
  43. package/dist/agent-farm/commands/status.js.map +1 -1
  44. package/dist/agent-farm/commands/stop.d.ts +4 -4
  45. package/dist/agent-farm/commands/stop.js +9 -9
  46. package/dist/agent-farm/commands/stop.js.map +1 -1
  47. package/dist/agent-farm/db/index.d.ts.map +1 -1
  48. package/dist/agent-farm/db/index.js +82 -7
  49. package/dist/agent-farm/db/index.js.map +1 -1
  50. package/dist/agent-farm/db/schema.d.ts +2 -2
  51. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  52. package/dist/agent-farm/db/schema.js +21 -4
  53. package/dist/agent-farm/db/schema.js.map +1 -1
  54. package/dist/agent-farm/lib/tower-client.d.ts +36 -26
  55. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  56. package/dist/agent-farm/lib/tower-client.js +50 -25
  57. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  58. package/dist/agent-farm/lib/tunnel-client.d.ts +12 -2
  59. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  60. package/dist/agent-farm/lib/tunnel-client.js +59 -1
  61. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  62. package/dist/agent-farm/servers/overview.d.ts +111 -0
  63. package/dist/agent-farm/servers/overview.d.ts.map +1 -0
  64. package/dist/agent-farm/servers/overview.js +385 -0
  65. package/dist/agent-farm/servers/overview.js.map +1 -0
  66. package/dist/agent-farm/servers/tower-instances.d.ts +18 -20
  67. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  68. package/dist/agent-farm/servers/tower-instances.js +97 -100
  69. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  70. package/dist/agent-farm/servers/tower-messages.d.ts +87 -0
  71. package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -0
  72. package/dist/agent-farm/servers/tower-messages.js +202 -0
  73. package/dist/agent-farm/servers/tower-messages.js.map +1 -0
  74. package/dist/agent-farm/servers/tower-routes.d.ts +1 -1
  75. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  76. package/dist/agent-farm/servers/tower-routes.js +343 -174
  77. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  78. package/dist/agent-farm/servers/tower-server.js +50 -21
  79. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  80. package/dist/agent-farm/servers/tower-terminals.d.ts +35 -31
  81. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  82. package/dist/agent-farm/servers/tower-terminals.js +208 -184
  83. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  84. package/dist/agent-farm/servers/tower-tunnel.d.ts +2 -2
  85. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  86. package/dist/agent-farm/servers/tower-tunnel.js +12 -12
  87. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  88. package/dist/agent-farm/servers/tower-types.d.ts +8 -12
  89. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
  90. package/dist/agent-farm/servers/tower-utils.d.ts +9 -9
  91. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  92. package/dist/agent-farm/servers/tower-utils.js +18 -18
  93. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  94. package/dist/agent-farm/servers/tower-websocket.d.ts +2 -2
  95. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  96. package/dist/agent-farm/servers/tower-websocket.js +39 -18
  97. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  98. package/dist/agent-farm/types.d.ts +5 -6
  99. package/dist/agent-farm/types.d.ts.map +1 -1
  100. package/dist/agent-farm/utils/agent-names.d.ts +85 -0
  101. package/dist/agent-farm/utils/agent-names.d.ts.map +1 -0
  102. package/dist/agent-farm/utils/agent-names.js +140 -0
  103. package/dist/agent-farm/utils/agent-names.js.map +1 -0
  104. package/dist/agent-farm/utils/config.d.ts +1 -1
  105. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  106. package/dist/agent-farm/utils/config.js +16 -16
  107. package/dist/agent-farm/utils/config.js.map +1 -1
  108. package/dist/agent-farm/utils/file-tabs.d.ts +3 -3
  109. package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -1
  110. package/dist/agent-farm/utils/file-tabs.js +9 -9
  111. package/dist/agent-farm/utils/file-tabs.js.map +1 -1
  112. package/dist/agent-farm/utils/index.d.ts +0 -1
  113. package/dist/agent-farm/utils/index.d.ts.map +1 -1
  114. package/dist/agent-farm/utils/index.js +0 -1
  115. package/dist/agent-farm/utils/index.js.map +1 -1
  116. package/dist/agent-farm/utils/message-format.d.ts +17 -0
  117. package/dist/agent-farm/utils/message-format.d.ts.map +1 -0
  118. package/dist/agent-farm/utils/message-format.js +41 -0
  119. package/dist/agent-farm/utils/message-format.js.map +1 -0
  120. package/dist/agent-farm/utils/notifications.d.ts +4 -4
  121. package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
  122. package/dist/agent-farm/utils/notifications.js +18 -18
  123. package/dist/agent-farm/utils/notifications.js.map +1 -1
  124. package/dist/cli.d.ts.map +1 -1
  125. package/dist/cli.js +26 -1
  126. package/dist/cli.js.map +1 -1
  127. package/dist/commands/adopt.d.ts +2 -2
  128. package/dist/commands/adopt.d.ts.map +1 -1
  129. package/dist/commands/adopt.js +13 -15
  130. package/dist/commands/adopt.js.map +1 -1
  131. package/dist/commands/consult/index.d.ts +26 -2
  132. package/dist/commands/consult/index.d.ts.map +1 -1
  133. package/dist/commands/consult/index.js +296 -83
  134. package/dist/commands/consult/index.js.map +1 -1
  135. package/dist/commands/consult/metrics.d.ts +90 -0
  136. package/dist/commands/consult/metrics.d.ts.map +1 -0
  137. package/dist/commands/consult/metrics.js +203 -0
  138. package/dist/commands/consult/metrics.js.map +1 -0
  139. package/dist/commands/consult/stats.d.ts +18 -0
  140. package/dist/commands/consult/stats.d.ts.map +1 -0
  141. package/dist/commands/consult/stats.js +150 -0
  142. package/dist/commands/consult/stats.js.map +1 -0
  143. package/dist/commands/consult/usage-extractor.d.ts +38 -0
  144. package/dist/commands/consult/usage-extractor.d.ts.map +1 -0
  145. package/dist/commands/consult/usage-extractor.js +99 -0
  146. package/dist/commands/consult/usage-extractor.js.map +1 -0
  147. package/dist/commands/doctor.d.ts.map +1 -1
  148. package/dist/commands/doctor.js +11 -9
  149. package/dist/commands/doctor.js.map +1 -1
  150. package/dist/commands/import.js +4 -4
  151. package/dist/commands/import.js.map +1 -1
  152. package/dist/commands/init.d.ts +2 -2
  153. package/dist/commands/init.d.ts.map +1 -1
  154. package/dist/commands/init.js +13 -15
  155. package/dist/commands/init.js.map +1 -1
  156. package/dist/commands/porch/index.d.ts +6 -6
  157. package/dist/commands/porch/index.d.ts.map +1 -1
  158. package/dist/commands/porch/index.js +37 -37
  159. package/dist/commands/porch/index.js.map +1 -1
  160. package/dist/commands/porch/next.d.ts +1 -1
  161. package/dist/commands/porch/next.d.ts.map +1 -1
  162. package/dist/commands/porch/next.js +86 -92
  163. package/dist/commands/porch/next.js.map +1 -1
  164. package/dist/commands/porch/notify.d.ts +11 -0
  165. package/dist/commands/porch/notify.d.ts.map +1 -0
  166. package/dist/commands/porch/notify.js +30 -0
  167. package/dist/commands/porch/notify.js.map +1 -0
  168. package/dist/commands/porch/plan.d.ts +1 -1
  169. package/dist/commands/porch/plan.d.ts.map +1 -1
  170. package/dist/commands/porch/plan.js +3 -3
  171. package/dist/commands/porch/plan.js.map +1 -1
  172. package/dist/commands/porch/prompts.d.ts +10 -1
  173. package/dist/commands/porch/prompts.d.ts.map +1 -1
  174. package/dist/commands/porch/prompts.js +59 -35
  175. package/dist/commands/porch/prompts.js.map +1 -1
  176. package/dist/commands/porch/protocol.d.ts +1 -1
  177. package/dist/commands/porch/protocol.d.ts.map +1 -1
  178. package/dist/commands/porch/protocol.js +8 -8
  179. package/dist/commands/porch/protocol.js.map +1 -1
  180. package/dist/commands/porch/state.d.ts +6 -6
  181. package/dist/commands/porch/state.d.ts.map +1 -1
  182. package/dist/commands/porch/state.js +14 -12
  183. package/dist/commands/porch/state.js.map +1 -1
  184. package/dist/commands/update.d.ts.map +1 -1
  185. package/dist/commands/update.js +10 -11
  186. package/dist/commands/update.js.map +1 -1
  187. package/dist/lib/github.d.ts +81 -0
  188. package/dist/lib/github.d.ts.map +1 -0
  189. package/dist/lib/github.js +141 -0
  190. package/dist/lib/github.js.map +1 -0
  191. package/dist/lib/scaffold.d.ts +13 -21
  192. package/dist/lib/scaffold.d.ts.map +1 -1
  193. package/dist/lib/scaffold.js +34 -57
  194. package/dist/lib/scaffold.js.map +1 -1
  195. package/dist/lib/skeleton.d.ts +7 -7
  196. package/dist/lib/skeleton.d.ts.map +1 -1
  197. package/dist/lib/skeleton.js +10 -10
  198. package/dist/lib/skeleton.js.map +1 -1
  199. package/dist/terminal/index.d.ts +14 -0
  200. package/dist/terminal/index.d.ts.map +1 -1
  201. package/dist/terminal/index.js +12 -0
  202. package/dist/terminal/index.js.map +1 -1
  203. package/dist/terminal/pty-manager.d.ts +1 -1
  204. package/dist/terminal/pty-manager.d.ts.map +1 -1
  205. package/dist/terminal/pty-manager.js +10 -7
  206. package/dist/terminal/pty-manager.js.map +1 -1
  207. package/dist/terminal/pty-session.js +3 -3
  208. package/dist/terminal/pty-session.js.map +1 -1
  209. package/dist/terminal/session-manager.d.ts +64 -0
  210. package/dist/terminal/session-manager.d.ts.map +1 -1
  211. package/dist/terminal/session-manager.js +299 -10
  212. package/dist/terminal/session-manager.js.map +1 -1
  213. package/dist/terminal/shellper-client.d.ts +2 -1
  214. package/dist/terminal/shellper-client.d.ts.map +1 -1
  215. package/dist/terminal/shellper-client.js +4 -2
  216. package/dist/terminal/shellper-client.js.map +1 -1
  217. package/dist/terminal/shellper-main.js +33 -4
  218. package/dist/terminal/shellper-main.js.map +1 -1
  219. package/dist/terminal/shellper-process.d.ts +24 -7
  220. package/dist/terminal/shellper-process.d.ts.map +1 -1
  221. package/dist/terminal/shellper-process.js +139 -36
  222. package/dist/terminal/shellper-process.js.map +1 -1
  223. package/dist/terminal/shellper-protocol.d.ts +1 -0
  224. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  225. package/dist/terminal/shellper-protocol.js.map +1 -1
  226. package/package.json +4 -1
  227. package/skeleton/.claude/skills/af/SKILL.md +7 -7
  228. package/skeleton/.claude/skills/consult/SKILL.md +1 -1
  229. package/skeleton/builders.md +2 -2
  230. package/skeleton/maintain/.gitkeep +1 -1
  231. package/skeleton/porch/prompts/specify.md +1 -1
  232. package/skeleton/protocols/bugfix/prompts/pr.md +15 -4
  233. package/skeleton/protocols/experiment/protocol.md +17 -17
  234. package/skeleton/protocols/maintain/prompts/audit.md +2 -2
  235. package/skeleton/protocols/maintain/prompts/sync.md +1 -1
  236. package/skeleton/protocols/maintain/prompts/verify.md +1 -1
  237. package/skeleton/protocols/maintain/protocol.md +8 -9
  238. package/skeleton/protocols/maintain/templates/maintenance-run.md +2 -2
  239. package/skeleton/protocols/spir/protocol.json +5 -5
  240. package/skeleton/protocols/spir/protocol.md +8 -8
  241. package/skeleton/protocols/tick/protocol.md +31 -31
  242. package/skeleton/resources/commands/agent-farm.md +14 -14
  243. package/skeleton/resources/commands/codev.md +0 -1
  244. package/skeleton/resources/commands/consult.md +3 -3
  245. package/skeleton/resources/spikes.md +3 -3
  246. package/skeleton/resources/workflow-reference.md +14 -14
  247. package/skeleton/roles/architect.md +25 -25
  248. package/skeleton/roles/builder.md +1 -1
  249. package/skeleton/roles/consultant.md +6 -0
  250. package/skeleton/templates/AGENTS.md +5 -5
  251. package/skeleton/templates/CLAUDE.md +5 -5
  252. package/skeleton/templates/lifecycle.md +9 -9
  253. package/templates/open.html +19 -16
  254. package/templates/tower.html +54 -94
  255. package/templates/vendor/marked.min.js +6 -0
  256. package/templates/vendor/prism-bash.min.js +1 -0
  257. package/templates/vendor/prism-css.min.js +1 -0
  258. package/templates/vendor/prism-javascript.min.js +1 -0
  259. package/templates/vendor/prism-json.min.js +1 -0
  260. package/templates/vendor/prism-markdown.min.js +1 -0
  261. package/templates/vendor/prism-markup.min.js +1 -0
  262. package/templates/vendor/prism-python.min.js +1 -0
  263. package/templates/vendor/prism-tomorrow.min.css +1 -0
  264. package/templates/vendor/prism-typescript.min.js +1 -0
  265. package/templates/vendor/prism-yaml.min.js +1 -0
  266. package/templates/vendor/prism.min.js +1 -0
  267. package/templates/vendor/purify.min.js +3 -0
  268. package/dashboard/dist/assets/index-4n9zpWLY.css +0 -32
  269. package/dashboard/dist/assets/index-b38SaXk5.js +0 -136
  270. package/dashboard/dist/assets/index-b38SaXk5.js.map +0 -1
  271. package/dist/agent-farm/hq-connector.d.ts +0 -19
  272. package/dist/agent-farm/hq-connector.d.ts.map +0 -1
  273. package/dist/agent-farm/hq-connector.js +0 -351
  274. package/dist/agent-farm/hq-connector.js.map +0 -1
  275. package/dist/agent-farm/utils/deps.d.ts +0 -51
  276. package/dist/agent-farm/utils/deps.d.ts.map +0 -1
  277. package/dist/agent-farm/utils/deps.js +0 -162
  278. package/dist/agent-farm/utils/deps.js.map +0 -1
  279. package/dist/agent-farm/utils/gate-status.d.ts +0 -16
  280. package/dist/agent-farm/utils/gate-status.d.ts.map +0 -1
  281. package/dist/agent-farm/utils/gate-status.js +0 -79
  282. package/dist/agent-farm/utils/gate-status.js.map +0 -1
  283. package/dist/agent-farm/utils/gate-watcher.d.ts +0 -38
  284. package/dist/agent-farm/utils/gate-watcher.d.ts.map +0 -1
  285. package/dist/agent-farm/utils/gate-watcher.js +0 -122
  286. package/dist/agent-farm/utils/gate-watcher.js.map +0 -1
  287. package/dist/agent-farm/utils/session.d.ts +0 -32
  288. package/dist/agent-farm/utils/session.d.ts.map +0 -1
  289. package/dist/agent-farm/utils/session.js +0 -57
  290. package/dist/agent-farm/utils/session.js.map +0 -1
  291. package/dist/lib/projectlist-parser.d.ts +0 -70
  292. package/dist/lib/projectlist-parser.d.ts.map +0 -1
  293. package/dist/lib/projectlist-parser.js +0 -200
  294. package/dist/lib/projectlist-parser.js.map +0 -1
  295. package/skeleton/templates/projectlist-archive.md +0 -21
  296. package/skeleton/templates/projectlist.md +0 -147
  297. package/templates/dashboard/css/dialogs.css +0 -149
  298. package/templates/dashboard/css/files.css +0 -558
  299. package/templates/dashboard/css/layout.css +0 -133
  300. package/templates/dashboard/css/projects.css +0 -501
  301. package/templates/dashboard/css/statusbar.css +0 -23
  302. package/templates/dashboard/css/tabs.css +0 -314
  303. package/templates/dashboard/css/utilities.css +0 -50
  304. package/templates/dashboard/css/variables.css +0 -45
  305. package/templates/dashboard/index.html +0 -149
  306. package/templates/dashboard/js/dialogs.js +0 -368
  307. package/templates/dashboard/js/files.js +0 -448
  308. package/templates/dashboard/js/main.js +0 -476
  309. package/templates/dashboard/js/projects.js +0 -544
  310. package/templates/dashboard/js/state.js +0 -91
  311. package/templates/dashboard/js/tabs.js +0 -518
  312. package/templates/dashboard/js/utils.js +0 -191
@@ -14,16 +14,24 @@
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';
19
20
  import { fileURLToPath } from 'node:url';
21
+ const execAsync = promisify(exec);
22
+ import { DEFAULT_COLS, defaultSessionOptions } from '../../terminal/index.js';
20
23
  import { parseJsonBody, isRequestAllowed } from '../utils/server-utils.js';
21
- import { isRateLimited, normalizeProjectPath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
24
+ import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
22
25
  import { handleTunnelEndpoint } from './tower-tunnel.js';
23
- import { getKnownProjectPaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, } from './tower-instances.js';
24
- import { getProjectTerminals, getTerminalManager, getProjectTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, deleteProjectTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForProject, } from './tower-terminals.js';
26
+ import { resolveTarget, broadcastMessage, isResolveError } from './tower-messages.js';
27
+ import { formatArchitectMessage, formatBuilderMessage } from '../utils/message-format.js';
28
+ import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, } from './tower-instances.js';
29
+ import { OverviewCache } from './overview.js';
30
+ import { getWorkspaceTerminals, getTerminalManager, getWorkspaceTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, removeTerminalFromRegistry, deleteWorkspaceTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForWorkspace, } from './tower-terminals.js';
25
31
  const __filename = fileURLToPath(import.meta.url);
26
32
  const __dirname = path.dirname(__filename);
33
+ // Singleton cache for overview endpoint (Spec 0126 Phase 4)
34
+ const overviewCache = new OverviewCache();
27
35
  // ============================================================================
28
36
  // Helper: read raw request body
29
37
  // ============================================================================
@@ -36,16 +44,19 @@ async function readBody(req) {
36
44
  }
37
45
  const ROUTES = {
38
46
  'GET /health': (_req, res) => handleHealthCheck(res),
39
- 'GET /api/projects': (_req, res) => handleListProjects(res),
47
+ 'GET /api/workspaces': (_req, res) => handleListWorkspaces(res),
40
48
  'POST /api/terminals': (req, res, _url, ctx) => handleTerminalCreate(req, res, ctx),
41
49
  'GET /api/terminals': (_req, res) => handleTerminalList(res),
42
50
  'GET /api/status': (_req, res) => handleStatus(res),
51
+ 'GET /api/overview': (_req, res, url) => handleOverview(res, url),
52
+ 'POST /api/overview/refresh': (_req, res) => handleOverviewRefresh(res),
43
53
  'GET /api/events': (req, res, _url, ctx) => handleSSEEvents(req, res, ctx),
44
54
  'POST /api/notify': (req, res, _url, ctx) => handleNotify(req, res, ctx),
45
55
  'GET /api/browse': (_req, res, url) => handleBrowse(res, url),
46
- 'POST /api/create': (req, res, _url, ctx) => handleCreateProject(req, res, ctx),
56
+ 'POST /api/create': (req, res, _url, ctx) => handleCreateWorkspace(req, res, ctx),
47
57
  'POST /api/launch': (req, res) => handleLaunchInstance(req, res),
48
58
  'POST /api/stop': (req, res) => handleStopInstance(req, res),
59
+ 'POST /api/send': (req, res, _url, ctx) => handleSend(req, res, ctx),
49
60
  'GET /': (_req, res, _url, ctx) => handleDashboard(res, ctx),
50
61
  'GET /index.html': (_req, res, _url, ctx) => handleDashboard(res, ctx),
51
62
  };
@@ -89,19 +100,19 @@ export async function handleRequest(req, res, ctx) {
89
100
  await handleTunnelEndpoint(req, res, tunnelSub);
90
101
  return;
91
102
  }
92
- // Project API: /api/projects/:encodedPath/activate|deactivate|status (Spec 0090 Phase 1)
93
- const projectApiMatch = url.pathname.match(/^\/api\/projects\/([^/]+)\/(activate|deactivate|status)$/);
94
- if (projectApiMatch) {
95
- return await handleProjectAction(req, res, ctx, projectApiMatch);
103
+ // Workspace API: /api/workspaces/:encodedPath/activate|deactivate|status (Spec 0090 Phase 1)
104
+ const workspaceApiMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)\/(activate|deactivate|status)$/);
105
+ if (workspaceApiMatch) {
106
+ return await handleWorkspaceAction(req, res, ctx, workspaceApiMatch);
96
107
  }
97
108
  // Terminal-specific routes: /api/terminals/:id/* (Spec 0090 Phase 2)
98
109
  const terminalRouteMatch = url.pathname.match(/^\/api\/terminals\/([^/]+)(\/.*)?$/);
99
110
  if (terminalRouteMatch) {
100
111
  return await handleTerminalRoutes(req, res, url, terminalRouteMatch);
101
112
  }
102
- // Project routes: /project/:base64urlPath/* (Spec 0090 Phase 4)
103
- if (url.pathname.startsWith('/project/')) {
104
- return await handleProjectRoutes(req, res, ctx, url);
113
+ // Workspace routes: /workspace/:base64urlPath/* (Spec 0090 Phase 4)
114
+ if (url.pathname.startsWith('/workspace/')) {
115
+ return await handleWorkspaceRoutes(req, res, ctx, url);
105
116
  }
106
117
  // 404 for everything else
107
118
  res.writeHead(404, { 'Content-Type': 'text/plain' });
@@ -123,59 +134,58 @@ async function handleHealthCheck(res) {
123
134
  res.end(JSON.stringify({
124
135
  status: 'healthy',
125
136
  uptime: process.uptime(),
126
- activeProjects: activeCount,
127
- totalProjects: instances.length,
137
+ activeWorkspaces: activeCount,
138
+ totalWorkspaces: instances.length,
128
139
  memoryUsage: process.memoryUsage().heapUsed,
129
140
  timestamp: new Date().toISOString(),
130
141
  }));
131
142
  }
132
- async function handleListProjects(res) {
143
+ async function handleListWorkspaces(res) {
133
144
  const instances = await getInstances();
134
- const projects = instances.map((i) => ({
135
- path: i.projectPath,
136
- name: i.projectName,
145
+ const workspaces = instances.map((i) => ({
146
+ path: i.workspacePath,
147
+ name: i.workspaceName,
137
148
  active: i.running,
138
149
  proxyUrl: i.proxyUrl,
139
150
  terminals: i.terminals.length,
140
151
  }));
141
152
  res.writeHead(200, { 'Content-Type': 'application/json' });
142
- res.end(JSON.stringify({ projects }));
153
+ res.end(JSON.stringify({ workspaces }));
143
154
  }
144
- async function handleProjectAction(req, res, ctx, match) {
155
+ async function handleWorkspaceAction(req, res, ctx, match) {
145
156
  const [, encodedPath, action] = match;
146
- let projectPath;
157
+ let workspacePath;
147
158
  try {
148
- projectPath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
149
- if (!projectPath || (!projectPath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(projectPath))) {
159
+ workspacePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
160
+ if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
150
161
  throw new Error('Invalid path');
151
162
  }
152
- projectPath = normalizeProjectPath(projectPath);
163
+ workspacePath = normalizeWorkspacePath(workspacePath);
153
164
  }
154
165
  catch {
155
166
  res.writeHead(400, { 'Content-Type': 'application/json' });
156
- res.end(JSON.stringify({ error: 'Invalid project path encoding' }));
167
+ res.end(JSON.stringify({ error: 'Invalid workspace path encoding' }));
157
168
  return;
158
169
  }
159
- // GET /api/projects/:path/status
170
+ // GET /api/workspaces/:path/status
160
171
  if (req.method === 'GET' && action === 'status') {
161
172
  const instances = await getInstances();
162
- const instance = instances.find((i) => i.projectPath === projectPath);
173
+ const instance = instances.find((i) => i.workspacePath === workspacePath);
163
174
  if (!instance) {
164
175
  res.writeHead(404, { 'Content-Type': 'application/json' });
165
- res.end(JSON.stringify({ error: 'Project not found' }));
176
+ res.end(JSON.stringify({ error: 'Workspace not found' }));
166
177
  return;
167
178
  }
168
179
  res.writeHead(200, { 'Content-Type': 'application/json' });
169
180
  res.end(JSON.stringify({
170
- path: instance.projectPath,
171
- name: instance.projectName,
181
+ path: instance.workspacePath,
182
+ name: instance.workspaceName,
172
183
  active: instance.running,
173
184
  terminals: instance.terminals,
174
- gateStatus: instance.gateStatus,
175
185
  }));
176
186
  return;
177
187
  }
178
- // POST /api/projects/:path/activate
188
+ // POST /api/workspaces/:path/activate
179
189
  if (req.method === 'POST' && action === 'activate') {
180
190
  // Rate limiting: 10 activations per minute per client
181
191
  const clientIp = req.socket.remoteAddress || '127.0.0.1';
@@ -184,7 +194,7 @@ async function handleProjectAction(req, res, ctx, match) {
184
194
  res.end(JSON.stringify({ error: 'Too many activations, try again later' }));
185
195
  return;
186
196
  }
187
- const result = await launchInstance(projectPath);
197
+ const result = await launchInstance(workspacePath);
188
198
  if (result.success) {
189
199
  res.writeHead(200, { 'Content-Type': 'application/json' });
190
200
  res.end(JSON.stringify({ success: true, adopted: result.adopted }));
@@ -195,17 +205,17 @@ async function handleProjectAction(req, res, ctx, match) {
195
205
  }
196
206
  return;
197
207
  }
198
- // POST /api/projects/:path/deactivate
208
+ // POST /api/workspaces/:path/deactivate
199
209
  if (req.method === 'POST' && action === 'deactivate') {
200
- const knownPaths = getKnownProjectPaths();
201
- const resolvedPath = fs.existsSync(projectPath) ? fs.realpathSync(projectPath) : projectPath;
202
- const isKnown = knownPaths.some((p) => p === projectPath || p === resolvedPath);
210
+ const knownPaths = getKnownWorkspacePaths();
211
+ const resolvedPath = fs.existsSync(workspacePath) ? fs.realpathSync(workspacePath) : workspacePath;
212
+ const isKnown = knownPaths.some((p) => p === workspacePath || p === resolvedPath);
203
213
  if (!isKnown) {
204
214
  res.writeHead(404, { 'Content-Type': 'application/json' });
205
- res.end(JSON.stringify({ ok: false, error: 'Project not found' }));
215
+ res.end(JSON.stringify({ ok: false, error: 'Workspace not found' }));
206
216
  return;
207
217
  }
208
- const result = await stopInstance(projectPath);
218
+ const result = await stopInstance(workspacePath);
209
219
  res.writeHead(200, { 'Content-Type': 'application/json' });
210
220
  res.end(JSON.stringify(result));
211
221
  return;
@@ -224,7 +234,7 @@ async function handleTerminalCreate(req, res, ctx) {
224
234
  const env = typeof body.env === 'object' && body.env !== null ? body.env : undefined;
225
235
  const label = typeof body.label === 'string' ? body.label : undefined;
226
236
  // Optional session persistence via shellper
227
- const projectPath = typeof body.projectPath === 'string' ? body.projectPath : null;
237
+ const workspacePath = typeof body.workspacePath === 'string' ? body.workspacePath : null;
228
238
  const termType = typeof body.type === 'string' && ['builder', 'shell'].includes(body.type) ? body.type : null;
229
239
  const roleId = typeof body.roleId === 'string' ? body.roleId : null;
230
240
  const requestPersistence = body.persistent === true;
@@ -244,9 +254,8 @@ async function handleTerminalCreate(req, res, ctx) {
244
254
  args: args || [],
245
255
  cwd,
246
256
  env: sessionEnv,
247
- cols: cols || 200,
248
- rows: 50,
249
- restartOnExit: false,
257
+ ...defaultSessionOptions(),
258
+ cols: cols || DEFAULT_COLS,
250
259
  });
251
260
  const replayData = client.getReplayData() ?? Buffer.alloc(0);
252
261
  const shellperInfo = shellperManager.getSessionInfo(sessionId);
@@ -260,16 +269,16 @@ async function handleTerminalCreate(req, res, ctx) {
260
269
  }
261
270
  info = session;
262
271
  persistent = true;
263
- if (projectPath && termType && roleId) {
264
- const entry = getProjectTerminalsEntry(normalizeProjectPath(projectPath));
272
+ if (workspacePath && termType && roleId) {
273
+ const entry = getWorkspaceTerminalsEntry(normalizeWorkspacePath(workspacePath));
265
274
  if (termType === 'builder') {
266
275
  entry.builders.set(roleId, session.id);
267
276
  }
268
277
  else {
269
278
  entry.shells.set(roleId, session.id);
270
279
  }
271
- saveTerminalSession(session.id, projectPath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
272
- ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for project ${projectPath}`);
280
+ saveTerminalSession(session.id, workspacePath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
281
+ ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for workspace ${workspacePath}`);
273
282
  }
274
283
  }
275
284
  catch (shellperErr) {
@@ -281,16 +290,16 @@ async function handleTerminalCreate(req, res, ctx) {
281
290
  if (!info) {
282
291
  info = await manager.createSession({ command, args, cols, rows, cwd, env, label });
283
292
  persistent = false;
284
- if (projectPath && termType && roleId) {
285
- const entry = getProjectTerminalsEntry(normalizeProjectPath(projectPath));
293
+ if (workspacePath && termType && roleId) {
294
+ const entry = getWorkspaceTerminalsEntry(normalizeWorkspacePath(workspacePath));
286
295
  if (termType === 'builder') {
287
296
  entry.builders.set(roleId, info.id);
288
297
  }
289
298
  else {
290
299
  entry.shells.set(roleId, info.id);
291
300
  }
292
- saveTerminalSession(info.id, projectPath, termType, roleId, info.pid);
293
- ctx.log('WARN', `Terminal ${info.id} for ${projectPath} is non-persistent (shellper unavailable)`);
301
+ saveTerminalSession(info.id, workspacePath, termType, roleId, info.pid);
302
+ ctx.log('WARN', `Terminal ${info.id} for ${workspacePath} is non-persistent (shellper unavailable)`);
294
303
  }
295
304
  }
296
305
  res.writeHead(201, { 'Content-Type': 'application/json' });
@@ -333,6 +342,9 @@ async function handleTerminalRoutes(req, res, url, match) {
333
342
  }
334
343
  // TICK-001: Delete from SQLite
335
344
  deleteTerminalSession(terminalId);
345
+ // Bugfix #290: Also remove from in-memory registry so dashboard
346
+ // stops showing tabs for cleaned-up builders
347
+ removeTerminalFromRegistry(terminalId);
336
348
  res.writeHead(204);
337
349
  res.end();
338
350
  return;
@@ -406,6 +418,36 @@ async function handleStatus(res) {
406
418
  res.writeHead(200, { 'Content-Type': 'application/json' });
407
419
  res.end(JSON.stringify({ instances }));
408
420
  }
421
+ async function handleOverview(res, url, workspaceOverride) {
422
+ // Accept workspace from: explicit override (workspace-scoped route), ?workspace= param, or first known path.
423
+ let workspaceRoot = workspaceOverride || url.searchParams.get('workspace');
424
+ if (!workspaceRoot) {
425
+ const knownPaths = getKnownWorkspacePaths();
426
+ workspaceRoot = knownPaths.find(p => !p.includes('/.builders/')) || null;
427
+ }
428
+ if (!workspaceRoot) {
429
+ res.writeHead(200, { 'Content-Type': 'application/json' });
430
+ res.end(JSON.stringify({ builders: [], pendingPRs: [], backlog: [] }));
431
+ return;
432
+ }
433
+ // Build set of active builder role_ids (lowercased) from live terminal sessions
434
+ const wsTerminals = getWorkspaceTerminals();
435
+ const entry = wsTerminals.get(normalizeWorkspacePath(workspaceRoot));
436
+ const activeBuilderRoleIds = new Set();
437
+ if (entry) {
438
+ for (const key of entry.builders.keys()) {
439
+ activeBuilderRoleIds.add(key.toLowerCase());
440
+ }
441
+ }
442
+ const data = await overviewCache.getOverview(workspaceRoot, activeBuilderRoleIds);
443
+ res.writeHead(200, { 'Content-Type': 'application/json' });
444
+ res.end(JSON.stringify(data));
445
+ }
446
+ function handleOverviewRefresh(res) {
447
+ overviewCache.invalidate();
448
+ res.writeHead(200, { 'Content-Type': 'application/json' });
449
+ res.end(JSON.stringify({ ok: true }));
450
+ }
409
451
  function handleSSEEvents(req, res, ctx) {
410
452
  const clientId = crypto.randomBytes(8).toString('hex');
411
453
  res.writeHead(200, {
@@ -429,7 +471,7 @@ async function handleNotify(req, res, ctx) {
429
471
  const type = typeof body.type === 'string' ? body.type : 'info';
430
472
  const title = typeof body.title === 'string' ? body.title : '';
431
473
  const messageBody = typeof body.body === 'string' ? body.body : '';
432
- const project = typeof body.project === 'string' ? body.project : undefined;
474
+ const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
433
475
  if (!title || !messageBody) {
434
476
  res.writeHead(400, { 'Content-Type': 'application/json' });
435
477
  res.end(JSON.stringify({ success: false, error: 'Missing title or body' }));
@@ -440,12 +482,113 @@ async function handleNotify(req, res, ctx) {
440
482
  type,
441
483
  title,
442
484
  body: messageBody,
443
- project,
485
+ workspace,
444
486
  });
445
487
  ctx.log('INFO', `Notification broadcast: ${title}`);
446
488
  res.writeHead(200, { 'Content-Type': 'application/json' });
447
489
  res.end(JSON.stringify({ success: true }));
448
490
  }
491
+ // ============================================================================
492
+ // POST /api/send — send a message to a resolved agent terminal
493
+ // ============================================================================
494
+ async function handleSend(req, res, ctx) {
495
+ const body = await parseJsonBody(req);
496
+ // Validate required fields
497
+ const to = typeof body.to === 'string' ? body.to.trim() : '';
498
+ const message = typeof body.message === 'string' ? body.message.trim() : '';
499
+ if (!to) {
500
+ res.writeHead(400, { 'Content-Type': 'application/json' });
501
+ res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Missing or empty "to" field' }));
502
+ return;
503
+ }
504
+ if (!message) {
505
+ res.writeHead(400, { 'Content-Type': 'application/json' });
506
+ res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Missing or empty "message" field' }));
507
+ return;
508
+ }
509
+ // Optional fields
510
+ const from = typeof body.from === 'string' ? body.from : undefined;
511
+ const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
512
+ const fromWorkspace = typeof body.fromWorkspace === 'string' ? body.fromWorkspace : undefined;
513
+ const options = typeof body.options === 'object' && body.options !== null
514
+ ? body.options
515
+ : {};
516
+ const raw = options.raw === true;
517
+ const noEnter = options.noEnter === true;
518
+ const interrupt = options.interrupt === true;
519
+ // Resolve the target address to a terminal ID
520
+ const result = resolveTarget(to, workspace);
521
+ if (isResolveError(result)) {
522
+ const statusCode = result.code === 'AMBIGUOUS' ? 409
523
+ : result.code === 'NO_CONTEXT' ? 400
524
+ : 404;
525
+ // Map NO_CONTEXT to INVALID_PARAMS per plan's error contract
526
+ const errorCode = result.code === 'NO_CONTEXT' ? 'INVALID_PARAMS' : result.code;
527
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
528
+ res.end(JSON.stringify({ error: errorCode, message: result.message }));
529
+ return;
530
+ }
531
+ // Get the terminal session
532
+ const manager = getTerminalManager();
533
+ const session = manager.getSession(result.terminalId);
534
+ if (!session) {
535
+ res.writeHead(404, { 'Content-Type': 'application/json' });
536
+ res.end(JSON.stringify({
537
+ error: 'NOT_FOUND',
538
+ message: `Terminal session ${result.terminalId} not found (agent '${result.agent}' resolved but terminal is gone).`,
539
+ }));
540
+ return;
541
+ }
542
+ // Format the message based on sender/target
543
+ const isArchitectTarget = result.agent === 'architect';
544
+ let formattedMessage;
545
+ if (isArchitectTarget && from) {
546
+ // Builder → Architect
547
+ formattedMessage = formatBuilderMessage(from, message, undefined, raw);
548
+ }
549
+ else if (!isArchitectTarget) {
550
+ // Architect → Builder (or any → builder)
551
+ formattedMessage = formatArchitectMessage(message, undefined, raw);
552
+ }
553
+ else {
554
+ // Unknown sender to architect — use raw
555
+ formattedMessage = raw ? message : formatArchitectMessage(message, undefined, false);
556
+ }
557
+ // Optionally interrupt first
558
+ if (interrupt) {
559
+ session.write('\x03'); // Ctrl+C
560
+ await new Promise(resolve => setTimeout(resolve, 100));
561
+ }
562
+ // Write the message to the terminal
563
+ session.write(formattedMessage);
564
+ // Send Enter to submit (unless noEnter)
565
+ if (!noEnter) {
566
+ session.write('\r');
567
+ }
568
+ // Broadcast structured message to WebSocket subscribers
569
+ const senderWorkspace = fromWorkspace ?? workspace ?? 'unknown';
570
+ broadcastMessage({
571
+ type: 'message',
572
+ from: {
573
+ project: path.basename(senderWorkspace),
574
+ agent: from ?? 'unknown',
575
+ },
576
+ to: {
577
+ project: path.basename(result.workspacePath),
578
+ agent: result.agent,
579
+ },
580
+ content: message,
581
+ metadata: { raw, source: 'api' },
582
+ timestamp: new Date().toISOString(),
583
+ });
584
+ ctx.log('INFO', `Message sent: ${from ?? 'unknown'} → ${result.agent} (terminal ${result.terminalId.slice(0, 8)}...)`);
585
+ res.writeHead(200, { 'Content-Type': 'application/json' });
586
+ res.end(JSON.stringify({
587
+ ok: true,
588
+ terminalId: result.terminalId,
589
+ resolvedTo: result.agent,
590
+ }));
591
+ }
449
592
  async function handleBrowse(res, url) {
450
593
  const inputPath = url.searchParams.get('path') || '';
451
594
  try {
@@ -458,19 +601,19 @@ async function handleBrowse(res, url) {
458
601
  res.end(JSON.stringify({ suggestions: [], error: err.message }));
459
602
  }
460
603
  }
461
- async function handleCreateProject(req, res, ctx) {
604
+ async function handleCreateWorkspace(req, res, ctx) {
462
605
  const body = await parseJsonBody(req);
463
606
  const parentPath = body.parent;
464
- const projectName = body.name;
465
- if (!parentPath || !projectName) {
607
+ const workspaceName = body.name;
608
+ if (!parentPath || !workspaceName) {
466
609
  res.writeHead(400, { 'Content-Type': 'application/json' });
467
610
  res.end(JSON.stringify({ success: false, error: 'Missing parent or name' }));
468
611
  return;
469
612
  }
470
- // Validate project name
471
- if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
613
+ // Validate workspace name
614
+ if (!/^[a-zA-Z0-9_-]+$/.test(workspaceName)) {
472
615
  res.writeHead(400, { 'Content-Type': 'application/json' });
473
- res.end(JSON.stringify({ success: false, error: 'Invalid project name' }));
616
+ res.end(JSON.stringify({ success: false, error: 'Invalid workspace name' }));
474
617
  return;
475
618
  }
476
619
  // Expand ~ to home directory
@@ -484,77 +627,76 @@ async function handleCreateProject(req, res, ctx) {
484
627
  res.end(JSON.stringify({ success: false, error: `Parent directory does not exist: ${parentPath}` }));
485
628
  return;
486
629
  }
487
- const projectPath = path.join(expandedParent, projectName);
488
- // Check if project already exists
489
- if (fs.existsSync(projectPath)) {
630
+ const workspacePath = path.join(expandedParent, workspaceName);
631
+ // Check if workspace already exists
632
+ if (fs.existsSync(workspacePath)) {
490
633
  res.writeHead(400, { 'Content-Type': 'application/json' });
491
- res.end(JSON.stringify({ success: false, error: `Directory already exists: ${projectPath}` }));
634
+ res.end(JSON.stringify({ success: false, error: `Directory already exists: ${workspacePath}` }));
492
635
  return;
493
636
  }
494
637
  try {
495
638
  // Run codev init (it creates the directory)
496
- execSync(`codev init --yes "${projectName}"`, {
639
+ await execAsync(`codev init --yes "${workspaceName}"`, {
497
640
  cwd: expandedParent,
498
- stdio: 'pipe',
499
641
  timeout: 60000,
500
642
  });
501
643
  // Launch the instance
502
- const launchResult = await launchInstance(projectPath);
644
+ const launchResult = await launchInstance(workspacePath);
503
645
  if (!launchResult.success) {
504
646
  res.writeHead(500, { 'Content-Type': 'application/json' });
505
647
  res.end(JSON.stringify({ success: false, error: launchResult.error }));
506
648
  return;
507
649
  }
508
650
  res.writeHead(200, { 'Content-Type': 'application/json' });
509
- res.end(JSON.stringify({ success: true, projectPath }));
651
+ res.end(JSON.stringify({ success: true, workspacePath }));
510
652
  }
511
653
  catch (err) {
512
654
  // Clean up on failure
513
655
  try {
514
- if (fs.existsSync(projectPath)) {
515
- fs.rmSync(projectPath, { recursive: true });
656
+ if (fs.existsSync(workspacePath)) {
657
+ fs.rmSync(workspacePath, { recursive: true });
516
658
  }
517
659
  }
518
660
  catch {
519
661
  // Ignore cleanup errors
520
662
  }
521
663
  res.writeHead(500, { 'Content-Type': 'application/json' });
522
- res.end(JSON.stringify({ success: false, error: `Failed to create project: ${err.message}` }));
664
+ res.end(JSON.stringify({ success: false, error: `Failed to create workspace: ${err.message}` }));
523
665
  }
524
666
  }
525
667
  async function handleLaunchInstance(req, res) {
526
668
  const body = await parseJsonBody(req);
527
- let projectPath = body.projectPath;
528
- if (!projectPath) {
669
+ let workspacePath = body.workspacePath;
670
+ if (!workspacePath) {
529
671
  res.writeHead(400, { 'Content-Type': 'application/json' });
530
- res.end(JSON.stringify({ success: false, error: 'Missing projectPath' }));
672
+ res.end(JSON.stringify({ success: false, error: 'Missing workspacePath' }));
531
673
  return;
532
674
  }
533
675
  // Expand ~ to home directory
534
- if (projectPath.startsWith('~')) {
535
- projectPath = projectPath.replace('~', homedir());
676
+ if (workspacePath.startsWith('~')) {
677
+ workspacePath = workspacePath.replace('~', homedir());
536
678
  }
537
679
  // Reject relative paths — tower daemon CWD is unpredictable
538
- if (!path.isAbsolute(projectPath)) {
680
+ if (!path.isAbsolute(workspacePath)) {
539
681
  res.writeHead(400, { 'Content-Type': 'application/json' });
540
682
  res.end(JSON.stringify({
541
683
  success: false,
542
- error: `Relative paths are not supported. Use an absolute path (e.g., /Users/.../project or ~/Development/project).`,
684
+ error: `Relative paths are not supported. Use an absolute path (e.g., /Users/.../workspace or ~/Development/workspace).`,
543
685
  }));
544
686
  return;
545
687
  }
546
688
  // Normalize path (resolve .. segments, trailing slashes)
547
- projectPath = path.resolve(projectPath);
548
- const result = await launchInstance(projectPath);
689
+ workspacePath = path.resolve(workspacePath);
690
+ const result = await launchInstance(workspacePath);
549
691
  res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
550
692
  res.end(JSON.stringify(result));
551
693
  }
552
694
  async function handleStopInstance(req, res) {
553
695
  const body = await parseJsonBody(req);
554
- const targetPath = body.projectPath;
696
+ const targetPath = body.workspacePath;
555
697
  if (!targetPath) {
556
698
  res.writeHead(400, { 'Content-Type': 'application/json' });
557
- res.end(JSON.stringify({ success: false, error: 'Missing projectPath' }));
699
+ res.end(JSON.stringify({ success: false, error: 'Missing workspacePath' }));
558
700
  return;
559
701
  }
560
702
  const result = await stopInstance(targetPath);
@@ -578,52 +720,52 @@ function handleDashboard(res, ctx) {
578
720
  }
579
721
  }
580
722
  // ============================================================================
581
- // Project-scoped route handler
723
+ // Workspace-scoped route handler
582
724
  // ============================================================================
583
- async function handleProjectRoutes(req, res, ctx, url) {
725
+ async function handleWorkspaceRoutes(req, res, ctx, url) {
584
726
  const pathParts = url.pathname.split('/');
585
- // ['', 'project', base64urlPath, ...rest]
727
+ // ['', 'workspace', base64urlPath, ...rest]
586
728
  const encodedPath = pathParts[2];
587
729
  const subPath = pathParts.slice(3).join('/');
588
730
  if (!encodedPath) {
589
731
  res.writeHead(400, { 'Content-Type': 'application/json' });
590
- res.end(JSON.stringify({ error: 'Missing project path' }));
732
+ res.end(JSON.stringify({ error: 'Missing workspace path' }));
591
733
  return;
592
734
  }
593
735
  // Decode Base64URL (RFC 4648)
594
- let projectPath;
736
+ let workspacePath;
595
737
  try {
596
- projectPath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
738
+ workspacePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
597
739
  // Support both POSIX (/) and Windows (C:\) paths
598
- if (!projectPath || (!projectPath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(projectPath))) {
599
- throw new Error('Invalid project path');
740
+ if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
741
+ throw new Error('Invalid workspace path');
600
742
  }
601
743
  // Normalize to resolve symlinks (e.g. /var/folders → /private/var/folders on macOS)
602
- projectPath = normalizeProjectPath(projectPath);
744
+ workspacePath = normalizeWorkspacePath(workspacePath);
603
745
  }
604
746
  catch {
605
747
  res.writeHead(400, { 'Content-Type': 'application/json' });
606
- res.end(JSON.stringify({ error: 'Invalid project path encoding' }));
748
+ res.end(JSON.stringify({ error: 'Invalid workspace path encoding' }));
607
749
  return;
608
750
  }
609
751
  // Phase 4 (Spec 0090): Tower handles everything directly
610
752
  const isApiCall = subPath.startsWith('api/') || subPath === 'api';
611
753
  const isWsPath = subPath.startsWith('ws/') || subPath === 'ws';
612
- // Tunnel endpoints are tower-level, not project-scoped, but the React
754
+ // Tunnel endpoints are tower-level, not workspace-scoped, but the React
613
755
  // dashboard uses relative paths (./api/tunnel/...) which resolve to
614
- // /project/<encoded>/api/tunnel/... in project context. Handle here by
756
+ // /workspace/<encoded>/api/tunnel/... in workspace context. Handle here by
615
757
  // extracting the tunnel sub-path and dispatching to handleTunnelEndpoint().
616
758
  if (subPath.startsWith('api/tunnel/')) {
617
759
  const tunnelSub = subPath.slice('api/tunnel/'.length); // e.g. "status", "connect", "disconnect"
618
760
  await handleTunnelEndpoint(req, res, tunnelSub);
619
761
  return;
620
762
  }
621
- // GET /file?path=<relative-path> — Read project file by path (for StatusPanel project list)
763
+ // GET /file?path=<relative-path> — Read workspace file by path
622
764
  if (req.method === 'GET' && subPath === 'file' && url.searchParams.has('path')) {
623
765
  const relPath = url.searchParams.get('path');
624
- const fullPath = path.resolve(projectPath, relPath);
625
- // Security: ensure resolved path stays within project directory
626
- if (!fullPath.startsWith(projectPath + path.sep) && fullPath !== projectPath) {
766
+ const fullPath = path.resolve(workspacePath, relPath);
767
+ // Security: ensure resolved path stays within workspace directory
768
+ if (!fullPath.startsWith(workspacePath + path.sep) && fullPath !== workspacePath) {
627
769
  res.writeHead(403, { 'Content-Type': 'text/plain' });
628
770
  res.end('Forbidden');
629
771
  return;
@@ -643,7 +785,7 @@ async function handleProjectRoutes(req, res, ctx, url) {
643
785
  // 1. Not an API call
644
786
  // 2. Not a WebSocket path
645
787
  // 3. React dashboard is available
646
- // 4. Project doesn't need to be running for static files
788
+ // 4. Workspace doesn't need to be running for static files
647
789
  if (!isApiCall && !isWsPath && ctx.hasReactDashboard) {
648
790
  // Determine which static file to serve
649
791
  let staticPath;
@@ -664,61 +806,61 @@ async function handleProjectRoutes(req, res, ctx, url) {
664
806
  return;
665
807
  }
666
808
  }
667
- // Phase 4 (Spec 0090): Handle project APIs directly instead of proxying to dashboard-server
809
+ // Phase 4 (Spec 0090): Handle workspace APIs directly instead of proxying to dashboard-server
668
810
  if (isApiCall) {
669
811
  const apiPath = subPath.replace(/^api\/?/, '');
670
- // GET /api/state - Return project state (architect, builders, shells)
812
+ // GET /api/state - Return workspace state (architect, builders, shells)
671
813
  if (req.method === 'GET' && (apiPath === 'state' || apiPath === '')) {
672
- return handleProjectState(res, projectPath);
814
+ return handleWorkspaceState(res, workspacePath);
673
815
  }
674
816
  // POST /api/tabs/shell - Create a new shell terminal
675
817
  if (req.method === 'POST' && apiPath === 'tabs/shell') {
676
- return handleProjectShellCreate(res, ctx, projectPath);
818
+ return handleWorkspaceShellCreate(res, ctx, workspacePath);
677
819
  }
678
820
  // POST /api/tabs/file - Create a file tab (Spec 0092)
679
821
  if (req.method === 'POST' && apiPath === 'tabs/file') {
680
- return handleProjectFileTabCreate(req, res, ctx, projectPath);
822
+ return handleWorkspaceFileTabCreate(req, res, ctx, workspacePath);
681
823
  }
682
824
  // GET /api/file/:id - Get file content as JSON (Spec 0092)
683
825
  const fileGetMatch = apiPath.match(/^file\/([^/]+)$/);
684
826
  if (req.method === 'GET' && fileGetMatch) {
685
- return handleProjectFileGet(res, ctx, projectPath, fileGetMatch[1]);
827
+ return handleWorkspaceFileGet(res, ctx, workspacePath, fileGetMatch[1]);
686
828
  }
687
829
  // GET /api/file/:id/raw - Get raw file content (for images/video) (Spec 0092)
688
830
  const fileRawMatch = apiPath.match(/^file\/([^/]+)\/raw$/);
689
831
  if (req.method === 'GET' && fileRawMatch) {
690
- return handleProjectFileRaw(res, ctx, projectPath, fileRawMatch[1]);
832
+ return handleWorkspaceFileRaw(res, ctx, workspacePath, fileRawMatch[1]);
691
833
  }
692
834
  // POST /api/file/:id/save - Save file content (Spec 0092)
693
835
  const fileSaveMatch = apiPath.match(/^file\/([^/]+)\/save$/);
694
836
  if (req.method === 'POST' && fileSaveMatch) {
695
- return handleProjectFileSave(req, res, ctx, projectPath, fileSaveMatch[1]);
837
+ return handleWorkspaceFileSave(req, res, ctx, workspacePath, fileSaveMatch[1]);
696
838
  }
697
839
  // DELETE /api/tabs/:id - Delete a terminal or file tab
698
840
  const deleteMatch = apiPath.match(/^tabs\/(.+)$/);
699
841
  if (req.method === 'DELETE' && deleteMatch) {
700
- return handleProjectTabDelete(res, ctx, projectPath, deleteMatch[1]);
842
+ return handleWorkspaceTabDelete(res, ctx, workspacePath, deleteMatch[1]);
701
843
  }
702
- // POST /api/stop - Stop all terminals for project
844
+ // POST /api/stop - Stop all terminals for workspace
703
845
  if (req.method === 'POST' && apiPath === 'stop') {
704
- return handleProjectStopAll(res, projectPath);
846
+ return handleWorkspaceStopAll(res, workspacePath);
705
847
  }
706
- // GET /api/files - Return project directory tree for file browser (Spec 0092)
848
+ // GET /api/files - Return workspace directory tree for file browser (Spec 0092)
707
849
  if (req.method === 'GET' && apiPath === 'files') {
708
- return handleProjectFiles(res, url, projectPath);
850
+ return handleWorkspaceFiles(res, url, workspacePath);
709
851
  }
710
852
  // GET /api/git/status - Return git status for file browser (Spec 0092)
711
853
  if (req.method === 'GET' && apiPath === 'git/status') {
712
- return handleProjectGitStatus(res, ctx, projectPath);
854
+ return handleWorkspaceGitStatus(res, ctx, workspacePath);
713
855
  }
714
856
  // GET /api/files/recent - Return recently opened file tabs (Spec 0092)
715
857
  if (req.method === 'GET' && apiPath === 'files/recent') {
716
- return handleProjectRecentFiles(res, projectPath);
858
+ return handleWorkspaceRecentFiles(res, workspacePath);
717
859
  }
718
860
  // GET /api/annotate/:tabId/* — Serve rich annotator template and sub-APIs
719
861
  const annotateMatch = apiPath.match(/^annotate\/([^/]+)(\/(.*))?$/);
720
862
  if (annotateMatch) {
721
- return handleProjectAnnotate(req, res, ctx, url, projectPath, annotateMatch);
863
+ return handleWorkspaceAnnotate(req, res, ctx, url, workspacePath, annotateMatch);
722
864
  }
723
865
  // POST /api/paste-image - Upload pasted image to temp file (Issue #252)
724
866
  if (req.method === 'POST' && apiPath === 'paste-image') {
@@ -771,6 +913,14 @@ async function handleProjectRoutes(req, res, ctx, url) {
771
913
  });
772
914
  return;
773
915
  }
916
+ // GET /api/overview - Work view overview data (Spec 0126 Phase 4)
917
+ if (req.method === 'GET' && apiPath === 'overview') {
918
+ return handleOverview(res, url, workspacePath);
919
+ }
920
+ // POST /api/overview/refresh - Invalidate overview cache (Spec 0126 Phase 4)
921
+ if (req.method === 'POST' && apiPath === 'overview/refresh') {
922
+ return handleOverviewRefresh(res);
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 }));
@@ -794,24 +944,23 @@ async function handleProjectRoutes(req, res, ctx, url) {
794
944
  res.end('Not found');
795
945
  }
796
946
  // ============================================================================
797
- // Project API sub-handlers
947
+ // Workspace API sub-handlers
798
948
  // ============================================================================
799
- async function handleProjectState(res, projectPath) {
800
- // Refresh cache via getTerminalsForProject (handles SQLite sync
949
+ async function handleWorkspaceState(res, workspacePath) {
950
+ // Refresh cache via getTerminalsForWorkspace (handles SQLite sync
801
951
  // and shellper reconnection in one place)
802
- const encodedPath = Buffer.from(projectPath).toString('base64url');
803
- const proxyUrl = `/project/${encodedPath}/`;
804
- const { gateStatus } = await getTerminalsForProject(projectPath, proxyUrl);
952
+ const encodedPath = Buffer.from(workspacePath).toString('base64url');
953
+ const proxyUrl = `/workspace/${encodedPath}/`;
954
+ await getTerminalsForWorkspace(workspacePath, proxyUrl);
805
955
  // Now read from the refreshed cache
806
- const entry = getProjectTerminalsEntry(projectPath);
956
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
807
957
  const manager = getTerminalManager();
808
958
  const state = {
809
959
  architect: null,
810
960
  builders: [],
811
961
  utils: [],
812
962
  annotations: [],
813
- projectName: path.basename(projectPath),
814
- gateStatus,
963
+ workspaceName: path.basename(workspacePath),
815
964
  };
816
965
  // Add architect if exists
817
966
  if (entry.architect) {
@@ -845,7 +994,7 @@ async function handleProjectState(res, projectPath) {
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',
@@ -870,10 +1019,10 @@ async function handleProjectState(res, projectPath) {
870
1019
  res.writeHead(200, { 'Content-Type': 'application/json' });
871
1020
  res.end(JSON.stringify(state));
872
1021
  }
873
- async function handleProjectShellCreate(res, ctx, projectPath) {
1022
+ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
874
1023
  try {
875
1024
  const manager = getTerminalManager();
876
- const shellId = getNextShellId(projectPath);
1025
+ const shellId = getNextShellId(workspacePath);
877
1026
  const shellCmd = process.env.SHELL || '/bin/bash';
878
1027
  const shellArgs = [];
879
1028
  let shellCreated = false;
@@ -889,25 +1038,23 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
889
1038
  sessionId,
890
1039
  command: shellCmd,
891
1040
  args: shellArgs,
892
- cwd: projectPath,
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);
900
1047
  const session = manager.createSessionRaw({
901
1048
  label: `Shell ${shellId.replace('shell-', '')}`,
902
- cwd: projectPath,
1049
+ cwd: workspacePath,
903
1050
  });
904
1051
  const ptySession = manager.getSession(session.id);
905
1052
  if (ptySession) {
906
1053
  ptySession.attachShellper(client, replayData, shellperInfo.pid, sessionId);
907
1054
  }
908
- const entry = getProjectTerminalsEntry(projectPath);
1055
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
909
1056
  entry.shells.set(shellId, session.id);
910
- saveTerminalSession(session.id, projectPath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
1057
+ saveTerminalSession(session.id, workspacePath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
911
1058
  shellCreated = true;
912
1059
  res.writeHead(200, { 'Content-Type': 'application/json' });
913
1060
  res.end(JSON.stringify({
@@ -928,14 +1075,14 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
928
1075
  const session = await manager.createSession({
929
1076
  command: shellCmd,
930
1077
  args: shellArgs,
931
- cwd: projectPath,
1078
+ cwd: workspacePath,
932
1079
  label: `Shell ${shellId.replace('shell-', '')}`,
933
1080
  env: process.env,
934
1081
  });
935
- const entry = getProjectTerminalsEntry(projectPath);
1082
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
936
1083
  entry.shells.set(shellId, session.id);
937
- saveTerminalSession(session.id, projectPath, 'shell', shellId, session.pid);
938
- ctx.log('WARN', `Shell ${shellId} for ${projectPath} is non-persistent (shellper unavailable)`);
1084
+ saveTerminalSession(session.id, workspacePath, 'shell', shellId, session.pid);
1085
+ ctx.log('WARN', `Shell ${shellId} for ${workspacePath} is non-persistent (shellper unavailable)`);
939
1086
  res.writeHead(200, { 'Content-Type': 'application/json' });
940
1087
  res.end(JSON.stringify({
941
1088
  id: shellId,
@@ -952,7 +1099,7 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
952
1099
  res.end(JSON.stringify({ error: err.message }));
953
1100
  }
954
1101
  }
955
- async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
1102
+ async function handleWorkspaceFileTabCreate(req, res, ctx, workspacePath) {
956
1103
  try {
957
1104
  const body = await readBody(req);
958
1105
  const { path: filePath, line, terminalId } = JSON.parse(body || '{}');
@@ -973,12 +1120,12 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
973
1120
  fullPath = path.join(session.cwd, filePath);
974
1121
  }
975
1122
  else {
976
- ctx.log('WARN', `Terminal session ${terminalId} not found, falling back to project root`);
977
- fullPath = path.join(projectPath, filePath);
1123
+ ctx.log('WARN', `Terminal session ${terminalId} not found, falling back to workspace root`);
1124
+ fullPath = path.join(workspacePath, filePath);
978
1125
  }
979
1126
  }
980
1127
  else {
981
- fullPath = path.join(projectPath, filePath);
1128
+ fullPath = path.join(workspacePath, filePath);
982
1129
  }
983
1130
  // Security: symlink-aware containment check
984
1131
  // For non-existent files, resolve the parent directory to handle
@@ -995,23 +1142,23 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
995
1142
  resolvedPath = path.resolve(fullPath);
996
1143
  }
997
1144
  }
998
- let normalizedProject;
1145
+ let normalizedWorkspace;
999
1146
  try {
1000
- normalizedProject = fs.realpathSync(projectPath);
1147
+ normalizedWorkspace = fs.realpathSync(workspacePath);
1001
1148
  }
1002
1149
  catch {
1003
- normalizedProject = path.resolve(projectPath);
1150
+ normalizedWorkspace = path.resolve(workspacePath);
1004
1151
  }
1005
- const isWithinProject = resolvedPath.startsWith(normalizedProject + path.sep)
1006
- || resolvedPath === normalizedProject;
1007
- if (!isWithinProject) {
1152
+ const isWithinWorkspace = resolvedPath.startsWith(normalizedWorkspace + path.sep)
1153
+ || resolvedPath === normalizedWorkspace;
1154
+ if (!isWithinWorkspace) {
1008
1155
  res.writeHead(403, { 'Content-Type': 'application/json' });
1009
- res.end(JSON.stringify({ error: 'Path outside project' }));
1156
+ res.end(JSON.stringify({ error: 'Path outside workspace' }));
1010
1157
  return;
1011
1158
  }
1012
1159
  // Non-existent files still create a tab (spec 0101: file viewer shows "File not found")
1013
1160
  const fileExists = fs.existsSync(fullPath);
1014
- const entry = getProjectTerminalsEntry(projectPath);
1161
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1015
1162
  // Check if already open
1016
1163
  for (const [id, tab] of entry.fileTabs) {
1017
1164
  if (tab.path === fullPath) {
@@ -1024,7 +1171,7 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
1024
1171
  const id = `file-${crypto.randomUUID()}`;
1025
1172
  const createdAt = Date.now();
1026
1173
  entry.fileTabs.set(id, { id, path: fullPath, createdAt });
1027
- saveFileTab(id, projectPath, fullPath, createdAt);
1174
+ saveFileTab(id, workspacePath, fullPath, createdAt);
1028
1175
  ctx.log('INFO', `Created file tab: ${id} for ${path.basename(fullPath)}`);
1029
1176
  res.writeHead(200, { 'Content-Type': 'application/json' });
1030
1177
  res.end(JSON.stringify({ id, existing: false, line, notFound: !fileExists }));
@@ -1035,8 +1182,8 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
1035
1182
  res.end(JSON.stringify({ error: err.message }));
1036
1183
  }
1037
1184
  }
1038
- function handleProjectFileGet(res, ctx, projectPath, tabId) {
1039
- const entry = getProjectTerminalsEntry(projectPath);
1185
+ function handleWorkspaceFileGet(res, ctx, workspacePath, tabId) {
1186
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1040
1187
  const tab = entry.fileTabs.get(tabId);
1041
1188
  if (!tab) {
1042
1189
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -1083,8 +1230,8 @@ function handleProjectFileGet(res, ctx, projectPath, tabId) {
1083
1230
  res.end(JSON.stringify({ error: err.message }));
1084
1231
  }
1085
1232
  }
1086
- function handleProjectFileRaw(res, ctx, projectPath, tabId) {
1087
- const entry = getProjectTerminalsEntry(projectPath);
1233
+ function handleWorkspaceFileRaw(res, ctx, workspacePath, tabId) {
1234
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1088
1235
  const tab = entry.fileTabs.get(tabId);
1089
1236
  if (!tab) {
1090
1237
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -1107,8 +1254,8 @@ function handleProjectFileRaw(res, ctx, projectPath, tabId) {
1107
1254
  res.end(JSON.stringify({ error: err.message }));
1108
1255
  }
1109
1256
  }
1110
- async function handleProjectFileSave(req, res, ctx, projectPath, tabId) {
1111
- const entry = getProjectTerminalsEntry(projectPath);
1257
+ async function handleWorkspaceFileSave(req, res, ctx, workspacePath, tabId) {
1258
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1112
1259
  const tab = entry.fileTabs.get(tabId);
1113
1260
  if (!tab) {
1114
1261
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -1134,8 +1281,8 @@ async function handleProjectFileSave(req, res, ctx, projectPath, tabId) {
1134
1281
  res.end(JSON.stringify({ error: err.message }));
1135
1282
  }
1136
1283
  }
1137
- async function handleProjectTabDelete(res, ctx, projectPath, tabId) {
1138
- const entry = getProjectTerminalsEntry(projectPath);
1284
+ async function handleWorkspaceTabDelete(res, ctx, workspacePath, tabId) {
1285
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1139
1286
  const manager = getTerminalManager();
1140
1287
  // Check if it's a file tab first (Spec 0092, write-through: in-memory + SQLite)
1141
1288
  if (tabId.startsWith('file-')) {
@@ -1185,8 +1332,8 @@ async function handleProjectTabDelete(res, ctx, projectPath, tabId) {
1185
1332
  res.end(JSON.stringify({ error: 'Tab not found' }));
1186
1333
  }
1187
1334
  }
1188
- async function handleProjectStopAll(res, projectPath) {
1189
- const entry = getProjectTerminalsEntry(projectPath);
1335
+ async function handleWorkspaceStopAll(res, workspacePath) {
1336
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1190
1337
  const manager = getTerminalManager();
1191
1338
  // Kill all terminals (disable shellper auto-restart if applicable)
1192
1339
  if (entry.architect) {
@@ -1199,13 +1346,13 @@ async function handleProjectStopAll(res, projectPath) {
1199
1346
  await killTerminalWithShellper(manager, terminalId);
1200
1347
  }
1201
1348
  // Clear registry
1202
- getProjectTerminals().delete(projectPath);
1349
+ getWorkspaceTerminals().delete(workspacePath);
1203
1350
  // TICK-001: Delete all terminal sessions from SQLite
1204
- deleteProjectTerminalSessions(projectPath);
1351
+ deleteWorkspaceTerminalSessions(workspacePath);
1205
1352
  res.writeHead(200, { 'Content-Type': 'application/json' });
1206
1353
  res.end(JSON.stringify({ ok: true }));
1207
1354
  }
1208
- function handleProjectFiles(res, url, projectPath) {
1355
+ function handleWorkspaceFiles(res, url, workspacePath) {
1209
1356
  const maxDepth = parseInt(url.searchParams.get('depth') || '3', 10);
1210
1357
  const ignore = new Set(['.git', 'node_modules', '.builders', 'dist', '.agent-farm', '.next', '.cache', '__pycache__']);
1211
1358
  function readTree(dir, depth) {
@@ -1226,7 +1373,7 @@ function handleProjectFiles(res, url, projectPath) {
1226
1373
  })
1227
1374
  .map(e => {
1228
1375
  const fullPath = path.join(dir, e.name);
1229
- const relativePath = path.relative(projectPath, fullPath);
1376
+ const relativePath = path.relative(workspacePath, fullPath);
1230
1377
  if (e.isDirectory()) {
1231
1378
  return { name: e.name, path: relativePath, type: 'directory', children: readTree(fullPath, depth - 1) };
1232
1379
  }
@@ -1237,15 +1384,15 @@ function handleProjectFiles(res, url, projectPath) {
1237
1384
  return [];
1238
1385
  }
1239
1386
  }
1240
- const tree = readTree(projectPath, maxDepth);
1387
+ const tree = readTree(workspacePath, maxDepth);
1241
1388
  res.writeHead(200, { 'Content-Type': 'application/json' });
1242
1389
  res.end(JSON.stringify(tree));
1243
1390
  }
1244
- function handleProjectGitStatus(res, ctx, projectPath) {
1391
+ async function handleWorkspaceGitStatus(res, ctx, workspacePath) {
1245
1392
  try {
1246
1393
  // Get git status in porcelain format for parsing
1247
- const result = execSync('git status --porcelain', {
1248
- cwd: projectPath,
1394
+ const { stdout: result } = await execAsync('git status --porcelain', {
1395
+ cwd: workspacePath,
1249
1396
  encoding: 'utf-8',
1250
1397
  timeout: 5000,
1251
1398
  });
@@ -1282,8 +1429,8 @@ function handleProjectGitStatus(res, ctx, projectPath) {
1282
1429
  res.end(JSON.stringify({ modified: [], staged: [], untracked: [], error: err.message }));
1283
1430
  }
1284
1431
  }
1285
- function handleProjectRecentFiles(res, projectPath) {
1286
- const entry = getProjectTerminalsEntry(projectPath);
1432
+ function handleWorkspaceRecentFiles(res, workspacePath) {
1433
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1287
1434
  // Get all file tabs sorted by creation time (most recent first)
1288
1435
  const recentFiles = Array.from(entry.fileTabs.values())
1289
1436
  .sort((a, b) => b.createdAt - a.createdAt)
@@ -1292,15 +1439,15 @@ function handleProjectRecentFiles(res, projectPath) {
1292
1439
  id: tab.id,
1293
1440
  path: tab.path,
1294
1441
  name: path.basename(tab.path),
1295
- relativePath: path.relative(projectPath, tab.path),
1442
+ relativePath: path.relative(workspacePath, tab.path),
1296
1443
  }));
1297
1444
  res.writeHead(200, { 'Content-Type': 'application/json' });
1298
1445
  res.end(JSON.stringify(recentFiles));
1299
1446
  }
1300
- function handleProjectAnnotate(req, res, ctx, url, projectPath, annotateMatch) {
1447
+ function handleWorkspaceAnnotate(req, res, ctx, url, workspacePath, annotateMatch) {
1301
1448
  const tabId = annotateMatch[1];
1302
1449
  const subRoute = annotateMatch[3] || '';
1303
- const entry = getProjectTerminalsEntry(projectPath);
1450
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1304
1451
  const tab = entry.fileTabs.get(tabId);
1305
1452
  if (!tab) {
1306
1453
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -1387,6 +1534,28 @@ function handleProjectAnnotate(req, res, ctx, url, projectPath, annotateMatch) {
1387
1534
  }
1388
1535
  return;
1389
1536
  }
1537
+ // Sub-route: GET /vendor/* — serve bundled vendor libraries (PrismJS, marked, DOMPurify)
1538
+ if (req.method === 'GET' && subRoute.startsWith('vendor/')) {
1539
+ const vendorFile = subRoute.slice('vendor/'.length);
1540
+ // Security: only allow known file extensions and no path traversal
1541
+ if (vendorFile.includes('..') || vendorFile.includes('/') || !/\.(js|css)$/.test(vendorFile)) {
1542
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
1543
+ res.end('Bad request');
1544
+ return;
1545
+ }
1546
+ const vendorPath = path.resolve(__dirname, `../../../templates/vendor/${vendorFile}`);
1547
+ try {
1548
+ const content = fs.readFileSync(vendorPath);
1549
+ const contentType = vendorFile.endsWith('.css') ? 'text/css' : 'application/javascript';
1550
+ res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=86400' });
1551
+ res.end(content);
1552
+ }
1553
+ catch {
1554
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
1555
+ res.end('Not found');
1556
+ }
1557
+ return;
1558
+ }
1390
1559
  // Default: serve the annotator HTML template
1391
1560
  if (req.method === 'GET' && (subRoute === '' || subRoute === undefined)) {
1392
1561
  try {