@cluesmith/codev 2.0.0-rc.4 → 2.0.0-rc.40

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 (316) hide show
  1. package/bin/porch.js +6 -35
  2. package/dashboard/dist/assets/index-BLqoFC1H.js +120 -0
  3. package/dashboard/dist/assets/index-BLqoFC1H.js.map +1 -0
  4. package/dashboard/dist/assets/index-CXwnJkPh.css +32 -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 +93 -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 +13 -6
  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 +27 -79
  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 +9 -0
  39. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  40. package/dist/agent-farm/commands/tower.js +54 -18
  41. package/dist/agent-farm/commands/tower.js.map +1 -1
  42. package/dist/agent-farm/db/index.d.ts.map +1 -1
  43. package/dist/agent-farm/db/index.js +15 -0
  44. package/dist/agent-farm/db/index.js.map +1 -1
  45. package/dist/agent-farm/db/schema.d.ts +1 -1
  46. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  47. package/dist/agent-farm/db/schema.js +6 -3
  48. package/dist/agent-farm/db/schema.js.map +1 -1
  49. package/dist/agent-farm/db/types.d.ts +3 -0
  50. package/dist/agent-farm/db/types.d.ts.map +1 -1
  51. package/dist/agent-farm/db/types.js +3 -0
  52. package/dist/agent-farm/db/types.js.map +1 -1
  53. package/dist/agent-farm/hq-connector.d.ts +2 -2
  54. package/dist/agent-farm/hq-connector.js +2 -2
  55. package/dist/agent-farm/servers/dashboard-server.js +435 -131
  56. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  57. package/dist/agent-farm/servers/tower-server.js +430 -17
  58. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  59. package/dist/agent-farm/state.d.ts +4 -10
  60. package/dist/agent-farm/state.d.ts.map +1 -1
  61. package/dist/agent-farm/state.js +30 -31
  62. package/dist/agent-farm/state.js.map +1 -1
  63. package/dist/agent-farm/types.d.ts +48 -0
  64. package/dist/agent-farm/types.d.ts.map +1 -1
  65. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  66. package/dist/agent-farm/utils/config.js +12 -11
  67. package/dist/agent-farm/utils/config.js.map +1 -1
  68. package/dist/agent-farm/utils/deps.d.ts.map +1 -1
  69. package/dist/agent-farm/utils/deps.js +0 -16
  70. package/dist/agent-farm/utils/deps.js.map +1 -1
  71. package/dist/agent-farm/utils/notifications.d.ts +30 -0
  72. package/dist/agent-farm/utils/notifications.d.ts.map +1 -0
  73. package/dist/agent-farm/utils/notifications.js +121 -0
  74. package/dist/agent-farm/utils/notifications.js.map +1 -0
  75. package/dist/agent-farm/utils/server-utils.d.ts +2 -1
  76. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  77. package/dist/agent-farm/utils/server-utils.js +11 -1
  78. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  79. package/dist/agent-farm/utils/shell.d.ts +9 -22
  80. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  81. package/dist/agent-farm/utils/shell.js +34 -34
  82. package/dist/agent-farm/utils/shell.js.map +1 -1
  83. package/dist/agent-farm/utils/terminal-ports.d.ts +1 -1
  84. package/dist/agent-farm/utils/terminal-ports.js +1 -1
  85. package/dist/cli.d.ts.map +1 -1
  86. package/dist/cli.js +5 -54
  87. package/dist/cli.js.map +1 -1
  88. package/dist/commands/adopt.d.ts.map +1 -1
  89. package/dist/commands/adopt.js +39 -4
  90. package/dist/commands/adopt.js.map +1 -1
  91. package/dist/commands/consult/index.d.ts.map +1 -1
  92. package/dist/commands/consult/index.js +63 -3
  93. package/dist/commands/consult/index.js.map +1 -1
  94. package/dist/commands/doctor.d.ts.map +1 -1
  95. package/dist/commands/doctor.js +0 -15
  96. package/dist/commands/doctor.js.map +1 -1
  97. package/dist/commands/init.d.ts.map +1 -1
  98. package/dist/commands/init.js +31 -2
  99. package/dist/commands/init.js.map +1 -1
  100. package/dist/commands/porch/build-counter.d.ts +5 -0
  101. package/dist/commands/porch/build-counter.d.ts.map +1 -0
  102. package/dist/commands/porch/build-counter.js +5 -0
  103. package/dist/commands/porch/build-counter.js.map +1 -0
  104. package/dist/commands/porch/checks.d.ts +16 -29
  105. package/dist/commands/porch/checks.d.ts.map +1 -1
  106. package/dist/commands/porch/checks.js +90 -144
  107. package/dist/commands/porch/checks.js.map +1 -1
  108. package/dist/commands/porch/claude.d.ts +27 -0
  109. package/dist/commands/porch/claude.d.ts.map +1 -0
  110. package/dist/commands/porch/claude.js +107 -0
  111. package/dist/commands/porch/claude.js.map +1 -0
  112. package/dist/commands/porch/index.d.ts +21 -43
  113. package/dist/commands/porch/index.d.ts.map +1 -1
  114. package/dist/commands/porch/index.js +466 -926
  115. package/dist/commands/porch/index.js.map +1 -1
  116. package/dist/commands/porch/plan.d.ts +70 -0
  117. package/dist/commands/porch/plan.d.ts.map +1 -0
  118. package/dist/commands/porch/plan.js +190 -0
  119. package/dist/commands/porch/plan.js.map +1 -0
  120. package/dist/commands/porch/prompts.d.ts +19 -0
  121. package/dist/commands/porch/prompts.d.ts.map +1 -0
  122. package/dist/commands/porch/prompts.js +250 -0
  123. package/dist/commands/porch/prompts.js.map +1 -0
  124. package/dist/commands/porch/protocol.d.ts +59 -0
  125. package/dist/commands/porch/protocol.d.ts.map +1 -0
  126. package/dist/commands/porch/protocol.js +260 -0
  127. package/dist/commands/porch/protocol.js.map +1 -0
  128. package/dist/commands/porch/run.d.ts +40 -0
  129. package/dist/commands/porch/run.d.ts.map +1 -0
  130. package/dist/commands/porch/run.js +893 -0
  131. package/dist/commands/porch/run.js.map +1 -0
  132. package/dist/commands/porch/state.d.ts +23 -112
  133. package/dist/commands/porch/state.d.ts.map +1 -1
  134. package/dist/commands/porch/state.js +81 -699
  135. package/dist/commands/porch/state.js.map +1 -1
  136. package/dist/commands/porch/types.d.ts +72 -173
  137. package/dist/commands/porch/types.d.ts.map +1 -1
  138. package/dist/commands/porch/types.js +2 -1
  139. package/dist/commands/porch/types.js.map +1 -1
  140. package/dist/commands/update.d.ts.map +1 -1
  141. package/dist/commands/update.js +22 -0
  142. package/dist/commands/update.js.map +1 -1
  143. package/dist/lib/scaffold.d.ts +24 -0
  144. package/dist/lib/scaffold.d.ts.map +1 -1
  145. package/dist/lib/scaffold.js +78 -0
  146. package/dist/lib/scaffold.js.map +1 -1
  147. package/dist/terminal/index.d.ts +8 -0
  148. package/dist/terminal/index.d.ts.map +1 -0
  149. package/dist/terminal/index.js +5 -0
  150. package/dist/terminal/index.js.map +1 -0
  151. package/dist/terminal/pty-manager.d.ts +60 -0
  152. package/dist/terminal/pty-manager.d.ts.map +1 -0
  153. package/dist/terminal/pty-manager.js +334 -0
  154. package/dist/terminal/pty-manager.js.map +1 -0
  155. package/dist/terminal/pty-session.d.ts +79 -0
  156. package/dist/terminal/pty-session.d.ts.map +1 -0
  157. package/dist/terminal/pty-session.js +215 -0
  158. package/dist/terminal/pty-session.js.map +1 -0
  159. package/dist/terminal/ring-buffer.d.ts +27 -0
  160. package/dist/terminal/ring-buffer.d.ts.map +1 -0
  161. package/dist/terminal/ring-buffer.js +74 -0
  162. package/dist/terminal/ring-buffer.js.map +1 -0
  163. package/dist/terminal/ws-protocol.d.ts +27 -0
  164. package/dist/terminal/ws-protocol.d.ts.map +1 -0
  165. package/dist/terminal/ws-protocol.js +44 -0
  166. package/dist/terminal/ws-protocol.js.map +1 -0
  167. package/package.json +18 -3
  168. package/skeleton/DEPENDENCIES.md +3 -29
  169. package/skeleton/builders.md +1 -1
  170. package/skeleton/protocol-schema.json +282 -0
  171. package/skeleton/protocols/bugfix/builder-prompt.md +49 -0
  172. package/skeleton/protocols/bugfix/protocol.json +14 -2
  173. package/skeleton/protocols/experiment/builder-prompt.md +47 -0
  174. package/skeleton/protocols/experiment/protocol.json +101 -0
  175. package/skeleton/protocols/maintain/builder-prompt.md +41 -0
  176. package/skeleton/protocols/maintain/prompts/audit.md +111 -0
  177. package/skeleton/protocols/maintain/prompts/clean.md +91 -0
  178. package/skeleton/protocols/maintain/prompts/sync.md +113 -0
  179. package/skeleton/protocols/maintain/prompts/verify.md +110 -0
  180. package/skeleton/protocols/maintain/protocol.json +141 -0
  181. package/skeleton/protocols/maintain/protocol.md +13 -7
  182. package/skeleton/protocols/protocol-schema.json +53 -0
  183. package/skeleton/protocols/spider/builder-prompt.md +53 -0
  184. package/skeleton/protocols/spider/prompts/implement.md +109 -50
  185. package/skeleton/protocols/spider/prompts/specify.md +29 -4
  186. package/skeleton/protocols/spider/protocol.json +96 -154
  187. package/skeleton/protocols/spider/protocol.md +26 -16
  188. package/skeleton/protocols/spider/templates/plan.md +14 -0
  189. package/skeleton/protocols/tick/builder-prompt.md +51 -0
  190. package/skeleton/protocols/tick/protocol.json +7 -2
  191. package/skeleton/resources/commands/agent-farm.md +25 -43
  192. package/skeleton/resources/commands/overview.md +6 -16
  193. package/skeleton/resources/workflow-reference.md +2 -2
  194. package/skeleton/roles/architect.md +152 -315
  195. package/skeleton/roles/builder.md +109 -218
  196. package/skeleton/templates/AGENTS.md +1 -1
  197. package/skeleton/templates/CLAUDE.md +1 -1
  198. package/skeleton/templates/cheatsheet.md +4 -2
  199. package/templates/dashboard/index.html +17 -43
  200. package/templates/dashboard/js/dialogs.js +7 -7
  201. package/templates/dashboard/js/files.js +2 -2
  202. package/templates/dashboard/js/main.js +3 -3
  203. package/templates/dashboard/js/projects.js +3 -3
  204. package/templates/dashboard/js/tabs.js +1 -1
  205. package/templates/dashboard/js/utils.js +22 -87
  206. package/templates/tower.html +497 -26
  207. package/dist/agent-farm/commands/kickoff.d.ts +0 -19
  208. package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
  209. package/dist/agent-farm/commands/kickoff.js +0 -331
  210. package/dist/agent-farm/commands/kickoff.js.map +0 -1
  211. package/dist/agent-farm/commands/rename.d.ts +0 -13
  212. package/dist/agent-farm/commands/rename.d.ts.map +0 -1
  213. package/dist/agent-farm/commands/rename.js +0 -33
  214. package/dist/agent-farm/commands/rename.js.map +0 -1
  215. package/dist/agent-farm/commands/tutorial.d.ts +0 -10
  216. package/dist/agent-farm/commands/tutorial.d.ts.map +0 -1
  217. package/dist/agent-farm/commands/tutorial.js +0 -49
  218. package/dist/agent-farm/commands/tutorial.js.map +0 -1
  219. package/dist/agent-farm/commands/util.d.ts.map +0 -1
  220. package/dist/agent-farm/commands/util.js.map +0 -1
  221. package/dist/agent-farm/tutorial/index.d.ts +0 -8
  222. package/dist/agent-farm/tutorial/index.d.ts.map +0 -1
  223. package/dist/agent-farm/tutorial/index.js +0 -8
  224. package/dist/agent-farm/tutorial/index.js.map +0 -1
  225. package/dist/agent-farm/tutorial/prompts.d.ts +0 -57
  226. package/dist/agent-farm/tutorial/prompts.d.ts.map +0 -1
  227. package/dist/agent-farm/tutorial/prompts.js +0 -147
  228. package/dist/agent-farm/tutorial/prompts.js.map +0 -1
  229. package/dist/agent-farm/tutorial/runner.d.ts +0 -52
  230. package/dist/agent-farm/tutorial/runner.d.ts.map +0 -1
  231. package/dist/agent-farm/tutorial/runner.js +0 -204
  232. package/dist/agent-farm/tutorial/runner.js.map +0 -1
  233. package/dist/agent-farm/tutorial/state.d.ts +0 -26
  234. package/dist/agent-farm/tutorial/state.d.ts.map +0 -1
  235. package/dist/agent-farm/tutorial/state.js +0 -89
  236. package/dist/agent-farm/tutorial/state.js.map +0 -1
  237. package/dist/agent-farm/tutorial/steps/first-spec.d.ts +0 -7
  238. package/dist/agent-farm/tutorial/steps/first-spec.d.ts.map +0 -1
  239. package/dist/agent-farm/tutorial/steps/first-spec.js +0 -136
  240. package/dist/agent-farm/tutorial/steps/first-spec.js.map +0 -1
  241. package/dist/agent-farm/tutorial/steps/implementation.d.ts +0 -7
  242. package/dist/agent-farm/tutorial/steps/implementation.d.ts.map +0 -1
  243. package/dist/agent-farm/tutorial/steps/implementation.js +0 -76
  244. package/dist/agent-farm/tutorial/steps/implementation.js.map +0 -1
  245. package/dist/agent-farm/tutorial/steps/index.d.ts +0 -10
  246. package/dist/agent-farm/tutorial/steps/index.d.ts.map +0 -1
  247. package/dist/agent-farm/tutorial/steps/index.js +0 -10
  248. package/dist/agent-farm/tutorial/steps/index.js.map +0 -1
  249. package/dist/agent-farm/tutorial/steps/planning.d.ts +0 -7
  250. package/dist/agent-farm/tutorial/steps/planning.d.ts.map +0 -1
  251. package/dist/agent-farm/tutorial/steps/planning.js +0 -143
  252. package/dist/agent-farm/tutorial/steps/planning.js.map +0 -1
  253. package/dist/agent-farm/tutorial/steps/review.d.ts +0 -7
  254. package/dist/agent-farm/tutorial/steps/review.d.ts.map +0 -1
  255. package/dist/agent-farm/tutorial/steps/review.js +0 -78
  256. package/dist/agent-farm/tutorial/steps/review.js.map +0 -1
  257. package/dist/agent-farm/tutorial/steps/setup.d.ts +0 -7
  258. package/dist/agent-farm/tutorial/steps/setup.d.ts.map +0 -1
  259. package/dist/agent-farm/tutorial/steps/setup.js +0 -126
  260. package/dist/agent-farm/tutorial/steps/setup.js.map +0 -1
  261. package/dist/agent-farm/tutorial/steps/welcome.d.ts +0 -7
  262. package/dist/agent-farm/tutorial/steps/welcome.d.ts.map +0 -1
  263. package/dist/agent-farm/tutorial/steps/welcome.js +0 -50
  264. package/dist/agent-farm/tutorial/steps/welcome.js.map +0 -1
  265. package/dist/commands/pcheck/cache.d.ts +0 -48
  266. package/dist/commands/pcheck/cache.d.ts.map +0 -1
  267. package/dist/commands/pcheck/cache.js +0 -170
  268. package/dist/commands/pcheck/cache.js.map +0 -1
  269. package/dist/commands/pcheck/evaluator.d.ts +0 -15
  270. package/dist/commands/pcheck/evaluator.d.ts.map +0 -1
  271. package/dist/commands/pcheck/evaluator.js +0 -246
  272. package/dist/commands/pcheck/evaluator.js.map +0 -1
  273. package/dist/commands/pcheck/index.d.ts +0 -12
  274. package/dist/commands/pcheck/index.d.ts.map +0 -1
  275. package/dist/commands/pcheck/index.js +0 -249
  276. package/dist/commands/pcheck/index.js.map +0 -1
  277. package/dist/commands/pcheck/parser.d.ts +0 -39
  278. package/dist/commands/pcheck/parser.d.ts.map +0 -1
  279. package/dist/commands/pcheck/parser.js +0 -155
  280. package/dist/commands/pcheck/parser.js.map +0 -1
  281. package/dist/commands/pcheck/types.d.ts +0 -82
  282. package/dist/commands/pcheck/types.d.ts.map +0 -1
  283. package/dist/commands/pcheck/types.js +0 -5
  284. package/dist/commands/pcheck/types.js.map +0 -1
  285. package/dist/commands/porch/consultation.d.ts +0 -56
  286. package/dist/commands/porch/consultation.d.ts.map +0 -1
  287. package/dist/commands/porch/consultation.js +0 -330
  288. package/dist/commands/porch/consultation.js.map +0 -1
  289. package/dist/commands/porch/notifications.d.ts +0 -99
  290. package/dist/commands/porch/notifications.d.ts.map +0 -1
  291. package/dist/commands/porch/notifications.js +0 -223
  292. package/dist/commands/porch/notifications.js.map +0 -1
  293. package/dist/commands/porch/plan-parser.d.ts +0 -38
  294. package/dist/commands/porch/plan-parser.d.ts.map +0 -1
  295. package/dist/commands/porch/plan-parser.js +0 -166
  296. package/dist/commands/porch/plan-parser.js.map +0 -1
  297. package/dist/commands/porch/protocol-loader.d.ts +0 -46
  298. package/dist/commands/porch/protocol-loader.d.ts.map +0 -1
  299. package/dist/commands/porch/protocol-loader.js +0 -249
  300. package/dist/commands/porch/protocol-loader.js.map +0 -1
  301. package/dist/commands/porch/signal-parser.d.ts +0 -88
  302. package/dist/commands/porch/signal-parser.d.ts.map +0 -1
  303. package/dist/commands/porch/signal-parser.js +0 -148
  304. package/dist/commands/porch/signal-parser.js.map +0 -1
  305. package/dist/commands/tower.d.ts +0 -16
  306. package/dist/commands/tower.d.ts.map +0 -1
  307. package/dist/commands/tower.js +0 -21
  308. package/dist/commands/tower.js.map +0 -1
  309. package/skeleton/config.json +0 -7
  310. package/skeleton/porch/protocols/bugfix.json +0 -85
  311. package/skeleton/porch/protocols/spider.json +0 -135
  312. package/skeleton/porch/protocols/tick.json +0 -76
  313. package/skeleton/protocols/spider/prompts/defend.md +0 -215
  314. package/skeleton/protocols/spider/prompts/evaluate.md +0 -241
  315. package/templates/dashboard/css/activity.css +0 -151
  316. 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,147 @@ 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, 'af-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
