@cluesmith/codev 3.0.6 → 3.1.1

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 (118) hide show
  1. package/dashboard-dist/assets/{index-CR-uu485.js → index-CM6A5r59.js} +39 -39
  2. package/dashboard-dist/assets/index-CM6A5r59.js.map +1 -0
  3. package/dashboard-dist/assets/index-Cl5tt1AP.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 +14 -0
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/send.d.ts +13 -2
  9. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  10. package/dist/agent-farm/commands/send.js +49 -14
  11. package/dist/agent-farm/commands/send.js.map +1 -1
  12. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/status.js +41 -13
  14. package/dist/agent-farm/commands/status.js.map +1 -1
  15. package/dist/agent-farm/commands/stop.d.ts.map +1 -1
  16. package/dist/agent-farm/commands/stop.js +9 -5
  17. package/dist/agent-farm/commands/stop.js.map +1 -1
  18. package/dist/agent-farm/commands/workspace-remove-architect.d.ts +16 -0
  19. package/dist/agent-farm/commands/workspace-remove-architect.d.ts.map +1 -0
  20. package/dist/agent-farm/commands/workspace-remove-architect.js +40 -0
  21. package/dist/agent-farm/commands/workspace-remove-architect.js.map +1 -0
  22. package/dist/agent-farm/servers/overview.d.ts +26 -0
  23. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  24. package/dist/agent-farm/servers/overview.js +31 -12
  25. package/dist/agent-farm/servers/overview.js.map +1 -1
  26. package/dist/agent-farm/servers/tower-instances.d.ts +25 -0
  27. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  28. package/dist/agent-farm/servers/tower-instances.js +298 -45
  29. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  30. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  31. package/dist/agent-farm/servers/tower-routes.js +240 -6
  32. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  33. package/dist/agent-farm/servers/tower-server.js +8 -0
  34. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  35. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  36. package/dist/agent-farm/servers/tower-terminals.js +76 -12
  37. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  38. package/dist/agent-farm/servers/tower-types.d.ts +24 -0
  39. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
  40. package/dist/agent-farm/servers/worktree-config-watcher.d.ts +36 -0
  41. package/dist/agent-farm/servers/worktree-config-watcher.d.ts.map +1 -0
  42. package/dist/agent-farm/servers/worktree-config-watcher.js +80 -0
  43. package/dist/agent-farm/servers/worktree-config-watcher.js.map +1 -0
  44. package/dist/agent-farm/state.d.ts +29 -5
  45. package/dist/agent-farm/state.d.ts.map +1 -1
  46. package/dist/agent-farm/state.js +54 -12
  47. package/dist/agent-farm/state.js.map +1 -1
  48. package/dist/agent-farm/types.d.ts +23 -0
  49. package/dist/agent-farm/types.d.ts.map +1 -1
  50. package/dist/agent-farm/utils/architect-name.d.ts +5 -0
  51. package/dist/agent-farm/utils/architect-name.d.ts.map +1 -1
  52. package/dist/agent-farm/utils/architect-name.js +8 -0
  53. package/dist/agent-farm/utils/architect-name.js.map +1 -1
  54. package/dist/agent-farm/utils/config.d.ts +2 -12
  55. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  56. package/dist/agent-farm/utils/config.js +20 -0
  57. package/dist/agent-farm/utils/config.js.map +1 -1
  58. package/dist/cli.d.ts.map +1 -1
  59. package/dist/cli.js +2 -0
  60. package/dist/cli.js.map +1 -1
  61. package/dist/commands/consult/index.d.ts +4 -0
  62. package/dist/commands/consult/index.d.ts.map +1 -1
  63. package/dist/commands/consult/index.js +160 -31
  64. package/dist/commands/consult/index.js.map +1 -1
  65. package/dist/commands/porch/artifacts.d.ts +32 -0
  66. package/dist/commands/porch/artifacts.d.ts.map +1 -1
  67. package/dist/commands/porch/artifacts.js +129 -0
  68. package/dist/commands/porch/artifacts.js.map +1 -1
  69. package/dist/lib/config.d.ts +17 -2
  70. package/dist/lib/config.d.ts.map +1 -1
  71. package/dist/lib/config.js +30 -2
  72. package/dist/lib/config.js.map +1 -1
  73. package/dist/lib/default-branch.d.ts +11 -0
  74. package/dist/lib/default-branch.d.ts.map +1 -0
  75. package/dist/lib/default-branch.js +23 -0
  76. package/dist/lib/default-branch.js.map +1 -0
  77. package/dist/lib/github.d.ts +1 -1
  78. package/dist/lib/github.d.ts.map +1 -1
  79. package/dist/lib/github.js +5 -1
  80. package/dist/lib/github.js.map +1 -1
  81. package/dist/terminal/pty-session.d.ts.map +1 -1
  82. package/dist/terminal/pty-session.js +5 -0
  83. package/dist/terminal/pty-session.js.map +1 -1
  84. package/dist/terminal/shellper-client.d.ts +15 -0
  85. package/dist/terminal/shellper-client.d.ts.map +1 -1
  86. package/dist/terminal/shellper-client.js +37 -0
  87. package/dist/terminal/shellper-client.js.map +1 -1
  88. package/dist/terminal/shellper-process.d.ts +1 -0
  89. package/dist/terminal/shellper-process.d.ts.map +1 -1
  90. package/dist/terminal/shellper-process.js +8 -0
  91. package/dist/terminal/shellper-process.js.map +1 -1
  92. package/dist/terminal/shellper-protocol.d.ts +12 -0
  93. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  94. package/dist/terminal/shellper-protocol.js.map +1 -1
  95. package/package.json +3 -3
  96. package/scripts/forge/gitea/issue-list.sh +24 -1
  97. package/scripts/forge/gitea/pr-list.sh +21 -1
  98. package/scripts/forge/gitea/recently-closed.sh +19 -1
  99. package/scripts/forge/gitea/recently-merged.sh +25 -1
  100. package/scripts/forge/github/pr-search.sh +2 -2
  101. package/skeleton/protocols/air/consult-types/pr-review.md +5 -1
  102. package/skeleton/protocols/aspir/builder-prompt.md +3 -3
  103. package/skeleton/protocols/aspir/consult-types/pr-review.md +5 -1
  104. package/skeleton/protocols/bugfix/consult-types/pr-review.md +2 -1
  105. package/skeleton/protocols/maintain/consult-types/pr-review.md +5 -1
  106. package/skeleton/protocols/pir/builder-prompt.md +2 -2
  107. package/skeleton/protocols/pir/consult-types/pr-review.md +3 -2
  108. package/skeleton/protocols/pir/prompts/implement.md +15 -7
  109. package/skeleton/protocols/pir/prompts/review.md +27 -21
  110. package/skeleton/protocols/pir/protocol.json +1 -1
  111. package/skeleton/protocols/pir/protocol.md +4 -6
  112. package/skeleton/protocols/spir/builder-prompt.md +3 -3
  113. package/skeleton/protocols/spir/consult-types/pr-review.md +5 -1
  114. package/skeleton/roles/builder.md +21 -0
  115. package/skeleton/templates/AGENTS.md +20 -0
  116. package/skeleton/templates/CLAUDE.md +20 -0
  117. package/dashboard-dist/assets/index-CR-uu485.js.map +0 -1
  118. package/dashboard-dist/assets/index-nCeIzV_d.css +0 -32
