@cluesmith/codev 2.0.3 → 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 (184) 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 +31 -13
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/attach.d.ts +19 -0
  9. package/dist/agent-farm/commands/attach.d.ts.map +1 -1
  10. package/dist/agent-farm/commands/attach.js +172 -12
  11. package/dist/agent-farm/commands/attach.js.map +1 -1
  12. package/dist/agent-farm/commands/send.d.ts +22 -2
  13. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  14. package/dist/agent-farm/commands/send.js +97 -178
  15. package/dist/agent-farm/commands/send.js.map +1 -1
  16. package/dist/agent-farm/commands/spawn-roles.d.ts +3 -9
  17. package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -1
  18. package/dist/agent-farm/commands/spawn-roles.js +14 -53
  19. package/dist/agent-farm/commands/spawn-roles.js.map +1 -1
  20. package/dist/agent-farm/commands/spawn-worktree.d.ts +10 -16
  21. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  22. package/dist/agent-farm/commands/spawn-worktree.js +24 -5
  23. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  24. package/dist/agent-farm/commands/spawn.d.ts +8 -6
  25. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  26. package/dist/agent-farm/commands/spawn.js +180 -66
  27. package/dist/agent-farm/commands/spawn.js.map +1 -1
  28. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  29. package/dist/agent-farm/commands/status.js +1 -12
  30. package/dist/agent-farm/commands/status.js.map +1 -1
  31. package/dist/agent-farm/lib/tower-client.d.ts +16 -6
  32. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  33. package/dist/agent-farm/lib/tower-client.js +25 -0
  34. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  35. package/dist/agent-farm/servers/overview.d.ts +111 -0
  36. package/dist/agent-farm/servers/overview.d.ts.map +1 -0
  37. package/dist/agent-farm/servers/overview.js +385 -0
  38. package/dist/agent-farm/servers/overview.js.map +1 -0
  39. package/dist/agent-farm/servers/tower-instances.d.ts +1 -3
  40. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  41. package/dist/agent-farm/servers/tower-instances.js +10 -13
  42. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  43. package/dist/agent-farm/servers/tower-messages.d.ts +87 -0
  44. package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -0
  45. package/dist/agent-farm/servers/tower-messages.js +202 -0
  46. package/dist/agent-farm/servers/tower-messages.js.map +1 -0
  47. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  48. package/dist/agent-farm/servers/tower-routes.js +164 -17
  49. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  50. package/dist/agent-farm/servers/tower-server.js +27 -2
  51. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  52. package/dist/agent-farm/servers/tower-terminals.d.ts +8 -2
  53. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  54. package/dist/agent-farm/servers/tower-terminals.js +125 -80
  55. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  56. package/dist/agent-farm/servers/tower-types.d.ts +0 -2
  57. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
  58. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  59. package/dist/agent-farm/servers/tower-websocket.js +25 -4
  60. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  61. package/dist/agent-farm/types.d.ts +3 -4
  62. package/dist/agent-farm/types.d.ts.map +1 -1
  63. package/dist/agent-farm/utils/agent-names.d.ts +85 -0
  64. package/dist/agent-farm/utils/agent-names.d.ts.map +1 -0
  65. package/dist/agent-farm/utils/agent-names.js +140 -0
  66. package/dist/agent-farm/utils/agent-names.js.map +1 -0
  67. package/dist/agent-farm/utils/message-format.d.ts +17 -0
  68. package/dist/agent-farm/utils/message-format.d.ts.map +1 -0
  69. package/dist/agent-farm/utils/message-format.js +41 -0
  70. package/dist/agent-farm/utils/message-format.js.map +1 -0
  71. package/dist/cli.d.ts.map +1 -1
  72. package/dist/cli.js +26 -1
  73. package/dist/cli.js.map +1 -1
  74. package/dist/commands/adopt.d.ts.map +1 -1
  75. package/dist/commands/adopt.js +1 -13
  76. package/dist/commands/adopt.js.map +1 -1
  77. package/dist/commands/consult/index.d.ts +25 -1
  78. package/dist/commands/consult/index.d.ts.map +1 -1
  79. package/dist/commands/consult/index.js +251 -39
  80. package/dist/commands/consult/index.js.map +1 -1
  81. package/dist/commands/consult/metrics.d.ts +90 -0
  82. package/dist/commands/consult/metrics.d.ts.map +1 -0
  83. package/dist/commands/consult/metrics.js +203 -0
  84. package/dist/commands/consult/metrics.js.map +1 -0
  85. package/dist/commands/consult/stats.d.ts +18 -0
  86. package/dist/commands/consult/stats.d.ts.map +1 -0
  87. package/dist/commands/consult/stats.js +150 -0
  88. package/dist/commands/consult/stats.js.map +1 -0
  89. package/dist/commands/consult/usage-extractor.d.ts +38 -0
  90. package/dist/commands/consult/usage-extractor.d.ts.map +1 -0
  91. package/dist/commands/consult/usage-extractor.js +99 -0
  92. package/dist/commands/consult/usage-extractor.js.map +1 -0
  93. package/dist/commands/doctor.d.ts.map +1 -1
  94. package/dist/commands/doctor.js +5 -3
  95. package/dist/commands/doctor.js.map +1 -1
  96. package/dist/commands/init.d.ts.map +1 -1
  97. package/dist/commands/init.js +1 -13
  98. package/dist/commands/init.js.map +1 -1
  99. package/dist/commands/porch/next.d.ts.map +1 -1
  100. package/dist/commands/porch/next.js +53 -62
  101. package/dist/commands/porch/next.js.map +1 -1
  102. package/dist/commands/porch/prompts.d.ts +10 -1
  103. package/dist/commands/porch/prompts.d.ts.map +1 -1
  104. package/dist/commands/porch/prompts.js +50 -26
  105. package/dist/commands/porch/prompts.js.map +1 -1
  106. package/dist/commands/porch/protocol.js +2 -2
  107. package/dist/commands/porch/state.d.ts.map +1 -1
  108. package/dist/commands/porch/state.js +3 -1
  109. package/dist/commands/porch/state.js.map +1 -1
  110. package/dist/commands/update.d.ts.map +1 -1
  111. package/dist/commands/update.js +0 -10
  112. package/dist/commands/update.js.map +1 -1
  113. package/dist/lib/github.d.ts +81 -0
  114. package/dist/lib/github.d.ts.map +1 -0
  115. package/dist/lib/github.js +141 -0
  116. package/dist/lib/github.js.map +1 -0
  117. package/dist/lib/scaffold.d.ts +0 -21
  118. package/dist/lib/scaffold.d.ts.map +1 -1
  119. package/dist/lib/scaffold.js +0 -57
  120. package/dist/lib/scaffold.js.map +1 -1
  121. package/dist/terminal/index.d.ts +14 -0
  122. package/dist/terminal/index.d.ts.map +1 -1
  123. package/dist/terminal/index.js +12 -0
  124. package/dist/terminal/index.js.map +1 -1
  125. package/dist/terminal/pty-manager.d.ts.map +1 -1
  126. package/dist/terminal/pty-manager.js +7 -4
  127. package/dist/terminal/pty-manager.js.map +1 -1
  128. package/dist/terminal/pty-session.js +3 -3
  129. package/dist/terminal/pty-session.js.map +1 -1
  130. package/dist/terminal/session-manager.d.ts +64 -0
  131. package/dist/terminal/session-manager.d.ts.map +1 -1
  132. package/dist/terminal/session-manager.js +299 -10
  133. package/dist/terminal/session-manager.js.map +1 -1
  134. package/dist/terminal/shellper-client.d.ts +2 -1
  135. package/dist/terminal/shellper-client.d.ts.map +1 -1
  136. package/dist/terminal/shellper-client.js +4 -2
  137. package/dist/terminal/shellper-client.js.map +1 -1
  138. package/dist/terminal/shellper-main.js +33 -4
  139. package/dist/terminal/shellper-main.js.map +1 -1
  140. package/dist/terminal/shellper-process.d.ts +24 -7
  141. package/dist/terminal/shellper-process.d.ts.map +1 -1
  142. package/dist/terminal/shellper-process.js +139 -36
  143. package/dist/terminal/shellper-process.js.map +1 -1
  144. package/dist/terminal/shellper-protocol.d.ts +1 -0
  145. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  146. package/dist/terminal/shellper-protocol.js.map +1 -1
  147. package/package.json +4 -1
  148. package/skeleton/.claude/skills/af/SKILL.md +7 -7
  149. package/skeleton/.claude/skills/consult/SKILL.md +1 -1
  150. package/skeleton/builders.md +2 -2
  151. package/skeleton/maintain/.gitkeep +1 -1
  152. package/skeleton/porch/prompts/specify.md +1 -1
  153. package/skeleton/protocols/bugfix/prompts/pr.md +15 -4
  154. package/skeleton/protocols/experiment/protocol.md +17 -17
  155. package/skeleton/protocols/maintain/prompts/audit.md +2 -2
  156. package/skeleton/protocols/maintain/prompts/sync.md +1 -1
  157. package/skeleton/protocols/maintain/prompts/verify.md +1 -1
  158. package/skeleton/protocols/maintain/protocol.md +8 -9
  159. package/skeleton/protocols/maintain/templates/maintenance-run.md +2 -2
  160. package/skeleton/protocols/spir/protocol.json +5 -5
  161. package/skeleton/protocols/spir/protocol.md +8 -8
  162. package/skeleton/protocols/tick/protocol.md +31 -31
  163. package/skeleton/resources/commands/agent-farm.md +14 -14
  164. package/skeleton/resources/commands/codev.md +0 -1
  165. package/skeleton/resources/commands/consult.md +3 -3
  166. package/skeleton/resources/spikes.md +3 -3
  167. package/skeleton/resources/workflow-reference.md +14 -14
  168. package/skeleton/roles/architect.md +25 -25
  169. package/skeleton/roles/builder.md +1 -1
  170. package/skeleton/roles/consultant.md +6 -0
  171. package/skeleton/templates/AGENTS.md +5 -5
  172. package/skeleton/templates/CLAUDE.md +5 -5
  173. package/skeleton/templates/lifecycle.md +9 -9
  174. package/templates/open.html +6 -3
  175. package/templates/tower.html +1 -41
  176. package/dashboard/dist/assets/index-4n9zpWLY.css +0 -32
  177. package/dashboard/dist/assets/index-UsH9ixz1.js +0 -136
  178. package/dashboard/dist/assets/index-UsH9ixz1.js.map +0 -1
  179. package/dist/agent-farm/utils/gate-status.d.ts +0 -16
  180. package/dist/agent-farm/utils/gate-status.d.ts.map +0 -1
  181. package/dist/agent-farm/utils/gate-status.js +0 -79
  182. package/dist/agent-farm/utils/gate-status.js.map +0 -1
  183. package/skeleton/templates/projectlist-archive.md +0 -21
  184. package/skeleton/templates/projectlist.md +0 -147
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Message routing, address resolution, and WebSocket message bus for Tower server.
3
+ * Spec 0110: Messaging Infrastructure — Phases 2 & 3
4
+ *
5
+ * Resolves `[project:]agent` addresses to terminal IDs by querying
6
+ * the workspace terminal registry maintained by tower-terminals.ts.
7
+ * Manages WebSocket subscribers and broadcasts structured message frames.
8
+ */
9
+ import type { WebSocket } from 'ws';
10
+ /**
11
+ * Structured message frame broadcast to WebSocket subscribers.
12
+ */
13
+ export interface MessageFrame {
14
+ type: 'message';
15
+ timestamp: string;
16
+ from: {
17
+ project: string;
18
+ agent: string;
19
+ };
20
+ to: {
21
+ project: string;
22
+ agent: string;
23
+ };
24
+ content: string;
25
+ metadata: {
26
+ raw?: boolean;
27
+ source?: string;
28
+ };
29
+ }
30
+ /**
31
+ * Add a WebSocket subscriber to the message bus.
32
+ * @param ws - The WebSocket connection
33
+ * @param projectFilter - Optional project name to filter messages by
34
+ */
35
+ export declare function addSubscriber(ws: WebSocket, projectFilter?: string): void;
36
+ /**
37
+ * Remove a WebSocket subscriber from the message bus.
38
+ * @param ws - The WebSocket connection to remove
39
+ */
40
+ export declare function removeSubscriber(ws: WebSocket): void;
41
+ /**
42
+ * Get the count of active subscribers (for testing/monitoring).
43
+ */
44
+ export declare function getSubscriberCount(): number;
45
+ /**
46
+ * Result of resolving a target address to a terminal.
47
+ */
48
+ export interface ResolveResult {
49
+ terminalId: string;
50
+ workspacePath: string;
51
+ agent: string;
52
+ }
53
+ /**
54
+ * Error from address resolution — distinguishes "not found" from "ambiguous".
55
+ */
56
+ export interface ResolveError {
57
+ code: 'NOT_FOUND' | 'AMBIGUOUS' | 'NO_CONTEXT';
58
+ message: string;
59
+ }
60
+ /**
61
+ * Resolve a `[project:]agent` address to a terminal ID.
62
+ *
63
+ * Resolution logic:
64
+ * 1. Parse target using parseAddress() (case-insensitive)
65
+ * 2. If project specified: find workspace by basename match
66
+ * - Multiple basename matches → AMBIGUOUS error
67
+ * 3. If no project: use fallbackWorkspace
68
+ * - Missing fallbackWorkspace → NO_CONTEXT error
69
+ * 4. Within workspace: match agent against architect, then builders map
70
+ * - Exact match (case-insensitive) first, then tail match
71
+ * - Multiple tail matches → AMBIGUOUS error
72
+ *
73
+ * @param target - Address string: "agent" or "project:agent"
74
+ * @param fallbackWorkspace - Workspace path when no project: prefix is given
75
+ * @returns ResolveResult on success, ResolveError on failure
76
+ */
77
+ export declare function resolveTarget(target: string, fallbackWorkspace?: string): ResolveResult | ResolveError;
78
+ /**
79
+ * Broadcast a structured message frame to all WebSocket subscribers.
80
+ * Filters by project if the subscriber has a projectFilter set.
81
+ */
82
+ export declare function broadcastMessage(message: MessageFrame): void;
83
+ /**
84
+ * Helper to check if a resolve result is an error.
85
+ */
86
+ export declare function isResolveError(result: ResolveResult | ResolveError): result is ResolveError;
87
+ //# sourceMappingURL=tower-messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tower-messages.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-messages.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAQpC;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,EAAE,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE;QAAE,GAAG,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AAcD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,SAAS,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAEzE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAOpD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;IAC/C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,iBAAiB,CAAC,EAAE,MAAM,GACzB,aAAa,GAAG,YAAY,CA6B9B;AA8GD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAkB5D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,YAAY,GAAG,MAAM,IAAI,YAAY,CAE3F"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Message routing, address resolution, and WebSocket message bus for Tower server.
3
+ * Spec 0110: Messaging Infrastructure — Phases 2 & 3
4
+ *
5
+ * Resolves `[project:]agent` addresses to terminal IDs by querying
6
+ * the workspace terminal registry maintained by tower-terminals.ts.
7
+ * Manages WebSocket subscribers and broadcasts structured message frames.
8
+ */
9
+ import path from 'node:path';
10
+ import { parseAddress, stripLeadingZeros } from '../utils/agent-names.js';
11
+ import { getWorkspaceTerminals } from './tower-terminals.js';
12
+ /** Active WebSocket subscribers for the message bus. */
13
+ const messageSubscribers = new Set();
14
+ /**
15
+ * Add a WebSocket subscriber to the message bus.
16
+ * @param ws - The WebSocket connection
17
+ * @param projectFilter - Optional project name to filter messages by
18
+ */
19
+ export function addSubscriber(ws, projectFilter) {
20
+ messageSubscribers.add({ ws, projectFilter });
21
+ }
22
+ /**
23
+ * Remove a WebSocket subscriber from the message bus.
24
+ * @param ws - The WebSocket connection to remove
25
+ */
26
+ export function removeSubscriber(ws) {
27
+ for (const sub of messageSubscribers) {
28
+ if (sub.ws === ws) {
29
+ messageSubscribers.delete(sub);
30
+ return;
31
+ }
32
+ }
33
+ }
34
+ /**
35
+ * Get the count of active subscribers (for testing/monitoring).
36
+ */
37
+ export function getSubscriberCount() {
38
+ return messageSubscribers.size;
39
+ }
40
+ /**
41
+ * Resolve a `[project:]agent` address to a terminal ID.
42
+ *
43
+ * Resolution logic:
44
+ * 1. Parse target using parseAddress() (case-insensitive)
45
+ * 2. If project specified: find workspace by basename match
46
+ * - Multiple basename matches → AMBIGUOUS error
47
+ * 3. If no project: use fallbackWorkspace
48
+ * - Missing fallbackWorkspace → NO_CONTEXT error
49
+ * 4. Within workspace: match agent against architect, then builders map
50
+ * - Exact match (case-insensitive) first, then tail match
51
+ * - Multiple tail matches → AMBIGUOUS error
52
+ *
53
+ * @param target - Address string: "agent" or "project:agent"
54
+ * @param fallbackWorkspace - Workspace path when no project: prefix is given
55
+ * @returns ResolveResult on success, ResolveError on failure
56
+ */
57
+ export function resolveTarget(target, fallbackWorkspace) {
58
+ const { project, agent } = parseAddress(target);
59
+ // Validate: empty or whitespace-only agent is a malformed address
60
+ if (!agent || !agent.trim()) {
61
+ return {
62
+ code: 'NO_CONTEXT',
63
+ message: 'Malformed address: agent name is empty.',
64
+ };
65
+ }
66
+ // Determine the workspace path
67
+ let workspacePath;
68
+ if (project) {
69
+ const result = findWorkspaceByBasename(project);
70
+ if ('code' in result)
71
+ return result;
72
+ workspacePath = result.workspacePath;
73
+ }
74
+ else {
75
+ if (!fallbackWorkspace) {
76
+ return {
77
+ code: 'NO_CONTEXT',
78
+ message: 'Cannot resolve agent without project context.',
79
+ };
80
+ }
81
+ workspacePath = fallbackWorkspace;
82
+ }
83
+ // Resolve agent within the workspace
84
+ return resolveAgentInWorkspace(agent, workspacePath);
85
+ }
86
+ /**
87
+ * Find a workspace path by matching the basename of registered workspace paths.
88
+ */
89
+ function findWorkspaceByBasename(projectName) {
90
+ const allWorkspaces = getWorkspaceTerminals();
91
+ const matches = [];
92
+ for (const wsPath of allWorkspaces.keys()) {
93
+ if (path.basename(wsPath).toLowerCase() === projectName) {
94
+ matches.push(wsPath);
95
+ }
96
+ }
97
+ if (matches.length === 0) {
98
+ return {
99
+ code: 'NOT_FOUND',
100
+ message: `Project '${projectName}' not found. No workspace with that basename is registered.`,
101
+ };
102
+ }
103
+ if (matches.length > 1) {
104
+ return {
105
+ code: 'AMBIGUOUS',
106
+ message: `Project '${projectName}' is ambiguous — matches ${matches.length} workspaces: ${matches.join(', ')}`,
107
+ };
108
+ }
109
+ return { workspacePath: matches[0] };
110
+ }
111
+ /**
112
+ * Resolve an agent name to a terminal ID within a specific workspace.
113
+ *
114
+ * Checks architect first, then builders by exact match, then builders by tail match.
115
+ */
116
+ function resolveAgentInWorkspace(agent, workspacePath) {
117
+ const allWorkspaces = getWorkspaceTerminals();
118
+ const entry = allWorkspaces.get(workspacePath);
119
+ if (!entry) {
120
+ return {
121
+ code: 'NOT_FOUND',
122
+ message: `Workspace '${workspacePath}' has no registered terminals.`,
123
+ };
124
+ }
125
+ // Check architect
126
+ if (agent === 'architect' || agent === 'arch') {
127
+ if (!entry.architect) {
128
+ return {
129
+ code: 'NOT_FOUND',
130
+ message: `No architect terminal found in workspace '${path.basename(workspacePath)}'.`,
131
+ };
132
+ }
133
+ return { terminalId: entry.architect, workspacePath, agent: 'architect' };
134
+ }
135
+ // Check builders — exact match (case-insensitive)
136
+ for (const [builderId, terminalId] of entry.builders) {
137
+ if (builderId.toLowerCase() === agent) {
138
+ return { terminalId, workspacePath, agent: builderId };
139
+ }
140
+ }
141
+ // Check builders — tail match with leading-zero stripping
142
+ const strippedAgent = stripLeadingZeros(agent).toLowerCase();
143
+ const tailMatches = [];
144
+ for (const [builderId, terminalId] of entry.builders) {
145
+ if (builderId.toLowerCase().endsWith(`-${strippedAgent}`)) {
146
+ tailMatches.push({ builderId, terminalId });
147
+ }
148
+ }
149
+ if (tailMatches.length === 1) {
150
+ return {
151
+ terminalId: tailMatches[0].terminalId,
152
+ workspacePath,
153
+ agent: tailMatches[0].builderId,
154
+ };
155
+ }
156
+ if (tailMatches.length > 1) {
157
+ const candidates = tailMatches.map(m => m.builderId).join(', ');
158
+ return {
159
+ code: 'AMBIGUOUS',
160
+ message: `Agent '${agent}' is ambiguous — matches ${tailMatches.length} builders: ${candidates}. Use the full name.`,
161
+ };
162
+ }
163
+ // Check shells — exact match
164
+ for (const [shellId, terminalId] of entry.shells) {
165
+ if (shellId.toLowerCase() === agent) {
166
+ return { terminalId, workspacePath, agent: shellId };
167
+ }
168
+ }
169
+ return {
170
+ code: 'NOT_FOUND',
171
+ message: `Agent '${agent}' not found in workspace '${path.basename(workspacePath)}'.`,
172
+ };
173
+ }
174
+ /**
175
+ * Broadcast a structured message frame to all WebSocket subscribers.
176
+ * Filters by project if the subscriber has a projectFilter set.
177
+ */
178
+ export function broadcastMessage(message) {
179
+ const payload = JSON.stringify(message);
180
+ for (const sub of messageSubscribers) {
181
+ // Apply project filter: message must involve the filtered project (from or to)
182
+ if (sub.projectFilter) {
183
+ if (message.from.project !== sub.projectFilter && message.to.project !== sub.projectFilter) {
184
+ continue;
185
+ }
186
+ }
187
+ try {
188
+ sub.ws.send(payload);
189
+ }
190
+ catch {
191
+ // If send fails, subscriber is likely disconnected — remove it
192
+ messageSubscribers.delete(sub);
193
+ }
194
+ }
195
+ }
196
+ /**
197
+ * Helper to check if a resolve result is an error.
198
+ */
199
+ export function isResolveError(result) {
200
+ return 'code' in result;
201
+ }
202
+ //# sourceMappingURL=tower-messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tower-messages.js","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-messages.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tower-routes.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAO7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAuClD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,kBAAkB,EAAE,MAAM,cAAc,GAAG,IAAI,CAAC;IAChD,qBAAqB,EAAE,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACjH,YAAY,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;IAC1C,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AA6CD,wBAAsB,aAAa,CACjC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,GAAG,EAAE,YAAY,GAChB,OAAO,CAAC,IAAI,CAAC,CAuEf"}
1
+ {"version":3,"file":"tower-routes.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAU7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAGxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AA8ClD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,kBAAkB,EAAE,MAAM,cAAc,GAAG,IAAI,CAAC;IAChD,qBAAqB,EAAE,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACjH,YAAY,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;IAC1C,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAgDD,wBAAsB,aAAa,CACjC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,GAAG,EAAE,YAAY,GAChB,OAAO,CAAC,IAAI,CAAC,CAuEf"}
@@ -14,16 +14,24 @@
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import crypto from 'node:crypto';
17
- import { execSync } from 'node:child_process';
17
+ import { exec } from 'node:child_process';
18
+ import { promisify } from 'node:util';
18
19
  import { homedir, tmpdir } from 'node:os';
