@cluesmith/codev 2.0.0-rc.3 → 2.0.0-rc.32

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 (307) hide show
  1. package/bin/porch.js +6 -35
  2. package/dashboard/dist/assets/index-CXwnJkPh.css +32 -0
  3. package/dashboard/dist/assets/index-D429K6qO.js +120 -0
  4. package/dashboard/dist/assets/index-D429K6qO.js.map +1 -0
  5. package/dashboard/dist/index.html +13 -0
  6. package/dist/agent-farm/cli.d.ts.map +1 -1
  7. package/dist/agent-farm/cli.js +74 -64
  8. package/dist/agent-farm/cli.js.map +1 -1
  9. package/dist/agent-farm/commands/architect.d.ts.map +1 -1
  10. package/dist/agent-farm/commands/architect.js +0 -2
  11. package/dist/agent-farm/commands/architect.js.map +1 -1
  12. package/dist/agent-farm/commands/attach.d.ts +13 -0
  13. package/dist/agent-farm/commands/attach.d.ts.map +1 -0
  14. package/dist/agent-farm/commands/attach.js +179 -0
  15. package/dist/agent-farm/commands/attach.js.map +1 -0
  16. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/cleanup.js +30 -3
  18. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  19. package/dist/agent-farm/commands/consult.js +1 -1
  20. package/dist/agent-farm/commands/consult.js.map +1 -1
  21. package/dist/agent-farm/commands/index.d.ts +2 -2
  22. package/dist/agent-farm/commands/index.d.ts.map +1 -1
  23. package/dist/agent-farm/commands/index.js +2 -2
  24. package/dist/agent-farm/commands/index.js.map +1 -1
  25. package/dist/agent-farm/commands/{util.d.ts → shell.d.ts} +5 -5
  26. package/dist/agent-farm/commands/shell.d.ts.map +1 -0
  27. package/dist/agent-farm/commands/{util.js → shell.js} +23 -36
  28. package/dist/agent-farm/commands/shell.js.map +1 -0
  29. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  30. package/dist/agent-farm/commands/spawn.js +455 -217
  31. package/dist/agent-farm/commands/spawn.js.map +1 -1
  32. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  33. package/dist/agent-farm/commands/start.js +21 -74
  34. package/dist/agent-farm/commands/start.js.map +1 -1
  35. package/dist/agent-farm/commands/stop.d.ts.map +1 -1
  36. package/dist/agent-farm/commands/stop.js +79 -10
  37. package/dist/agent-farm/commands/stop.js.map +1 -1
  38. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  39. package/dist/agent-farm/commands/tower.js +2 -1
  40. package/dist/agent-farm/commands/tower.js.map +1 -1
  41. package/dist/agent-farm/db/index.d.ts.map +1 -1
  42. package/dist/agent-farm/db/index.js +15 -0
  43. package/dist/agent-farm/db/index.js.map +1 -1
  44. package/dist/agent-farm/db/schema.d.ts +1 -1
  45. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  46. package/dist/agent-farm/db/schema.js +6 -3
  47. package/dist/agent-farm/db/schema.js.map +1 -1
  48. package/dist/agent-farm/db/types.d.ts +3 -0
  49. package/dist/agent-farm/db/types.d.ts.map +1 -1
  50. package/dist/agent-farm/db/types.js +3 -0
  51. package/dist/agent-farm/db/types.js.map +1 -1
  52. package/dist/agent-farm/hq-connector.d.ts +2 -2
  53. package/dist/agent-farm/hq-connector.js +2 -2
  54. package/dist/agent-farm/servers/dashboard-server.js +408 -127
  55. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  56. package/dist/agent-farm/servers/tower-server.js +353 -16
  57. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  58. package/dist/agent-farm/state.d.ts +4 -10
  59. package/dist/agent-farm/state.d.ts.map +1 -1
  60. package/dist/agent-farm/state.js +30 -31
  61. package/dist/agent-farm/state.js.map +1 -1
  62. package/dist/agent-farm/types.d.ts +48 -0
  63. package/dist/agent-farm/types.d.ts.map +1 -1
  64. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  65. package/dist/agent-farm/utils/config.js +1 -0
  66. package/dist/agent-farm/utils/config.js.map +1 -1
  67. package/dist/agent-farm/utils/deps.d.ts.map +1 -1
  68. package/dist/agent-farm/utils/deps.js +0 -16
  69. package/dist/agent-farm/utils/deps.js.map +1 -1
  70. package/dist/agent-farm/utils/notifications.d.ts +30 -0
  71. package/dist/agent-farm/utils/notifications.d.ts.map +1 -0
  72. package/dist/agent-farm/utils/notifications.js +121 -0
  73. package/dist/agent-farm/utils/notifications.js.map +1 -0
  74. package/dist/agent-farm/utils/server-utils.d.ts +2 -1
  75. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  76. package/dist/agent-farm/utils/server-utils.js +11 -1
  77. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  78. package/dist/agent-farm/utils/shell.d.ts +9 -22
  79. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  80. package/dist/agent-farm/utils/shell.js +34 -34
  81. package/dist/agent-farm/utils/shell.js.map +1 -1
  82. package/dist/agent-farm/utils/terminal-ports.d.ts +1 -1
  83. package/dist/agent-farm/utils/terminal-ports.js +1 -1
  84. package/dist/cli.d.ts.map +1 -1
  85. package/dist/cli.js +5 -54
  86. package/dist/cli.js.map +1 -1
  87. package/dist/commands/adopt.d.ts.map +1 -1
  88. package/dist/commands/adopt.js +20 -4
  89. package/dist/commands/adopt.js.map +1 -1
  90. package/dist/commands/consult/index.d.ts.map +1 -1
  91. package/dist/commands/consult/index.js +63 -3
  92. package/dist/commands/consult/index.js.map +1 -1
  93. package/dist/commands/doctor.d.ts.map +1 -1
  94. package/dist/commands/doctor.js +0 -15
  95. package/dist/commands/doctor.js.map +1 -1
  96. package/dist/commands/init.d.ts.map +1 -1
  97. package/dist/commands/init.js +18 -2
  98. package/dist/commands/init.js.map +1 -1
  99. package/dist/commands/porch/build-counter.d.ts +5 -0
  100. package/dist/commands/porch/build-counter.d.ts.map +1 -0
  101. package/dist/commands/porch/build-counter.js +5 -0
  102. package/dist/commands/porch/build-counter.js.map +1 -0
  103. package/dist/commands/porch/checks.d.ts +16 -29
  104. package/dist/commands/porch/checks.d.ts.map +1 -1
  105. package/dist/commands/porch/checks.js +90 -144
  106. package/dist/commands/porch/checks.js.map +1 -1
  107. package/dist/commands/porch/claude.d.ts +27 -0
  108. package/dist/commands/porch/claude.d.ts.map +1 -0
  109. package/dist/commands/porch/claude.js +107 -0
  110. package/dist/commands/porch/claude.js.map +1 -0
  111. package/dist/commands/porch/index.d.ts +21 -43
  112. package/dist/commands/porch/index.d.ts.map +1 -1
  113. package/dist/commands/porch/index.js +469 -753
  114. package/dist/commands/porch/index.js.map +1 -1
  115. package/dist/commands/porch/plan.d.ts +70 -0
  116. package/dist/commands/porch/plan.d.ts.map +1 -0
  117. package/dist/commands/porch/plan.js +190 -0
  118. package/dist/commands/porch/plan.js.map +1 -0
  119. package/dist/commands/porch/prompts.d.ts +19 -0
  120. package/dist/commands/porch/prompts.d.ts.map +1 -0
  121. package/dist/commands/porch/prompts.js +250 -0
  122. package/dist/commands/porch/prompts.js.map +1 -0
  123. package/dist/commands/porch/protocol.d.ts +59 -0
  124. package/dist/commands/porch/protocol.d.ts.map +1 -0
  125. package/dist/commands/porch/protocol.js +260 -0
  126. package/dist/commands/porch/protocol.js.map +1 -0
  127. package/dist/commands/porch/run.d.ts +40 -0
  128. package/dist/commands/porch/run.d.ts.map +1 -0
  129. package/dist/commands/porch/run.js +893 -0
  130. package/dist/commands/porch/run.js.map +1 -0
  131. package/dist/commands/porch/state.d.ts +23 -112
  132. package/dist/commands/porch/state.d.ts.map +1 -1
  133. package/dist/commands/porch/state.js +81 -685
  134. package/dist/commands/porch/state.js.map +1 -1
  135. package/dist/commands/porch/types.d.ts +72 -173
  136. package/dist/commands/porch/types.d.ts.map +1 -1
  137. package/dist/commands/porch/types.js +2 -1
  138. package/dist/commands/porch/types.js.map +1 -1
  139. package/dist/commands/update.d.ts.map +1 -1
  140. package/dist/commands/update.js +12 -0
  141. package/dist/commands/update.js.map +1 -1
  142. package/dist/lib/scaffold.d.ts +24 -0
  143. package/dist/lib/scaffold.d.ts.map +1 -1
  144. package/dist/lib/scaffold.js +78 -0
  145. package/dist/lib/scaffold.js.map +1 -1
  146. package/dist/terminal/index.d.ts +8 -0
  147. package/dist/terminal/index.d.ts.map +1 -0
  148. package/dist/terminal/index.js +5 -0
  149. package/dist/terminal/index.js.map +1 -0
  150. package/dist/terminal/pty-manager.d.ts +60 -0
  151. package/dist/terminal/pty-manager.d.ts.map +1 -0
  152. package/dist/terminal/pty-manager.js +334 -0
  153. package/dist/terminal/pty-manager.js.map +1 -0
  154. package/dist/terminal/pty-session.d.ts +79 -0
  155. package/dist/terminal/pty-session.d.ts.map +1 -0
  156. package/dist/terminal/pty-session.js +215 -0
  157. package/dist/terminal/pty-session.js.map +1 -0
  158. package/dist/terminal/ring-buffer.d.ts +27 -0
  159. package/dist/terminal/ring-buffer.d.ts.map +1 -0
  160. package/dist/terminal/ring-buffer.js +74 -0
  161. package/dist/terminal/ring-buffer.js.map +1 -0
  162. package/dist/terminal/ws-protocol.d.ts +27 -0
  163. package/dist/terminal/ws-protocol.d.ts.map +1 -0
  164. package/dist/terminal/ws-protocol.js +44 -0
  165. package/dist/terminal/ws-protocol.js.map +1 -0
  166. package/package.json +18 -3
  167. package/skeleton/DEPENDENCIES.md +3 -29
  168. package/skeleton/builders.md +1 -1
  169. package/skeleton/protocol-schema.json +282 -0
  170. package/skeleton/protocols/bugfix/builder-prompt.md +49 -0
  171. package/skeleton/protocols/bugfix/protocol.json +14 -2
  172. package/skeleton/protocols/experiment/builder-prompt.md +47 -0
  173. package/skeleton/protocols/experiment/protocol.json +101 -0
  174. package/skeleton/protocols/maintain/builder-prompt.md +41 -0
  175. package/skeleton/protocols/maintain/protocol.json +114 -0
  176. package/skeleton/protocols/protocol-schema.json +53 -0
  177. package/skeleton/protocols/spider/builder-prompt.md +53 -0
  178. package/skeleton/protocols/spider/prompts/implement.md +208 -0
  179. package/skeleton/protocols/spider/prompts/plan.md +214 -0
  180. package/skeleton/protocols/spider/prompts/review.md +217 -0
  181. package/skeleton/protocols/spider/prompts/specify.md +192 -0
  182. package/skeleton/protocols/spider/protocol.json +96 -148
  183. package/skeleton/protocols/spider/protocol.md +26 -16
  184. package/skeleton/protocols/spider/templates/plan.md +14 -0
  185. package/skeleton/protocols/tick/builder-prompt.md +51 -0
  186. package/skeleton/protocols/tick/protocol.json +7 -2
  187. package/skeleton/resources/commands/agent-farm.md +23 -41
  188. package/skeleton/resources/commands/overview.md +5 -5
  189. package/skeleton/resources/workflow-reference.md +2 -2
  190. package/skeleton/roles/architect.md +152 -315
  191. package/skeleton/roles/builder.md +109 -218
  192. package/skeleton/templates/cheatsheet.md +4 -2
  193. package/templates/dashboard/index.html +17 -43
  194. package/templates/dashboard/js/dialogs.js +7 -7
  195. package/templates/dashboard/js/files.js +2 -2
  196. package/templates/dashboard/js/main.js +3 -3
  197. package/templates/dashboard/js/projects.js +3 -3
  198. package/templates/dashboard/js/tabs.js +1 -1
  199. package/templates/dashboard/js/utils.js +22 -87
  200. package/templates/tower.html +474 -17
  201. package/dist/agent-farm/commands/kickoff.d.ts +0 -19
  202. package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
  203. package/dist/agent-farm/commands/kickoff.js +0 -323
  204. package/dist/agent-farm/commands/kickoff.js.map +0 -1
  205. package/dist/agent-farm/commands/rename.d.ts +0 -13
  206. package/dist/agent-farm/commands/rename.d.ts.map +0 -1
  207. package/dist/agent-farm/commands/rename.js +0 -33
  208. package/dist/agent-farm/commands/rename.js.map +0 -1
  209. package/dist/agent-farm/commands/tutorial.d.ts +0 -10
  210. package/dist/agent-farm/commands/tutorial.d.ts.map +0 -1
  211. package/dist/agent-farm/commands/tutorial.js +0 -49
  212. package/dist/agent-farm/commands/tutorial.js.map +0 -1
  213. package/dist/agent-farm/commands/util.d.ts.map +0 -1
  214. package/dist/agent-farm/commands/util.js.map +0 -1
  215. package/dist/agent-farm/tutorial/index.d.ts +0 -8
  216. package/dist/agent-farm/tutorial/index.d.ts.map +0 -1
  217. package/dist/agent-farm/tutorial/index.js +0 -8
  218. package/dist/agent-farm/tutorial/index.js.map +0 -1
  219. package/dist/agent-farm/tutorial/prompts.d.ts +0 -57
  220. package/dist/agent-farm/tutorial/prompts.d.ts.map +0 -1
  221. package/dist/agent-farm/tutorial/prompts.js +0 -147
  222. package/dist/agent-farm/tutorial/prompts.js.map +0 -1
  223. package/dist/agent-farm/tutorial/runner.d.ts +0 -52
  224. package/dist/agent-farm/tutorial/runner.d.ts.map +0 -1
  225. package/dist/agent-farm/tutorial/runner.js +0 -204
  226. package/dist/agent-farm/tutorial/runner.js.map +0 -1
  227. package/dist/agent-farm/tutorial/state.d.ts +0 -26
  228. package/dist/agent-farm/tutorial/state.d.ts.map +0 -1
  229. package/dist/agent-farm/tutorial/state.js +0 -89
  230. package/dist/agent-farm/tutorial/state.js.map +0 -1
  231. package/dist/agent-farm/tutorial/steps/first-spec.d.ts +0 -7
  232. package/dist/agent-farm/tutorial/steps/first-spec.d.ts.map +0 -1
  233. package/dist/agent-farm/tutorial/steps/first-spec.js +0 -136
  234. package/dist/agent-farm/tutorial/steps/first-spec.js.map +0 -1
  235. package/dist/agent-farm/tutorial/steps/implementation.d.ts +0 -7
  236. package/dist/agent-farm/tutorial/steps/implementation.d.ts.map +0 -1
  237. package/dist/agent-farm/tutorial/steps/implementation.js +0 -76
  238. package/dist/agent-farm/tutorial/steps/implementation.js.map +0 -1
  239. package/dist/agent-farm/tutorial/steps/index.d.ts +0 -10
  240. package/dist/agent-farm/tutorial/steps/index.d.ts.map +0 -1
  241. package/dist/agent-farm/tutorial/steps/index.js +0 -10
  242. package/dist/agent-farm/tutorial/steps/index.js.map +0 -1
  243. package/dist/agent-farm/tutorial/steps/planning.d.ts +0 -7
  244. package/dist/agent-farm/tutorial/steps/planning.d.ts.map +0 -1
  245. package/dist/agent-farm/tutorial/steps/planning.js +0 -143
  246. package/dist/agent-farm/tutorial/steps/planning.js.map +0 -1
  247. package/dist/agent-farm/tutorial/steps/review.d.ts +0 -7
  248. package/dist/agent-farm/tutorial/steps/review.d.ts.map +0 -1
  249. package/dist/agent-farm/tutorial/steps/review.js +0 -78
  250. package/dist/agent-farm/tutorial/steps/review.js.map +0 -1
  251. package/dist/agent-farm/tutorial/steps/setup.d.ts +0 -7
  252. package/dist/agent-farm/tutorial/steps/setup.d.ts.map +0 -1
  253. package/dist/agent-farm/tutorial/steps/setup.js +0 -126
  254. package/dist/agent-farm/tutorial/steps/setup.js.map +0 -1
  255. package/dist/agent-farm/tutorial/steps/welcome.d.ts +0 -7
  256. package/dist/agent-farm/tutorial/steps/welcome.d.ts.map +0 -1
  257. package/dist/agent-farm/tutorial/steps/welcome.js +0 -50
  258. package/dist/agent-farm/tutorial/steps/welcome.js.map +0 -1
  259. package/dist/commands/pcheck/cache.d.ts +0 -48
  260. package/dist/commands/pcheck/cache.d.ts.map +0 -1
  261. package/dist/commands/pcheck/cache.js +0 -170
  262. package/dist/commands/pcheck/cache.js.map +0 -1
  263. package/dist/commands/pcheck/evaluator.d.ts +0 -15
  264. package/dist/commands/pcheck/evaluator.d.ts.map +0 -1
  265. package/dist/commands/pcheck/evaluator.js +0 -246
  266. package/dist/commands/pcheck/evaluator.js.map +0 -1
  267. package/dist/commands/pcheck/index.d.ts +0 -12
  268. package/dist/commands/pcheck/index.d.ts.map +0 -1
  269. package/dist/commands/pcheck/index.js +0 -249
  270. package/dist/commands/pcheck/index.js.map +0 -1
  271. package/dist/commands/pcheck/parser.d.ts +0 -39
  272. package/dist/commands/pcheck/parser.d.ts.map +0 -1
  273. package/dist/commands/pcheck/parser.js +0 -155
  274. package/dist/commands/pcheck/parser.js.map +0 -1
  275. package/dist/commands/pcheck/types.d.ts +0 -82
  276. package/dist/commands/pcheck/types.d.ts.map +0 -1
  277. package/dist/commands/pcheck/types.js +0 -5
  278. package/dist/commands/pcheck/types.js.map +0 -1
  279. package/dist/commands/porch/consultation.d.ts +0 -56
  280. package/dist/commands/porch/consultation.d.ts.map +0 -1
  281. package/dist/commands/porch/consultation.js +0 -330
  282. package/dist/commands/porch/consultation.js.map +0 -1
  283. package/dist/commands/porch/notifications.d.ts +0 -99
  284. package/dist/commands/porch/notifications.d.ts.map +0 -1
  285. package/dist/commands/porch/notifications.js +0 -223
  286. package/dist/commands/porch/notifications.js.map +0 -1
  287. package/dist/commands/porch/plan-parser.d.ts +0 -38
  288. package/dist/commands/porch/plan-parser.d.ts.map +0 -1
  289. package/dist/commands/porch/plan-parser.js +0 -166
  290. package/dist/commands/porch/plan-parser.js.map +0 -1
  291. package/dist/commands/porch/protocol-loader.d.ts +0 -46
  292. package/dist/commands/porch/protocol-loader.d.ts.map +0 -1
  293. package/dist/commands/porch/protocol-loader.js +0 -249
  294. package/dist/commands/porch/protocol-loader.js.map +0 -1
  295. package/dist/commands/porch/signal-parser.d.ts +0 -88
  296. package/dist/commands/porch/signal-parser.d.ts.map +0 -1
  297. package/dist/commands/porch/signal-parser.js +0 -148
  298. package/dist/commands/porch/signal-parser.js.map +0 -1
  299. package/dist/commands/tower.d.ts +0 -16
  300. package/dist/commands/tower.d.ts.map +0 -1
  301. package/dist/commands/tower.js +0 -21
  302. package/dist/commands/tower.js.map +0 -1
  303. package/skeleton/porch/protocols/bugfix.json +0 -85
  304. package/skeleton/porch/protocols/spider.json +0 -135
  305. package/skeleton/porch/protocols/tick.json +0 -76
  306. package/templates/dashboard/css/activity.css +0 -151
  307. package/templates/dashboard/js/activity.js +0 -112