@@ -22,17 +22,20 @@ import { readCloudConfig } from '../lib/cloud-config.js';
22
22
  import { fileURLToPath } from 'node:url';
23
23
  import { version } from '../../version.js';
24
24
  const execAsync = promisify(exec);
25
+ import { getBuilders, setArchitectByName } from '../state.js';
25
26
  import { DEFAULT_COLS, defaultSessionOptions } from '../../terminal/index.js';
26
27
  import { parseJsonBody, isRequestAllowed } from '../utils/server-utils.js';
27
28
  import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
28
29
  import { handleTunnelEndpoint } from './tower-tunnel.js';
30
+ import { getWorktreeConfig } from '../utils/config.js';
31
+ import { ensureWorktreeConfigWatcher } from './worktree-config-watcher.js';
29
32
  import { hasTeam, loadTeamMembers, loadMessages } from '../../lib/team.js';
30
33
  import { fetchTeamGitHubData } from '../../lib/team-github.js';
31
34
  import { resolveTarget, broadcastMessage, isResolveError } from './tower-messages.js';
32
35
  import { formatArchitectMessage, formatBuilderMessage } from '../utils/message-format.js';
33
36
  import { SendBuffer } from './send-buffer.js';
34
37
  import { writeMessageToSession } from './message-write.js';