19
20
  import { fileURLToPath } from 'node:url';
21
+ const execAsync = promisify(exec);
22
+ import { DEFAULT_COLS, defaultSessionOptions } from '../../terminal/index.js';
20
23
  import { parseJsonBody, isRequestAllowed } from '../utils/server-utils.js';
21
24
  import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
22
25
  import { handleTunnelEndpoint } from './tower-tunnel.js';
26
+ import { resolveTarget, broadcastMessage, isResolveError } from './tower-messages.js';
27
+ import { formatArchitectMessage, formatBuilderMessage } from '../utils/message-format.js';
23
28
  import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, } from './tower-instances.js';
24
- import { getWorkspaceTerminals, getTerminalManager, getWorkspaceTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, deleteWorkspaceTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForWorkspace, } from './tower-terminals.js';
29
+ import { OverviewCache } from './overview.js';
30
+ import { getWorkspaceTerminals, getTerminalManager, getWorkspaceTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, removeTerminalFromRegistry, deleteWorkspaceTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForWorkspace, } from './tower-terminals.js';
25
31
  const __filename = fileURLToPath(import.meta.url);
26
32
  const __dirname = path.dirname(__filename);
33
+ // Singleton cache for overview endpoint (Spec 0126 Phase 4)
34
+ const overviewCache = new OverviewCache();
27
35
  // ============================================================================
