@cluesmith/codev 2.0.2 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/dashboard/dist/assets/index-B-s8BA2l.js +135 -0
  2. package/dashboard/dist/assets/index-B-s8BA2l.js.map +1 -0
  3. package/dashboard/dist/assets/index-DB2AxRP7.css +32 -0
  4. package/dashboard/dist/index.html +2 -2
  5. package/dist/agent-farm/cli.d.ts.map +1 -1
  6. package/dist/agent-farm/cli.js +32 -14
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/architect.d.ts +1 -1
  9. package/dist/agent-farm/commands/architect.js +3 -3
  10. package/dist/agent-farm/commands/architect.js.map +1 -1
  11. package/dist/agent-farm/commands/attach.d.ts +19 -0
  12. package/dist/agent-farm/commands/attach.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/attach.js +172 -12
  14. package/dist/agent-farm/commands/attach.js.map +1 -1
  15. package/dist/agent-farm/commands/cleanup.js +6 -6
  16. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  17. package/dist/agent-farm/commands/open.js +5 -5
  18. package/dist/agent-farm/commands/open.js.map +1 -1
  19. package/dist/agent-farm/commands/send.d.ts +22 -2
  20. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  21. package/dist/agent-farm/commands/send.js +100 -181
  22. package/dist/agent-farm/commands/send.js.map +1 -1
  23. package/dist/agent-farm/commands/shell.js +5 -5
  24. package/dist/agent-farm/commands/shell.js.map +1 -1
  25. package/dist/agent-farm/commands/spawn-roles.d.ts +3 -9
  26. package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -1
  27. package/dist/agent-farm/commands/spawn-roles.js +14 -53
  28. package/dist/agent-farm/commands/spawn-roles.js.map +1 -1
  29. package/dist/agent-farm/commands/spawn-worktree.d.ts +11 -17
  30. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  31. package/dist/agent-farm/commands/spawn-worktree.js +32 -13
  32. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  33. package/dist/agent-farm/commands/spawn.d.ts +8 -6
  34. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  35. package/dist/agent-farm/commands/spawn.js +183 -69
  36. package/dist/agent-farm/commands/spawn.js.map +1 -1
  37. package/dist/agent-farm/commands/start.d.ts +4 -4
  38. package/dist/agent-farm/commands/start.js +16 -16
  39. package/dist/agent-farm/commands/start.js.map +1 -1
  40. package/dist/agent-farm/commands/status.d.ts +1 -1
  41. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  42. package/dist/agent-farm/commands/status.js +15 -26
  43. package/dist/agent-farm/commands/status.js.map +1 -1
  44. package/dist/agent-farm/commands/stop.d.ts +4 -4
  45. package/dist/agent-farm/commands/stop.js +9 -9
  46. package/dist/agent-farm/commands/stop.js.map +1 -1
  47. package/dist/agent-farm/db/index.d.ts.map +1 -1
  48. package/dist/agent-farm/db/index.js +82 -7
  49. package/dist/agent-farm/db/index.js.map +1 -1
  50. package/dist/agent-farm/db/schema.d.ts +2 -2
  51. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  52. package/dist/agent-farm/db/schema.js +21 -4
  53. package/dist/agent-farm/db/schema.js.map +1 -1
  54. package/dist/agent-farm/lib/tower-client.d.ts +36 -26
  55. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  56. package/dist/agent-farm/lib/tower-client.js +50 -25
  57. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  58. package/dist/agent-farm/lib/tunnel-client.d.ts +12 -2
  59. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  60. package/dist/agent-farm/lib/tunnel-client.js +59 -1
  61. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  62. package/dist/agent-farm/servers/overview.d.ts +111 -0
  63. package/dist/agent-farm/servers/overview.d.ts.map +1 -0
  64. package/dist/agent-farm/servers/overview.js +385 -0
  65. package/dist/agent-farm/servers/overview.js.map +1 -0
  66. package/dist/agent-farm/servers/tower-instances.d.ts +18 -20
  67. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  68. package/dist/agent-farm/servers/tower-instances.js +97 -100
  69. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  70. package/dist/agent-farm/servers/tower-messages.d.ts +87 -0
  71. package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -0
  72. package/dist/agent-farm/servers/tower-messages.js +202 -0
  73. package/dist/agent-farm/servers/tower-messages.js.map +1 -0
  74. package/dist/agent-farm/servers/tower-routes.d.ts +1 -1
  75. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  76. package/dist/agent-farm/servers/tower-routes.js +343 -174
  77. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  78. package/dist/agent-farm/servers/tower-server.js +50 -21
  79. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  80. package/dist/agent-farm/servers/tower-terminals.d.ts +35 -31
  81. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  82. package/dist/agent-farm/servers/tower-terminals.js +208 -184
  83. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  84. package/dist/agent-farm/servers/tower-tunnel.d.ts +2 -2
  85. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  86. package/dist/agent-farm/servers/tower-tunnel.js +12 -12
  87. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  88. package/dist/agent-farm/servers/tower-types.d.ts +8 -12
  89. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
  90. package/dist/agent-farm/servers/tower-utils.d.ts +9 -9
  91. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  92. package/dist/agent-farm/servers/tower-utils.js +18 -18
  93. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  94. package/dist/agent-farm/servers/tower-websocket.d.ts +2 -2
  95. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  96. package/dist/agent-farm/servers/tower-websocket.js +39 -18
  97. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  98. package/dist/agent-farm/types.d.ts +5 -6
  99. package/dist/agent-farm/types.d.ts.map +1 -1
  100. package/dist/agent-farm/utils/agent-names.d.ts +85 -0
  101. package/dist/agent-farm/utils/agent-names.d.ts.map +1 -0
  102. package/dist/agent-farm/utils/agent-names.js +140 -0
  103. package/dist/agent-farm/utils/agent-names.js.map +1 -0
  104. package/dist/agent-farm/utils/config.d.ts +1 -1
  105. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  106. package/dist/agent-farm/utils/config.js +16 -16
  107. package/dist/agent-farm/utils/config.js.map +1 -1
  108. package/dist/agent-farm/utils/file-tabs.d.ts +3 -3
  109. package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -1
  110. package/dist/agent-farm/utils/file-tabs.js +9 -9
  111. package/dist/agent-farm/utils/file-tabs.js.map +1 -1
  112. package/dist/agent-farm/utils/index.d.ts +0 -1
  113. package/dist/agent-farm/utils/index.d.ts.map +1 -1
  114. package/dist/agent-farm/utils/index.js +0 -1
  115. package/dist/agent-farm/utils/index.js.map +1 -1
  116. package/dist/agent-farm/utils/message-format.d.ts +17 -0
  117. package/dist/agent-farm/utils/message-format.d.ts.map +1 -0
  118. package/dist/agent-farm/utils/message-format.js +41 -0
  119. package/dist/agent-farm/utils/message-format.js.map +1 -0
  120. package/dist/agent-farm/utils/notifications.d.ts +4 -4
  121. package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
  122. package/dist/agent-farm/utils/notifications.js +18 -18
  123. package/dist/agent-farm/utils/notifications.js.map +1 -1
  124. package/dist/cli.d.ts.map +1 -1
  125. package/dist/cli.js +26 -1
  126. package/dist/cli.js.map +1 -1
  127. package/dist/commands/adopt.d.ts +2 -2
  128. package/dist/commands/adopt.d.ts.map +1 -1
  129. package/dist/commands/adopt.js +13 -15
  130. package/dist/commands/adopt.js.map +1 -1
  131. package/dist/commands/consult/index.d.ts +26 -2
  132. package/dist/commands/consult/index.d.ts.map +1 -1
  133. package/dist/commands/consult/index.js +296 -83
  134. package/dist/commands/consult/index.js.map +1 -1
  135. package/dist/commands/consult/metrics.d.ts +90 -0
  136. package/dist/commands/consult/metrics.d.ts.map +1 -0
  137. package/dist/commands/consult/metrics.js +203 -0
  138. package/dist/commands/consult/metrics.js.map +1 -0
  139. package/dist/commands/consult/stats.d.ts +18 -0
  140. package/dist/commands/consult/stats.d.ts.map +1 -0
  141. package/dist/commands/consult/stats.js +150 -0
  142. package/dist/commands/consult/stats.js.map +1 -0
  143. package/dist/commands/consult/usage-extractor.d.ts +38 -0
  144. package/dist/commands/consult/usage-extractor.d.ts.map +1 -0
  145. package/dist/commands/consult/usage-extractor.js +99 -0
  146. package/dist/commands/consult/usage-extractor.js.map +1 -0
  147. package/dist/commands/doctor.d.ts.map +1 -1
  148. package/dist/commands/doctor.js +11 -9
  149. package/dist/commands/doctor.js.map +1 -1
  150. package/dist/commands/import.js +4 -4
  151. package/dist/commands/import.js.map +1 -1
  152. package/dist/commands/init.d.ts +2 -2
  153. package/dist/commands/init.d.ts.map +1 -1
  154. package/dist/commands/init.js +13 -15
  155. package/dist/commands/init.js.map +1 -1
  156. package/dist/commands/porch/index.d.ts +6 -6
  157. package/dist/commands/porch/index.d.ts.map +1 -1
  158. package/dist/commands/porch/index.js +37 -37
  159. package/dist/commands/porch/index.js.map +1 -1
  160. package/dist/commands/porch/next.d.ts +1 -1
  161. package/dist/commands/porch/next.d.ts.map +1 -1
  162. package/dist/commands/porch/next.js +86 -92
  163. package/dist/commands/porch/next.js.map +1 -1
  164. package/dist/commands/porch/notify.d.ts +11 -0
  165. package/dist/commands/porch/notify.d.ts.map +1 -0
  166. package/dist/commands/porch/notify.js +30 -0
  167. package/dist/commands/porch/notify.js.map +1 -0
  168. package/dist/commands/porch/plan.d.ts +1 -1
  169. package/dist/commands/porch/plan.d.ts.map +1 -1
  170. package/dist/commands/porch/plan.js +3 -3
  171. package/dist/commands/porch/plan.js.map +1 -1
  172. package/dist/commands/porch/prompts.d.ts +10 -1
  173. package/dist/commands/porch/prompts.d.ts.map +1 -1
  174. package/dist/commands/porch/prompts.js +59 -35
  175. package/dist/commands/porch/prompts.js.map +1 -1
  176. package/dist/commands/porch/protocol.d.ts +1 -1
  177. package/dist/commands/porch/protocol.d.ts.map +1 -1
  178. package/dist/commands/porch/protocol.js +8 -8
  179. package/dist/commands/porch/protocol.js.map +1 -1
  180. package/dist/commands/porch/state.d.ts +6 -6
  181. package/dist/commands/porch/state.d.ts.map +1 -1
  182. package/dist/commands/porch/state.js +14 -12
  183. package/dist/commands/porch/state.js.map +1 -1
  184. package/dist/commands/update.d.ts.map +1 -1
  185. package/dist/commands/update.js +10 -11
  186. package/dist/commands/update.js.map +1 -1
  187. package/dist/lib/github.d.ts +81 -0
  188. package/dist/lib/github.d.ts.map +1 -0
  189. package/dist/lib/github.js +141 -0
  190. package/dist/lib/github.js.map +1 -0
  191. package/dist/lib/scaffold.d.ts +13 -21
  192. package/dist/lib/scaffold.d.ts.map +1 -1
  193. package/dist/lib/scaffold.js +34 -57
  194. package/dist/lib/scaffold.js.map +1 -1
  195. package/dist/lib/skeleton.d.ts +7 -7
  196. package/dist/lib/skeleton.d.ts.map +1 -1
  197. package/dist/lib/skeleton.js +10 -10
  198. package/dist/lib/skeleton.js.map +1 -1
  199. package/dist/terminal/index.d.ts +14 -0
  200. package/dist/terminal/index.d.ts.map +1 -1
  201. package/dist/terminal/index.js +12 -0
  202. package/dist/terminal/index.js.map +1 -1
  203. package/dist/terminal/pty-manager.d.ts +1 -1
  204. package/dist/terminal/pty-manager.d.ts.map +1 -1
  205. package/dist/terminal/pty-manager.js +10 -7
  206. package/dist/terminal/pty-manager.js.map +1 -1
  207. package/dist/terminal/pty-session.js +3 -3
  208. package/dist/terminal/pty-session.js.map +1 -1
  209. package/dist/terminal/session-manager.d.ts +64 -0
  210. package/dist/terminal/session-manager.d.ts.map +1 -1
  211. package/dist/terminal/session-manager.js +299 -10
  212. package/dist/terminal/session-manager.js.map +1 -1
  213. package/dist/terminal/shellper-client.d.ts +2 -1
  214. package/dist/terminal/shellper-client.d.ts.map +1 -1
  215. package/dist/terminal/shellper-client.js +4 -2
  216. package/dist/terminal/shellper-client.js.map +1 -1
  217. package/dist/terminal/shellper-main.js +33 -4
  218. package/dist/terminal/shellper-main.js.map +1 -1
  219. package/dist/terminal/shellper-process.d.ts +24 -7
  220. package/dist/terminal/shellper-process.d.ts.map +1 -1
  221. package/dist/terminal/shellper-process.js +139 -36
  222. package/dist/terminal/shellper-process.js.map +1 -1
  223. package/dist/terminal/shellper-protocol.d.ts +1 -0
  224. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  225. package/dist/terminal/shellper-protocol.js.map +1 -1
  226. package/package.json +4 -1
  227. package/skeleton/.claude/skills/af/SKILL.md +7 -7
  228. package/skeleton/.claude/skills/consult/SKILL.md +1 -1
  229. package/skeleton/builders.md +2 -2
  230. package/skeleton/maintain/.gitkeep +1 -1
  231. package/skeleton/porch/prompts/specify.md +1 -1
  232. package/skeleton/protocols/bugfix/prompts/pr.md +15 -4
  233. package/skeleton/protocols/experiment/protocol.md +17 -17
  234. package/skeleton/protocols/maintain/prompts/audit.md +2 -2
  235. package/skeleton/protocols/maintain/prompts/sync.md +1 -1
  236. package/skeleton/protocols/maintain/prompts/verify.md +1 -1
  237. package/skeleton/protocols/maintain/protocol.md +8 -9
  238. package/skeleton/protocols/maintain/templates/maintenance-run.md +2 -2
  239. package/skeleton/protocols/spir/protocol.json +5 -5
  240. package/skeleton/protocols/spir/protocol.md +8 -8
  241. package/skeleton/protocols/tick/protocol.md +31 -31
  242. package/skeleton/resources/commands/agent-farm.md +14 -14
  243. package/skeleton/resources/commands/codev.md +0 -1
  244. package/skeleton/resources/commands/consult.md +3 -3
  245. package/skeleton/resources/spikes.md +3 -3
  246. package/skeleton/resources/workflow-reference.md +14 -14
  247. package/skeleton/roles/architect.md +25 -25
  248. package/skeleton/roles/builder.md +1 -1
  249. package/skeleton/roles/consultant.md +6 -0
  250. package/skeleton/templates/AGENTS.md +5 -5
  251. package/skeleton/templates/CLAUDE.md +5 -5
  252. package/skeleton/templates/lifecycle.md +9 -9
  253. package/templates/open.html +19 -16
  254. package/templates/tower.html +54 -94
  255. package/templates/vendor/marked.min.js +6 -0
  256. package/templates/vendor/prism-bash.min.js +1 -0
  257. package/templates/vendor/prism-css.min.js +1 -0
  258. package/templates/vendor/prism-javascript.min.js +1 -0
  259. package/templates/vendor/prism-json.min.js +1 -0
  260. package/templates/vendor/prism-markdown.min.js +1 -0
  261. package/templates/vendor/prism-markup.min.js +1 -0
  262. package/templates/vendor/prism-python.min.js +1 -0
  263. package/templates/vendor/prism-tomorrow.min.css +1 -0
  264. package/templates/vendor/prism-typescript.min.js +1 -0
  265. package/templates/vendor/prism-yaml.min.js +1 -0
  266. package/templates/vendor/prism.min.js +1 -0
  267. package/templates/vendor/purify.min.js +3 -0
  268. package/dashboard/dist/assets/index-4n9zpWLY.css +0 -32
  269. package/dashboard/dist/assets/index-b38SaXk5.js +0 -136
  270. package/dashboard/dist/assets/index-b38SaXk5.js.map +0 -1
  271. package/dist/agent-farm/hq-connector.d.ts +0 -19
  272. package/dist/agent-farm/hq-connector.d.ts.map +0 -1
  273. package/dist/agent-farm/hq-connector.js +0 -351
  274. package/dist/agent-farm/hq-connector.js.map +0 -1
  275. package/dist/agent-farm/utils/deps.d.ts +0 -51
  276. package/dist/agent-farm/utils/deps.d.ts.map +0 -1
  277. package/dist/agent-farm/utils/deps.js +0 -162
  278. package/dist/agent-farm/utils/deps.js.map +0 -1
  279. package/dist/agent-farm/utils/gate-status.d.ts +0 -16
  280. package/dist/agent-farm/utils/gate-status.d.ts.map +0 -1
  281. package/dist/agent-farm/utils/gate-status.js +0 -79
  282. package/dist/agent-farm/utils/gate-status.js.map +0 -1
  283. package/dist/agent-farm/utils/gate-watcher.d.ts +0 -38
  284. package/dist/agent-farm/utils/gate-watcher.d.ts.map +0 -1
  285. package/dist/agent-farm/utils/gate-watcher.js +0 -122
  286. package/dist/agent-farm/utils/gate-watcher.js.map +0 -1
  287. package/dist/agent-farm/utils/session.d.ts +0 -32
  288. package/dist/agent-farm/utils/session.d.ts.map +0 -1
  289. package/dist/agent-farm/utils/session.js +0 -57
  290. package/dist/agent-farm/utils/session.js.map +0 -1
  291. package/dist/lib/projectlist-parser.d.ts +0 -70
  292. package/dist/lib/projectlist-parser.d.ts.map +0 -1
  293. package/dist/lib/projectlist-parser.js +0 -200
  294. package/dist/lib/projectlist-parser.js.map +0 -1
  295. package/skeleton/templates/projectlist-archive.md +0 -21
  296. package/skeleton/templates/projectlist.md +0 -147
  297. package/templates/dashboard/css/dialogs.css +0 -149
  298. package/templates/dashboard/css/files.css +0 -558
  299. package/templates/dashboard/css/layout.css +0 -133
  300. package/templates/dashboard/css/projects.css +0 -501
  301. package/templates/dashboard/css/statusbar.css +0 -23
  302. package/templates/dashboard/css/tabs.css +0 -314
  303. package/templates/dashboard/css/utilities.css +0 -50
  304. package/templates/dashboard/css/variables.css +0 -45
  305. package/templates/dashboard/index.html +0 -149
  306. package/templates/dashboard/js/dialogs.js +0 -368
  307. package/templates/dashboard/js/files.js +0 -448
  308. package/templates/dashboard/js/main.js +0 -476
  309. package/templates/dashboard/js/projects.js +0 -544
  310. package/templates/dashboard/js/state.js +0 -91
  311. package/templates/dashboard/js/tabs.js +0 -518
  312. package/templates/dashboard/js/utils.js +0 -191