35
- import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, addArchitect, } from './tower-instances.js';
38
+ import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, addArchitect, removeArchitect, } from './tower-instances.js';
36
39
  import { OverviewCache } from './overview.js';
37
40
  import { fetchIssue } from '../../lib/github.js';
38
41
  import { computeAnalytics } from './analytics.js';
@@ -66,8 +69,9 @@ const ROUTES = {
66
69
  'POST /api/terminals': (req, res, _url, ctx) => handleTerminalCreate(req, res, ctx),
67
70
  'GET /api/terminals': (_req, res) => handleTerminalList(res),
68
71
  'GET /api/status': (_req, res) => handleStatus(res),
69
- 'GET /api/overview': (_req, res, url) => handleOverview(res, url),
72
+ 'GET /api/overview': (_req, res, url, ctx) => handleOverview(res, url, undefined, ctx),
70
73
  'GET /api/issue': (_req, res, url) => handleIssueView(res, url),
74
+ 'GET /api/worktree-config': (_req, res, url) => handleWorktreeConfigView(res, url),
71
75
  'GET /api/analytics': (_req, res, url) => handleAnalytics(res, url),
72
76
  'POST /api/overview/refresh': (_req, res, _url, ctx) => handleOverviewRefresh(res, ctx),
73
77
  'GET /api/events': (req, res, _url, ctx) => handleSSEEvents(req, res, ctx),
@@ -129,7 +133,12 @@ export async function handleRequest(req, res, ctx) {
129
133
  // Workspace API: /api/workspaces/:encodedPath/architects (Spec 755 — multi-architect)
130
134
  const architectsMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)\/architects$/);
131
135
  if (architectsMatch) {
132
- return await handleAddArchitect(req, res, architectsMatch);
136
+ return await handleAddArchitect(req, res, architectsMatch, ctx);
137
+ }
138
+ // Workspace API: DELETE /api/workspaces/:encodedPath/architects/:name (Spec 786)
139
+ const architectRemoveMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)\/architects\/([^/]+)$/);
140
+ if (architectRemoveMatch) {
141
+ return await handleRemoveArchitect(req, res, architectRemoveMatch, ctx);
133
142
  }
134
143
  // Terminal-specific routes: /api/terminals/:id/* (Spec 0090 Phase 2)
135
144
  const terminalRouteMatch = url.pathname.match(/^\/api\/terminals\/([^/]+)(\/.*)?$/);
