@proletariat/cli 0.3.55 → 0.3.57

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 (246) hide show
  1. package/README.md +74 -5
  2. package/bin/validate-better-sqlite3.cjs +44 -5
  3. package/dist/commands/agent/cleanup.d.ts +1 -0
  4. package/dist/commands/agent/cleanup.js +11 -1
  5. package/dist/commands/agent/cleanup.js.map +1 -1
  6. package/dist/commands/agent/rebuild.js +1 -1
  7. package/dist/commands/agent/rebuild.js.map +1 -1
  8. package/dist/commands/asana/import.d.ts +14 -0
  9. package/dist/commands/asana/import.js +240 -0
  10. package/dist/commands/asana/import.js.map +1 -0
  11. package/dist/commands/claude-yolo.d.ts +20 -0
  12. package/dist/commands/claude-yolo.js +86 -0
  13. package/dist/commands/claude-yolo.js.map +1 -0
  14. package/dist/commands/codex/index.d.ts +23 -0
  15. package/dist/commands/codex/index.js +168 -0
  16. package/dist/commands/codex/index.js.map +1 -0
  17. package/dist/commands/codex-yolo.d.ts +20 -0
  18. package/dist/commands/codex-yolo.js +86 -0
  19. package/dist/commands/codex-yolo.js.map +1 -0
  20. package/dist/commands/config/index.js +2 -2
  21. package/dist/commands/config/index.js.map +1 -1
  22. package/dist/commands/dashboard.d.ts +38 -0
  23. package/dist/commands/dashboard.js +352 -0
  24. package/dist/commands/dashboard.js.map +1 -0
  25. package/dist/commands/docker/clean.js +1 -1
  26. package/dist/commands/docker/clean.js.map +1 -1
  27. package/dist/commands/docker/list.js +1 -1
  28. package/dist/commands/docker/list.js.map +1 -1
  29. package/dist/commands/docker/logs.js +1 -1
  30. package/dist/commands/docker/logs.js.map +1 -1
  31. package/dist/commands/docker/restart.js +1 -1
  32. package/dist/commands/docker/restart.js.map +1 -1
  33. package/dist/commands/docker/shell.js +1 -1
  34. package/dist/commands/docker/shell.js.map +1 -1
  35. package/dist/commands/docker/start.js +1 -1
  36. package/dist/commands/docker/start.js.map +1 -1
  37. package/dist/commands/docker/stop.js +1 -1
  38. package/dist/commands/docker/stop.js.map +1 -1
  39. package/dist/commands/docker/sync.js +1 -1
  40. package/dist/commands/docker/sync.js.map +1 -1
  41. package/dist/commands/execution/config.js +2 -2
  42. package/dist/commands/execution/config.js.map +1 -1
  43. package/dist/commands/execution/list.js +1 -1
  44. package/dist/commands/execution/list.js.map +1 -1
  45. package/dist/commands/execution/logs.js +10 -8
  46. package/dist/commands/execution/logs.js.map +1 -1
  47. package/dist/commands/execution/stop.js +1 -1
  48. package/dist/commands/execution/stop.js.map +1 -1
  49. package/dist/commands/execution/view.js +1 -1
  50. package/dist/commands/execution/view.js.map +1 -1
  51. package/dist/commands/init.d.ts +0 -16
  52. package/dist/commands/init.js +50 -160
  53. package/dist/commands/init.js.map +1 -1
  54. package/dist/commands/logs.d.ts +20 -0
  55. package/dist/commands/logs.js +84 -0
  56. package/dist/commands/logs.js.map +1 -0
  57. package/dist/commands/mcp-server.js +1 -1
  58. package/dist/commands/mcp-server.js.map +1 -1
  59. package/dist/commands/new.d.ts +27 -0
  60. package/dist/commands/new.js +184 -0
  61. package/dist/commands/new.js.map +1 -0
  62. package/dist/commands/orchestrator/attach.d.ts +1 -0
  63. package/dist/commands/orchestrator/attach.js +159 -26
  64. package/dist/commands/orchestrator/attach.js.map +1 -1
  65. package/dist/commands/orchestrator/start.d.ts +20 -0
  66. package/dist/commands/orchestrator/start.js +123 -14
  67. package/dist/commands/orchestrator/start.js.map +1 -1
  68. package/dist/commands/orchestrator/status.d.ts +4 -0
  69. package/dist/commands/orchestrator/status.js +70 -29
  70. package/dist/commands/orchestrator/status.js.map +1 -1
  71. package/dist/commands/orchestrator/stop.js +57 -23
  72. package/dist/commands/orchestrator/stop.js.map +1 -1
  73. package/dist/commands/peek.d.ts +20 -0
  74. package/dist/commands/peek.js +84 -0
  75. package/dist/commands/peek.js.map +1 -0
  76. package/dist/commands/poke.d.ts +20 -0
  77. package/dist/commands/poke.js +89 -0
  78. package/dist/commands/poke.js.map +1 -0
  79. package/dist/commands/pr/link.js +1 -1
  80. package/dist/commands/pr/link.js.map +1 -1
  81. package/dist/commands/pr/status.js +1 -1
  82. package/dist/commands/pr/status.js.map +1 -1
  83. package/dist/commands/ps.d.ts +18 -0
  84. package/dist/commands/ps.js +123 -0
  85. package/dist/commands/ps.js.map +1 -0
  86. package/dist/commands/qa/index.js +1 -1
  87. package/dist/commands/qa/index.js.map +1 -1
  88. package/dist/commands/repo/add.js +1 -1
  89. package/dist/commands/repo/add.js.map +1 -1
  90. package/dist/commands/repo/fix-remotes.d.ts +14 -0
  91. package/dist/commands/repo/fix-remotes.js +154 -0
  92. package/dist/commands/repo/fix-remotes.js.map +1 -0
  93. package/dist/commands/repo/list.js +1 -1
  94. package/dist/commands/repo/list.js.map +1 -1
  95. package/dist/commands/repo/remove.js +1 -1
  96. package/dist/commands/repo/remove.js.map +1 -1
  97. package/dist/commands/repo/view.js +1 -1
  98. package/dist/commands/repo/view.js.map +1 -1
  99. package/dist/commands/run/index.d.ts +25 -0
  100. package/dist/commands/run/index.js +212 -0
  101. package/dist/commands/run/index.js.map +1 -0
  102. package/dist/commands/session/index.js +4 -0
  103. package/dist/commands/session/index.js.map +1 -1
  104. package/dist/commands/session/prune.d.ts +18 -0
  105. package/dist/commands/session/prune.js +327 -0
  106. package/dist/commands/session/prune.js.map +1 -0
  107. package/dist/commands/shortcut/connect.d.ts +15 -0
  108. package/dist/commands/shortcut/connect.js +210 -0
  109. package/dist/commands/shortcut/connect.js.map +1 -0
  110. package/dist/commands/stop.d.ts +19 -0
  111. package/dist/commands/stop.js +83 -0
  112. package/dist/commands/stop.js.map +1 -0
  113. package/dist/commands/work/asana.d.ts +23 -0
  114. package/dist/commands/work/asana.js +213 -0
  115. package/dist/commands/work/asana.js.map +1 -0
  116. package/dist/commands/work/complete.js +1 -1
  117. package/dist/commands/work/complete.js.map +1 -1
  118. package/dist/commands/work/ready.js +1 -1
  119. package/dist/commands/work/ready.js.map +1 -1
  120. package/dist/commands/work/review.js +1 -1
  121. package/dist/commands/work/review.js.map +1 -1
  122. package/dist/commands/work/revise.js +1 -1
  123. package/dist/commands/work/revise.js.map +1 -1
  124. package/dist/commands/work/shortcut.d.ts +23 -0
  125. package/dist/commands/work/shortcut.js +212 -0
  126. package/dist/commands/work/shortcut.js.map +1 -0
  127. package/dist/commands/work/spawn.d.ts +3 -0
  128. package/dist/commands/work/spawn.js +175 -2
  129. package/dist/commands/work/spawn.js.map +1 -1
  130. package/dist/commands/work/start.js +54 -45
  131. package/dist/commands/work/start.js.map +1 -1
  132. package/dist/commands/work/watch.js +1 -1
  133. package/dist/commands/work/watch.js.map +1 -1
  134. package/dist/commands/workspace/add.js +1 -1
  135. package/dist/commands/workspace/add.js.map +1 -1
  136. package/dist/commands/workspace/list.js +1 -1
  137. package/dist/commands/workspace/list.js.map +1 -1
  138. package/dist/hooks/init.d.ts +1 -1
  139. package/dist/hooks/init.js +6 -6
  140. package/dist/hooks/init.js.map +1 -1
  141. package/dist/lib/agent-naming.d.ts +15 -0
  142. package/dist/lib/agent-naming.js +32 -0
  143. package/dist/lib/agent-naming.js.map +1 -0
  144. package/dist/lib/agents/commands.d.ts +2 -0
  145. package/dist/lib/agents/commands.js +56 -51
  146. package/dist/lib/agents/commands.js.map +1 -1
  147. package/dist/lib/agents/index.js +11 -7
  148. package/dist/lib/agents/index.js.map +1 -1
  149. package/dist/lib/asana/client.d.ts +8 -0
  150. package/dist/lib/asana/client.js +26 -7
  151. package/dist/lib/asana/client.js.map +1 -1
  152. package/dist/lib/asana/types.d.ts +20 -0
  153. package/dist/lib/branch/index.d.ts +36 -0
  154. package/dist/lib/branch/index.js +129 -0
  155. package/dist/lib/branch/index.js.map +1 -1
  156. package/dist/lib/database/index.js +1 -1
  157. package/dist/lib/database/index.js.map +1 -1
  158. package/dist/lib/database/native-validation.d.ts +2 -0
  159. package/dist/lib/database/native-validation.js +30 -1
  160. package/dist/lib/database/native-validation.js.map +1 -1
  161. package/dist/lib/events/emitting-runner.d.ts +30 -0
  162. package/dist/lib/events/emitting-runner.js +95 -0
  163. package/dist/lib/events/emitting-runner.js.map +1 -0
  164. package/dist/lib/events/event-bus.d.ts +54 -0
  165. package/dist/lib/events/event-bus.js +107 -0
  166. package/dist/lib/events/event-bus.js.map +1 -0
  167. package/dist/lib/events/events.d.ts +67 -0
  168. package/dist/lib/events/events.js +8 -0
  169. package/dist/lib/events/events.js.map +1 -0
  170. package/dist/lib/events/index.d.ts +7 -0
  171. package/dist/lib/events/index.js +6 -0
  172. package/dist/lib/events/index.js.map +1 -0
  173. package/dist/lib/execution/devcontainer.d.ts +27 -0
  174. package/dist/lib/execution/devcontainer.js +80 -0
  175. package/dist/lib/execution/devcontainer.js.map +1 -1
  176. package/dist/lib/execution/runners.d.ts +28 -0
  177. package/dist/lib/execution/runners.js +536 -35
  178. package/dist/lib/execution/runners.js.map +1 -1
  179. package/dist/lib/execution/spawner.js +38 -17
  180. package/dist/lib/execution/spawner.js.map +1 -1
  181. package/dist/lib/execution/types.d.ts +2 -0
  182. package/dist/lib/execution/types.js.map +1 -1
  183. package/dist/lib/external-issues/adapters.d.ts +34 -0
  184. package/dist/lib/external-issues/adapters.js +191 -0
  185. package/dist/lib/external-issues/adapters.js.map +1 -1
  186. package/dist/lib/external-issues/asana.d.ts +75 -0
  187. package/dist/lib/external-issues/asana.js +243 -0
  188. package/dist/lib/external-issues/asana.js.map +1 -0
  189. package/dist/lib/external-issues/index.d.ts +4 -2
  190. package/dist/lib/external-issues/index.js +6 -2
  191. package/dist/lib/external-issues/index.js.map +1 -1
  192. package/dist/lib/external-issues/linear.js.map +1 -1
  193. package/dist/lib/external-issues/mapping-store.js +1 -1
  194. package/dist/lib/external-issues/shortcut.d.ts +86 -0
  195. package/dist/lib/external-issues/shortcut.js +274 -0
  196. package/dist/lib/external-issues/shortcut.js.map +1 -0
  197. package/dist/lib/external-issues/types.d.ts +3 -3
  198. package/dist/lib/external-issues/types.js +1 -1
  199. package/dist/lib/external-issues/types.js.map +1 -1
  200. package/dist/lib/mcp/tools/cli-passthrough.js +1 -1
  201. package/dist/lib/mcp/tools/cli-passthrough.js.map +1 -1
  202. package/dist/lib/mcp/tools/work.js +1 -1
  203. package/dist/lib/mcp/tools/work.js.map +1 -1
  204. package/dist/lib/pmo/index.d.ts +2 -2
  205. package/dist/lib/pmo/index.js +3 -3
  206. package/dist/lib/pmo/index.js.map +1 -1
  207. package/dist/lib/pmo/sync-manager.js +1 -1
  208. package/dist/lib/pmo/sync-manager.js.map +1 -1
  209. package/dist/lib/pmo/watcher.js +1 -1
  210. package/dist/lib/pmo/watcher.js.map +1 -1
  211. package/dist/lib/repos/git.d.ts +44 -0
  212. package/dist/lib/repos/git.js +127 -0
  213. package/dist/lib/repos/git.js.map +1 -1
  214. package/dist/lib/repos/index.js +36 -3
  215. package/dist/lib/repos/index.js.map +1 -1
  216. package/dist/lib/runners/agent-runner.d.ts +92 -0
  217. package/dist/lib/runners/agent-runner.js +9 -0
  218. package/dist/lib/runners/agent-runner.js.map +1 -0
  219. package/dist/lib/runners/claude-code-runner.d.ts +53 -0
  220. package/dist/lib/runners/claude-code-runner.js +132 -0
  221. package/dist/lib/runners/claude-code-runner.js.map +1 -0
  222. package/dist/lib/runners/index.d.ts +34 -0
  223. package/dist/lib/runners/index.js +97 -0
  224. package/dist/lib/runners/index.js.map +1 -0
  225. package/dist/lib/session-store.d.ts +70 -0
  226. package/dist/lib/session-store.js +167 -0
  227. package/dist/lib/session-store.js.map +1 -0
  228. package/dist/lib/shortcut/config.d.ts +44 -0
  229. package/dist/lib/shortcut/config.js +98 -0
  230. package/dist/lib/shortcut/config.js.map +1 -0
  231. package/dist/lib/shortcut/index.d.ts +7 -0
  232. package/dist/lib/shortcut/index.js +7 -0
  233. package/dist/lib/shortcut/index.js.map +1 -0
  234. package/dist/lib/work-source/client.d.ts +10 -0
  235. package/dist/lib/work-source/client.js +74 -0
  236. package/dist/lib/work-source/client.js.map +1 -0
  237. package/dist/lib/work-source/config.d.ts +1 -1
  238. package/dist/lib/work-source/config.js +17 -1
  239. package/dist/lib/work-source/config.js.map +1 -1
  240. package/dist/lib/work-source/index.d.ts +1 -0
  241. package/dist/lib/work-source/index.js +1 -0
  242. package/dist/lib/work-source/index.js.map +1 -1
  243. package/dist/lib/workspace.js +2 -2
  244. package/dist/lib/workspace.js.map +1 -1
  245. package/oclif.manifest.json +8135 -6979
  246. package/package.json +1 -1