@@ -10,14 +10,14 @@ import net from 'node:net';
10
10
  import httpProxy from 'http-proxy';
11
11
  import { spawn, execSync, exec } from 'node:child_process';
12
12
  import { promisify } from 'node:util';
13
- import { randomUUID } from 'node:crypto';
13
+ import { randomUUID, timingSafeEqual } from 'node:crypto';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  const execAsync = promisify(exec);
16
16
  import { Command } from 'commander';
17
17
  import { getPortForTerminal } from '../utils/terminal-ports.js';
18
18
  import { escapeHtml, parseJsonBody, isRequestAllowed as isRequestAllowedBase, } from '../utils/server-utils.js';
19
- import { loadState, getAnnotations, addAnnotation, removeAnnotation, getUtils, tryAddUtil, removeUtil, getBuilder, getBuilders, removeBuilder, upsertBuilder, clearState, } from '../state.js';
20
- import { spawnTtyd } from '../utils/shell.js';
19
+ import { loadState, getAnnotations, addAnnotation, removeAnnotation, getUtils, addUtil, removeUtil, updateUtil, getBuilder, getBuilders, removeBuilder, upsertBuilder, clearState, getArchitect, setArchitect, } from '../state.js';
20
+ import { TerminalManager } from '../../terminal/pty-manager.js';
21
21
  const __filename = fileURLToPath(import.meta.url);