@@ -190,7 +199,7 @@ async function handleListWorkspaces(res) {
190
199
  * Returns 200 { success: true, name, terminalId } on success,
191
200
  * 400 / 404 with { success: false, error } otherwise.
192
201
  */
193
- async function handleAddArchitect(req, res, match) {
202
+ async function handleAddArchitect(req, res, match, ctx) {
194
203
  if (req.method !== 'POST') {
195
204
  res.writeHead(405, { 'Content-Type': 'application/json' });
196
205
  res.end(JSON.stringify({ error: 'Method not allowed' }));
@@ -221,6 +230,16 @@ async function handleAddArchitect(req, res, match) {
221
230
  }
222
231
  const result = await addArchitect(workspacePath, body.name);
223
232
  if (result.success) {
233
+ // Spec 823: emit an `architects-updated` SSE event so VSCode's
234
+ // WorkspaceProvider tree refreshes when the add happens via the CLI
235
+ // (today the tree only refreshes when add is triggered from within
236
+ // VSCode itself). Mirrors `worktree-config-updated`'s broadcast shape.
237
+ ctx.broadcastNotification({
238
+ type: 'architects-updated',
239
+ title: 'Architects updated',
240
+ body: JSON.stringify({ workspace: workspacePath }),
241
+ workspace: workspacePath,
242
+ });
224
243
  res.writeHead(200, { 'Content-Type': 'application/json' });
225
244
  res.end(JSON.stringify({ success: true, name: result.name, terminalId: result.terminalId }));
226
245
  }
@@ -231,6 +250,59 @@ async function handleAddArchitect(req, res, match) {
231
250
  res.end(JSON.stringify({ success: false, error: result.error }));
232
251
  }
233
252
  }
253
+ /**
254
+ * DELETE /api/workspaces/:encodedPath/architects/:name (Spec 786)
255
+ *
256
+ * Removes a named sibling architect from an active workspace. Refuses to
257
+ * remove `main` (returns 400). Returns 404 when the workspace isn't active or
258
+ * the named architect isn't registered.
259
+ *
260
+ * Removing an architect with in-flight builders is allowed (per OQ-A) — the
261
+ * builders fall back to `main` via the existing `tower-messages.ts:336` chain.
262
+ */
263
+ async function handleRemoveArchitect(req, res, match, ctx) {
264
+ if (req.method !== 'DELETE') {
265
+ res.writeHead(405, { 'Content-Type': 'application/json' });
266
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
267
+ return;
268
+ }
269
+ const [, encodedPath, encodedName] = match;
270
+ let workspacePath;
271
+ try {
272
+ workspacePath = decodeWorkspacePath(encodedPath);
273
+ if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
274
+ throw new Error('Invalid path');
275
+ }
276
+ workspacePath = normalizeWorkspacePath(workspacePath);
277
+ }
278
+ catch {
279
+ res.writeHead(400, { 'Content-Type': 'application/json' });
280
+ res.end(JSON.stringify({ error: 'Invalid workspace path encoding' }));
281
+ return;
282
+ }
283
+ const name = decodeURIComponent(encodedName);
284
+ const result = await removeArchitect(workspacePath, name);
285
+ if (result.success) {
286
+ // Spec 823: emit `architects-updated` so VSCode's WorkspaceProvider
287
+ // refreshes when remove happens via CLI (the dashboard polls and
288
+ // doesn't need an explicit event; VSCode subscribes to this notification).
289
+ ctx.broadcastNotification({
290
+ type: 'architects-updated',
291
+ title: 'Architects updated',
292
+ body: JSON.stringify({ workspace: workspacePath }),
293
+ workspace: workspacePath,
294
+ });
295
+ res.writeHead(200, { 'Content-Type': 'application/json' });
296
+ res.end(JSON.stringify({ success: true }));
297
+ }
298
+ else {
299
+ // Distinguish "not registered" / "not running" (404) from validation
300
+ // errors like "Cannot remove main" (400).
301
+ const status = result.error?.includes('not running') || result.error?.includes('not found') ? 404 : 400;
302
+ res.writeHead(status, { 'Content-Type': 'application/json' });
303
+ res.end(JSON.stringify({ success: false, error: result.error }));
304
+ }
305
+ }
234
306
  async function handleWorkspaceAction(req, res, ctx, match) {
235
307
  const [, encodedPath, action] = match;
236
308
  let workspacePath;
@@ -578,7 +650,7 @@ async function handleStatus(res) {
578
650
  res.writeHead(200, { 'Content-Type': 'application/json' });
579
651
  res.end(JSON.stringify({ instances }));
580
652
  }
581
- async function handleOverview(res, url, workspaceOverride) {
653
+ async function handleOverview(res, url, workspaceOverride, ctx) {
582
654
  // Accept workspace from: explicit override (workspace-scoped route), ?workspace= param, or first known path.
583
655
  let workspaceRoot = workspaceOverride || url.searchParams.get('workspace');
584
656
  if (!workspaceRoot) {
@@ -601,6 +673,29 @@ async function handleOverview(res, url, workspaceOverride) {
601
673
  activeBuilderRoleIds.add(key.toLowerCase());
602
674
  }
603
675
  const data = await overviewCache.getOverview(workspaceRoot, activeBuilderRoleIds);
676
+ // Enrich each builder with `lastDataAt` — the wall-clock time Tower
677
+ // last received a DATA frame from that builder's shellper. The UI uses
678
+ // this to flag builders silent past a threshold as "waiting for input"
679
+ // (separate from formal porch gates, which `blocked` already covers).
680
+ //
681
+ // Pattern mirrors /api/state's builder loop (around line 1572): iterate
682
+ // `entry.builders` (Map<roleId, PtySession ID>), look up the session,
683
+ // do the work. Discovery already stamps `roleId` on each builder, so
684
+ // matching the runtime registry back to the discovery list is a single
685
+ // `find`. Builders with no attached session keep `lastDataAt = null`
686
+ // (the discover-time default).
687
+ const terminalManager = getTerminalManager();
688
+ for (const [builderId, terminalId] of entry.builders) {
689
+ const ptySession = terminalManager.getSession(terminalId);
690
+ if (!ptySession) {
691
+ continue;
692
+ }
693
+ const builder = data.builders.find(b => b.roleId === builderId.toLowerCase());
694
+ if (!builder) {
695
+ continue;
696
+ }
697
+ builder.lastDataAt = new Date(ptySession.lastDataAt).toISOString();
698
+ }
604
699
  res.writeHead(200, { 'Content-Type': 'application/json' });
605
700
  res.end(JSON.stringify(data));
606
701
  }
@@ -628,6 +723,42 @@ async function handleIssueView(res, url) {
628
723
  res.writeHead(200, { 'Content-Type': 'application/json' });
629
724
  res.end(JSON.stringify(issue));
630
725
  }
726
+ /**
727
+ * GET /api/worktree-config — returns the canonical `ResolvedWorktreeConfig`
728
+ * for the requested workspace (defaults / cache / global / project /
729
+ * project-local, deep-merged per `lib/config.ts:loadConfig`). This is
730
+ * the single source of truth for any client that needs to act on
731
+ * worktree config (currently the VSCode extension's "Open Dev URL"
732
+ * surface; the dashboard is welcome to use it too).
733
+ *
734
+ * Side effect: lazily installs a directory watcher on the workspace's
735
+ * `.codev/` so any subsequent edit to `config.json` /
736
+ * `config.local.json` fans out a `worktree-config-updated` SSE event
737
+ * — clients refetch via this same endpoint and re-render.
738
+ */
739
+ function handleWorktreeConfigView(res, url) {
740
+ let workspaceRoot = url.searchParams.get('workspace');
741
+ if (!workspaceRoot) {
742
+ const knownPaths = getKnownWorkspacePaths();
743
+ workspaceRoot = knownPaths.find(p => !p.includes('/.builders/')) || null;
744
+ }
745
+ if (!workspaceRoot) {
746
+ res.writeHead(400, { 'Content-Type': 'application/json' });
747
+ res.end(JSON.stringify({ error: 'Missing workspace' }));
748
+ return;
749
+ }
750
+ try {
751
+ const config = getWorktreeConfig(workspaceRoot);
752
+ ensureWorktreeConfigWatcher(workspaceRoot);
753
+ res.writeHead(200, { 'Content-Type': 'application/json' });
754
+ res.end(JSON.stringify(config));
755
+ }
756
+ catch (error) {
757
+ const message = error instanceof Error ? error.message : String(error);
758
+ res.writeHead(500, { 'Content-Type': 'application/json' });
759
+ res.end(JSON.stringify({ error: `Failed to resolve worktree config: ${message}` }));
760
+ }
761
+ }
631
762
  function handleOverviewRefresh(res, ctx) {
632
763
  overviewCache.invalidate();
633
764
  // Bugfix #388: Broadcast SSE event so all connected dashboard clients
@@ -1082,6 +1213,36 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
1082
1213
  if (req.method === 'POST' && apiPath === 'stop') {
1083
1214
  return handleWorkspaceStopAll(res, workspacePath);
1084
1215
  }
1216
+ // DELETE /api/architects/:name - Remove a sibling architect (Spec 786 Phase 4)
1217
+ // Workspace-scoped variant of /api/workspaces/:encoded/architects/:name —
1218
+ // the workspace path comes from the /workspace/<base64>/ URL prefix already
1219
+ // resolved above. Used by the dashboard's close-button → confirmation
1220
+ // modal flow.
1221
+ const archDeleteMatch = apiPath.match(/^architects\/([^/]+)$/);
1222
+ if (req.method === 'DELETE' && archDeleteMatch) {
1223
+ const name = decodeURIComponent(archDeleteMatch[1]);
1224
+ const result = await removeArchitect(workspacePath, name);
1225
+ if (result.success) {
1226
+ // Spec 823 Phase 4 (iter-1 Codex): emit architects-updated from
1227
+ // every successful remove path, not just the /api/workspaces/
1228
+ // route, so the VSCode tree refreshes when the dashboard's
1229
+ // close-button → confirmation modal triggers the remove.
1230
+ ctx.broadcastNotification({
1231
+ type: 'architects-updated',
1232
+ title: 'Architects updated',
1233
+ body: JSON.stringify({ workspace: workspacePath }),
1234
+ workspace: workspacePath,
1235
+ });
1236
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1237
+ res.end(JSON.stringify({ success: true }));
1238
+ }
1239
+ else {
1240
+ const status = result.error?.includes('not running') || result.error?.includes('not found') ? 404 : 400;
1241
+ res.writeHead(status, { 'Content-Type': 'application/json' });
1242
+ res.end(JSON.stringify({ success: false, error: result.error }));
1243
+ }
1244
+ return;
1245
+ }
1085
1246
  // GET /api/files - Return workspace directory tree for file browser (Spec 0092)
1086
1247
  if (req.method === 'GET' && apiPath === 'files') {
1087
1248
  return handleWorkspaceFiles(res, url, workspacePath);
@@ -1156,7 +1317,7 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
1156
1317
  }
1157
1318
  // GET /api/overview - Work view overview data (Spec 0126 Phase 4)
1158
1319
  if (req.method === 'GET' && apiPath === 'overview') {
1159
- return handleOverview(res, url, workspacePath);
1320
+ return handleOverview(res, url, workspacePath, ctx);
1160
1321
  }
1161
1322
  // POST /api/overview/refresh - Invalidate overview cache (Spec 0126 Phase 4)
1162
1323
  if (req.method === 'POST' && apiPath === 'overview/refresh') {
@@ -1260,6 +1421,23 @@ async function handleWorkspaceState(res, workspacePath) {
1260
1421
  });
1261
1422
  }
1262
1423
  }
1424
+ // Spec 786 Phase 4: build a lookup from builder id → spawned_by_architect so
1425
+ // the dashboard's remove-architect confirmation modal can show which builders
1426
+ // would lose their spawning architect (informational; per OQ-A removal
1427
+ // proceeds regardless and they fall back to `main` routing). The data lives
1428
+ // in `state.db.builders.spawned_by_architect` but the in-memory cache used by
1429
+ // /api/state doesn't carry it — we read it explicitly here. Single query
1430
+ // amortised across all builders rather than per-builder lookups.
1431
+ const spawnedByMap = new Map();
1432
+ try {
1433
+ for (const b of getBuilders()) {
1434
+ spawnedByMap.set(b.id, b.spawnedByArchitect ?? null);
1435
+ }
1436
+ }
1437
+ catch {
1438
+ // DB unavailable — modal degrades to "no in-flight builders" display.
1439
+ // Acceptable since the modal text is informational per OQ-A.
1440
+ }
1263
1441
  // Add builders from refreshed cache
1264
1442
  for (const [builderId, terminalId] of entry.builders) {
1265
1443
  const session = manager.getSession(terminalId);
@@ -1276,6 +1454,10 @@ async function handleWorkspaceState(res, workspacePath) {
1276
1454
  type: 'spec',
1277
1455
  terminalId,
1278
1456
  persistent: isSessionPersistent(terminalId, session),
1457
+ // Spec 786 Phase 4: surface spawning architect to the dashboard so the
1458
+ // remove-architect modal can show affected builders. May be undefined
1459
+ // when the builder row isn't in state.db (e.g. ephemeral test builders).
1460
+ spawnedByArchitect: spawnedByMap.get(builderId) ?? null,
1279
1461
  });
1280
1462
  }
1281
1463
  }
@@ -1614,6 +1796,36 @@ async function handleWorkspaceTabDelete(res, ctx, workspacePath, tabId) {
1614
1796
  entry.architects.delete(name);
1615
1797
  }
1616
1798
  }
1799
+ else if (tabId.startsWith('architect:')) {
1800
+ // Spec 786 Phase 4 / PR iter-1 review fix: sibling architect tabs (Spec
1801
+ // 761 ids `architect:<name>`) are closable from the mobile TabBar, which
1802
+ // dispatches `DELETE /api/tabs/<tabId>`. Route the sibling close through
1803
+ // `removeArchitect()` so the full lifecycle (kills PTY, deletes state.db
1804
+ // row, suppresses cascaded delete via intentional-stop flag) runs the
1805
+ // same way as the desktop close button + CLI.
1806
+ const name = tabId.slice('architect:'.length);
1807
+ const result = await removeArchitect(workspacePath, name);
1808
+ if (result.success) {
1809
+ // Spec 823 Phase 4 (iter-1 Codex): emit architects-updated so the
1810
+ // VSCode tree refreshes when the remove originates from the mobile
1811
+ // TabBar close (which doesn't trigger the dashboard's add/remove
1812
+ // SSE event from within VSCode).
1813
+ ctx.broadcastNotification({
1814
+ type: 'architects-updated',
1815
+ title: 'Architects updated',
1816
+ body: JSON.stringify({ workspace: workspacePath }),
1817
+ workspace: workspacePath,
1818
+ });
1819
+ res.writeHead(204);
1820
+ res.end();
1821
+ }
1822
+ else {
1823
+ const status = result.error?.includes('not found') || result.error?.includes('not running') ? 404 : 400;
1824
+ res.writeHead(status, { 'Content-Type': 'application/json' });
1825
+ res.end(JSON.stringify({ success: false, error: result.error }));
1826
+ }
1827
+ return;
1828
+ }
1617
1829
  if (terminalId) {
1618
1830
  // Disable shellper auto-restart if applicable, then kill the PtySession
1619
1831
  await killTerminalWithShellper(manager, terminalId);
@@ -1630,6 +1842,28 @@ async function handleWorkspaceTabDelete(res, ctx, workspacePath, tabId) {
1630
1842
  async function handleWorkspaceStopAll(res, workspacePath) {
1631
1843
  const entry = getWorkspaceTerminalsEntry(workspacePath);
1632
1844
  const manager = getTerminalManager();
1845
+ // Spec 786 PR iter-2 race-fix (Codex): explicitly delete every architect's
1846
+ // `state.db.architect` row BEFORE killing or clearing the registry. The
1847
+ // cascaded architect exit handlers in tower-instances.ts (lines 452-...,
1848
+ // 501-..., etc.) and tower-terminals.ts work by scanning
1849
+ // `currentEntry.architects` to recover the architect name from a dead
1850
+ // terminal id. But stop-all clears that registry synchronously after the
1851
+ // kills, before node-pty's async 'exit' events fire — so by the time the
1852
+ // exit handlers look up the name, it's already gone, and they silently
1853
+ // skip the row deletion. Result: stale `state.db.architect` rows survive
1854
+ // what's supposed to be a full wipe, and `launchInstance` reconciliation
1855
+ // re-spawns them on the next workspace start.
1856
+ //
1857
+ // The intentional-stop flag isn't the right tool here either — stop-all
1858
+ // explicitly wants the rows gone. Pre-emptive deletion makes it
1859
+ // explicit and order-independent: even if the exit handler somehow runs
1860
+ // first, `setArchitectByName(name, null)` is idempotent.
1861
+ for (const name of entry.architects.keys()) {
1862
+ try {
1863
+ setArchitectByName(name, null);
1864
+ }
1865
+ catch { /* best-effort cleanup */ }
1866
+ }
1633
1867
  // Kill all terminals (disable shellper auto-restart if applicable).
1634
1868
  // Spec 755: iterate all named architects, not just the singleton.
1635
1869
  for (const terminalId of entry.architects.values()) {