+ // Listen for exit and auto-restart
176
+ session.on('exit', (exitCode) => {
177
+ console.log(`Architect terminal exited (code=${exitCode}), will attempt restart...`);
178
+ // Clear the terminalId so we can recreate
179
+ const arch = getArchitect();
180
+ if (arch) {
181
+ setArchitect({ ...arch, terminalId: undefined });
182
+ }
183
+ // Schedule restart after a brief delay
184
+ setTimeout(() => {
185
+ console.log('Attempting to restart architect terminal...');
186
+ initArchitectTerminal().catch((err) => {
187
+ console.error('Failed to restart architect terminal:', err.message);
188
+ });
189
+ }, 2000);
190
+ });
191
+ }
192
+ catch (err) {
193
+ console.error('Failed to create architect terminal session:', err.message);
194
+ }
195
+ }
196
+ // Poll for architect state and create PTY session once available
197
+ // start.ts writes architect to DB before spawning this server, but there can be a small delay
198
+ (async function waitForArchitectAndInit() {
199
+ for (let attempt = 0; attempt < 30; attempt++) {
200
+ await new Promise((resolve) => setTimeout(resolve, 500));
201
+ try {
202
+ const arch = getArchitect();
203
+ if (!arch)
204
+ continue;
205
+ if (arch.terminalId)
206
+ return; // Already has terminal
207
+ if (!arch.tmuxSession)
208
+ continue; // No tmux session yet
209
+ console.log(`initArchitectTerminal: attempt ${attempt + 1}, tmux=${arch.tmuxSession}`);
210
+ await initArchitectTerminal();
211
+ const updated = getArchitect();
212
+ if (updated?.terminalId) {
213
+ console.log(`initArchitectTerminal: success, terminalId=${updated.terminalId}`);
214
+ return;
215
+ }
216
+ console.log(`initArchitectTerminal: attempt ${attempt + 1} failed, terminalId still unset`);
217
+ }
218
+ catch (err) {
219
+ console.error(`initArchitectTerminal: attempt ${attempt + 1} error:`, err.message);
220
+ }
221
+ }
222
+ console.warn('initArchitectTerminal: gave up after 30 attempts');
223
+ })();
224
+ // Log telemetry
225
+ try {
226
+ const metricsPath = path.join(projectRoot, '.agent-farm', 'metrics.log');
227
+ fs.mkdirSync(path.dirname(metricsPath), { recursive: true });
228
+ fs.appendFileSync(metricsPath, JSON.stringify({
229
+ event: 'backend_selected',
230
+ backend: 'node-pty',
231
+ timestamp: new Date().toISOString(),
232
+ }) + '\n');
233
+ }
234
+ catch { /* ignore */ }
94
235
  // Clean up dead processes from state (called on state load)