22
22
  const __dirname = path.dirname(__filename);
23
23
  // Default dashboard port
@@ -91,6 +91,131 @@ function findTemplatePath(filename, required = false) {
91
91
  const projectRoot = findProjectRoot();
92
92
  // Use modular dashboard template (Spec 0060)
93
93
  const templatePath = findTemplatePath('dashboard/index.html', true);
94
+ // Terminal backend is always node-pty (Spec 0085)
95
+ const terminalBackend = 'node-pty';
96
+ // Load dashboard frontend preference from config (Spec 0085)
97
+ function loadDashboardFrontend() {
98
+ const configPath = path.resolve(projectRoot, 'codev', 'config.json');
99
+ if (fs.existsSync(configPath)) {
100
+ try {
101
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
102
+ return config?.dashboard?.frontend ?? 'react';
103
+ }
104
+ catch { /* ignore */ }
105
+ }
106
+ return 'react';
107
+ }
108
+ const dashboardFrontend = loadDashboardFrontend();
109
+ // React dashboard dist path (built by Vite)
110
+ const reactDashboardPath = path.resolve(__dirname, '../../../dashboard/dist');
111
+ const useReactDashboard = dashboardFrontend === 'react' && fs.existsSync(reactDashboardPath);
112
+ if (useReactDashboard) {
113
+ console.log('Dashboard frontend: React');
114
+ }
115
+ else if (dashboardFrontend === 'react') {
116
+ console.log('Dashboard frontend: React (dist not found, falling back to legacy)');
117
+ }
118
+ else {
119
+ console.log('Dashboard frontend: legacy');
120
+ }
121
+ const terminalManager = new TerminalManager({ projectRoot });
122
+ console.log('Terminal backend: node-pty');
123
+ // Clear stale terminalIds on startup — TerminalManager starts empty, so any
124
+ // persisted terminalId from a previous run is no longer valid.
125
+ {
126
+ const arch = getArchitect();
127
+ if (arch?.terminalId) {
128
+ setArchitect({ ...arch, terminalId: undefined });
129
+ }
130
+ for (const builder of getBuilders()) {
131
+ if (builder.terminalId) {
132
+ upsertBuilder({ ...builder, terminalId: undefined });
133
+ }
134
+ }
135
+ for (const util of getUtils()) {
136
+ if (util.terminalId) {
137
+ updateUtil(util.id, { terminalId: undefined });
138
+ }
139
+ }
140
+ }
141
+ // Auto-create architect PTY session if architect exists with a tmux session
142
+ async function initArchitectTerminal() {
143
+ const architect = getArchitect();
144
+ if (!architect || !architect.tmuxSession || architect.terminalId)
145
+ return;
146
+ try {
147
+ // Verify the tmux session actually exists before trying to attach.
148
+ // If it doesn't exist, tmux attach exits immediately, leaving a dead terminalId.
149
+ const { spawnSync } = await import('node:child_process');
150
+ const probe = spawnSync('tmux', ['has-session', '-t', architect.tmuxSession], { stdio: 'ignore' });
151
+ if (probe.status !== 0) {
152
+ console.log(`initArchitectTerminal: tmux session '${architect.tmuxSession}' does not exist yet`);
153
+ return;
154
+ }
155
+ // Use tmux directly (not via bash -c) to avoid DA response chaff.
156
+ // bash -c creates a brief window where readline echoes DA responses as text.
157
+ const info = await terminalManager.createSession({
158
+ command: 'tmux',
159
+ args: ['attach-session', '-t', architect.tmuxSession],
160
+ cwd: projectRoot,
161
+ cols: 200,
162
+ rows: 50,
163
+ label: 'architect',
164
+ });
165
+ // Wait to detect immediate exit (e.g., tmux session disappeared between check and attach)
166
+ await new Promise((resolve) => setTimeout(resolve, 500));
167
+ const session = terminalManager.getSession(info.id);
168
+ if (!session || session.info.exitCode !== undefined) {
169
+ console.error(`initArchitectTerminal: PTY exited immediately (exit=${session?.info.exitCode})`);
170
+ terminalManager.killSession(info.id);
171
+ return;
172
+ }
173
+ setArchitect({ ...architect, terminalId: info.id });
174
+ console.log(`Architect terminal session created: ${info.id}`);
175
+ }
176
+ catch (err) {
177
+ console.error('Failed to create architect terminal session:', err.message);
178
+ }
179
+ }
180
+ // Poll for architect state and create PTY session once available
181
+ // start.ts writes architect to DB before spawning this server, but there can be a small delay
182
+ (async function waitForArchitectAndInit() {
183
+ for (let attempt = 0; attempt < 30; attempt++) {
184
+ await new Promise((resolve) => setTimeout(resolve, 500));
185
+ try {
186
+ const arch = getArchitect();
187
+ if (!arch)
188
+ continue;
189
+ if (arch.terminalId)
190
+ return; // Already has terminal
191
+ if (!arch.tmuxSession)
192
+ continue; // No tmux session yet
193
+ console.log(`initArchitectTerminal: attempt ${attempt + 1}, tmux=${arch.tmuxSession}`);
194
+ await initArchitectTerminal();
195
+ const updated = getArchitect();
196
+ if (updated?.terminalId) {
197
+ console.log(`initArchitectTerminal: success, terminalId=${updated.terminalId}`);
198
+ return;
199
+ }
200
+ console.log(`initArchitectTerminal: attempt ${attempt + 1} failed, terminalId still unset`);
201
+ }
202
+ catch (err) {
203
+ console.error(`initArchitectTerminal: attempt ${attempt + 1} error:`, err.message);
204
+ }
205
+ }
206
+ console.warn('initArchitectTerminal: gave up after 30 attempts');
207
+ })();
208
+ // Log telemetry
209
+ try {
210
+ const metricsPath = path.join(projectRoot, '.agent-farm', 'metrics.log');
211
+ fs.mkdirSync(path.dirname(metricsPath), { recursive: true });
212
+ fs.appendFileSync(metricsPath, JSON.stringify({
213
+ event: 'backend_selected',
214
+ backend: 'node-pty',
215
+ timestamp: new Date().toISOString(),
216
+ }) + '\n');
217
+ }
218
+ catch { /* ignore */ }
94
219
  // Clean up dead processes from state (called on state load)
95
220
  function cleanupDeadProcesses() {
96
221
  // Clean up dead shell processes
@@ -217,6 +342,9 @@ async function killProcessGracefully(pid, tmuxSession) {
217
342
  if (tmuxSession) {
218
343
  killTmuxSession(tmuxSession);
219
344
  }
345
+ // Guard: PID 0 sends signal to entire process group — never do that
346
+ if (!pid || pid <= 0)
347
+ return;
220
348
  try {
221
349
  // First try SIGTERM
222
350
  process.kill(pid, 'SIGTERM');
@@ -281,42 +409,24 @@ function tmuxSessionExists(sessionName) {
281
409
  return false;
282
410
  }
283
411
  }
284
- // Create a persistent tmux session and attach ttyd to it
285
- // Idempotent: if session exists, just spawn ttyd to attach to it
286
- function spawnTmuxWithTtyd(sessionName, shellCommand, ttydPort, cwd) {
412
+ // Create a PTY terminal session via the TerminalManager.
413
+ // Returns the terminal session ID, or null on failure.
414
+ async function createTerminalSession(shellCommand, cwd, label) {
415
+ if (!terminalManager)
416
+ return null;
287
417
  try {
288
- // Only create session if it doesn't exist (idempotent)
289
- if (!tmuxSessionExists(sessionName)) {
290
- // Create tmux session with the shell command
291
- execSync(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 "${shellCommand}"`, { cwd, stdio: 'ignore' });
292
- // Hide the tmux status bar (dashboard has its own tabs)
293
- execSync(`tmux set-option -t "${sessionName}" status off`, { stdio: 'ignore' });
294
- // Enable mouse support in the session
295
- execSync(`tmux set-option -t "${sessionName}" -g mouse on`, { stdio: 'ignore' });
296
- // Enable OSC 52 clipboard (allows copy to browser clipboard via ttyd)
297
- execSync(`tmux set-option -t "${sessionName}" -g set-clipboard on`, { stdio: 'ignore' });
298
- // Enable passthrough for hyperlinks and clipboard
299
- execSync(`tmux set-option -t "${sessionName}" -g allow-passthrough on`, { stdio: 'ignore' });
300
- // Copy selection to clipboard when mouse is released
301
- // Use copy-pipe-and-cancel with pbcopy to directly copy to system clipboard
302
- // (OSC 52 via set-clipboard doesn't work reliably through ttyd/xterm.js)
303
- execSync(`tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
304
- execSync(`tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
305
- }
306
- // Start ttyd to attach to the tmux session
307
- const customIndexPath = findTemplatePath('ttyd-index.html');
308
- const ttydProcess = spawnTtyd({
309
- port: ttydPort,
310
- sessionName,
418
+ const info = await terminalManager.createSession({
419
+ command: '/bin/bash',
420
+ args: ['-c', shellCommand],
311
421
  cwd,
312
- customIndexPath: customIndexPath ?? undefined,
422
+ cols: 200,
423
+ rows: 50,
424
+ label,
313
425
  });
314
- return ttydProcess?.pid ?? null;
426
+ return info.id;
315
427
  }
316
428
  catch (err) {
317
- console.error(`Failed to create tmux session ${sessionName}:`, err.message);
318
- // Cleanup any partial session
319
- killTmuxSession(sessionName);
429
+ console.error(`Failed to create terminal session:`, err.message);
320
430
  return null;
321
431
  }
322
432
  }
@@ -336,7 +446,7 @@ function generateShortId() {
336
446
  * Spawn a worktree builder - creates git worktree and starts builder CLI
337
447
  * Similar to shell spawning but with git worktree isolation
338
448
  */
339
- function spawnWorktreeBuilder(builderPort, state) {
449
+ async function spawnWorktreeBuilder(builderPort, state) {
340
450
  const shortId = generateShortId();
341
451
  const builderId = `worktree-${shortId}`;
342
452
  const branchName = `builder/worktree-${shortId}`;
@@ -364,29 +474,10 @@ function spawnWorktreeBuilder(builderPort, state) {
364
474
  // Use default
365
475
  }
366
476
  }
367
- // Create tmux session with builder command
368
- execSync(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 -c "${worktreePath}" "${builderCommand}"`, { cwd: worktreePath, stdio: 'ignore' });
369
- // Hide the tmux status bar (dashboard has its own tabs)
370
- execSync(`tmux set-option -t "${sessionName}" status off`, { stdio: 'ignore' });
371
- // Enable mouse support
372
- execSync(`tmux set-option -t "${sessionName}" -g mouse on`, { stdio: 'ignore' });
373
- execSync(`tmux set-option -t "${sessionName}" -g set-clipboard on`, { stdio: 'ignore' });
374
- execSync(`tmux set-option -t "${sessionName}" -g allow-passthrough on`, { stdio: 'ignore' });
375
- // Copy selection to clipboard when mouse is released (pbcopy for macOS)
376
- execSync(`tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
377
- execSync(`tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
378
- // Start ttyd connecting to the tmux session
379
- const customIndexPath = findTemplatePath('ttyd-index.html');
380
- const ttydProcess = spawnTtyd({
381
- port: builderPort,
382
- sessionName,
383
- cwd: worktreePath,
384
- customIndexPath: customIndexPath ?? undefined,
385
- });
386
- const pid = ttydProcess?.pid ?? null;
387
- if (!pid) {
477
+ // Create PTY terminal session via node-pty
478
+ const terminalId = await createTerminalSession(builderCommand, worktreePath, `builder-${builderId}`);
479
+ if (!terminalId) {
388
480
  // Cleanup on failure
389
- killTmuxSession(sessionName);
390
481
  try {
391
482
  execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectRoot, stdio: 'ignore' });
392
483
  execSync(`git branch -D "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
@@ -399,16 +490,17 @@ function spawnWorktreeBuilder(builderPort, state) {
399
490
  const builder = {
400
491
  id: builderId,
401
492
  name: `Worktree ${shortId}`,
402
- port: builderPort,
403
- pid,
493
+ port: 0,
494
+ pid: 0,
404
495
  status: 'implementing',
405
496
  phase: 'interactive',
406
497
  worktree: worktreePath,
407
498
  branch: branchName,
408
499
  tmuxSession: sessionName,
409
500
  type: 'worktree',
501
+ terminalId,
410
502
  };
411
- return { builder, pid };
503
+ return { builder, pid: 0 };
412
504
  }
413
505
  catch (err) {
414
506
  console.error(`Failed to spawn worktree builder:`, err.message);
@@ -888,6 +980,115 @@ function isRequestAllowed(req) {
888
980
  }
889
981
  return isRequestAllowedBase(req);
890
982
  }
983
+ /**
984
+ * Timing-safe token comparison to prevent timing attacks
985
+ */
986
+ function isValidToken(provided, expected) {
987
+ if (!provided)
988
+ return false;
989
+ // Ensure both strings are same length for timing-safe comparison
990
+ const providedBuf = Buffer.from(provided);
991
+ const expectedBuf = Buffer.from(expected);
992
+ if (providedBuf.length !== expectedBuf.length) {
993
+ // Still do a comparison to maintain constant time
994
+ timingSafeEqual(expectedBuf, expectedBuf);
995
+ return false;
996
+ }
997
+ return timingSafeEqual(providedBuf, expectedBuf);
998
+ }
999
+ /**
1000
+ * Generate HTML for login page
1001
+ */
1002
+ function getLoginPageHtml() {
1003
+ return `<!DOCTYPE html>
1004
+ <html>
1005
+ <head>
1006
+ <title>Dashboard Login</title>
1007
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1008
+ <style>
1009
+ body { font-family: system-ui; background: #1a1a2e; color: #eee;
1010
+ display: flex; justify-content: center; align-items: center;
1011
+ min-height: 100vh; margin: 0; }
1012
+ .login { background: #16213e; padding: 2rem; border-radius: 8px;
1013
+ max-width: 400px; width: 90%; }
1014
+ h1 { margin-top: 0; }
1015
+ input { width: 100%; padding: 0.75rem; margin: 0.5rem 0;
1016
+ border: 1px solid #444; border-radius: 4px;
1017
+ background: #0f0f23; color: #eee; font-size: 1rem;
1018
+ box-sizing: border-box; }
1019
+ button { width: 100%; padding: 0.75rem; margin-top: 1rem;
1020
+ background: #4a7c59; color: white; border: none;
1021
+ border-radius: 4px; font-size: 1rem; cursor: pointer; }
1022
+ button:hover { background: #5a9c69; }
1023
+ .error { color: #ff6b6b; margin-top: 0.5rem; display: none; }
1024
+ </style>
1025
+ </head>
1026
+ <body>
1027
+ <div class="login">
1028
+ <h1>Agent Farm Login</h1>
1029
+ <p>Enter your API key to access the dashboard.</p>
1030
+ <input type="password" id="key" placeholder="API Key" autofocus>
1031
+ <div class="error" id="error">Invalid API key</div>
1032
+ <button onclick="login()">Login</button>
1033
+ </div>
1034
+ <script>
1035
+ // Check for key in URL (from QR code scan) or localStorage
1036
+ (async function() {
1037
+ const urlParams = new URLSearchParams(window.location.search);
1038
+ const keyFromUrl = urlParams.get('key');
1039
+ const keyFromStorage = localStorage.getItem('codev_web_key');
1040
+ const key = keyFromUrl || keyFromStorage;
1041
+
1042
+ if (key) {
1043
+ if (keyFromUrl) {
1044
+ localStorage.setItem('codev_web_key', keyFromUrl);
1045
+ }
1046
+ await verifyAndLoadDashboard(key);
1047
+ }
1048
+ })();
1049
+
1050
+ async function verifyAndLoadDashboard(key) {
1051
+ try {
1052
+ // Fetch the actual dashboard with auth header
1053
+ const res = await fetch(window.location.pathname, {
1054
+ headers: {
1055
+ 'Authorization': 'Bearer ' + key,
1056
+ 'Accept': 'text/html'
1057
+ }
1058
+ });
1059
+ if (res.ok) {
1060
+ // Replace entire page with dashboard
1061
+ const html = await res.text();
1062
+ document.open();
1063
+ document.write(html);
1064
+ document.close();
1065
+ // Clean URL without reload
1066
+ history.replaceState({}, '', window.location.pathname);
1067
+ } else {
1068
+ // Key invalid
1069
+ localStorage.removeItem('codev_web_key');
1070
+ document.getElementById('error').style.display = 'block';
1071
+ document.getElementById('error').textContent = 'Invalid API key';
1072
+ }
1073
+ } catch (e) {
1074
+ document.getElementById('error').style.display = 'block';
1075
+ document.getElementById('error').textContent = 'Connection error';
1076
+ }
1077
+ }
1078
+
1079
+ async function login() {
1080
+ const key = document.getElementById('key').value;
1081
+ if (!key) return;
1082
+ localStorage.setItem('codev_web_key', key);
1083
+ await verifyAndLoadDashboard(key);
1084
+ }
1085
+ document.getElementById('key').addEventListener('keypress', (e) => {
1086
+ if (e.key === 'Enter') login();
1087
+ });
1088
+ </script>
1089
+ </body>
1090
+ </html>`;
1091
+ }
891
1092
  // Create server
892
1093
  const server = http.createServer(async (req, res) => {
893
1094
  // Security: Validate Host and Origin headers
@@ -896,17 +1097,37 @@ const server = http.createServer(async (req, res) => {
896
1097
  res.end('Forbidden');
897
1098
  return;
898
1099
  }
1100
+ // CRITICAL: When CODEV_WEB_KEY is set, ALL requests require auth
1101
+ // NO localhost bypass - tunnel daemons (cloudflared) run locally and proxy
1102
+ // to localhost, so checking remoteAddress would incorrectly trust remote traffic
1103
+ const webKey = process.env.CODEV_WEB_KEY;
1104
+ if (webKey) {
1105
+ const authHeader = req.headers.authorization;
1106
+ const token = authHeader?.replace('Bearer ', '');
1107
+ if (!isValidToken(token, webKey)) {
1108
+ // Return login page for HTML requests, 401 for API
1109
+ if (req.headers.accept?.includes('text/html')) {
1110
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1111
+ res.end(getLoginPageHtml());
1112
+ return;
1113
+ }
1114
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1115
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
1116
+ return;
1117
+ }
1118
+ }
1119
+ // When CODEV_WEB_KEY is NOT set: no auth required (local dev mode only)
899
1120
  // CORS headers
900
1121
  const origin = req.headers.origin;
901
- if (insecureRemoteMode) {
902
- // Allow any origin in insecure remote mode
1122
+ if (insecureRemoteMode || webKey) {
1123
+ // Allow any origin in insecure remote mode or when using auth (tunnel access)
903
1124
  res.setHeader('Access-Control-Allow-Origin', origin || '*');
904
1125
  }
905
1126
  else if (origin && (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:'))) {
906
1127
  res.setHeader('Access-Control-Allow-Origin', origin);
907
1128
  }
908
1129
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
909
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
1130
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
910
1131
  // Prevent caching of API responses
911
1132
  res.setHeader('Cache-Control', 'no-store');
912
1133
  if (req.method === 'OPTIONS') {
@@ -916,6 +1137,12 @@ const server = http.createServer(async (req, res) => {
916
1137
  }
917
1138
  const url = new URL(req.url || '/', `http://localhost:${port}`);
918
1139
  try {
1140
+ // Spec 0085: node-pty terminal manager REST API routes
1141
+ if (terminalManager && url.pathname.startsWith('/api/terminals')) {
1142
+ if (terminalManager.handleRequest(req, res)) {
1143
+ return;
1144
+ }
1145
+ }
919
1146
  // API: Get state
920
1147
  if (req.method === 'GET' && url.pathname === '/api/state') {
921
1148
  const state = loadStateWithCleanup();
@@ -1025,14 +1252,12 @@ const server = http.createServer(async (req, res) => {
1025
1252
  // Find available port for builder
1026
1253
  const builderPort = await findAvailablePort(CONFIG.builderPortStart, builderState);
1027
1254
  // Spawn worktree builder
1028
- const result = spawnWorktreeBuilder(builderPort, builderState);
1255
+ const result = await spawnWorktreeBuilder(builderPort, builderState);
1029
1256
  if (!result) {
1030
1257
  res.writeHead(500, { 'Content-Type': 'text/plain' });
1031
1258
  res.end('Failed to spawn worktree builder');
1032
1259
  return;
1033
1260
  }
1034
- // Wait for ttyd to be ready
1035
- await new Promise((resolve) => setTimeout(resolve, 500));
1036
1261
  // Save builder to state
1037
1262
  upsertBuilder(result.builder);
1038
1263
  res.writeHead(201, { 'Content-Type': 'application/json' });
@@ -1165,49 +1390,25 @@ const server = http.createServer(async (req, res) => {
1165
1390
  const shellCommand = command
1166
1391
  ? `${shell} -c '${command.replace(/'/g, "'\\''")}; exec ${shell}'`
1167
1392
  : shell;
1168
- // Retry loop for concurrent port allocation race conditions
1169
- const MAX_PORT_RETRIES = 5;
1170
- let utilPort = null;
1171
- let pid = null;
1172
- for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
1173
- // Get fresh state on each attempt to see newly allocated ports
1174
- const currentState = loadState();
1175
- const candidatePort = await findAvailablePort(CONFIG.utilPortStart, currentState);
1176
- // Start tmux session with ttyd attached (use cwd which may be worktree)
1177
- const spawnedPid = spawnTmuxWithTtyd(sessionName, shellCommand, candidatePort, cwd);
1178
- if (!spawnedPid) {
1179
- res.writeHead(500, { 'Content-Type': 'text/plain' });
1180
- res.end('Failed to start shell');
1181
- return;
1182
- }
1183
- // Wait for ttyd to be ready
1184
- await new Promise((resolve) => setTimeout(resolve, 500));
1185
- // Try to add util record - may fail if port was taken by concurrent request
1186
- const util = {
1187
- id,
1188
- name: utilName,
1189
- port: candidatePort,
1190
- pid: spawnedPid,
1191
- tmuxSession: sessionName,
1192
- worktreePath: worktreePath, // Track for cleanup on tab close
1193
- };
1194
- if (tryAddUtil(util)) {
1195
- // Success - port reserved
1196
- utilPort = candidatePort;
1197
- pid = spawnedPid;
1198
- break;
1199
- }
1200
- // Port conflict - kill the spawned process and retry
1201
- console.log(`[info] Port ${candidatePort} conflict, retrying (attempt ${attempt + 1}/${MAX_PORT_RETRIES})`);
1202
- await killProcessGracefully(spawnedPid);
1203
- }
1204
- if (utilPort === null || pid === null) {
1393
+ // Create PTY terminal session via node-pty
1394
+ const terminalId = await createTerminalSession(shellCommand, cwd, `shell-${utilName}`);
1395
+ if (!terminalId) {
1205
1396
  res.writeHead(500, { 'Content-Type': 'text/plain' });
1206
- res.end('Failed to allocate port after multiple retries');
1397
+ res.end('Failed to create terminal session');
1207
1398
  return;
1208
1399
  }
1400
+ const util = {
1401
+ id,
1402
+ name: utilName,
1403
+ port: 0,
1404
+ pid: 0,
1405
+ tmuxSession: sessionName,
1406
+ worktreePath: worktreePath,
1407
+ terminalId,
1408
+ };
1409
+ addUtil(util);
1209
1410
  res.writeHead(201, { 'Content-Type': 'application/json' });
1210
- res.end(JSON.stringify({ success: true, id, port: utilPort, name: utilName }));
1411
+ res.end(JSON.stringify({ success: true, id, port: 0, name: utilName, terminalId }));
1211
1412
  return;
1212
1413
  }
1213
1414
  // API: Check if tab process is running (Bugfix #132)
@@ -1228,7 +1429,14 @@ const server = http.createServer(async (req, res) => {
1228
1429
  const util = tabUtils.find((u) => u.id === utilId);
1229
1430
  if (util) {
1230
1431
  found = true;
1231
- running = isProcessRunning(util.pid);
1432
+ // Check tmux session status (Spec 0076)
1433
+ if (util.tmuxSession) {
1434
+ running = tmuxSessionExists(util.tmuxSession);
1435
+ }
1436
+ else {
1437
+ // Fallback for shells without tmux session (shouldn't happen in practice)
1438
+ running = isProcessRunning(util.pid);
1439
+ }
1232
1440
  }
1233
1441
  }
1234
1442
  // Check if it's a builder tab
@@ -1237,7 +1445,14 @@ const server = http.createServer(async (req, res) => {
1237
1445
  const builder = getBuilder(builderId);
1238
1446
  if (builder) {
1239
1447
  found = true;
1240
- running = isProcessRunning(builder.pid);
1448
+ // Check tmux session status (Spec 0076)
1449
+ if (builder.tmuxSession) {
1450
+ running = tmuxSessionExists(builder.tmuxSession);
1451
+ }
1452
+ else {
1453
+ // Fallback for builders without tmux session (shouldn't happen in practice)
1454
+ running = isProcessRunning(builder.pid);
1455
+ }
1241
1456
  }
1242
1457
  }
1243
1458
  if (found) {
@@ -1281,6 +1496,10 @@ const server = http.createServer(async (req, res) => {
1281
1496
  const tabUtils = getUtils();
1282
1497
  const util = tabUtils.find((u) => u.id === utilId);
1283
1498
  if (util) {
1499
+ // Kill PTY session if present
1500
+ if (util.terminalId && terminalManager) {
1501
+ terminalManager.killSession(util.terminalId);
1502
+ }
1284
1503
  await killProcessGracefully(util.pid, util.tmuxSession);
1285
1504
  // Note: worktrees are NOT cleaned up on tab close - they may contain useful context
1286
1505
  // Users can manually clean up with `git worktree list` and `git worktree remove`
@@ -1660,20 +1879,6 @@ const server = http.createServer(async (req, res) => {
1660
1879
  }
1661
1880
  return;
1662
1881
  }
1663
- // API: Get daily activity summary (Spec 0059)
1664
- if (req.method === 'GET' && url.pathname === '/api/activity-summary') {
1665
- try {
1666
- const activitySummary = await collectActivitySummary(projectRoot);
1667
- res.writeHead(200, { 'Content-Type': 'application/json' });
1668
- res.end(JSON.stringify(activitySummary));
1669
- }
1670
- catch (err) {
1671
- console.error('Activity summary error:', err);
1672
- res.writeHead(500, { 'Content-Type': 'application/json' });
1673
- res.end(JSON.stringify({ error: err.message }));
1674
- }
1675
- return;
1676
- }
1677
1882
  // API: Hot reload check (Spec 0060)
1678
1883
  // Returns modification times for all dashboard CSS/JS files
1679
1884
  if (req.method === 'GET' && url.pathname === '/api/hot-reload') {
@@ -1751,7 +1956,7 @@ const server = http.createServer(async (req, res) => {
1751
1956
  return;
1752
1957
  }
1753
1958
  // Terminal proxy route (Spec 0062 - Secure Remote Access)
1754
- // Routes /terminal/:id to the appropriate ttyd instance
1959
+ // Routes /terminal/:id to the appropriate terminal instance
1755
1960
  const terminalMatch = url.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
1756
1961
  if (terminalMatch) {
1757
1962
  const terminalId = terminalMatch[1];
@@ -1784,8 +1989,49 @@ const server = http.createServer(async (req, res) => {
1784
1989
  terminalProxy.web(req, res, { target: `http://localhost:${annotation.port}` });
1785
1990
  return;
1786
1991
  }
1787
- // Serve dashboard
1788
- if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
1992
+ // Serve dashboard (Spec 0085: React or legacy based on config)
1993
+ if (useReactDashboard && req.method === 'GET') {
1994
+ // Serve React dashboard static files
1995
+ const filePath = url.pathname === '/' || url.pathname === '/index.html'
1996
+ ? path.join(reactDashboardPath, 'index.html')
1997
+ : path.join(reactDashboardPath, url.pathname);
1998
+ // Security: Prevent path traversal
1999
+ const resolved = path.resolve(filePath);
2000
+ if (!resolved.startsWith(reactDashboardPath)) {
2001
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
2002
+ res.end('Forbidden');
2003
+ return;
2004
+ }
2005
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
2006
+ const ext = path.extname(resolved);
2007
+ const mimeTypes = {
2008
+ '.html': 'text/html; charset=utf-8',
2009
+ '.js': 'application/javascript',
2010
+ '.css': 'text/css',
2011
+ '.json': 'application/json',
2012
+ '.svg': 'image/svg+xml',
2013
+ '.png': 'image/png',
2014
+ '.ico': 'image/x-icon',
2015
+ '.map': 'application/json',
2016
+ };
2017
+ const contentType = mimeTypes[ext] ?? 'application/octet-stream';
2018
+ // Cache static assets (hashed filenames) but not index.html
2019
+ if (ext !== '.html') {
2020
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
2021
+ }
2022
+ res.writeHead(200, { 'Content-Type': contentType });
2023
+ fs.createReadStream(resolved).pipe(res);
2024
+ return;
2025
+ }
2026
+ // SPA fallback: serve index.html for client-side routing
2027
+ if (!url.pathname.startsWith('/api/') && !url.pathname.startsWith('/ws/') && !url.pathname.startsWith('/terminal/') && !url.pathname.startsWith('/annotation/')) {
2028
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
2029
+ fs.createReadStream(path.join(reactDashboardPath, 'index.html')).pipe(res);
2030
+ return;
2031
+ }
2032
+ }
2033
+ if (!useReactDashboard && req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
2034
+ // Legacy vanilla JS dashboard
1789
2035
  try {
1790
2036
  let template = fs.readFileSync(templatePath, 'utf-8');
1791
2037
  const state = loadStateWithCleanup();
@@ -1814,15 +2060,37 @@ const server = http.createServer(async (req, res) => {
1814
2060
  res.end('Internal server error: ' + err.message);
1815
2061
  }
1816
2062
  });
2063
+ // Spec 0085: Attach node-pty WebSocket handler for /ws/terminal/:id routes
2064
+ if (terminalManager) {
2065
+ terminalManager.attachWebSocket(server);
2066
+ }
1817
2067
  // WebSocket upgrade handler for terminal proxy (Spec 0062)
1818
- // ttyd uses WebSocket for bidirectional terminal communication
2068
+ // WebSocket for bidirectional terminal communication
1819
2069
  server.on('upgrade', (req, socket, head) => {
1820
- // Security check
2070
+ // Security check for non-auth mode
1821
2071
  const host = req.headers.host;
1822
- if (!insecureRemoteMode && host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
2072
+ if (!insecureRemoteMode && !process.env.CODEV_WEB_KEY && host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
1823
2073
  socket.destroy();
1824
2074
  return;
1825
2075
  }
2076
+ // CRITICAL: When CODEV_WEB_KEY is set, ALL WebSocket upgrades require auth
2077
+ // NO localhost bypass - tunnel daemons run locally, so remoteAddress is unreliable
2078
+ const webKey = process.env.CODEV_WEB_KEY;
2079
+ if (webKey && !insecureRemoteMode) {
2080
+ // Check Sec-WebSocket-Protocol for auth token
2081
+ // Format: "auth-<token>, tty" or just "tty"
2082
+ const protocols = req.headers['sec-websocket-protocol']?.split(',').map((p) => p.trim()) || [];
2083
+ const authProtocol = protocols.find((p) => p.startsWith('auth-'));
2084
+ const token = authProtocol?.substring(5); // Remove 'auth-' prefix
2085
+ if (!isValidToken(token, webKey)) {
2086
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
2087
+ socket.destroy();
2088
+ return;
2089
+ }
2090
+ // Remove auth protocol from the list before forwarding
2091
+ const cleanProtocols = protocols.filter((p) => !p.startsWith('auth-'));
2092
+ req.headers['sec-websocket-protocol'] = cleanProtocols.join(', ') || 'tty';
2093
+ }
1826
2094
  const reqUrl = new URL(req.url || '/', `http://localhost:${port}`);
1827
2095
  const terminalMatch = reqUrl.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
1828
2096
  if (terminalMatch) {
@@ -1869,4 +2137,17 @@ else {
1869
2137
  console.log(`Dashboard: http://localhost:${port}`);
1870
2138
  });
1871
2139
  }
2140
+ // Spec 0085: Graceful shutdown for node-pty terminal manager
2141
+ process.on('SIGTERM', () => {
2142
+ if (terminalManager) {
2143
+ terminalManager.shutdown();
2144
+ }
2145
+ process.exit(0);
2146
+ });
2147
+ process.on('SIGINT', () => {
2148
+ if (terminalManager) {
2149
+ terminalManager.shutdown();
2150
+ }
2151
+ process.exit(0);
2152
+ });
1872
2153
  //# sourceMappingURL=dashboard-server.js.map