@@ -8,9 +8,10 @@ import { spawn, execSync } from 'node:child_process';
8
8
  import * as fs from 'node:fs';
9
9
  import * as path from 'node:path';
10
10
  import * as os from 'node:os';
11
+ import { fileURLToPath } from 'node:url';
11
12
  import { DEFAULT_EXECUTION_CONFIG, } from './types.js';
12
13
  import { getSetTitleCommands } from '../terminal.js';
13
- import { readDevcontainerJson } from './devcontainer.js';
14
+ import { readDevcontainerJson, generateOrchestratorDockerfile } from './devcontainer.js';
14
15
  import { getCodexCommand, resolveCodexExecutionContext, validateCodexMode } from './codex-adapter.js';
15
16
  // =============================================================================
16
17
  // Terminal Title Helpers
@@ -387,24 +388,134 @@ export function runExecutorPreflight(environment, executor, options) {
387
388
  }
388
389
  return { ok: true };
389
390
  }
391
+ const ORCHESTRATOR_COMMAND_REGISTRY = [
392
+ {
393
+ title: 'Agent Lifecycle',
394
+ commands: [
395
+ { cmd: 'prlt work start <ticket> --ephemeral --skip-permissions --create-pr --display background --action implement --run-on-host --yes', desc: 'Spawn an agent for a ticket', checkPath: 'work/start' },
396
+ { cmd: 'prlt session list', desc: 'List running sessions', checkPath: 'session/list' },
397
+ { cmd: 'prlt session inspect <agent>', desc: 'Inspect session details', checkPath: 'session/inspect' },
398
+ { cmd: 'prlt session poke <agent> \'message\'', desc: 'Send message to agent', checkPath: 'session/poke' },
399
+ { cmd: 'prlt session peek <agent> --lines 200', desc: 'Read agent output', checkPath: 'session/peek' },
400
+ { cmd: 'prlt session health', desc: 'Check health of all sessions', checkPath: 'session/health' },
401
+ { cmd: 'prlt session restart <agent>', desc: 'Restart a stuck agent', checkPath: 'session/restart' },
402
+ { cmd: 'prlt session exec <agent> -- git status', desc: 'Run command in agent context', checkPath: 'session/exec' },
403
+ { cmd: 'prlt session prune', desc: 'Clean up dead sessions', checkPath: 'session/prune' },
404
+ ],
405
+ },
406
+ {
407
+ title: 'Board Management',
408
+ commands: [
409
+ { cmd: 'prlt board view', desc: 'View the board', checkPath: 'board/view' },
410
+ { cmd: 'prlt ticket list', desc: 'List tickets', checkPath: 'ticket/list' },
411
+ { cmd: 'prlt ticket show <id>', desc: 'Show ticket details', checkPath: 'ticket/show' },
412
+ { cmd: 'prlt ticket create --title \'x\' --description \'y\'', desc: 'Create a ticket', checkPath: 'ticket/create' },
413
+ { cmd: 'prlt ticket edit <id> --title \'...\' --add-ac \'...\'', desc: 'Edit ticket fields', checkPath: 'ticket/edit' },
414
+ ],
415
+ },
416
+ {
417
+ title: 'PR Workflow',
418
+ commands: [
419
+ { cmd: 'gh pr list', desc: 'List open PRs' },
420
+ { cmd: 'gh pr view <num>', desc: 'View PR details' },
421
+ { cmd: 'gh pr checks <num>', desc: 'Check CI status' },
422
+ { cmd: 'gh pr merge <num> --squash', desc: 'Merge PR (squash only)' },
423
+ ],
424
+ },
425
+ ];
426
+ const ORCHESTRATOR_ANTI_PATTERNS = [
427
+ { bad: 'docker exec <container> ...', good: 'prlt session exec', checkPath: 'session/exec' },
428
+ { bad: 'tmux send-keys ...', good: 'prlt session poke', checkPath: 'session/poke' },
429
+ { bad: 'tmux capture-pane ...', good: 'prlt session peek', checkPath: 'session/peek' },
430
+ { bad: 'Direct git operations on agent worktrees', good: 'prlt session exec', checkPath: 'session/exec' },
431
+ ];
390
432
  /**
391
- * Build a role-specific prompt for orchestrator sessions.
392
- * Unlike ticket workers, orchestrators are long-running managers that delegate work.
433
+ * Resolve the commands directory for dynamic command availability checks.
434
+ * Looks for compiled command files under dist/commands/.
393
435
  */