95
236
  function cleanupDeadProcesses() {
96
237
  // Clean up dead shell processes
@@ -217,6 +358,9 @@ async function killProcessGracefully(pid, tmuxSession) {
217
358
  if (tmuxSession) {
218
359
  killTmuxSession(tmuxSession);
219
360
  }
361
+ // Guard: PID 0 sends signal to entire process group — never do that
362
+ if (!pid || pid <= 0)
363
+ return;
220
364
  try {
221
365
  // First try SIGTERM
222
366
  process.kill(pid, 'SIGTERM');
@@ -281,42 +425,24 @@ function tmuxSessionExists(sessionName) {
281
425
  return false;
282
426
  }
283
427
  }
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) {
428
+ // Create a PTY terminal session via the TerminalManager.
429
+ // Returns the terminal session ID, or null on failure.
430
+ async function createTerminalSession(shellCommand, cwd, label) {
431
+ if (!terminalManager)
432
+ return null;
287
433
  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,
434
+ const info = await terminalManager.createSession({
435
+ command: '/bin/bash',
436
+ args: ['-c', shellCommand],
311
437
  cwd,
312
- customIndexPath: customIndexPath ?? undefined,
438
+ cols: 200,
439
+ rows: 50,
440
+ label,
313
441
  });
314
- return ttydProcess?.pid ?? null;
442
+ return info.id;
315
443
  }
316
444
  catch (err) {
317
- console.error(`Failed to create tmux session ${sessionName}:`, err.message);
318
- // Cleanup any partial session
319
- killTmuxSession(sessionName);
445
+ console.error(`Failed to create terminal session:`, err.message);
320
446
  return null;
321
447
  }
322
448
  }
@@ -336,7 +462,7 @@ function generateShortId() {
336
462
  * Spawn a worktree builder - creates git worktree and starts builder CLI
337
463
  * Similar to shell spawning but with git worktree isolation
338
464
  */
339
- function spawnWorktreeBuilder(builderPort, state) {
465
+ async function spawnWorktreeBuilder(builderPort, state) {
340
466
  const shortId = generateShortId();
341
467
  const builderId = `worktree-${shortId}`;
342
468
  const branchName = `builder/worktree-${shortId}`;
@@ -351,42 +477,23 @@ function spawnWorktreeBuilder(builderPort, state) {
351
477
  // Create git branch and worktree
352
478
  execSync(`git branch "${branchName}" HEAD`, { cwd: projectRoot, stdio: 'ignore' });
353
479
  execSync(`git worktree add "${worktreePath}" "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
354
- // Get builder command from config or use default shell
355
- const configPath = path.resolve(projectRoot, 'codev', 'config.json');
480
+ // Get builder command from af-config.json or use default shell
481
+ const afConfigPath = path.resolve(projectRoot, 'af-config.json');
356
482
  const defaultShell = process.env.SHELL || 'bash';
357
483
  let builderCommand = defaultShell;
358
- if (fs.existsSync(configPath)) {
484
+ if (fs.existsSync(afConfigPath)) {
359
485
  try {
360
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
486
+ const config = JSON.parse(fs.readFileSync(afConfigPath, 'utf-8'));
361
487
  builderCommand = config?.shell?.builder || defaultShell;
362
488
  }
363
489
  catch {
364
490
  // Use default
365
491
  }
366
492
  }
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) {
493
+ // Create PTY terminal session via node-pty
494
+ const terminalId = await createTerminalSession(builderCommand, worktreePath, `builder-${builderId}`);
495
+ if (!terminalId) {
388
496
  // Cleanup on failure
389
- killTmuxSession(sessionName);
390
497
  try {
391
498
  execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectRoot, stdio: 'ignore' });
392
499
  execSync(`git branch -D "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
@@ -399,16 +506,17 @@ function spawnWorktreeBuilder(builderPort, state) {
399
506
  const builder = {
400
507
  id: builderId,
401
508
  name: `Worktree ${shortId}`,
402
- port: builderPort,
403
- pid,
509
+ port: 0,
510
+ pid: 0,
404
511
  status: 'implementing',
405
512
  phase: 'interactive',
406
513
  worktree: worktreePath,
407
514
  branch: branchName,
408
515
  tmuxSession: sessionName,
409
516
  type: 'worktree',
517
+ terminalId,
410
518
  };
411
- return { builder, pid };
519
+ return { builder, pid: 0 };
412
520
  }
413
521
  catch (err) {
414
522
  console.error(`Failed to spawn worktree builder:`, err.message);
@@ -888,6 +996,115 @@ function isRequestAllowed(req) {
888
996
  }
889
997
  return isRequestAllowedBase(req);
890
998
  }
999
+ /**
1000
+ * Timing-safe token comparison to prevent timing attacks
1001
+ */
1002
+ function isValidToken(provided, expected) {
1003
+ if (!provided)
1004
+ return false;
1005
+ // Ensure both strings are same length for timing-safe comparison
1006
+ const providedBuf = Buffer.from(provided);
1007
+ const expectedBuf = Buffer.from(expected);
1008
+ if (providedBuf.length !== expectedBuf.length) {
1009
+ // Still do a comparison to maintain constant time
1010
+ timingSafeEqual(expectedBuf, expectedBuf);
1011
+ return false;
1012
+ }
1013
+ return timingSafeEqual(providedBuf, expectedBuf);
1014
+ }
1015
+ /**
1016
+ * Generate HTML for login page
1017
+ */
1018
+ function getLoginPageHtml() {
1019
+ return `<!DOCTYPE html>
1020
+ <html>
1021
+ <head>
1022
+ <title>Dashboard Login</title>
1023
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1024
+ <style>
1025
+ body { font-family: system-ui; background: #1a1a2e; color: #eee;
1026
+ display: flex; justify-content: center; align-items: center;
1027
+ min-height: 100vh; margin: 0; }
1028
+ .login { background: #16213e; padding: 2rem; border-radius: 8px;
1029
+ max-width: 400px; width: 90%; }
1030
+ h1 { margin-top: 0; }
1031
+ input { width: 100%; padding: 0.75rem; margin: 0.5rem 0;
1032
+ border: 1px solid #444; border-radius: 4px;
1033
+ background: #0f0f23; color: #eee; font-size: 1rem;
1034
+ box-sizing: border-box; }
1035
+ button { width: 100%; padding: 0.75rem; margin-top: 1rem;
1036
+ background: #4a7c59; color: white; border: none;
1037
+ border-radius: 4px; font-size: 1rem; cursor: pointer; }
1038
+ button:hover { background: #5a9c69; }
1039
+ .error { color: #ff6b6b; margin-top: 0.5rem; display: none; }
1040
+ </style>
1041
+ </head>
1042
+ <body>
1043
+ <div class="login">
1044
+ <h1>Agent Farm Login</h1>
1045
+ <p>Enter your API key to access the dashboard.</p>
1046
+ <input type="password" id="key" placeholder="API Key" autofocus>
1047
+ <div class="error" id="error">Invalid API key</div>
1048
+ <button onclick="login()">Login</button>
1049
+ </div>
1050
+ <script>
1051
+ // Check for key in URL (from QR code scan) or localStorage
1052
+ (async function() {
1053
+ const urlParams = new URLSearchParams(window.location.search);
1054
+ const keyFromUrl = urlParams.get('key');
1055
+ const keyFromStorage = localStorage.getItem('codev_web_key');
1056
+ const key = keyFromUrl || keyFromStorage;
1057
+
1058
+ if (key) {
1059
+ if (keyFromUrl) {
1060
+ localStorage.setItem('codev_web_key', keyFromUrl);
1061
+ }
1062
+ await verifyAndLoadDashboard(key);
1063
+ }
1064
+ })();
1065
+
1066
+ async function verifyAndLoadDashboard(key) {
1067
+ try {
1068
+ // Fetch the actual dashboard with auth header
1069
+ const res = await fetch(window.location.pathname, {
1070
+ headers: {
1071
+ 'Authorization': 'Bearer ' + key,
1072
+ 'Accept': 'text/html'
1073
+ }
1074
+ });
1075
+ if (res.ok) {
1076
+ // Replace entire page with dashboard
1077
+ const html = await res.text();
1078
+ document.open();
1079
+ document.write(html);
1080
+ document.close();
1081
+ // Clean URL without reload
1082
+ history.replaceState({}, '', window.location.pathname);
1083
+ } else {
1084
+ // Key invalid
1085
+ localStorage.removeItem('codev_web_key');
1086
+ document.getElementById('error').style.display = 'block';
1087
+ document.getElementById('error').textContent = 'Invalid API key';
1088
+ }
1089
+ } catch (e) {
1090
+ document.getElementById('error').style.display = 'block';
1091
+ document.getElementById('error').textContent = 'Connection error';
1092
+ }
1093
+ }
1094
+
1095
+ async function login() {
1096
+ const key = document.getElementById('key').value;
1097
+ if (!key) return;
1098
+ localStorage.setItem('codev_web_key', key);
1099
+ await verifyAndLoadDashboard(key);
1100
+ }
1101
+ document.getElementById('key').addEventListener('keypress', (e) => {
1102
+ if (e.key === 'Enter') login();
1103
+ });
1104
+ </script>
1105
+ </body>
1106
+ </html>`;
1107
+ }
891
1108
  // Create server
892
1109
  const server = http.createServer(async (req, res) => {
893
1110
  // Security: Validate Host and Origin headers
@@ -896,17 +1113,37 @@ const server = http.createServer(async (req, res) => {
896
1113
  res.end('Forbidden');
897
1114
  return;
898
1115
  }
1116
+ // CRITICAL: When CODEV_WEB_KEY is set, ALL requests require auth
1117
+ // NO localhost bypass - tunnel daemons (cloudflared) run locally and proxy
1118
+ // to localhost, so checking remoteAddress would incorrectly trust remote traffic
1119
+ const webKey = process.env.CODEV_WEB_KEY;
1120
+ if (webKey) {
1121
+ const authHeader = req.headers.authorization;
1122
+ const token = authHeader?.replace('Bearer ', '');
1123
+ if (!isValidToken(token, webKey)) {
1124
+ // Return login page for HTML requests, 401 for API
1125
+ if (req.headers.accept?.includes('text/html')) {
1126
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1127
+ res.end(getLoginPageHtml());
1128
+ return;
1129
+ }
1130
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1131
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
1132
+ return;
1133
+ }
1134
+ }
1135
+ // When CODEV_WEB_KEY is NOT set: no auth required (local dev mode only)
899
1136
  // CORS headers
900
1137
  const origin = req.headers.origin;
901
- if (insecureRemoteMode) {
902
- // Allow any origin in insecure remote mode
1138
+ if (insecureRemoteMode || webKey) {
1139
+ // Allow any origin in insecure remote mode or when using auth (tunnel access)
903
1140
  res.setHeader('Access-Control-Allow-Origin', origin || '*');
904
1141
  }
905
1142
  else if (origin && (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:'))) {
906
1143
  res.setHeader('Access-Control-Allow-Origin', origin);
907
1144
  }
908
1145
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
909
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
1146
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
910
1147
  // Prevent caching of API responses
911
1148
  res.setHeader('Cache-Control', 'no-store');
912
1149
  if (req.method === 'OPTIONS') {
@@ -916,6 +1153,12 @@ const server = http.createServer(async (req, res) => {
916
1153
  }
917
1154
  const url = new URL(req.url || '/', `http://localhost:${port}`);
918
1155
  try {
1156
+ // Spec 0085: node-pty terminal manager REST API routes
1157
+ if (terminalManager && url.pathname.startsWith('/api/terminals')) {
1158
+ if (terminalManager.handleRequest(req, res)) {
1159
+ return;
1160
+ }
1161
+ }
919
1162
  // API: Get state
920
1163
  if (req.method === 'GET' && url.pathname === '/api/state') {
921
1164
  const state = loadStateWithCleanup();
@@ -1025,14 +1268,12 @@ const server = http.createServer(async (req, res) => {
1025
1268
  // Find available port for builder
1026
1269
  const builderPort = await findAvailablePort(CONFIG.builderPortStart, builderState);
1027
1270
  // Spawn worktree builder
1028
- const result = spawnWorktreeBuilder(builderPort, builderState);
1271
+ const result = await spawnWorktreeBuilder(builderPort, builderState);
1029
1272
  if (!result) {
1030
1273
  res.writeHead(500, { 'Content-Type': 'text/plain' });
1031
1274
  res.end('Failed to spawn worktree builder');
1032
1275
  return;
1033
1276
  }
1034
- // Wait for ttyd to be ready
1035
- await new Promise((resolve) => setTimeout(resolve, 500));
1036
1277
  // Save builder to state
1037
1278
  upsertBuilder(result.builder);
1038
1279
  res.writeHead(201, { 'Content-Type': 'application/json' });
@@ -1165,49 +1406,25 @@ const server = http.createServer(async (req, res) => {
1165
1406
  const shellCommand = command
1166
1407
  ? `${shell} -c '${command.replace(/'/g, "'\\''")}; exec ${shell}'`
1167
1408
  : 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) {
1409
+ // Create PTY terminal session via node-pty
1410
+ const terminalId = await createTerminalSession(shellCommand, cwd, `shell-${utilName}`);
1411
+ if (!terminalId) {
1205
1412
  res.writeHead(500, { 'Content-Type': 'text/plain' });
1206
- res.end('Failed to allocate port after multiple retries');
1413
+ res.end('Failed to create terminal session');
1207
1414
  return;
1208
1415
  }
1416
+ const util = {
1417
+ id,
1418
+ name: utilName,
1419
+ port: 0,
1420
+ pid: 0,
1421
+ tmuxSession: sessionName,
1422
+ worktreePath: worktreePath,
1423
+ terminalId,
1424
+ };
1425
+ addUtil(util);
1209
1426
  res.writeHead(201, { 'Content-Type': 'application/json' });
1210
- res.end(JSON.stringify({ success: true, id, port: utilPort, name: utilName }));
1427
+ res.end(JSON.stringify({ success: true, id, port: 0, name: utilName, terminalId }));
1211
1428
  return;
1212
1429
  }
1213
1430
  // API: Check if tab process is running (Bugfix #132)
@@ -1228,7 +1445,14 @@ const server = http.createServer(async (req, res) => {
1228
1445
  const util = tabUtils.find((u) => u.id === utilId);
1229
1446
  if (util) {
1230
1447
  found = true;
1231
- running = isProcessRunning(util.pid);
1448
+ // Check tmux session status (Spec 0076)
1449
+ if (util.tmuxSession) {
1450
+ running = tmuxSessionExists(util.tmuxSession);
1451
+ }
1452
+ else {
1453
+ // Fallback for shells without tmux session (shouldn't happen in practice)
1454
+ running = isProcessRunning(util.pid);
1455
+ }
1232
1456
  }
1233
1457
  }
1234
1458
  // Check if it's a builder tab
@@ -1237,7 +1461,14 @@ const server = http.createServer(async (req, res) => {
1237
1461
  const builder = getBuilder(builderId);
1238
1462
  if (builder) {
1239
1463
  found = true;
1240
- running = isProcessRunning(builder.pid);
1464
+ // Check tmux session status (Spec 0076)
1465
+ if (builder.tmuxSession) {
1466
+ running = tmuxSessionExists(builder.tmuxSession);
1467
+ }
1468
+ else {
1469
+ // Fallback for builders without tmux session (shouldn't happen in practice)
1470
+ running = isProcessRunning(builder.pid);
1471
+ }
1241
1472
  }
1242
1473
  }
1243
1474
  if (found) {
@@ -1281,6 +1512,10 @@ const server = http.createServer(async (req, res) => {
1281
1512
  const tabUtils = getUtils();
1282
1513
  const util = tabUtils.find((u) => u.id === utilId);
1283
1514
  if (util) {
1515
+ // Kill PTY session if present
1516
+ if (util.terminalId && terminalManager) {
1517
+ terminalManager.killSession(util.terminalId);
1518
+ }
1284
1519
  await killProcessGracefully(util.pid, util.tmuxSession);
1285
1520
  // Note: worktrees are NOT cleaned up on tab close - they may contain useful context
1286
1521
  // Users can manually clean up with `git worktree list` and `git worktree remove`
@@ -1470,6 +1705,13 @@ const server = http.createServer(async (req, res) => {
1470
1705
  res.end(`File not found: ${filePath}`);
1471
1706
  return;
1472
1707
  }
1708
+ // Check if it's a directory
1709
+ const stat = fs.statSync(fullPath);
1710
+ if (stat.isDirectory()) {
1711
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
1712
+ res.end(`Cannot read directory as file: ${filePath}`);
1713
+ return;
1714
+ }
1473
1715
  // Read and return file contents
1474
1716
  try {
1475
1717
  const content = fs.readFileSync(fullPath, 'utf-8');
@@ -1660,20 +1902,6 @@ const server = http.createServer(async (req, res) => {
1660
1902
  }
1661
1903
  return;
1662
1904
  }
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
1905
  // API: Hot reload check (Spec 0060)
1678
1906
  // Returns modification times for all dashboard CSS/JS files
1679
1907
  if (req.method === 'GET' && url.pathname === '/api/hot-reload') {
@@ -1751,7 +1979,7 @@ const server = http.createServer(async (req, res) => {
1751
1979
  return;
1752
1980
  }
1753
1981
  // Terminal proxy route (Spec 0062 - Secure Remote Access)
1754
- // Routes /terminal/:id to the appropriate ttyd instance
1982
+ // Routes /terminal/:id to the appropriate terminal instance
1755
1983
  const terminalMatch = url.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
1756
1984
  if (terminalMatch) {
1757
1985
  const terminalId = terminalMatch[1];
@@ -1784,8 +2012,49 @@ const server = http.createServer(async (req, res) => {
1784
2012
  terminalProxy.web(req, res, { target: `http://localhost:${annotation.port}` });
1785
2013
  return;
1786
2014
  }
1787
- // Serve dashboard
1788
- if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
2015
+ // Serve dashboard (Spec 0085: React or legacy based on config)
2016
+ if (useReactDashboard && req.method === 'GET') {
2017
+ // Serve React dashboard static files
2018
+ const filePath = url.pathname === '/' || url.pathname === '/index.html'
2019
+ ? path.join(reactDashboardPath, 'index.html')
2020
+ : path.join(reactDashboardPath, url.pathname);
2021
+ // Security: Prevent path traversal
2022
+ const resolved = path.resolve(filePath);
2023
+ if (!resolved.startsWith(reactDashboardPath)) {
2024
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
2025
+ res.end('Forbidden');
2026
+ return;
2027
+ }
2028
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
2029
+ const ext = path.extname(resolved);
2030
+ const mimeTypes = {
2031
+ '.html': 'text/html; charset=utf-8',
2032
+ '.js': 'application/javascript',
2033
+ '.css': 'text/css',
2034
+ '.json': 'application/json',
2035
+ '.svg': 'image/svg+xml',
2036
+ '.png': 'image/png',
2037
+ '.ico': 'image/x-icon',
2038
+ '.map': 'application/json',
2039
+ };
2040
+ const contentType = mimeTypes[ext] ?? 'application/octet-stream';
2041
+ // Cache static assets (hashed filenames) but not index.html
2042
+ if (ext !== '.html') {
2043
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
2044
+ }
2045
+ res.writeHead(200, { 'Content-Type': contentType });
2046
+ fs.createReadStream(resolved).pipe(res);
2047
+ return;
2048
+ }
2049
+ // SPA fallback: serve index.html for client-side routing
2050
+ if (!url.pathname.startsWith('/api/') && !url.pathname.startsWith('/ws/') && !url.pathname.startsWith('/terminal/') && !url.pathname.startsWith('/annotation/')) {
2051
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
2052
+ fs.createReadStream(path.join(reactDashboardPath, 'index.html')).pipe(res);
2053
+ return;
2054
+ }
2055
+ }
2056
+ if (!useReactDashboard && req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
2057
+ // Legacy vanilla JS dashboard
1789
2058
  try {
1790
2059
  let template = fs.readFileSync(templatePath, 'utf-8');
1791
2060
  const state = loadStateWithCleanup();
@@ -1814,15 +2083,37 @@ const server = http.createServer(async (req, res) => {
1814
2083
  res.end('Internal server error: ' + err.message);
1815
2084
  }
1816
2085
  });
2086
+ // Spec 0085: Attach node-pty WebSocket handler for /ws/terminal/:id routes
2087
+ if (terminalManager) {
2088
+ terminalManager.attachWebSocket(server);
2089
+ }
1817
2090
  // WebSocket upgrade handler for terminal proxy (Spec 0062)
1818
- // ttyd uses WebSocket for bidirectional terminal communication
2091
+ // WebSocket for bidirectional terminal communication
1819
2092
  server.on('upgrade', (req, socket, head) => {
1820
- // Security check
2093
+ // Security check for non-auth mode
1821
2094
  const host = req.headers.host;
1822
- if (!insecureRemoteMode && host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
2095
+ if (!insecureRemoteMode && !process.env.CODEV_WEB_KEY && host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
1823
2096
  socket.destroy();
1824
2097
  return;
1825
2098
  }
2099
+ // CRITICAL: When CODEV_WEB_KEY is set, ALL WebSocket upgrades require auth
2100
+ // NO localhost bypass - tunnel daemons run locally, so remoteAddress is unreliable
2101
+ const webKey = process.env.CODEV_WEB_KEY;
2102
+ if (webKey && !insecureRemoteMode) {
2103
+ // Check Sec-WebSocket-Protocol for auth token
2104
+ // Format: "auth-<token>, tty" or just "tty"
2105
+ const protocols = req.headers['sec-websocket-protocol']?.split(',').map((p) => p.trim()) || [];
2106
+ const authProtocol = protocols.find((p) => p.startsWith('auth-'));
2107
+ const token = authProtocol?.substring(5); // Remove 'auth-' prefix
2108
+ if (!isValidToken(token, webKey)) {
2109
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
2110
+ socket.destroy();
2111
+ return;
2112
+ }
2113
+ // Remove auth protocol from the list before forwarding
2114
+ const cleanProtocols = protocols.filter((p) => !p.startsWith('auth-'));
2115
+ req.headers['sec-websocket-protocol'] = cleanProtocols.join(', ') || 'tty';
2116
+ }
1826
2117
  const reqUrl = new URL(req.url || '/', `http://localhost:${port}`);
1827
2118
  const terminalMatch = reqUrl.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
1828
2119
  if (terminalMatch) {
@@ -1869,4 +2160,17 @@ else {
1869
2160
  console.log(`Dashboard: http://localhost:${port}`);
1870
2161
  });
1871
2162
  }
2163
+ // Spec 0085: Graceful shutdown for node-pty terminal manager
2164
+ process.on('SIGTERM', () => {
2165
+ if (terminalManager) {
2166
+ terminalManager.shutdown();
2167
+ }
2168
+ process.exit(0);
2169
+ });
2170
+ process.on('SIGINT', () => {
2171
+ if (terminalManager) {
2172
+ terminalManager.shutdown();
2173
+ }
2174
+ process.exit(0);
2175
+ });
1872
2176
  //# sourceMappingURL=dashboard-server.js.map