@@ -3,31 +3,25 @@
3
3
  * Spec 0105: Tower Server Decomposition — Phase 4
4
4
  *
5
5
  * Contains: terminal session CRUD, file tab persistence, shell ID allocation,
6
- * terminal reconciliation, gate watcher, and terminal list assembly.
6
+ * terminal reconciliation, and terminal list assembly.
7
7
  */
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { homedir } from 'node:os';
11
11
  import { getGlobalDb } from '../db/index.js';
12
- import { getGateStatusForProject } from '../utils/gate-status.js';
13
- import { GateWatcher } from '../utils/gate-watcher.js';
14
- import { saveFileTab as saveFileTabToDb, deleteFileTab as deleteFileTabFromDb, loadFileTabsForProject as loadFileTabsFromDb, } from '../utils/file-tabs.js';
12
+ import { saveFileTab as saveFileTabToDb, deleteFileTab as deleteFileTabFromDb, loadFileTabsForWorkspace as loadFileTabsFromDb, } from '../utils/file-tabs.js';
15
13
  import { TerminalManager } from '../../terminal/pty-manager.js';
16
- import { normalizeProjectPath, buildArchitectArgs } from './tower-utils.js';
14
+ import { normalizeWorkspacePath, buildArchitectArgs } from './tower-utils.js';
17
15
  // ============================================================================