28
36
  // Helper: read raw request body
29
37
  // ============================================================================
@@ -40,12 +48,15 @@ const ROUTES = {
40
48
  'POST /api/terminals': (req, res, _url, ctx) => handleTerminalCreate(req, res, ctx),
41
49
  'GET /api/terminals': (_req, res) => handleTerminalList(res),
42
50
  'GET /api/status': (_req, res) => handleStatus(res),
51
+ 'GET /api/overview': (_req, res, url) => handleOverview(res, url),
52
+ 'POST /api/overview/refresh': (_req, res) => handleOverviewRefresh(res),
43
53
  'GET /api/events': (req, res, _url, ctx) => handleSSEEvents(req, res, ctx),
44
54
  'POST /api/notify': (req, res, _url, ctx) => handleNotify(req, res, ctx),
45
55
  'GET /api/browse': (_req, res, url) => handleBrowse(res, url),
46
56
  'POST /api/create': (req, res, _url, ctx) => handleCreateWorkspace(req, res, ctx),
47
57
  'POST /api/launch': (req, res) => handleLaunchInstance(req, res),
48
58
  'POST /api/stop': (req, res) => handleStopInstance(req, res),
59
+ 'POST /api/send': (req, res, _url, ctx) => handleSend(req, res, ctx),
49
60
  'GET /': (_req, res, _url, ctx) => handleDashboard(res, ctx),
50
61
  'GET /index.html': (_req, res, _url, ctx) => handleDashboard(res, ctx),
51
62
  };