394
- function buildOrchestratorPrompt(context) {
395
- const hqName = context.hqName || 'workspace';
396
- let prompt = `# Orchestrator: ${hqName}\n\n`;
397
- prompt += `You are the orchestrator for the **${hqName}** workspace using the prlt ecosystem.\n\n`;
436
+ let _commandsDir = null;
437
+ function getCommandsDir() {
438
+ if (_commandsDir === null) {
439
+ const currentFile = fileURLToPath(import.meta.url);
440
+ // From dist/lib/execution/runners.js → dist/commands/
441
+ _commandsDir = path.resolve(path.dirname(currentFile), '..', '..', 'commands');
442
+ }
443
+ return _commandsDir;
444
+ }
445
+ function isCommandAvailable(checkPath) {
446
+ const dir = getCommandsDir();
447
+ // Check for compiled .js file or directory (which would contain index.js)
448
+ return fs.existsSync(path.join(dir, `${checkPath}.js`)) || fs.existsSync(path.join(dir, checkPath));
449
+ }
450
+ /**
451
+ * Build the dynamic command reference section for the orchestrator prompt.
452
+ * Only includes commands that are actually available in this build.
453
+ */
454
+ function buildOrchestratorCommandReference() {
455
+ let ref = '';
456
+ for (const category of ORCHESTRATOR_COMMAND_REGISTRY) {
457
+ const available = category.commands.filter(c => !c.checkPath || isCommandAvailable(c.checkPath));
458
+ if (available.length === 0)
459
+ continue;
460
+ ref += `### ${category.title}\n`;
461
+ for (const cmd of available) {
462
+ ref += `- \`${cmd.cmd}\` — ${cmd.desc}\n`;
463
+ }
464
+ ref += '\n';
465
+ }
466
+ return ref;
467
+ }
468
+ /**
469
+ * Build the anti-patterns section for the orchestrator prompt.
470
+ * Only includes anti-patterns where the prlt replacement is available.
471
+ */
472
+ function buildOrchestratorAntiPatterns() {
473
+ const available = ORCHESTRATOR_ANTI_PATTERNS.filter(ap => !ap.checkPath || isCommandAvailable(ap.checkPath));
474
+ if (available.length === 0)
475
+ return '';
476
+ let section = `## Anti-Patterns — NEVER DO\n\n`;
477
+ for (const ap of available) {
478
+ section += `- \`${ap.bad}\` → use \`${ap.good}\` instead\n`;
479
+ }
480
+ section += `\n`;
481
+ return section;
482
+ }
483
+ /**
484
+ * Build the shared orchestrator prompt body (role, runtime, commands, anti-patterns).
485
+ * Used by both buildOrchestratorSystemPrompt and buildOrchestratorPrompt.
486
+ */
487
+ function buildOrchestratorBody(hqName, context) {
488
+ let prompt = '';
489
+ // Runtime declaration
490
+ prompt += `## prlt Is Your Orchestration Runtime\n\n`;
491
+ prompt += `prlt is your orchestration runtime. NEVER use raw docker exec, tmux send-keys, or direct container access. `;
492
+ prompt += `All orchestration goes through prlt. Every agent interaction, session management, and board operation `;
493
+ prompt += `has a dedicated prlt command. Using raw infrastructure commands bypasses session tracking, breaks `;
494
+ prompt += `health monitoring, and creates orphaned processes.\n\n`;
495
+ // Role
398
496
  prompt += `## Your Role\n`;
399
- prompt += `- View and manage work using \`prlt\` CLI commands or MCP tools\n`;
400
- prompt += `- Delegate tasks to agents do NOT implement work yourself\n`;
401
- prompt += `- Monitor agent progress, review completed work, and manage the board\n`;
402
- prompt += `- Spawn agents for Ready tickets, review PRs, merge completed work\n\n`;
403
- prompt += `## Available Tools\n`;
404
- prompt += `- Board: \`prlt board\`, \`prlt ticket list\`, \`prlt ticket show <id>\`, \`prlt work status\`\n`;
405
- prompt += `- Agents: \`prlt work start <ticket-id>\`, \`prlt session list\`, \`prlt session peek <session>\`, \`prlt session poke <session> "message"\`\n`;
406
- prompt += `- PRs: \`gh pr list\`, \`gh pr view\`, \`gh pr merge\`\n`;
407
- prompt += `- MCP: All prlt MCP tools are available\n\n`;
497
+ prompt += `- Plan and prioritize work across the board\n`;
498
+ prompt += `- Delegate implementation to agents via \`prlt work start\`\n`;
499
+ prompt += `- Monitor agent progress and review completed work\n`;
500
+ prompt += `- Merge completed PRs via \`gh pr merge --squash\`\n`;
501
+ prompt += `- Never write code or make changes to source files yourself\n\n`;
502
+ // Command reference (dynamically generated)
503
+ prompt += `## Command Reference\n\n`;
504
+ prompt += buildOrchestratorCommandReference();
505
+ // Spawning agents (detailed example)
506
+ prompt += `## Spawning Agents\n`;
507
+ prompt += `\`\`\`\n`;
508
+ prompt += `script -q /dev/null prlt work start TKT-XXXX --ephemeral --skip-permissions --create-pr --display background --action implement --run-on-host --yes\n`;
509
+ prompt += `\`\`\`\n`;
510
+ prompt += `- Review: \`--action review-comment\`\n`;
511
+ prompt += `- Fix: \`--action review-fix\`\n\n`;
512
+ // Anti-patterns (dynamically generated)
513
+ prompt += buildOrchestratorAntiPatterns();
514
+ // Workflow
515
+ prompt += `## Workflow\n`;
516
+ prompt += `- Squash merge only: \`gh pr merge --squash\`\n`;
517
+ prompt += `- After merging: subsequent PRs from parallel agents will need rebase\n`;
518
+ prompt += `- Kill stale sessions after their PRs are merged\n\n`;
408
519
  // Load .orchestrator-context.md from HQ root if it exists
409
520
  if (context.hqPath) {
410
521
  const contextFilePath = path.join(context.hqPath, '.orchestrator-context.md');
@@ -420,6 +531,29 @@ function buildOrchestratorPrompt(context) {
420
531
  }
421
532
  }
422
533
  }
534
+ return prompt;
535
+ }
536
+ /**
537
+ * Build the system prompt for orchestrator sessions.
538
+ * This is injected via Claude Code's --system-prompt flag so the orchestrator
539
+ * knows its role immediately without relying on CLAUDE.md.
540
+ */
541
+ export function buildOrchestratorSystemPrompt(context) {
542
+ const hqName = context.hqName || 'workspace';
543
+ let prompt = `You are an orchestrator for the **${hqName}** project. `;
544
+ prompt += `Do not implement any work yourself. `;
545
+ prompt += `Your job is to review, plan, investigate, delegate (via \`prlt work start\`), and review completed work.\n\n`;
546
+ prompt += buildOrchestratorBody(hqName, context);
547
+ return prompt;
548
+ }
549
+ function buildOrchestratorPrompt(context) {
550
+ // Full prompt including role context — used for non-Claude executors that
551
+ // don't support --system-prompt. For Claude Code, runHost() splits this into
552
+ // a system prompt (role/tools) + a shorter user message.
553
+ const hqName = context.hqName || 'workspace';
554
+ let prompt = `# Orchestrator: ${hqName}\n\n`;
555
+ prompt += `You are the orchestrator for the **${hqName}** workspace using the prlt ecosystem.\n\n`;
556
+ prompt += buildOrchestratorBody(hqName, context);
423
557
  // Include user's custom prompt or action content
424
558
  if (context.actionPrompt) {
425
559
  prompt += `## Instructions\n\n${context.actionPrompt}\n`;
@@ -566,8 +700,24 @@ export async function runHost(context, executor, config, displayMode = 'terminal
566
700
  const timestamp = Date.now();
567
701
  const scriptPath = path.join(baseDir, `exec-${context.ticketId}-${timestamp}.sh`);
568
702
  const promptPath = path.join(baseDir, `prompt-${context.ticketId}-${timestamp}.txt`);
569
- // Write prompt to separate file to avoid any shell escaping issues
570
- fs.writeFileSync(promptPath, prompt, { mode: 0o644 });
703
+ // For orchestrator sessions with Claude Code, split the prompt:
704
+ // - System prompt (role/tools/context) injected via --system-prompt flag
705
+ // - User message (action instructions or default) → passed as the initial message
706
+ // Non-Claude executors get the full combined prompt as the user message.
707
+ let systemPromptPath = null;
708
+ if (context.isOrchestrator && isClaudeExecutor(executor)) {
709
+ const systemPrompt = buildOrchestratorSystemPrompt(context);
710
+ systemPromptPath = path.join(baseDir, `system-prompt-${context.ticketId}-${timestamp}.txt`);
711
+ fs.writeFileSync(systemPromptPath, systemPrompt, { mode: 0o644 });
712
+ // Override user message: just action instructions or a default startup message
713
+ const userMessage = context.actionPrompt
714
+ || 'You are now running as the orchestrator. Check the board status and report what you see.';
715
+ fs.writeFileSync(promptPath, userMessage, { mode: 0o644 });
716
+ }
717
+ else {
718
+ // Write full prompt (includes role context for non-Claude executors)
719
+ fs.writeFileSync(promptPath, prompt, { mode: 0o644 });
720
+ }
571
721
  // Build the executor command using getExecutorCommand() output
572
722
  // For Claude Code, we also support outputMode and additional flags
573
723
  // For non-Claude executors, we use the command as-is from getExecutorCommand()
@@ -579,7 +729,9 @@ export async function runHost(context, executor, config, displayMode = 'terminal
579
729
  const printFlag = config.outputMode === 'print' ? '-p ' : '';
580
730
  // --effort high: skips the effort level prompt for automated agents (TKT-1134)
581
731
  const effortFlag = skipPermissions ? '--effort high ' : '';
582
- executorInvocation = `${cmd} ${permissionsFlag}${effortFlag}${printFlag}"$(cat "$PROMPT_PATH")"`;
732
+ // Orchestrator sessions inject their role via --system-prompt
733
+ const systemPromptFlag = systemPromptPath ? '--system-prompt "$(cat "$SYSTEM_PROMPT_PATH")" ' : '';
734
+ executorInvocation = `${cmd} ${permissionsFlag}${effortFlag}${printFlag}${systemPromptFlag}"$(cat "$PROMPT_PATH")"`;
583
735
  }
584
736
  else {
585
737
  // Non-Claude executors: build command from getExecutorCommand() args
@@ -589,10 +741,24 @@ export async function runHost(context, executor, config, displayMode = 'terminal
589
741
  }
590
742
  // Build script that runs executor and keeps shell open after completion
591
743
  const setTitleCmds = getSetTitleCommands(windowTitle);
744
+ const systemPromptVar = systemPromptPath ? `\nSYSTEM_PROMPT_PATH="${systemPromptPath}"` : '';
745
+ // Ephemeral agents auto-close after completion instead of dropping to interactive shell
746
+ const postExecBlock = context.isEphemeral
747
+ ? `
748
+ echo ""
749
+ echo "✅ Ephemeral agent work complete. Session will auto-close in 5s..."
750
+ sleep 5
751
+ exit 0
752
+ `
753
+ : `
754
+ echo ""
755
+ echo "✅ Agent work complete. Press Enter to close or run more commands."
756
+ exec $SHELL
757
+ `;
592
758
  const scriptContent = `#!/bin/bash
593
759
  # Auto-generated script for ticket ${context.ticketId}
594
760
  SCRIPT_PATH="${scriptPath}"
595
- PROMPT_PATH="${promptPath}"
761
+ PROMPT_PATH="${promptPath}"${systemPromptVar}
596
762
  ${setTitleCmds}
597
763
  echo "🚀 Starting: ${sessionName}"
598
764
  echo ""
@@ -601,12 +767,8 @@ cd "${context.worktreePath}"
601
767
  (unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT; ${executorInvocation})
602
768
 
603
769
  # Clean up script and prompt files
604
- rm -f "$SCRIPT_PATH" "$PROMPT_PATH"
605
-
606
- echo ""
607
- echo "✅ Agent work complete. Press Enter to close or run more commands."
608
- exec $SHELL
609
- `;
770
+ rm -f "$SCRIPT_PATH" "$PROMPT_PATH"${systemPromptPath ? ' "$SYSTEM_PROMPT_PATH"' : ''}
771
+ ${postExecBlock}`;
610
772
  fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
611
773
  try {
612
774
  // Check if tmux is available
@@ -640,8 +802,10 @@ exec $SHELL
640
802
  if (displayMode === 'foreground') {
641
803
  try {
642
804
  // Clear screen and attach - this blocks until user detaches or claude exits
643
- // Use -CC for iTerm when control mode is enabled
644
- const fgTmuxAttach = buildTmuxAttachCommand(useControlMode);
805
+ // Never use -CC in foreground mode: control mode sends raw tmux protocol
806
+ // sequences (%begin, %output, %end) that render as garbled text unless
807
+ // iTerm's native CC handler is active (only happens in new tabs opened via AppleScript)
808
+ const fgTmuxAttach = buildTmuxAttachCommand(false);
645
809
  execSync(`clear && ${fgTmuxAttach} -t "${sessionName}"`, { stdio: 'inherit' });
646
810
  return {
647
811
  success: true,
@@ -1617,6 +1781,14 @@ async function runDevcontainerInTerminal(context, devcontainerCmd, config) {
1617
1781
  // Write script - run the command directly
1618
1782
  // No auth check needed - if auth is required, Claude will show "Invalid API key"
1619
1783
  // and user can run /login from there
1784
+ // Ephemeral agents auto-close after completion
1785
+ const postExecBlock = context.isEphemeral
1786
+ ? `echo ""
1787
+ echo "✅ Ephemeral agent work complete. Session will auto-close in 5s..."
1788
+ sleep 5
1789
+ exit 0`
1790
+ : `# Keep shell open after completion
1791
+ exec $SHELL`;
1620
1792
  const scriptContent = `#!/bin/bash
1621
1793
  # Auto-generated script for ticket ${context.ticketId}
1622
1794
  ${setTitleCmds}
@@ -1629,8 +1801,7 @@ ${devcontainerCmd}
1629
1801
  # Clean up script file
1630
1802
  rm -f "${scriptPath}"
1631
1803
 
1632
- # Keep shell open after completion
1633
- exec $SHELL
1804
+ ${postExecBlock}
1634
1805
  `;
1635
1806
  fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
1636
1807
  // Check if we should open in background (don't steal focus)
@@ -1855,6 +2026,15 @@ async function runDevcontainerInTmux(context, devcontainerCmd, config, displayMo
1855
2026
  // Unset CI to prevent Claude from detecting CI environment which suppresses TUI output
1856
2027
  // Unset CLAUDECODE to allow Claude Code to run (prevents nested session error)
1857
2028
  // Note: We keep DEVCONTAINER set so prlt workspace detection works correctly
2029
+ // Ephemeral agents auto-close after completion
2030
+ const containerPostExec = context.isEphemeral
2031
+ ? `echo ""
2032
+ echo "✅ Ephemeral agent work complete. Session will auto-close in 5s..."
2033
+ sleep 5
2034
+ exit 0`
2035
+ : `echo ""
2036
+ echo "✅ Agent work complete. Press Enter to close or run more commands."
2037
+ exec bash`;
1858
2038
  const tmuxScript = `#!/bin/bash
1859
2039
  export TERM=xterm-256color
1860
2040
  export COLORTERM=truecolor
@@ -1863,9 +2043,7 @@ unset CLAUDECODE
1863
2043
  echo "🚀 Starting: ${sessionName}"
1864
2044
  echo ""
1865
2045
  ${claudeCmd}
1866
- echo ""
1867
- echo "✅ Agent work complete. Press Enter to close or run more commands."
1868
- exec bash
2046
+ ${containerPostExec}
1869
2047
  `;
1870
2048
  const scriptPath = `/tmp/prlt-${sessionName}.sh`;
1871
2049
  // Write script and start tmux session inside container
@@ -1945,8 +2123,10 @@ exec bash
1945
2123
  if (displayMode === 'foreground') {
1946
2124
  try {
1947
2125
  // Clear screen and attach - this blocks until user detaches or claude exits
1948
- // Use -CC for iTerm when control mode is enabled
1949
- const fgTmuxAttach = buildTmuxAttachCommand(useControlMode, true);
2126
+ // Never use -CC in foreground mode: control mode sends raw tmux protocol
2127
+ // sequences (%begin, %output, %end) that render as garbled text unless
2128
+ // iTerm's native CC handler is active (only happens in new tabs opened via AppleScript)
2129
+ const fgTmuxAttach = buildTmuxAttachCommand(false, true);
1950
2130
  execSync(`clear && docker exec -it ${actualContainerId} ${fgTmuxAttach} -t "${sessionName}"`, { stdio: 'inherit' });
1951
2131
  return {
1952
2132
  success: true,
@@ -2211,6 +2391,327 @@ export async function runDocker(context, executor, config) {
2211
2391
  }
2212
2392
  }
2213
2393
  // =============================================================================
2394
+ // Orchestrator Docker Runner (Sibling Container Pattern)
2395
+ // =============================================================================
2396
+ /**
2397
+ * Run orchestrator in a Docker container using the sibling container pattern.
2398
+ *
2399
+ * Architecture:
2400
+ * ```
2401
+ * Host Docker daemon
2402
+ * ├── orchestrator container (has /var/run/docker.sock mounted)
2403
+ * ├── agent-1 container (spawned by orchestrator, sibling)
2404
+ * ├── agent-2 container (spawned by orchestrator, sibling)
2405
+ * ```
2406
+ *
2407
+ * The orchestrator container needs:
2408
+ * - HQ directory mounted (proletariat-hq)
2409
+ * - Docker socket mounted (/var/run/docker.sock) — so it can spawn agent containers as siblings
2410
+ * - prlt CLI installed in the container
2411
+ * - OAuth credentials for Claude Code (via Docker volume)
2412
+ * - tmux for session persistence inside the container
2413
+ */
2414
+ export async function runOrchestratorInDocker(context, executor, config, options) {
2415
+ const displayMode = options?.displayMode || 'background';
2416
+ const hqPath = context.hqPath || context.worktreePath;
2417
+ const hqName = context.hqName || 'default';
2418
+ const orchestratorName = context.agentName || 'main';
2419
+ // Container name matches tmux session name for consistency
2420
+ const containerName = `prlt-orchestrator-${(hqName).replace(/[^a-zA-Z0-9._-]/g, '-')}-${(orchestratorName).replace(/[^a-zA-Z0-9._-]/g, '-')}`;
2421
+ const imageName = `prlt-orchestrator-${(hqName).replace(/[^a-zA-Z0-9._-]/g, '-')}:latest`;
2422
+ try {
2423
+ // Check Docker is running
2424
+ if (!isDockerRunning()) {
2425
+ return {
2426
+ success: false,
2427
+ error: 'Docker is not running. Please start Docker Desktop and try again.',
2428
+ };
2429
+ }
2430
+ // Check if container already exists and is running
2431
+ if (containerExists(containerName)) {
2432
+ if (isContainerRunning(containerName)) {
2433
+ return {
2434
+ success: false,
2435
+ error: `Orchestrator container "${containerName}" is already running. Use "prlt orchestrator attach" to reattach.`,
2436
+ };
2437
+ }
2438
+ // Remove stopped container
2439
+ try {
2440
+ execSync(`docker rm -f ${containerName}`, { stdio: 'pipe' });
2441
+ }
2442
+ catch {
2443
+ // Ignore removal errors
2444
+ }
2445
+ }
2446
+ // Generate Dockerfile
2447
+ const orchestratorDockerOptions = {
2448
+ orchestratorName,
2449
+ hqPath,
2450
+ executor,
2451
+ };
2452
+ const dockerfileContent = generateOrchestratorDockerfile(orchestratorDockerOptions);
2453
+ // Write Dockerfile to temp directory
2454
+ const buildDir = path.join(hqPath, '.proletariat', 'orchestrator-docker');
2455
+ fs.mkdirSync(buildDir, { recursive: true });
2456
+ const dockerfilePath = path.join(buildDir, 'Dockerfile');
2457
+ fs.writeFileSync(dockerfilePath, dockerfileContent);
2458
+ // Build the image
2459
+ const hostPrltVersion = getHostPrltVersion();
2460
+ const buildArgs = {
2461
+ PRLT_VERSION: hostPrltVersion || 'latest',
2462
+ };
2463
+ const buildArgFlags = Object.entries(buildArgs)
2464
+ .map(([key, value]) => `--build-arg ${key}="${value}"`)
2465
+ .join(' ');
2466
+ console.debug(`[runners:orchestrator-docker] Building image: ${imageName}`);
2467
+ try {
2468
+ execSync(`docker build -t ${imageName} -f "${dockerfilePath}" ${buildArgFlags} "${buildDir}"`, { stdio: 'pipe' });
2469
+ }
2470
+ catch (buildError) {
2471
+ return {
2472
+ success: false,
2473
+ error: `Failed to build orchestrator Docker image: ${buildError instanceof Error ? buildError.message : buildError}`,
2474
+ };
2475
+ }
2476
+ // Build mount flags for docker run
2477
+ const mounts = [
2478
+ // Mount HQ directory
2479
+ `-v "${hqPath}:/hq:cached"`,
2480
+ // Docker socket for sibling container pattern
2481
+ `-v /var/run/docker.sock:/var/run/docker.sock`,
2482
+ // Claude credentials volume (shared with agent containers)
2483
+ ...(executor === 'claude-code' ? ['-v "claude-credentials:/home/node/.claude"'] : []),
2484
+ // Persistent bash history
2485
+ '-v "claude-bash-history:/commandhistory"',
2486
+ ];
2487
+ // Build environment variables
2488
+ const envVars = [
2489
+ `-e PRLT_HQ_PATH=/hq`,
2490
+ `-e PRLT_AGENT_NAME="orchestrator-${orchestratorName}"`,
2491
+ `-e PRLT_HOST_PATH="${hqPath}"`,
2492
+ // Pass through GitHub tokens for agent spawning
2493
+ ...(process.env.GITHUB_TOKEN ? [`-e GITHUB_TOKEN="${process.env.GITHUB_TOKEN}"`] : []),
2494
+ ...(process.env.GH_TOKEN ? [`-e GH_TOKEN="${process.env.GH_TOKEN}"`] : []),
2495
+ // Pass ANTHROPIC_API_KEY if available (for cases where OAuth is not set up)
2496
+ ...(process.env.ANTHROPIC_API_KEY ? [`-e ANTHROPIC_API_KEY="${process.env.ANTHROPIC_API_KEY}"`] : []),
2497
+ ];
2498
+ // Create and start container
2499
+ const createCmd = [
2500
+ 'docker run -d',
2501
+ `--name ${containerName}`,
2502
+ '--user node',
2503
+ '-w /hq',
2504
+ ...mounts,
2505
+ ...envVars,
2506
+ `--memory=${config.devcontainer.memory}`,
2507
+ `--cpus=${config.devcontainer.cpus}`,
2508
+ imageName,
2509
+ 'sleep infinity', // Keep container running
2510
+ ].join(' ');
2511
+ console.debug(`[runners:orchestrator-docker] Creating container: ${createCmd}`);
2512
+ execSync(createCmd, { stdio: 'pipe' });
2513
+ const containerId = getContainerId(containerName);
2514
+ if (!containerId) {
2515
+ return {
2516
+ success: false,
2517
+ error: 'Failed to get container ID after creation',
2518
+ };
2519
+ }
2520
+ // Fix Docker socket permissions inside the container
2521
+ // The socket is owned by root on the host; we need the node user to access it
2522
+ try {
2523
+ execSync(`docker exec --user root ${containerId} chmod 666 /var/run/docker.sock`, { stdio: 'pipe' });
2524
+ }
2525
+ catch {
2526
+ console.debug('[runners:orchestrator-docker] Failed to fix Docker socket permissions (may already be accessible)');
2527
+ }
2528
+ // Copy Claude Code settings to container (for bypassing prompts)
2529
+ if (executor === 'claude-code') {
2530
+ try {
2531
+ const hostClaudeJson = path.join(os.homedir(), '.claude.json');
2532
+ let settings = {};
2533
+ if (fs.existsSync(hostClaudeJson)) {
2534
+ try {
2535
+ settings = JSON.parse(fs.readFileSync(hostClaudeJson, 'utf-8'));
2536
+ }
2537
+ catch {
2538
+ // Use empty settings
2539
+ }
2540
+ }
2541
+ if (config.permissionMode === 'danger') {
2542
+ settings.bypassPermissionsModeAccepted = true;
2543
+ }
2544
+ settings.numStartups = settings.numStartups || 1;
2545
+ settings.hasCompletedOnboarding = true;
2546
+ settings.theme = settings.theme || 'dark';
2547
+ if (!settings.tipsHistory || typeof settings.tipsHistory !== 'object') {
2548
+ settings.tipsHistory = {};
2549
+ }
2550
+ const tips = settings.tipsHistory;
2551
+ tips['new-user-warmup'] = tips['new-user-warmup'] || 1;
2552
+ settings.effortCalloutDismissed = true;
2553
+ if (!settings.projects || typeof settings.projects !== 'object') {
2554
+ settings.projects = {};
2555
+ }
2556
+ const projects = settings.projects;
2557
+ for (const projectPath of ['/hq', '/']) {
2558
+ if (!projects[projectPath])
2559
+ projects[projectPath] = {};
2560
+ projects[projectPath].hasTrustDialogAccepted = true;
2561
+ projects[projectPath].hasCompletedProjectOnboarding = true;
2562
+ }
2563
+ execSync(`docker exec -i ${containerId} bash -c 'cat > /home/node/.claude.json'`, { input: JSON.stringify(settings), stdio: ['pipe', 'pipe', 'pipe'] });
2564
+ const claudeSettings = JSON.stringify({ skipDangerousModePermissionPrompt: true });
2565
+ execSync(`docker exec -i ${containerId} bash -c 'mkdir -p /home/node/.claude && cat > /home/node/.claude/settings.json'`, { input: claudeSettings, stdio: ['pipe', 'pipe', 'pipe'] });
2566
+ }
2567
+ catch (error) {
2568
+ console.debug('[runners:orchestrator-docker] Failed to copy Claude settings:', error);
2569
+ }
2570
+ }
2571
+ // Build the prompt and write to temp file inside container
2572
+ const prompt = buildPrompt(context);
2573
+ const promptPath = `/tmp/orchestrator-prompt-${Date.now()}.txt`;
2574
+ try {
2575
+ execSync(`docker exec -i ${containerId} bash -c 'cat > ${promptPath}'`, { input: prompt, stdio: ['pipe', 'pipe', 'pipe'] });
2576
+ }
2577
+ catch {
2578
+ return {
2579
+ success: false,
2580
+ error: 'Failed to write prompt to container',
2581
+ };
2582
+ }
2583
+ // Build executor command
2584
+ const skipPermissions = config.permissionMode === 'danger';
2585
+ const permissionsFlag = skipPermissions ? '--dangerously-skip-permissions ' : '';
2586
+ const effortFlag = skipPermissions ? '--effort high ' : '';
2587
+ const executorCmd = executor === 'claude-code'
2588
+ ? `claude ${permissionsFlag}${effortFlag}"$(cat ${promptPath})"`
2589
+ : `claude ${permissionsFlag}${effortFlag}"$(cat ${promptPath})"`;
2590
+ // Build tmux session name (reuses the same name as host tmux for consistency)
2591
+ const tmuxSessionName = options?.sessionName || containerName;
2592
+ // Create tmux session inside container with the executor command
2593
+ const tmuxCmd = `tmux new-session -d -s "${tmuxSessionName}" -n "${tmuxSessionName}" bash -c '(unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT; cd /hq && ${executorCmd}); echo ""; echo "Orchestrator complete. Press Enter to close."; exec bash'`;
2594
+ try {
2595
+ execSync(`docker exec ${containerId} bash -c '${tmuxCmd.replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
2596
+ }
2597
+ catch (tmuxError) {
2598
+ // Fallback: try simpler command without subshell
2599
+ console.debug('[runners:orchestrator-docker] tmux creation failed, trying simpler approach:', tmuxError);
2600
+ try {
2601
+ // Write a script inside the container
2602
+ const scriptContent = `#!/bin/bash
2603
+ cd /hq
2604
+ unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT
2605
+ ${executor === 'claude-code' ? `claude ${permissionsFlag}${effortFlag}"$(cat ${promptPath})"` : `claude "$(cat ${promptPath})"`}
2606
+ echo ""
2607
+ echo "Orchestrator complete. Press Enter to close."
2608
+ exec bash
2609
+ `;
2610
+ execSync(`docker exec -i ${containerId} bash -c 'cat > /tmp/orchestrator-start.sh && chmod +x /tmp/orchestrator-start.sh'`, { input: scriptContent, stdio: ['pipe', 'pipe', 'pipe'] });
2611
+ execSync(`docker exec ${containerId} tmux new-session -d -s "${tmuxSessionName}" /tmp/orchestrator-start.sh`, { stdio: 'pipe' });
2612
+ }
2613
+ catch (fallbackError) {
2614
+ return {
2615
+ success: false,
2616
+ error: `Failed to create tmux session in container: ${fallbackError instanceof Error ? fallbackError.message : fallbackError}`,
2617
+ };
2618
+ }
2619
+ }
2620
+ // Handle display mode
2621
+ if (displayMode === 'foreground') {
2622
+ // Attach to tmux inside the container in current terminal
2623
+ try {
2624
+ const child = spawn('docker', ['exec', '-it', containerId, 'tmux', 'attach', '-t', tmuxSessionName], {
2625
+ stdio: 'inherit',
2626
+ });
2627
+ await new Promise((resolve) => {
2628
+ child.on('close', () => resolve());
2629
+ });
2630
+ }
2631
+ catch {
2632
+ // User detached - that's fine
2633
+ }
2634
+ }
2635
+ else if (displayMode === 'terminal' && process.platform === 'darwin') {
2636
+ // Open a new terminal tab that attaches to the container's tmux
2637
+ const baseDir = path.join(hqPath, '.proletariat', 'scripts');
2638
+ fs.mkdirSync(baseDir, { recursive: true });
2639
+ const scriptPath = path.join(baseDir, `orch-docker-attach-${Date.now()}.sh`);
2640
+ const scriptContent = `#!/bin/bash
2641
+ echo -ne "\\033]0;Orchestrator (Docker)\\007"
2642
+ echo -ne "\\033]1;Orchestrator (Docker)\\007"
2643
+ docker exec -it ${containerId} tmux attach -t "${tmuxSessionName}"
2644
+ rm -f "${scriptPath}"
2645
+ exec $SHELL
2646
+ `;
2647
+ fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
2648
+ const terminalApp = config.terminal.app;
2649
+ try {
2650
+ switch (terminalApp) {
2651
+ case 'iTerm':
2652
+ execSync(`osascript -e '
2653
+ tell application "iTerm"
2654
+ activate
2655
+ tell current window
2656
+ set newTab to (create tab with default profile)
2657
+ tell current session of newTab
2658
+ set name to "Orchestrator (Docker)"
2659
+ write text "${scriptPath}"
2660
+ end tell
2661
+ end tell
2662
+ end tell
2663
+ '`);
2664
+ break;
2665
+ case 'Ghostty':
2666
+ execSync(`osascript -e '
2667
+ tell application "Ghostty"
2668
+ activate
2669
+ end tell
2670
+ tell application "System Events"
2671
+ tell process "Ghostty"
2672
+ keystroke "t" using command down
2673
+ delay 0.3
2674
+ keystroke "${scriptPath}"
2675
+ keystroke return
2676
+ end tell
2677
+ end tell
2678
+ '`);
2679
+ break;
2680
+ default:
2681
+ execSync(`osascript -e '
2682
+ tell application "Terminal"
2683
+ activate
2684
+ tell application "System Events"
2685
+ tell process "Terminal"
2686
+ keystroke "t" using command down
2687
+ end tell
2688
+ end tell
2689
+ delay 0.3
2690
+ do script "${scriptPath}" in front window
2691
+ end tell
2692
+ '`);
2693
+ break;
2694
+ }
2695
+ }
2696
+ catch {
2697
+ console.debug('[runners:orchestrator-docker] Failed to open terminal tab, running in background');
2698
+ }
2699
+ }
2700
+ // 'background' display mode: container is already running, nothing more to do
2701
+ return {
2702
+ success: true,
2703
+ containerId,
2704
+ sessionId: tmuxSessionName,
2705
+ };
2706
+ }
2707
+ catch (error) {
2708
+ return {
2709
+ success: false,
2710
+ error: error instanceof Error ? error.message : 'Failed to start orchestrator in Docker',
2711
+ };
2712
+ }
2713
+ }
2714
+ // =============================================================================
2214
2715
  // VM Runner
2215
2716
  // =============================================================================
2216
2717
  export async function runVm(context, executor, config, host) {