18
16
  // Module-private state (lifecycle driven by orchestrator)
19
17
  // ============================================================================
20
18
  let _deps = null;
21
- /** Project terminal registry — tracks which terminals belong to which project */
22
- const projectTerminals = new Map();
19
+ /** Workspace terminal registry — tracks which terminals belong to which workspace */
20
+ const workspaceTerminals = new Map();
23
21
  /** Global TerminalManager instance (lazy singleton) */
24
22
  let terminalManager = null;
25
- /** Gate watcher state */
26
- const gateWatcher = new GateWatcher((...args) => {
27
- if (_deps)
28
- _deps.log(...args);
29
- });
30
- let gateWatcherInterval = null;
23
+ /** True while reconcileTerminalSessions() is running — blocks on-the-fly reconnection (Bugfix #274) */
24
+ let _reconciling = false;
31
25
  // ============================================================================
32
26
  // Lifecycle
33
27
  // ============================================================================
@@ -35,12 +29,12 @@ let gateWatcherInterval = null;
35
29
  export function initTerminals(deps) {
36
30
  _deps = deps;
37
31
  }
32
+ /** Check if reconciliation is currently in progress (Bugfix #274) */
33
+ export function isReconciling() {
34
+ return _reconciling;
35
+ }
38
36
  /** Tear down the terminal module */