@@ -171,7 +182,6 @@ async function handleWorkspaceAction(req, res, ctx, match) {
171
182
  name: instance.workspaceName,
172
183
  active: instance.running,
173
184
  terminals: instance.terminals,
174
- gateStatus: instance.gateStatus,
175
185
  }));
176
186
  return;
177
187
  }
@@ -244,9 +254,8 @@ async function handleTerminalCreate(req, res, ctx) {
244
254
  args: args || [],
245
255
  cwd,
246
256
  env: sessionEnv,
247
- cols: cols || 200,
248
- rows: 50,
249
- restartOnExit: false,
257
+ ...defaultSessionOptions(),
258
+ cols: cols || DEFAULT_COLS,
250
259
  });
251
260
  const replayData = client.getReplayData() ?? Buffer.alloc(0);
252
261
  const shellperInfo = shellperManager.getSessionInfo(sessionId);
@@ -333,6 +342,9 @@ async function handleTerminalRoutes(req, res, url, match) {
333
342
  }
334
343
  // TICK-001: Delete from SQLite
335
344
  deleteTerminalSession(terminalId);
345
+ // Bugfix #290: Also remove from in-memory registry so dashboard
346
+ // stops showing tabs for cleaned-up builders
347
+ removeTerminalFromRegistry(terminalId);
336
348
  res.writeHead(204);