39
37
  export function shutdownTerminals() {
40
- if (gateWatcherInterval) {
41
- clearInterval(gateWatcherInterval);
42
- gateWatcherInterval = null;
43
- }
44
38
  if (terminalManager) {
45
39
  terminalManager.shutdown();
46
40
  terminalManager = null;
@@ -50,19 +44,19 @@ export function shutdownTerminals() {
50
44
  // ============================================================================
51
45
  // Accessors for shared state
52
46
  // ============================================================================
53
- /** Get the project terminals registry (returns the Map reference) */
54
- export function getProjectTerminals() {
55
- return projectTerminals;
47
+ /** Get the workspace terminals registry (returns the Map reference) */
48
+ export function getWorkspaceTerminals() {
49
+ return workspaceTerminals;
56
50
  }
57
51
  /**
58
52
  * Get or create the global TerminalManager instance.
59
- * Uses a temporary directory as projectRoot since terminals can be for any project.
53
+ * Uses a temporary directory as workspaceRoot since terminals can be for any workspace.
60
54
  */
61
55
  export function getTerminalManager() {
62
56
  if (!terminalManager) {
63
- const projectRoot = process.env.HOME || '/tmp';
57
+ const workspaceRoot = process.env.HOME || '/tmp';
64
58
  terminalManager = new TerminalManager({
65
- projectRoot,
59
+ workspaceRoot: workspaceRoot,
66
60
  logDir: path.join(homedir(), '.agent-farm', 'logs'),
67
61
  maxSessions: 100,
68
62
  ringBufferLines: 10000,
@@ -77,15 +71,15 @@ export function getTerminalManager() {
77
71
  // Terminal session CRUD
78
72
  // ============================================================================
79
73
  /**
80
- * Get or create project terminal registry entry.
81
- * On first access for a project, hydrates file tabs from SQLite so
74
+ * Get or create workspace terminal registry entry.
75
+ * On first access for a workspace, hydrates file tabs from SQLite so
82
76
  * persisted tabs are available immediately (not just after /api/state).
83
77
  */
84
- export function getProjectTerminalsEntry(projectPath) {
85
- let entry = projectTerminals.get(projectPath);
78
+ export function getWorkspaceTerminalsEntry(workspacePath) {
79
+ let entry = workspaceTerminals.get(workspacePath);
86
80
  if (!entry) {
87
- entry = { builders: new Map(), shells: new Map(), fileTabs: loadFileTabsForProject(projectPath) };
88
- projectTerminals.set(projectPath, entry);
81
+ entry = { builders: new Map(), shells: new Map(), fileTabs: loadFileTabsForWorkspace(workspacePath) };
82
+ workspaceTerminals.set(workspacePath, entry);
89
83
  }
90
84
  // Migration: ensure fileTabs exists for older entries
91
85
  if (!entry.fileTabs) {
@@ -94,10 +88,10 @@ export function getProjectTerminalsEntry(projectPath) {
94
88
  return entry;
95
89
  }
96
90
  /**
97
- * Generate next shell ID for a project
91
+ * Generate next shell ID for a workspace
98
92
  */
99
- export function getNextShellId(projectPath) {
100
- const entry = getProjectTerminalsEntry(projectPath);
93
+ export function getNextShellId(workspacePath) {
94
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
101
95
  let maxId = 0;
102
96
  for (const id of entry.shells.keys()) {
103
97
  const num = parseInt(id.replace('shell-', ''), 10);
@@ -108,20 +102,20 @@ export function getNextShellId(projectPath) {
108
102
  }
109
103
  /**
110
104
  * Save a terminal session to SQLite.
111
- * Guards against race conditions by checking if project is still active.
105
+ * Guards against race conditions by checking if workspace is still active.
112
106
  */
113
- export function saveTerminalSession(terminalId, projectPath, type, roleId, pid, shellperSocket = null, shellperPid = null, shellperStartTime = null) {
107
+ export function saveTerminalSession(terminalId, workspacePath, type, roleId, pid, shellperSocket = null, shellperPid = null, shellperStartTime = null) {
114
108
  try {
115
- const normalizedPath = normalizeProjectPath(projectPath);
116
- // Race condition guard: only save if project is still in the active registry
109
+ const normalizedPath = normalizeWorkspacePath(workspacePath);
110
+ // Race condition guard: only save if workspace is still in the active registry
117
111
  // This prevents zombie rows when stop races with session creation
118
- if (!projectTerminals.has(normalizedPath) && !projectTerminals.has(projectPath)) {
119
- _deps?.log('INFO', `Skipping session save - project no longer active: ${projectPath}`);
112
+ if (!workspaceTerminals.has(normalizedPath) && !workspaceTerminals.has(workspacePath)) {
113
+ _deps?.log('INFO', `Skipping session save - workspace no longer active: ${workspacePath}`);
120
114
  return;
121
115
  }
122
116
  const db = getGlobalDb();
123
117
  db.prepare(`
124
- INSERT OR REPLACE INTO terminal_sessions (id, project_path, type, role_id, pid, shellper_socket, shellper_pid, shellper_start_time)
118
+ INSERT OR REPLACE INTO terminal_sessions (id, workspace_path, type, role_id, pid, shellper_socket, shellper_pid, shellper_start_time)
125
119
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
126
120
  `).run(terminalId, normalizedPath, type, roleId, pid, shellperSocket, shellperPid, shellperStartTime);
127
121
  _deps?.log('INFO', `Saved terminal session to SQLite: ${terminalId} (${type}) for ${path.basename(normalizedPath)}`);
@@ -150,32 +144,59 @@ export function deleteTerminalSession(terminalId) {
150
144
  }
151
145
  }
152
146
  /**
153
- * Delete all terminal sessions for a project from SQLite.
147
+ * Remove a terminal from the in-memory workspace registry.
148
+ * Scans all workspaces to find and remove the terminal by its ID.
149
+ * This is needed when a single terminal is killed (e.g. af cleanup)
150
+ * to keep the in-memory state consistent with SQLite.
151
+ * Bugfix #290: af cleanup didn't remove terminals from in-memory registry.
152
+ */
153
+ export function removeTerminalFromRegistry(terminalId) {
154
+ for (const [, entry] of workspaceTerminals) {
155
+ if (entry.architect === terminalId) {
156
+ entry.architect = undefined;
157
+ return;
158
+ }
159
+ for (const [builderId, tid] of entry.builders) {
160
+ if (tid === terminalId) {
161
+ entry.builders.delete(builderId);
162
+ return;
163
+ }
164
+ }
165
+ for (const [shellId, tid] of entry.shells) {
166
+ if (tid === terminalId) {
167
+ entry.shells.delete(shellId);
168
+ return;
169
+ }
170
+ }
171
+ }
172
+ }
173
+ /**
174
+ * Delete all terminal sessions for a workspace from SQLite.
154
175
  * Normalizes path to ensure consistent cleanup regardless of how path was provided.
155
176
  */
156
- export function deleteProjectTerminalSessions(projectPath) {
177
+ export function deleteWorkspaceTerminalSessions(workspacePath) {
157
178
  try {
158
- const normalizedPath = normalizeProjectPath(projectPath);
179
+ const normalizedPath = normalizeWorkspacePath(workspacePath);
159
180
  const db = getGlobalDb();
160
181
  // Delete both normalized and raw path to handle any inconsistencies
161
- db.prepare('DELETE FROM terminal_sessions WHERE project_path = ?').run(normalizedPath);
162
- if (normalizedPath !== projectPath) {
163
- db.prepare('DELETE FROM terminal_sessions WHERE project_path = ?').run(projectPath);
182
+ db.prepare('DELETE FROM terminal_sessions WHERE workspace_path = ?').run(normalizedPath);
183
+ if (normalizedPath !== workspacePath) {
184
+ db.prepare('DELETE FROM terminal_sessions WHERE workspace_path = ?').run(workspacePath);
164
185
  }
165
186
  }
166
187
  catch (err) {
167
- _deps?.log('WARN', `Failed to delete project terminal sessions: ${err.message}`);
188
+ _deps?.log('WARN', `Failed to delete workspace terminal sessions: ${err.message}`);
168
189
  }
169
190
  }
170
191
  /**
171
- * Get terminal sessions from SQLite for a project.
192
+ * Get terminal sessions from SQLite for a workspace.
172
193
  * Normalizes path for consistent lookup.
173
194
  */
174
- export function getTerminalSessionsForProject(projectPath) {
195
+ export function getTerminalSessionsForWorkspace(workspacePath) {
175
196
  try {
176
- const normalizedPath = normalizeProjectPath(projectPath);
197
+ const normalizedPath = normalizeWorkspacePath(workspacePath);
177
198
  const db = getGlobalDb();
178
- return db.prepare('SELECT * FROM terminal_sessions WHERE project_path = ?').all(normalizedPath);
199
+ return db.prepare('SELECT * FROM terminal_sessions WHERE workspace_path = ?').all(normalizedPath);
179
200
  }
180
201
  catch {
181
202
  return [];
@@ -188,9 +209,9 @@ export function getTerminalSessionsForProject(projectPath) {
188
209
  * Save a file tab to SQLite for persistence across Tower restarts.
189
210
  * Thin wrapper around utils/file-tabs.ts with error handling and path normalization.
190
211
  */
191
- export function saveFileTab(id, projectPath, filePath, createdAt) {
212
+ export function saveFileTab(id, workspacePath, filePath, createdAt) {
192
213
  try {
193
- const normalizedPath = normalizeProjectPath(projectPath);
214
+ const normalizedPath = normalizeWorkspacePath(workspacePath);
194
215
  saveFileTabToDb(getGlobalDb(), id, normalizedPath, filePath, createdAt);
195
216
  }
196
217
  catch (err) {
@@ -210,12 +231,12 @@ export function deleteFileTab(id) {
210
231
  }
211
232
  }
212
233
  /**
213
- * Load file tabs for a project from SQLite.
234
+ * Load file tabs for a workspace from SQLite.
214
235
  * Thin wrapper around utils/file-tabs.ts with error handling and path normalization.
215
236
  */
216
- export function loadFileTabsForProject(projectPath) {
237
+ export function loadFileTabsForWorkspace(workspacePath) {
217
238
  try {
218
- const normalizedPath = normalizeProjectPath(projectPath);
239
+ const normalizedPath = normalizeWorkspacePath(workspacePath);
219
240
  return loadFileTabsFromDb(getGlobalDb(), normalizedPath);
220
241
  }
221
242
  catch (err) {
@@ -260,6 +281,17 @@ export function processExists(pid) {
260
281
  export async function reconcileTerminalSessions() {
261
282
  if (!_deps)
262
283
  return;
284
+ _reconciling = true;
285
+ try {
286
+ await _reconcileTerminalSessionsInner();
287
+ }
288
+ finally {
289
+ _reconciling = false;
290
+ }
291
+ }
292
+ async function _reconcileTerminalSessionsInner() {
293
+ if (!_deps)
294
+ return; // Redundant guard for TypeScript narrowing
263
295
  const manager = getTerminalManager();
264
296
  const db = getGlobalDb();
265
297
  let shellperReconnected = 0;
@@ -281,12 +313,12 @@ export async function reconcileTerminalSessions() {
281
313
  if (shellperSessions.length > 0) {
282
314
  _deps.log('INFO', `Found ${shellperSessions.length} shellper session(s) in SQLite — reconnecting...`);
283
315
  }
316
+ const probeTasks = [];
284
317
  for (const dbSession of shellperSessions) {
285
- const projectPath = dbSession.project_path;
286
- // Skip sessions whose project path doesn't exist or is in temp directory
287
- if (!fs.existsSync(projectPath)) {
288
- _deps.log('INFO', `Skipping shellper session ${dbSession.id} — project path no longer exists: ${projectPath}`);
289
- // Kill orphaned shellper process before removing row
318
+ const workspacePath = dbSession.workspace_path;
319
+ // Skip sessions whose workspace path doesn't exist or is in temp directory
320
+ if (!fs.existsSync(workspacePath)) {
321
+ _deps.log('INFO', `Skipping shellper session ${dbSession.id} — workspace path no longer exists: ${workspacePath}`);
290
322
  if (dbSession.shellper_pid && processExists(dbSession.shellper_pid)) {
291
323
  try {
292
324
  process.kill(dbSession.shellper_pid, 'SIGTERM');
@@ -299,9 +331,8 @@ export async function reconcileTerminalSessions() {
299
331
  continue;
300
332
  }
301
333
  const tmpDirs = ['/tmp', '/private/tmp', '/var/folders', '/private/var/folders'];
302
- if (tmpDirs.some(d => projectPath === d || projectPath.startsWith(d + '/'))) {
303
- _deps.log('INFO', `Skipping shellper session ${dbSession.id} — project is in temp directory: ${projectPath}`);
304
- // Kill orphaned shellper process before removing row
334
+ if (tmpDirs.some(d => workspacePath === d || workspacePath.startsWith(d + '/'))) {
335
+ _deps.log('INFO', `Skipping shellper session ${dbSession.id} — workspace is in temp directory: ${workspacePath}`);
305
336
  if (dbSession.shellper_pid && processExists(dbSession.shellper_pid)) {
306
337
  try {
307
338
  process.kill(dbSession.shellper_pid, 'SIGTERM');
@@ -317,78 +348,98 @@ export async function reconcileTerminalSessions() {
317
348
  _deps.log('WARN', `Shellper manager not initialized — cannot reconnect ${dbSession.id}`);
318
349
  continue;
319
350
  }
320
- try {
321
- // For architect sessions, restore auto-restart behavior after reconnection
322
- let restartOptions;
323
- if (dbSession.type === 'architect') {
324
- let architectCmd = 'claude';
325
- const configPath = path.join(projectPath, 'af-config.json');
326
- if (fs.existsSync(configPath)) {
327
- try {
328
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
329
- if (config.shell?.architect) {
330
- architectCmd = config.shell.architect;
331
- }
351
+ // Build restart options for architect sessions (synchronous, no I/O)
352
+ let restartOptions;
353
+ if (dbSession.type === 'architect') {
354
+ let architectCmd = 'claude';
355
+ const configPath = path.join(workspacePath, 'af-config.json');
356
+ if (fs.existsSync(configPath)) {
357
+ try {
358
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
359
+ if (config.shell?.architect) {
360
+ architectCmd = config.shell.architect;
332
361
  }
333
- catch { /* use default */ }
334
362
  }
335
- const cmdParts = architectCmd.split(/\s+/);
336
- const cleanEnv = { ...process.env };
337
- delete cleanEnv['CLAUDECODE'];
338
- restartOptions = {
339
- command: cmdParts[0],
340
- args: buildArchitectArgs(cmdParts.slice(1), projectPath),
341
- cwd: projectPath,
342
- env: cleanEnv,
343
- restartDelay: 2000,
344
- maxRestarts: 50,
345
- };
363
+ catch { /* use default */ }
346
364
  }
347
- const client = await _deps.shellperManager.reconnectSession(dbSession.id, dbSession.shellper_socket, dbSession.shellper_pid, dbSession.shellper_start_time, restartOptions);
348
- if (!client) {
349
- _deps.log('INFO', `Shellper session ${dbSession.id} is stale (PID/socket dead) — will clean up`);
350
- continue; // Will be cleaned up in Phase 2
351
- }
352
- const replayData = client.getReplayData() ?? Buffer.alloc(0);
353
- const label = dbSession.type === 'architect' ? 'Architect' : `${dbSession.type} ${dbSession.role_id || 'unknown'}`;
354
- // Create a PtySession backed by the reconnected shellper client
355
- const session = manager.createSessionRaw({ label, cwd: projectPath });
356
- const ptySession = manager.getSession(session.id);
357
- if (ptySession) {
358
- ptySession.attachShellper(client, replayData, dbSession.shellper_pid, dbSession.id);
359
- }
360
- // Register in projectTerminals Map
361
- const entry = getProjectTerminalsEntry(projectPath);
362
- if (dbSession.type === 'architect') {
363
- entry.architect = session.id;
364
- }
365
- else if (dbSession.type === 'builder') {
366
- entry.builders.set(dbSession.role_id || dbSession.id, session.id);
367
- }
368
- else if (dbSession.type === 'shell') {
369
- entry.shells.set(dbSession.role_id || dbSession.id, session.id);
365
+ const cmdParts = architectCmd.split(/\s+/);
366
+ const cleanEnv = { ...process.env };
367
+ delete cleanEnv['CLAUDECODE'];
368
+ restartOptions = {
369
+ command: cmdParts[0],
370
+ args: buildArchitectArgs(cmdParts.slice(1), workspacePath),
371
+ cwd: workspacePath,
372
+ env: cleanEnv,
373
+ restartDelay: 2000,
374
+ maxRestarts: 50,
375
+ };
376
+ }
377
+ probeTasks.push({ dbSession, restartOptions });
378
+ }
379
+ // Probe shellper sockets in parallel with bounded concurrency (Spec 0122 Phase 2)
380
+ const CONCURRENCY_LIMIT = 5;
381
+ const probeResults = [];
382
+ for (let i = 0; i < probeTasks.length; i += CONCURRENCY_LIMIT) {
383
+ const batch = probeTasks.slice(i, i + CONCURRENCY_LIMIT);
384
+ const results = await Promise.allSettled(batch.map(async (task) => {
385
+ const client = await _deps.shellperManager.reconnectSession(task.dbSession.id, task.dbSession.shellper_socket, task.dbSession.shellper_pid, task.dbSession.shellper_start_time, task.restartOptions);
386
+ return { dbSession: task.dbSession, client, restartOptions: task.restartOptions };
387
+ }));
388
+ for (const result of results) {
389
+ if (result.status === 'fulfilled') {
390
+ probeResults.push(result.value);
370
391
  }
371
- // Update SQLite with new terminal ID
372
- db.prepare('DELETE FROM terminal_sessions WHERE id = ?').run(dbSession.id);
373
- saveTerminalSession(session.id, projectPath, dbSession.type, dbSession.role_id, dbSession.shellper_pid, dbSession.shellper_socket, dbSession.shellper_pid, dbSession.shellper_start_time);
374
- _deps.registerKnownProject(projectPath);
375
- // Clean up on exit
376
- if (ptySession) {
377
- ptySession.on('exit', () => {
378
- const currentEntry = getProjectTerminalsEntry(projectPath);
379
- if (dbSession.type === 'architect' && currentEntry.architect === session.id) {
380
- currentEntry.architect = undefined;
381
- }
382
- deleteTerminalSession(session.id);
383
- });
392
+ else {
393
+ // Find the corresponding task for error logging
394
+ const idx = results.indexOf(result);
395
+ const task = batch[idx];
396
+ _deps.log('WARN', `Failed to reconnect shellper session ${task.dbSession.id}: ${result.reason?.message ?? result.reason}`);
384
397
  }
385
- matchedSessionIds.add(dbSession.id);
386
- shellperReconnected++;
387
- _deps.log('INFO', `Reconnected shellper session → ${session.id} (${dbSession.type} for ${path.basename(projectPath)})`);
388
398
  }
389
- catch (err) {
390
- _deps.log('WARN', `Failed to reconnect shellper session ${dbSession.id}: ${err.message}`);
399
+ }
400
+ // Process probe results sequentially (shared state mutations)
401
+ for (const { dbSession, client } of probeResults) {
402
+ if (!client) {
403
+ _deps.log('INFO', `Shellper session ${dbSession.id} is stale (PID/socket dead) — will clean up`);
404
+ continue; // Will be cleaned up in Phase 2
405
+ }
406
+ const workspacePath = dbSession.workspace_path;
407
+ const replayData = client.getReplayData() ?? Buffer.alloc(0);
408
+ const label = dbSession.type === 'architect' ? 'Architect' : (dbSession.role_id || 'unknown');
409
+ // Create a PtySession backed by the reconnected shellper client
410
+ const session = manager.createSessionRaw({ label, cwd: workspacePath });
411
+ const ptySession = manager.getSession(session.id);
412
+ if (ptySession) {
413
+ ptySession.attachShellper(client, replayData, dbSession.shellper_pid, dbSession.id);
414
+ }
415
+ // Register in workspaceTerminals Map
416
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
417
+ if (dbSession.type === 'architect') {
418
+ entry.architect = session.id;
419
+ }
420
+ else if (dbSession.type === 'builder') {
421
+ entry.builders.set(dbSession.role_id || dbSession.id, session.id);
422
+ }
423
+ else if (dbSession.type === 'shell') {
424
+ entry.shells.set(dbSession.role_id || dbSession.id, session.id);
425
+ }
426
+ // Update SQLite with new terminal ID
427
+ db.prepare('DELETE FROM terminal_sessions WHERE id = ?').run(dbSession.id);
428
+ saveTerminalSession(session.id, workspacePath, dbSession.type, dbSession.role_id, dbSession.shellper_pid, dbSession.shellper_socket, dbSession.shellper_pid, dbSession.shellper_start_time);
429
+ _deps.registerKnownWorkspace(workspacePath);
430
+ // Clean up on exit
431
+ if (ptySession) {
432
+ ptySession.on('exit', () => {
433
+ const currentEntry = getWorkspaceTerminalsEntry(workspacePath);
434
+ if (dbSession.type === 'architect' && currentEntry.architect === session.id) {
435
+ currentEntry.architect = undefined;
436
+ }
437
+ deleteTerminalSession(session.id);
438
+ });
391
439
  }
440
+ matchedSessionIds.add(dbSession.id);
441
+ shellperReconnected++;
442
+ _deps.log('INFO', `Reconnected shellper session → ${session.id} (${dbSession.type} for ${path.basename(workspacePath)})`);
392
443
  }
393
444
  // ---- Phase 2: Sweep stale SQLite rows ----
394
445
  for (const session of allDbSessions) {
@@ -399,7 +450,7 @@ export async function reconcileTerminalSessions() {
399
450
  continue;
400
451
  // Stale row — kill orphaned process if any, then delete
401
452
  if (session.pid && processExists(session.pid)) {
402
- _deps.log('INFO', `Killing orphaned process: PID ${session.pid} (${session.type} for ${path.basename(session.project_path)})`);
453
+ _deps.log('INFO', `Killing orphaned process: PID ${session.pid} (${session.type} for ${path.basename(session.workspace_path)})`);
403
454
  try {
404
455
  process.kill(session.pid, 'SIGTERM');
405
456
  killed++;
@@ -418,74 +469,47 @@ export async function reconcileTerminalSessions() {
418
469
  }
419
470
  }
420
471
  // ============================================================================
421
- // Gate watcher
422
- // ============================================================================
423
- /** Start periodic gate status polling */
424
- export function startGateWatcher() {
425
- if (!_deps)
426
- return;
427
- gateWatcherInterval = setInterval(async () => {
428
- if (!_deps)
429
- return;
430
- const projectPaths = _deps.getKnownProjectPaths();
431
- for (const projectPath of projectPaths) {
432
- try {
433
- const gateStatus = getGateStatusForProject(projectPath);
434
- await gateWatcher.checkAndNotify(gateStatus, projectPath);
435
- }
436
- catch (err) {
437
- _deps.log('WARN', `Gate watcher error for ${projectPath}: ${err instanceof Error ? err.message : String(err)}`);
438
- }
439
- }
440
- }, 10_000);
441
- }
442
- /** Stop the gate watcher interval */
443
- export function stopGateWatcher() {
444
- if (gateWatcherInterval) {
445
- clearInterval(gateWatcherInterval);
446
- gateWatcherInterval = null;
447
- }
448
- }
449
- // ============================================================================
450
472
  // Terminal list assembly
451
473
  // ============================================================================
452
474
  /**
453
- * Get terminal list for a project from tower's registry.
475
+ * Get terminal list for a workspace from tower's registry.
454
476
  * Phase 4 (Spec 0090): Tower manages terminals directly, no dashboard-server fetch.
455
477
  * Returns architect, builders, and shells with their URLs.
456
478
  */
457
- export async function getTerminalsForProject(projectPath, proxyUrl) {
479
+ export async function getTerminalsForWorkspace(workspacePath, proxyUrl) {
458
480
  const manager = getTerminalManager();
459
481
  const terminals = [];
460
482
  // Query SQLite first, then augment with shellper reconnection
461
- const dbSessions = getTerminalSessionsForProject(projectPath);
483
+ const dbSessions = getTerminalSessionsForWorkspace(workspacePath);
462
484
  // Use normalized path for cache consistency
463
- const normalizedPath = normalizeProjectPath(projectPath);
485
+ const normalizedPath = normalizeWorkspacePath(workspacePath);
464
486
  // Build a fresh entry from SQLite, then replace atomically to avoid
465
487
  // destroying in-memory state that was registered via POST /api/terminals.
466
488
  // Previous approach cleared the cache then rebuilt, which lost terminals
467
489
  // if their SQLite rows were deleted by external interference (e.g., tests).
468
490
  const freshEntry = { builders: new Map(), shells: new Map(), fileTabs: new Map() };
469
491
  // Load file tabs from SQLite (persisted across restarts)
470
- const existingEntry = projectTerminals.get(normalizedPath);
492
+ const existingEntry = workspaceTerminals.get(normalizedPath);
471
493
  if (existingEntry && existingEntry.fileTabs.size > 0) {
472
494
  // Use in-memory state if already populated (avoids redundant DB reads)
473
495
  freshEntry.fileTabs = existingEntry.fileTabs;
474
496
  }
475
497
  else {
476
- freshEntry.fileTabs = loadFileTabsForProject(projectPath);
498
+ freshEntry.fileTabs = loadFileTabsForWorkspace(workspacePath);
477
499
  }
478
500
  for (const dbSession of dbSessions) {
479
501
  // Verify session still exists in TerminalManager (runtime state)
480
502
  let session = manager.getSession(dbSession.id);
481
- if (!session && dbSession.shellper_socket && _deps?.shellperManager) {
503
+ if (!session && dbSession.shellper_socket && _deps?.shellperManager && !_reconciling) {
482
504
  // PTY session gone but shellper may still be alive — reconnect on-the-fly
505
+ // Skip during reconciliation to avoid racing with reconcileTerminalSessions()
506
+ // which also reconnects to shellpers (Bugfix #274).
483
507
  try {
484
508
  // Restore auto-restart for architect sessions (same as startup reconciliation)
485
509
  let restartOptions;
486
510
  if (dbSession.type === 'architect') {
487
511
  let architectCmd = 'claude';
488
- const configPath = path.join(dbSession.project_path, 'af-config.json');
512
+ const configPath = path.join(dbSession.workspace_path, 'af-config.json');
489
513
  if (fs.existsSync(configPath)) {
490
514
  try {
491
515
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
@@ -500,39 +524,41 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
500
524
  delete cleanEnv['CLAUDECODE'];
501
525
  restartOptions = {
502
526
  command: cmdParts[0],
503
- args: buildArchitectArgs(cmdParts.slice(1), dbSession.project_path),
504
- cwd: dbSession.project_path,
527
+ args: buildArchitectArgs(cmdParts.slice(1), dbSession.workspace_path),
528
+ cwd: dbSession.workspace_path,
505
529
  env: cleanEnv,
506
530
  restartDelay: 2000,
507
531
  maxRestarts: 50,
508
532
  };
509
533
  }
534
+ _deps.log('INFO', `On-the-fly shellper reconnect for ${dbSession.id}`);
510
535
  const client = await _deps.shellperManager.reconnectSession(dbSession.id, dbSession.shellper_socket, dbSession.shellper_pid, dbSession.shellper_start_time, restartOptions);
511
536
  if (client) {
512
537
  const replayData = client.getReplayData() ?? Buffer.alloc(0);
513
- const label = dbSession.type === 'architect' ? 'Architect' : `${dbSession.type} ${dbSession.role_id || dbSession.id}`;
514
- const newSession = manager.createSessionRaw({ label, cwd: dbSession.project_path });
538
+ const label = dbSession.type === 'architect' ? 'Architect' : (dbSession.role_id || dbSession.id);
539
+ const newSession = manager.createSessionRaw({ label, cwd: dbSession.workspace_path });
515
540
  const ptySession = manager.getSession(newSession.id);
516
541
  if (ptySession) {
517
542
  ptySession.attachShellper(client, replayData, dbSession.shellper_pid, dbSession.id);
518
543
  // Clean up on exit (same as startup reconciliation path)
519
544
  ptySession.on('exit', () => {
520
- const currentEntry = getProjectTerminalsEntry(dbSession.project_path);
545
+ const currentEntry = getWorkspaceTerminalsEntry(dbSession.workspace_path);
521
546
  if (dbSession.type === 'architect' && currentEntry.architect === newSession.id) {
522
547
  currentEntry.architect = undefined;
523
548
  }
524
549
  deleteTerminalSession(newSession.id);
525
550
  });
526
551
  }
552
+ const originalSessionId = dbSession.id;
527
553
  deleteTerminalSession(dbSession.id);
528
- saveTerminalSession(newSession.id, dbSession.project_path, dbSession.type, dbSession.role_id, dbSession.shellper_pid, dbSession.shellper_socket, dbSession.shellper_pid, dbSession.shellper_start_time);
554
+ saveTerminalSession(newSession.id, dbSession.workspace_path, dbSession.type, dbSession.role_id, dbSession.shellper_pid, dbSession.shellper_socket, dbSession.shellper_pid, dbSession.shellper_start_time);
529
555
  dbSession.id = newSession.id;
530
556
  session = manager.getSession(newSession.id);
531
- _deps.log('INFO', `Reconnected to shellper on-the-fly → ${newSession.id}`);
557
+ _deps.log('INFO', `On-the-fly reconnect succeeded for ${originalSessionId} → ${newSession.id}`);
532
558
  }
533
559
  }
534
560
  catch (err) {
535
- _deps.log('WARN', `Failed shellper on-the-fly reconnect for ${dbSession.id}: ${err.message}`);
561
+ _deps.log('WARN', `On-the-fly reconnect failed for ${dbSession.id}: ${err.message}`);
536
562
  }
537
563
  }
538
564
  if (!session) {
@@ -556,8 +582,8 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
556
582
  terminals.push({
557
583
  type: 'builder',
558
584
  id: builderId,
559
- label: `Builder ${builderId}`,
560
- url: `${proxyUrl}?tab=builder-${builderId}`,
585
+ label: builderId,
586
+ url: `${proxyUrl}?tab=${builderId}`,
561
587
  active: true,
562
588
  });
563
589
  }
@@ -568,7 +594,7 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
568
594
  type: 'shell',
569
595
  id: shellId,
570
596
  label: `Shell ${shellId.replace('shell-', '')}`,
571
- url: `${proxyUrl}?tab=shell-${shellId}`,
597
+ url: `${proxyUrl}?tab=${shellId}`,
572
598
  active: true,
573
599
  });
574
600
  }
@@ -597,8 +623,8 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
597
623
  terminals.push({
598
624
  type: 'builder',
599
625
  id: builderId,
600
- label: `Builder ${builderId}`,
601
- url: `${proxyUrl}?tab=builder-${builderId}`,
626
+ label: builderId,
627
+ url: `${proxyUrl}?tab=${builderId}`,
602
628
  active: true,
603
629
  });
604
630
  }
@@ -613,7 +639,7 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
613
639
  type: 'shell',
614
640
  id: shellId,
615
641
  label: `Shell ${shellId.replace('shell-', '')}`,
616
- url: `${proxyUrl}?tab=shell-${shellId}`,
642
+ url: `${proxyUrl}?tab=${shellId}`,
617
643
  active: true,
618
644
  });
619
645
  }
@@ -621,9 +647,7 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
621
647
  }
622
648
  }
623
649
  // Atomically replace the cache entry
624
- projectTerminals.set(normalizedPath, freshEntry);
625
- // Read gate status from porch YAML files
626
- const gateStatus = getGateStatusForProject(projectPath);
627
- return { terminals, gateStatus };
650
+ workspaceTerminals.set(normalizedPath, freshEntry);
651
+ return { terminals };
628
652
  }
629
653
  //# sourceMappingURL=tower-terminals.js.map