337
349
  res.end();
338
350
  return;
@@ -406,6 +418,36 @@ async function handleStatus(res) {
406
418
  res.writeHead(200, { 'Content-Type': 'application/json' });
407
419
  res.end(JSON.stringify({ instances }));
408
420
  }
421
+ async function handleOverview(res, url, workspaceOverride) {
422
+ // Accept workspace from: explicit override (workspace-scoped route), ?workspace= param, or first known path.
423
+ let workspaceRoot = workspaceOverride || url.searchParams.get('workspace');
424
+ if (!workspaceRoot) {
425
+ const knownPaths = getKnownWorkspacePaths();
426
+ workspaceRoot = knownPaths.find(p => !p.includes('/.builders/')) || null;
427
+ }
428
+ if (!workspaceRoot) {
429
+ res.writeHead(200, { 'Content-Type': 'application/json' });
430
+ res.end(JSON.stringify({ builders: [], pendingPRs: [], backlog: [] }));
431
+ return;
432
+ }
433
+ // Build set of active builder role_ids (lowercased) from live terminal sessions
434
+ const wsTerminals = getWorkspaceTerminals();
435
+ const entry = wsTerminals.get(normalizeWorkspacePath(workspaceRoot));
436
+ const activeBuilderRoleIds = new Set();
437
+ if (entry) {
438
+ for (const key of entry.builders.keys()) {
439
+ activeBuilderRoleIds.add(key.toLowerCase());
440
+ }
441
+ }
442
+ const data = await overviewCache.getOverview(workspaceRoot, activeBuilderRoleIds);
443
+ res.writeHead(200, { 'Content-Type': 'application/json' });
444
+ res.end(JSON.stringify(data));
445
+ }
446
+ function handleOverviewRefresh(res) {
447
+ overviewCache.invalidate();
448
+ res.writeHead(200, { 'Content-Type': 'application/json' });
449
+ res.end(JSON.stringify({ ok: true }));
450
+ }
409
451
  function handleSSEEvents(req, res, ctx) {
410
452
  const clientId = crypto.randomBytes(8).toString('hex');
411
453
  res.writeHead(200, {
@@ -446,6 +488,107 @@ async function handleNotify(req, res, ctx) {
446
488
  res.writeHead(200, { 'Content-Type': 'application/json' });
447
489
  res.end(JSON.stringify({ success: true }));
448
490
  }
491
+ // ============================================================================
492
+ // POST /api/send — send a message to a resolved agent terminal
493
+ // ============================================================================
494
+ async function handleSend(req, res, ctx) {
495
+ const body = await parseJsonBody(req);
496
+ // Validate required fields
497
+ const to = typeof body.to === 'string' ? body.to.trim() : '';
498
+ const message = typeof body.message === 'string' ? body.message.trim() : '';
499
+ if (!to) {
500
+ res.writeHead(400, { 'Content-Type': 'application/json' });
501
+ res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Missing or empty "to" field' }));
502
+ return;
503
+ }
504
+ if (!message) {
505
+ res.writeHead(400, { 'Content-Type': 'application/json' });
506
+ res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Missing or empty "message" field' }));
507
+ return;
508
+ }
509
+ // Optional fields
510
+ const from = typeof body.from === 'string' ? body.from : undefined;
511
+ const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
512
+ const fromWorkspace = typeof body.fromWorkspace === 'string' ? body.fromWorkspace : undefined;
513
+ const options = typeof body.options === 'object' && body.options !== null
514
+ ? body.options
515
+ : {};
516
+ const raw = options.raw === true;
517
+ const noEnter = options.noEnter === true;
518
+ const interrupt = options.interrupt === true;
519
+ // Resolve the target address to a terminal ID
520
+ const result = resolveTarget(to, workspace);
521
+ if (isResolveError(result)) {
522
+ const statusCode = result.code === 'AMBIGUOUS' ? 409
523
+ : result.code === 'NO_CONTEXT' ? 400
524
+ : 404;
525
+ // Map NO_CONTEXT to INVALID_PARAMS per plan's error contract
526
+ const errorCode = result.code === 'NO_CONTEXT' ? 'INVALID_PARAMS' : result.code;
527
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
528
+ res.end(JSON.stringify({ error: errorCode, message: result.message }));
529
+ return;
530
+ }
531
+ // Get the terminal session
532
+ const manager = getTerminalManager();
533
+ const session = manager.getSession(result.terminalId);
534
+ if (!session) {
535
+ res.writeHead(404, { 'Content-Type': 'application/json' });
536
+ res.end(JSON.stringify({
537
+ error: 'NOT_FOUND',
538
+ message: `Terminal session ${result.terminalId} not found (agent '${result.agent}' resolved but terminal is gone).`,
539
+ }));
540
+ return;
541
+ }
542
+ // Format the message based on sender/target
543
+ const isArchitectTarget = result.agent === 'architect';
544
+ let formattedMessage;
545
+ if (isArchitectTarget && from) {
546
+ // Builder → Architect
547
+ formattedMessage = formatBuilderMessage(from, message, undefined, raw);
548
+ }
549
+ else if (!isArchitectTarget) {
550
+ // Architect → Builder (or any → builder)
551
+ formattedMessage = formatArchitectMessage(message, undefined, raw);
552
+ }
553
+ else {
554
+ // Unknown sender to architect — use raw
555
+ formattedMessage = raw ? message : formatArchitectMessage(message, undefined, false);
556
+ }
557
+ // Optionally interrupt first
558
+ if (interrupt) {
559
+ session.write('\x03'); // Ctrl+C
560
+ await new Promise(resolve => setTimeout(resolve, 100));
561
+ }
562
+ // Write the message to the terminal
563
+ session.write(formattedMessage);
564
+ // Send Enter to submit (unless noEnter)
565
+ if (!noEnter) {
566
+ session.write('\r');
567
+ }
568
+ // Broadcast structured message to WebSocket subscribers
569
+ const senderWorkspace = fromWorkspace ?? workspace ?? 'unknown';
570
+ broadcastMessage({
571
+ type: 'message',
572
+ from: {
573
+ project: path.basename(senderWorkspace),
574
+ agent: from ?? 'unknown',
575
+ },
576
+ to: {
577
+ project: path.basename(result.workspacePath),
578
+ agent: result.agent,
579
+ },
580
+ content: message,
581
+ metadata: { raw, source: 'api' },
582
+ timestamp: new Date().toISOString(),
583
+ });
584
+ ctx.log('INFO', `Message sent: ${from ?? 'unknown'} → ${result.agent} (terminal ${result.terminalId.slice(0, 8)}...)`);
585
+ res.writeHead(200, { 'Content-Type': 'application/json' });
586
+ res.end(JSON.stringify({
587
+ ok: true,
588
+ terminalId: result.terminalId,
589
+ resolvedTo: result.agent,
590
+ }));
591
+ }
449
592
  async function handleBrowse(res, url) {
450
593
  const inputPath = url.searchParams.get('path') || '';
451
594
  try {
@@ -493,9 +636,8 @@ async function handleCreateWorkspace(req, res, ctx) {
493
636
  }
494
637
  try {
495
638
  // Run codev init (it creates the directory)
496
- execSync(`codev init --yes "${workspaceName}"`, {
639
+ await execAsync(`codev init --yes "${workspaceName}"`, {
497
640
  cwd: expandedParent,
498
- stdio: 'pipe',
499
641
  timeout: 60000,
500
642
  });
501
643
  // Launch the instance
@@ -618,7 +760,7 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
618
760
  await handleTunnelEndpoint(req, res, tunnelSub);
619
761
  return;
620
762
  }
621
- // GET /file?path=<relative-path> — Read workspace file by path (for StatusPanel workspace list)
763
+ // GET /file?path=<relative-path> — Read workspace file by path
622
764
  if (req.method === 'GET' && subPath === 'file' && url.searchParams.has('path')) {
623
765
  const relPath = url.searchParams.get('path');
624
766
  const fullPath = path.resolve(workspacePath, relPath);
@@ -771,6 +913,14 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
771
913
  });
772
914
  return;
773
915
  }
916
+ // GET /api/overview - Work view overview data (Spec 0126 Phase 4)
917
+ if (req.method === 'GET' && apiPath === 'overview') {
918
+ return handleOverview(res, url, workspacePath);
919
+ }
920
+ // POST /api/overview/refresh - Invalidate overview cache (Spec 0126 Phase 4)
921
+ if (req.method === 'POST' && apiPath === 'overview/refresh') {
922
+ return handleOverviewRefresh(res);
923
+ }
774
924
  // Unhandled API route
775
925
  res.writeHead(404, { 'Content-Type': 'application/json' });
776
926
  res.end(JSON.stringify({ error: 'API endpoint not found', path: apiPath }));
@@ -801,7 +951,7 @@ async function handleWorkspaceState(res, workspacePath) {
801
951
  // and shellper reconnection in one place)
802
952
  const encodedPath = Buffer.from(workspacePath).toString('base64url');
803
953
  const proxyUrl = `/workspace/${encodedPath}/`;
804
- const { gateStatus } = await getTerminalsForWorkspace(workspacePath, proxyUrl);
954
+ await getTerminalsForWorkspace(workspacePath, proxyUrl);
805
955
  // Now read from the refreshed cache
806
956
  const entry = getWorkspaceTerminalsEntry(workspacePath);
807
957
  const manager = getTerminalManager();
@@ -811,7 +961,6 @@ async function handleWorkspaceState(res, workspacePath) {
811
961
  utils: [],
812
962
  annotations: [],
813
963
  workspaceName: path.basename(workspacePath),
814
- gateStatus,
815
964
  };
816
965
  // Add architect if exists
817
966
  if (entry.architect) {
@@ -845,7 +994,7 @@ async function handleWorkspaceState(res, workspacePath) {
845
994
  if (session) {
846
995
  state.builders.push({
847
996
  id: builderId,
848
- name: `Builder ${builderId}`,
997
+ name: builderId,
849
998
  port: 0,
850
999
  pid: session.pid || 0,
851
1000
  status: 'running',
@@ -891,9 +1040,7 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
891
1040
  args: shellArgs,
892
1041
  cwd: workspacePath,
893
1042
  env: shellEnv,
894
- cols: 200,
895
- rows: 50,
896
- restartOnExit: false,
1043
+ ...defaultSessionOptions(),
897
1044
  });
898
1045
  const replayData = client.getReplayData() ?? Buffer.alloc(0);
899
1046
  const shellperInfo = shellperManager.getSessionInfo(sessionId);
@@ -1241,10 +1388,10 @@ function handleWorkspaceFiles(res, url, workspacePath) {
1241
1388
  res.writeHead(200, { 'Content-Type': 'application/json' });
1242
1389
  res.end(JSON.stringify(tree));
1243
1390
  }
1244
- function handleWorkspaceGitStatus(res, ctx, workspacePath) {
1391
+ async function handleWorkspaceGitStatus(res, ctx, workspacePath) {
1245
1392
  try {
1246
1393
  // Get git status in porcelain format for parsing
1247
- const result = execSync('git status --porcelain', {
1394
+ const { stdout: result } = await execAsync('git status --porcelain', {
1248
1395
  cwd: workspacePath,
1249
1396
  encoding: 'utf-8',
1250
1397
  timeout: 5000,