@proletariat/cli 0.3.57 → 0.3.59

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 (219) hide show
  1. package/dist/commands/{spec/view.d.ts → dashboard/index.d.ts} +4 -6
  2. package/dist/commands/dashboard/index.js +117 -0
  3. package/dist/commands/dashboard/index.js.map +1 -0
  4. package/dist/commands/execution/config.js +5 -4
  5. package/dist/commands/execution/config.js.map +1 -1
  6. package/dist/commands/execution/stop.js +4 -2
  7. package/dist/commands/execution/stop.js.map +1 -1
  8. package/dist/commands/execution/view.js +3 -0
  9. package/dist/commands/execution/view.js.map +1 -1
  10. package/dist/commands/init.d.ts +1 -0
  11. package/dist/commands/init.js +40 -3
  12. package/dist/commands/init.js.map +1 -1
  13. package/dist/commands/mcp-server.js +1 -2
  14. package/dist/commands/mcp-server.js.map +1 -1
  15. package/dist/commands/media/add.d.ts +19 -0
  16. package/dist/commands/media/add.js +94 -0
  17. package/dist/commands/media/add.js.map +1 -0
  18. package/dist/commands/{spec → media}/index.d.ts +1 -1
  19. package/dist/commands/media/index.js +85 -0
  20. package/dist/commands/media/index.js.map +1 -0
  21. package/dist/commands/{spec/link/remove.d.ts → media/list.d.ts} +3 -6
  22. package/dist/commands/media/list.js +89 -0
  23. package/dist/commands/media/list.js.map +1 -0
  24. package/dist/commands/media/preprocess.d.ts +19 -0
  25. package/dist/commands/media/preprocess.js +91 -0
  26. package/dist/commands/media/preprocess.js.map +1 -0
  27. package/dist/commands/{spec/delete.d.ts → media/remove.d.ts} +2 -2
  28. package/dist/commands/media/remove.js +101 -0
  29. package/dist/commands/media/remove.js.map +1 -0
  30. package/dist/commands/{spec/link/index.d.ts → media/show.d.ts} +3 -3
  31. package/dist/commands/media/show.js +122 -0
  32. package/dist/commands/media/show.js.map +1 -0
  33. package/dist/commands/orchestrator/start.js +5 -0
  34. package/dist/commands/orchestrator/start.js.map +1 -1
  35. package/dist/commands/session/exec.d.ts +19 -0
  36. package/dist/commands/session/exec.js +205 -0
  37. package/dist/commands/session/exec.js.map +1 -0
  38. package/dist/commands/session/index.js +12 -0
  39. package/dist/commands/session/index.js.map +1 -1
  40. package/dist/commands/{spec/link/depends.d.ts → session/inspect.d.ts} +4 -4
  41. package/dist/commands/session/inspect.js +316 -0
  42. package/dist/commands/session/inspect.js.map +1 -0
  43. package/dist/commands/session/peek.d.ts +15 -0
  44. package/dist/commands/session/peek.js +141 -8
  45. package/dist/commands/session/peek.js.map +1 -1
  46. package/dist/commands/session/poke.d.ts +4 -1
  47. package/dist/commands/session/poke.js +175 -20
  48. package/dist/commands/session/poke.js.map +1 -1
  49. package/dist/commands/session/restart.d.ts +20 -0
  50. package/dist/commands/session/restart.js +320 -0
  51. package/dist/commands/session/restart.js.map +1 -0
  52. package/dist/commands/tools/add.d.ts +20 -0
  53. package/dist/commands/tools/add.js +129 -0
  54. package/dist/commands/tools/add.js.map +1 -0
  55. package/dist/commands/tools/check.d.ts +10 -0
  56. package/dist/commands/tools/check.js +75 -0
  57. package/dist/commands/tools/check.js.map +1 -0
  58. package/dist/commands/tools/detect.d.ts +11 -0
  59. package/dist/commands/tools/detect.js +107 -0
  60. package/dist/commands/tools/detect.js.map +1 -0
  61. package/dist/commands/tools/index.d.ts +11 -0
  62. package/dist/commands/tools/index.js +87 -0
  63. package/dist/commands/tools/index.js.map +1 -0
  64. package/dist/commands/tools/remove.d.ts +13 -0
  65. package/dist/commands/tools/remove.js +55 -0
  66. package/dist/commands/tools/remove.js.map +1 -0
  67. package/dist/commands/trello/configure.d.ts +16 -0
  68. package/dist/commands/trello/configure.js +259 -0
  69. package/dist/commands/trello/configure.js.map +1 -0
  70. package/dist/commands/{spec/plan.d.ts → trello/import.d.ts} +3 -5
  71. package/dist/commands/trello/import.js +241 -0
  72. package/dist/commands/trello/import.js.map +1 -0
  73. package/dist/commands/{spec/ticket.d.ts → trello/sync.d.ts} +5 -6
  74. package/dist/commands/trello/sync.js +190 -0
  75. package/dist/commands/trello/sync.js.map +1 -0
  76. package/dist/commands/work/start.d.ts +2 -0
  77. package/dist/commands/work/start.js +27 -41
  78. package/dist/commands/work/start.js.map +1 -1
  79. package/dist/lib/dashboard/data.d.ts +64 -0
  80. package/dist/lib/dashboard/data.js +259 -0
  81. package/dist/lib/dashboard/data.js.map +1 -0
  82. package/dist/lib/dashboard/html.d.ts +7 -0
  83. package/dist/lib/dashboard/html.js +682 -0
  84. package/dist/lib/dashboard/html.js.map +1 -0
  85. package/dist/lib/dashboard/server.d.ts +20 -0
  86. package/dist/lib/dashboard/server.js +114 -0
  87. package/dist/lib/dashboard/server.js.map +1 -0
  88. package/dist/lib/database/index.d.ts +49 -1
  89. package/dist/lib/database/index.js +127 -0
  90. package/dist/lib/database/index.js.map +1 -1
  91. package/dist/lib/execution/config.d.ts +8 -0
  92. package/dist/lib/execution/config.js +83 -4
  93. package/dist/lib/execution/config.js.map +1 -1
  94. package/dist/lib/execution/runners.d.ts +60 -4
  95. package/dist/lib/execution/runners.js +398 -79
  96. package/dist/lib/execution/runners.js.map +1 -1
  97. package/dist/lib/execution/spawner.d.ts +4 -2
  98. package/dist/lib/execution/spawner.js +54 -47
  99. package/dist/lib/execution/spawner.js.map +1 -1
  100. package/dist/lib/execution/types.d.ts +27 -5
  101. package/dist/lib/execution/types.js +24 -0
  102. package/dist/lib/execution/types.js.map +1 -1
  103. package/dist/lib/external-issues/adapters.d.ts +17 -0
  104. package/dist/lib/external-issues/adapters.js +88 -0
  105. package/dist/lib/external-issues/adapters.js.map +1 -1
  106. package/dist/lib/external-issues/mapping-store.js +1 -1
  107. package/dist/lib/external-issues/shortcut.js +2 -1
  108. package/dist/lib/external-issues/shortcut.js.map +1 -1
  109. package/dist/lib/external-issues/trello.d.ts +80 -0
  110. package/dist/lib/external-issues/trello.js +266 -0
  111. package/dist/lib/external-issues/trello.js.map +1 -0
  112. package/dist/lib/external-issues/types.d.ts +3 -3
  113. package/dist/lib/external-issues/types.js +1 -1
  114. package/dist/lib/external-issues/types.js.map +1 -1
  115. package/dist/lib/linear/client.d.ts +4 -3
  116. package/dist/lib/linear/client.js +185 -122
  117. package/dist/lib/linear/client.js.map +1 -1
  118. package/dist/lib/mcp/tools/cli-passthrough.js +77 -0
  119. package/dist/lib/mcp/tools/cli-passthrough.js.map +1 -1
  120. package/dist/lib/mcp/tools/index.d.ts +0 -1
  121. package/dist/lib/mcp/tools/index.js +0 -1
  122. package/dist/lib/mcp/tools/index.js.map +1 -1
  123. package/dist/lib/media/index.d.ts +91 -0
  124. package/dist/lib/media/index.js +475 -0
  125. package/dist/lib/media/index.js.map +1 -0
  126. package/dist/lib/onboarding/detect-tools.d.ts +15 -0
  127. package/dist/lib/onboarding/detect-tools.js +44 -0
  128. package/dist/lib/onboarding/detect-tools.js.map +1 -0
  129. package/dist/lib/onboarding/index.d.ts +2 -0
  130. package/dist/lib/onboarding/index.js +3 -0
  131. package/dist/lib/onboarding/index.js.map +1 -0
  132. package/dist/lib/onboarding/wizard.d.ts +25 -0
  133. package/dist/lib/onboarding/wizard.js +156 -0
  134. package/dist/lib/onboarding/wizard.js.map +1 -0
  135. package/dist/lib/pmo/schema.d.ts +2 -1
  136. package/dist/lib/pmo/schema.js +3 -1
  137. package/dist/lib/pmo/schema.js.map +1 -1
  138. package/dist/lib/runners/claude-code-runner.js +6 -0
  139. package/dist/lib/runners/claude-code-runner.js.map +1 -1
  140. package/dist/lib/tool-registry/detect.d.ts +20 -0
  141. package/dist/lib/tool-registry/detect.js +95 -0
  142. package/dist/lib/tool-registry/detect.js.map +1 -0
  143. package/dist/lib/tool-registry/index.d.ts +10 -0
  144. package/dist/lib/tool-registry/index.js +13 -0
  145. package/dist/lib/tool-registry/index.js.map +1 -0
  146. package/dist/lib/tool-registry/policy.d.ts +32 -0
  147. package/dist/lib/tool-registry/policy.js +97 -0
  148. package/dist/lib/tool-registry/policy.js.map +1 -0
  149. package/dist/lib/tool-registry/registry.d.ts +42 -0
  150. package/dist/lib/tool-registry/registry.js +120 -0
  151. package/dist/lib/tool-registry/registry.js.map +1 -0
  152. package/dist/lib/tool-registry/spawn.d.ts +50 -0
  153. package/dist/lib/tool-registry/spawn.js +103 -0
  154. package/dist/lib/tool-registry/spawn.js.map +1 -0
  155. package/dist/lib/tool-registry/types.d.ts +56 -0
  156. package/dist/lib/tool-registry/types.js +109 -0
  157. package/dist/lib/tool-registry/types.js.map +1 -0
  158. package/dist/lib/trello/client.d.ts +23 -0
  159. package/dist/lib/trello/client.js +114 -0
  160. package/dist/lib/trello/client.js.map +1 -0
  161. package/dist/lib/trello/config.d.ts +55 -0
  162. package/dist/lib/trello/config.js +127 -0
  163. package/dist/lib/trello/config.js.map +1 -0
  164. package/dist/lib/trello/index.d.ts +5 -0
  165. package/dist/lib/trello/index.js +5 -0
  166. package/dist/lib/trello/index.js.map +1 -0
  167. package/dist/lib/trello/mapper.d.ts +13 -0
  168. package/dist/lib/trello/mapper.js +71 -0
  169. package/dist/lib/trello/mapper.js.map +1 -0
  170. package/dist/lib/trello/sync.d.ts +13 -0
  171. package/dist/lib/trello/sync.js +38 -0
  172. package/dist/lib/trello/sync.js.map +1 -0
  173. package/dist/lib/trello/types.d.ts +53 -0
  174. package/dist/lib/trello/types.js +2 -0
  175. package/dist/lib/trello/types.js.map +1 -0
  176. package/dist/lib/work-source/client.js +17 -0
  177. package/dist/lib/work-source/client.js.map +1 -1
  178. package/dist/lib/work-source/config.d.ts +6 -1
  179. package/dist/lib/work-source/config.js +30 -1
  180. package/dist/lib/work-source/config.js.map +1 -1
  181. package/dist/lib/work-source/index.d.ts +1 -1
  182. package/dist/lib/work-source/index.js +1 -1
  183. package/dist/lib/work-source/index.js.map +1 -1
  184. package/oclif.manifest.json +6524 -6171
  185. package/package.json +6 -2
  186. package/dist/commands/spec/create.d.ts +0 -20
  187. package/dist/commands/spec/create.js +0 -171
  188. package/dist/commands/spec/create.js.map +0 -1
  189. package/dist/commands/spec/delete.js +0 -112
  190. package/dist/commands/spec/delete.js.map +0 -1
  191. package/dist/commands/spec/edit.d.ts +0 -23
  192. package/dist/commands/spec/edit.js +0 -262
  193. package/dist/commands/spec/edit.js.map +0 -1
  194. package/dist/commands/spec/index.js +0 -88
  195. package/dist/commands/spec/index.js.map +0 -1
  196. package/dist/commands/spec/link/depends.js +0 -87
  197. package/dist/commands/spec/link/depends.js.map +0 -1
  198. package/dist/commands/spec/link/index.js +0 -93
  199. package/dist/commands/spec/link/index.js.map +0 -1
  200. package/dist/commands/spec/link/remove.js +0 -91
  201. package/dist/commands/spec/link/remove.js.map +0 -1
  202. package/dist/commands/spec/list.d.ts +0 -14
  203. package/dist/commands/spec/list.js +0 -101
  204. package/dist/commands/spec/list.js.map +0 -1
  205. package/dist/commands/spec/plan.js +0 -102
  206. package/dist/commands/spec/plan.js.map +0 -1
  207. package/dist/commands/spec/ticket.js +0 -144
  208. package/dist/commands/spec/ticket.js.map +0 -1
  209. package/dist/commands/spec/view.js +0 -202
  210. package/dist/commands/spec/view.js.map +0 -1
  211. package/dist/lib/mcp/tools/spec.d.ts +0 -6
  212. package/dist/lib/mcp/tools/spec.js +0 -197
  213. package/dist/lib/mcp/tools/spec.js.map +0 -1
  214. package/dist/lib/pmo/spec-parser.d.ts +0 -25
  215. package/dist/lib/pmo/spec-parser.js +0 -206
  216. package/dist/lib/pmo/spec-parser.js.map +0 -1
  217. package/dist/lib/pmo/spec-types.d.ts +0 -43
  218. package/dist/lib/pmo/spec-types.js +0 -8
  219. package/dist/lib/pmo/spec-types.js.map +0 -1
@@ -2,17 +2,18 @@
2
2
  /**
3
3
  * Execution Runners
4
4
  *
5
- * Implementations for each execution environment (devcontainer, host, docker, vm).
5
+ * Implementations for each execution environment (host, sandbox, devcontainer, docker, cloud).
6
6
  */
7
7
  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
11
  import { fileURLToPath } from 'node:url';
12
- import { DEFAULT_EXECUTION_CONFIG, } from './types.js';
12
+ import { DEFAULT_EXECUTION_CONFIG, normalizeEnvironment, } from './types.js';
13
13
  import { getSetTitleCommands } from '../terminal.js';
14
14
  import { readDevcontainerJson, generateOrchestratorDockerfile } from './devcontainer.js';
15
15
  import { getCodexCommand, resolveCodexExecutionContext, validateCodexMode } from './codex-adapter.js';
16
+ import { resolveToolsForSpawn } from '../tool-registry/index.js';
16
17
  // =============================================================================
17
18
  // Terminal Title Helpers
18
19
  // =============================================================================
@@ -380,14 +381,84 @@ export function checkExecutorInContainer(executor, containerId) {
380
381
  * Run executor preflight checks for the target environment.
381
382
  */
382
383
  export function runExecutorPreflight(environment, executor, options) {
383
- if (environment === 'host') {
384
+ const env = normalizeEnvironment(environment);
385
+ if (env === 'host' || env === 'sandbox') {
384
386
  return checkExecutorOnHost(executor);
385
387
  }
386
- if (environment === 'devcontainer' && options?.containerId) {
388
+ if (env === 'devcontainer' && options?.containerId) {
387
389
  return checkExecutorInContainer(executor, options.containerId);
388
390
  }
389
391
  return { ok: true };
390
392
  }
393
+ const INTEGRATION_COMMANDS = [
394
+ {
395
+ provider: 'asana',
396
+ displayName: 'Asana',
397
+ commands: [
398
+ 'prlt asana connect — authenticate with Asana',
399
+ 'prlt asana sync --ticket TKT-XXX --create-missing --project <gid> — sync a PMO ticket to Asana',
400
+ 'prlt asana import — import Asana tasks into PMO',
401
+ ],
402
+ },
403
+ {
404
+ provider: 'linear',
405
+ displayName: 'Linear',
406
+ commands: [
407
+ 'prlt linear connect — authenticate with Linear',
408
+ 'prlt linear sync --ticket TKT-XXX --create-missing — sync a PMO ticket to Linear',
409
+ 'prlt linear import — import Linear issues into PMO',
410
+ ],
411
+ },
412
+ {
413
+ provider: 'jira',
414
+ displayName: 'Jira',
415
+ commands: [
416
+ 'prlt jira connect — authenticate with Jira',
417
+ 'prlt jira sync --ticket TKT-XXX --create-missing — sync a PMO ticket to Jira',
418
+ 'prlt jira import — import Jira issues into PMO',
419
+ ],
420
+ },
421
+ {
422
+ provider: 'shortcut',
423
+ displayName: 'Shortcut',
424
+ commands: [
425
+ 'prlt shortcut connect — authenticate with Shortcut',
426
+ 'prlt shortcut sync --ticket TKT-XXX --create-missing — sync a PMO ticket to Shortcut',
427
+ 'prlt shortcut import — import Shortcut stories into PMO',
428
+ ],
429
+ },
430
+ {
431
+ provider: 'monday',
432
+ displayName: 'Monday.com',
433
+ commands: [
434
+ 'prlt monday connect — authenticate with Monday.com',
435
+ 'prlt monday sync --ticket TKT-XXX --create-missing — sync a PMO ticket to Monday.com',
436
+ ],
437
+ },
438
+ ];
439
+ /**
440
+ * Build the integration commands section for agent prompts.
441
+ * Only includes integrations that are actually connected/configured.
442
+ * Returns empty string if no integrations are connected.
443
+ */
444
+ function buildIntegrationCommandsSection(connectedIntegrations) {
445
+ if (!connectedIntegrations || connectedIntegrations.length === 0)
446
+ return '';
447
+ const connected = INTEGRATION_COMMANDS.filter(ic => connectedIntegrations.includes(ic.provider));
448
+ if (connected.length === 0)
449
+ return '';
450
+ let section = `## Integration Commands\n\n`;
451
+ section += `The following external integrations are connected. Use these prlt commands to interact with them.\n\n`;
452
+ for (const integration of connected) {
453
+ section += `### ${integration.displayName}\n`;
454
+ for (const cmd of integration.commands) {
455
+ section += `- \`${cmd.split(' — ')[0]}\` — ${cmd.split(' — ')[1] || ''}\n`;
456
+ }
457
+ section += '\n';
458
+ }
459
+ section += `**ANTI-PATTERN:** Never use curl, raw API calls, or shell scripts to interact with external services (Asana, Linear, Jira, Shortcut, Monday.com, etc.). Always use the corresponding \`prlt\` commands.\n\n`;
460
+ return section;
461
+ }
391
462
  const ORCHESTRATOR_COMMAND_REGISTRY = [
392
463
  {
393
464
  title: 'Agent Lifecycle',
@@ -486,6 +557,18 @@ function buildOrchestratorAntiPatterns() {
486
557
  */
487
558
  function buildOrchestratorBody(hqName, context) {
488
559
  let prompt = '';
560
+ // Dynamic workspace context
561
+ const prltVersion = getHostPrltVersion();
562
+ prompt += `## Environment\n`;
563
+ if (prltVersion) {
564
+ prompt += `- **prlt version**: ${prltVersion}\n`;
565
+ }
566
+ prompt += `- **Available executors**: claude-code, codex\n`;
567
+ prompt += `- **Agent worktrees**: \`agents/temp/<agent-name>/<repo>\` — each agent gets an isolated git worktree\n`;
568
+ if (context.hqPath) {
569
+ prompt += `- **HQ path**: \`${context.hqPath}\`\n`;
570
+ }
571
+ prompt += `\n`;
489
572
  // Runtime declaration
490
573
  prompt += `## prlt Is Your Orchestration Runtime\n\n`;
491
574
  prompt += `prlt is your orchestration runtime. NEVER use raw docker exec, tmux send-keys, or direct container access. `;
@@ -494,10 +577,12 @@ function buildOrchestratorBody(hqName, context) {
494
577
  prompt += `health monitoring, and creates orphaned processes.\n\n`;
495
578
  // Role
496
579
  prompt += `## Your Role\n`;
497
- prompt += `- Plan and prioritize work across the board\n`;
580
+ prompt += `- Assess the current state of the board, running agents, and open PRs\n`;
581
+ prompt += `- Plan and prioritize work — decide what to tackle next and in what order\n`;
498
582
  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`;
583
+ prompt += `- Monitor agent progress via sessions and review completed work\n`;
584
+ prompt += `- Review and merge completed PRs via \`gh pr merge --squash\`\n`;
585
+ prompt += `- Coordinate parallel agents — handle rebases after merges\n`;
501
586
  prompt += `- Never write code or make changes to source files yourself\n\n`;
502
587
  // Command reference (dynamically generated)
503
588
  prompt += `## Command Reference\n\n`;
@@ -511,11 +596,20 @@ function buildOrchestratorBody(hqName, context) {
511
596
  prompt += `- Fix: \`--action review-fix\`\n\n`;
512
597
  // Anti-patterns (dynamically generated)
513
598
  prompt += buildOrchestratorAntiPatterns();
599
+ // Integration commands (only for connected integrations)
600
+ prompt += buildIntegrationCommandsSection(context.connectedIntegrations);
514
601
  // Workflow
515
602
  prompt += `## Workflow\n`;
516
603
  prompt += `- Squash merge only: \`gh pr merge --squash\`\n`;
517
604
  prompt += `- After merging: subsequent PRs from parallel agents will need rebase\n`;
518
605
  prompt += `- Kill stale sessions after their PRs are merged\n\n`;
606
+ // Tool registry (TKT-083): inject available tools into orchestrator prompt
607
+ if (context.hqPath) {
608
+ const toolsResult = resolveToolsForSpawn(context.hqPath, context.toolPolicy, path.join(context.hqPath, '.proletariat', 'scripts'));
609
+ if (toolsResult.promptSection) {
610
+ prompt += toolsResult.promptSection;
611
+ }
612
+ }
519
613
  // Load .orchestrator-context.md from HQ root if it exists
520
614
  if (context.hqPath) {
521
615
  const contextFilePath = path.join(context.hqPath, '.orchestrator-context.md');
@@ -540,9 +634,12 @@ function buildOrchestratorBody(hqName, context) {
540
634
  */
541
635
  export function buildOrchestratorSystemPrompt(context) {
542
636
  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`;
637
+ let prompt = `# Orchestrator: ${hqName}\n\n`;
638
+ prompt += `You are the orchestrator for the **${hqName}** headquarters — a technical project manager driving software delivery through delegated AI agents.\n\n`;
639
+ prompt += `**prlt** is an AI agent orchestration CLI. It manages software development by coordinating autonomous coding agents that work in isolated git worktrees. `;
640
+ prompt += `Your workspace (HQ) contains a PMO board for tracking tickets, agent worktrees under \`agents/temp/\`, and repo connections. `;
641
+ prompt += `Agents are spawned to implement, review, and fix code — you never write code yourself. `;
642
+ prompt += `Your job is to assess the state of the project, plan and prioritize work, delegate to agents, monitor their progress, review results, and merge completed PRs.\n\n`;
546
643
  prompt += buildOrchestratorBody(hqName, context);
547
644
  return prompt;
548
645
  }
@@ -552,7 +649,10 @@ function buildOrchestratorPrompt(context) {
552
649
  // a system prompt (role/tools) + a shorter user message.
553
650
  const hqName = context.hqName || 'workspace';
554
651
  let prompt = `# Orchestrator: ${hqName}\n\n`;
555
- prompt += `You are the orchestrator for the **${hqName}** workspace using the prlt ecosystem.\n\n`;
652
+ prompt += `You are the orchestrator for the **${hqName}** headquarters a technical project manager driving software delivery through delegated AI agents.\n\n`;
653
+ prompt += `**prlt** is an AI agent orchestration CLI. It manages software development by coordinating autonomous coding agents that work in isolated git worktrees. `;
654
+ prompt += `Your workspace (HQ) contains a PMO board for tracking tickets, agent worktrees under \`agents/temp/\`, and repo connections. `;
655
+ prompt += `Agents are spawned to implement, review, and fix code — you never write code yourself.\n\n`;
556
656
  prompt += buildOrchestratorBody(hqName, context);
557
657
  // Include user's custom prompt or action content
558
658
  if (context.actionPrompt) {
@@ -591,9 +691,6 @@ function buildPrompt(context) {
591
691
  if (context.epicTitle) {
592
692
  prompt += `**Epic:** ${context.epicTitle}\n`;
593
693
  }
594
- if (context.specId) {
595
- prompt += `**Spec:** ${context.specId}${context.specTitle ? ` - ${context.specTitle}` : ''}\n`;
596
- }
597
694
  if (context.ticketDescription) {
598
695
  prompt += `\n## Description\n\n${context.ticketDescription}\n`;
599
696
  }
@@ -606,10 +703,22 @@ function buildPrompt(context) {
606
703
  }
607
704
  // Note: Branch setup (fetch + checkout/create) is now handled programmatically
608
705
  // in work/start.ts before the agent spawns, so no prompt instructions needed
706
+ // Integration commands (only for connected integrations)
707
+ const integrationSection = buildIntegrationCommandsSection(context.connectedIntegrations);
708
+ if (integrationSection) {
709
+ prompt += `\n${integrationSection}`;
710
+ }
609
711
  // Additional instructions from --message flag (appended to any action)
610
712
  if (context.customMessage) {
611
713
  prompt += `\n## Additional Instructions\n\n${context.customMessage}\n`;
612
714
  }
715
+ // Tool registry (TKT-083): inject available tools into agent prompt
716
+ if (context.hqPath) {
717
+ const toolsResult = resolveToolsForSpawn(context.hqPath, context.toolPolicy, path.join(context.hqPath, '.proletariat', 'scripts'));
718
+ if (toolsResult.promptSection) {
719
+ prompt += `\n${toolsResult.promptSection}`;
720
+ }
721
+ }
613
722
  // END HOOK - Action-specific completion instructions
614
723
  prompt += `\n---\n\n## When Complete\n\n`;
615
724
  // For revisions, use the revision-specific end prompt
@@ -711,16 +820,27 @@ export async function runHost(context, executor, config, displayMode = 'terminal
711
820
  fs.writeFileSync(systemPromptPath, systemPrompt, { mode: 0o644 });
712
821
  // Override user message: just action instructions or a default startup message
713
822
  const userMessage = context.actionPrompt
714
- || 'You are now running as the orchestrator. Check the board status and report what you see.';
823
+ || 'Assess the current state of the project:\n'
824
+ + '1. Check the board: `prlt board view` — what tickets are in progress, blocked, or ready?\n'
825
+ + '2. List running agents: `prlt session list` — who is working on what? Any stale sessions?\n'
826
+ + '3. Check open PRs: `gh pr list` — any PRs ready for review or merge?\n'
827
+ + '4. Summarize what needs attention and recommend next actions.';
715
828
  fs.writeFileSync(promptPath, userMessage, { mode: 0o644 });
716
829
  }
717
830
  else {
718
831
  // Write full prompt (includes role context for non-Claude executors)
719
832
  fs.writeFileSync(promptPath, prompt, { mode: 0o644 });
720
833
  }
834
+ // Tool registry (TKT-083): generate MCP config for Claude Code
835
+ let mcpConfigPath = null;
836
+ if (context.hqPath && isClaudeExecutor(executor)) {
837
+ const toolsResult = resolveToolsForSpawn(context.hqPath, context.toolPolicy, baseDir);
838
+ mcpConfigPath = toolsResult.mcpConfigPath;
839
+ }
721
840
  // Build the executor command using getExecutorCommand() output
722
841
  // For Claude Code, we also support outputMode and additional flags
723
- // For non-Claude executors, we use the command as-is from getExecutorCommand()
842
+ // For Codex, we use the codex adapter for deterministic command building (TKT-080)
843
+ // For other executors, we use the command as-is from getExecutorCommand()
724
844
  let executorInvocation;
725
845
  if (isClaudeExecutor(executor)) {
726
846
  // Build flags based on config - Claude-specific flags
@@ -731,13 +851,28 @@ export async function runHost(context, executor, config, displayMode = 'terminal
731
851
  const effortFlag = skipPermissions ? '--effort high ' : '';
732
852
  // Orchestrator sessions inject their role via --system-prompt
733
853
  const systemPromptFlag = systemPromptPath ? '--system-prompt "$(cat "$SYSTEM_PROMPT_PATH")" ' : '';
734
- executorInvocation = `${cmd} ${permissionsFlag}${effortFlag}${printFlag}${systemPromptFlag}"$(cat "$PROMPT_PATH")"`;
854
+ // TKT-053: Disable plan mode for background agents — prevents silent stalls
855
+ // when there's no user to approve the plan mode transition
856
+ const disallowPlanFlag = displayMode === 'background' ? '--disallowedTools EnterPlanMode ' : '';
857
+ // Tool registry (TKT-083): pass MCP config to Claude Code via --mcp-config flag
858
+ const mcpConfigFlag = mcpConfigPath ? `--mcp-config "${mcpConfigPath}" ` : '';
859
+ executorInvocation = `${cmd} ${permissionsFlag}${effortFlag}${printFlag}${disallowPlanFlag}${systemPromptFlag}${mcpConfigFlag}"$(cat "$PROMPT_PATH")"`;
860
+ }
861
+ else if (executor === 'codex') {
862
+ // TKT-080: Use Codex adapter for deterministic command building.
863
+ // Uses PLACEHOLDER pattern for reliable prompt replacement (same as devcontainer runner).
864
+ const codexPermission = config.permissionMode;
865
+ const codexContext = resolveCodexExecutionContext(displayMode, config.outputMode);
866
+ const codexResult = getCodexCommand('PLACEHOLDER', codexPermission, codexContext);
867
+ const argsStr = codexResult.args.map(a => a === 'PLACEHOLDER' ? '"$(cat "$PROMPT_PATH")"' : a).join(' ');
868
+ executorInvocation = `${codexResult.cmd} ${argsStr}`;
735
869
  }
736
870
  else {
737
- // Non-Claude executors: build command from getExecutorCommand() args
738
- // Replace the prompt in args with a file read to avoid shell escaping
739
- const argsWithFile = args.map(a => a === prompt ? '"$(cat "$PROMPT_PATH")"' : `"${a}"`);
740
- executorInvocation = `${cmd} ${argsWithFile.join(' ')}`;
871
+ // Non-Claude, non-Codex executors: build command from getExecutorCommand() args
872
+ // Use PLACEHOLDER for reliable prompt replacement instead of fragile string comparison
873
+ const { cmd: execCmd, args: execArgs } = getExecutorCommand(executor, 'PLACEHOLDER', skipPermissions);
874
+ const argsWithFile = execArgs.map(a => a === 'PLACEHOLDER' ? '"$(cat "$PROMPT_PATH")"' : `"${a}"`);
875
+ executorInvocation = `${execCmd} ${argsWithFile.join(' ')}`;
741
876
  }
742
877
  // Build script that runs executor and keeps shell open after completion
743
878
  const setTitleCmds = getSetTitleCommands(windowTitle);
@@ -755,16 +890,25 @@ echo ""
755
890
  echo "✅ Agent work complete. Press Enter to close or run more commands."
756
891
  exec $SHELL
757
892
  `;
893
+ // Wrap with srt sandbox if running in sandbox environment
894
+ let finalInvocation = executorInvocation;
895
+ if (context.executionEnvironment === 'sandbox') {
896
+ // Build the srt wrapper command
897
+ // The inner command is the executor invocation that reads from PROMPT_PATH
898
+ const srtCmd = buildSrtCommand(`bash -c '${executorInvocation.replace(/'/g, "'\\''")}'`, context, config);
899
+ finalInvocation = srtCmd;
900
+ }
758
901
  const scriptContent = `#!/bin/bash
759
902
  # Auto-generated script for ticket ${context.ticketId}
760
903
  SCRIPT_PATH="${scriptPath}"
761
904
  PROMPT_PATH="${promptPath}"${systemPromptVar}
762
905
  ${setTitleCmds}
763
906
  echo "🚀 Starting: ${sessionName}"
907
+ ${context.executionEnvironment === 'sandbox' ? 'echo "🔒 Running in srt sandbox (filesystem + network isolation)"' : ''}
764
908
  echo ""
765
909
  cd "${context.worktreePath}"
766
910
  # Run executor in subshell with CLAUDECODE unset (prevents nested session error)
767
- (unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT; ${executorInvocation})
911
+ (unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT; ${finalInvocation})
768
912
 
769
913
  # Clean up script and prompt files
770
914
  rm -f "$SCRIPT_PATH" "$PROMPT_PATH"${systemPromptPath ? ' "$SYSTEM_PROMPT_PATH"' : ''}
@@ -1049,30 +1193,71 @@ export function getGitHubToken() {
1049
1193
  export function isGitHubTokenAvailable() {
1050
1194
  return getGitHubToken() !== null;
1051
1195
  }
1052
- // =============================================================================
1053
- // Docker Status Check
1054
- // =============================================================================
1055
1196
  /**
1056
- * Check if Docker daemon is running.
1057
- * Returns true if Docker is available and responsive.
1058
- * Uses retry logic to handle slow Docker Desktop startup.
1197
+ * Check Docker daemon health with fast detection (TKT-081).
1198
+ *
1199
+ * Uses `docker ps` with a 5-second timeout to quickly detect:
1200
+ * - Docker not installed
1201
+ * - Docker installed but daemon unresponsive (stuck on license, initializing, 500 errors)
1202
+ * - Docker ready
1203
+ *
1204
+ * Total worst-case time: ~5 seconds (single attempt with timeout).
1059
1205
  */
1060
- export function isDockerRunning() {
1061
- const maxRetries = 3;
1062
- const timeout = 10000; // 10 seconds
1063
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
1064
- try {
1065
- execSync('docker info', { stdio: 'pipe', timeout });
1066
- return true;
1206
+ export function checkDockerDaemon() {
1207
+ // First: is docker even installed?
1208
+ try {
1209
+ execSync('which docker', { stdio: 'pipe', timeout: 3000 });
1210
+ }
1211
+ catch {
1212
+ return {
1213
+ available: false,
1214
+ reason: 'not-installed',
1215
+ message: 'Docker is not installed.',
1216
+ };
1217
+ }
1218
+ // Second: is the daemon responsive? Use `docker ps` — it's lightweight and
1219
+ // fails fast when the daemon returns 500s or hangs on GUI prompts.
1220
+ const timeout = 5000; // 5 seconds — enough for a healthy daemon, fast fail otherwise
1221
+ try {
1222
+ execSync('docker ps -q --no-trunc', { stdio: 'pipe', timeout });
1223
+ return {
1224
+ available: true,
1225
+ reason: 'ready',
1226
+ message: 'Docker daemon is ready.',
1227
+ };
1228
+ }
1229
+ catch (error) {
1230
+ // Parse the error to give actionable feedback
1231
+ const stderr = error?.stderr?.toString() || '';
1232
+ const isTimeout = error?.killed === true;
1233
+ let message;
1234
+ if (isTimeout) {
1235
+ message = 'Docker daemon is not responding (timed out after 5s). Docker Desktop may be initializing or stuck — check for license/login prompts.';
1067
1236
  }
1068
- catch {
1069
- if (attempt === maxRetries) {
1070
- return false;
1071
- }
1072
- // Brief pause before retry
1237
+ else if (stderr.includes('500') || stderr.includes('Internal Server Error')) {
1238
+ message = 'Docker daemon is returning errors (500). Docker Desktop needs attention — check for license/login prompts.';
1239
+ }
1240
+ else if (stderr.includes('connect') || stderr.includes('Cannot connect') || stderr.includes('Is the docker daemon running')) {
1241
+ message = 'Docker daemon is not running. Start Docker Desktop and try again.';
1073
1242
  }
1243
+ else {
1244
+ message = `Docker daemon is not ready: ${stderr.trim() || 'unknown error'}. Check Docker Desktop status.`;
1245
+ }
1246
+ return {
1247
+ available: false,
1248
+ reason: 'daemon-not-ready',
1249
+ message,
1250
+ };
1074
1251
  }
1075
- return false;
1252
+ }
1253
+ /**
1254
+ * Check if Docker daemon is running.
1255
+ * Returns true if Docker is available and responsive.
1256
+ *
1257
+ * For detailed diagnostics, use checkDockerDaemon() instead.
1258
+ */
1259
+ export function isDockerRunning() {
1260
+ return checkDockerDaemon().available;
1076
1261
  }
1077
1262
  /**
1078
1263
  * Check if the devcontainer CLI is installed.
@@ -1129,7 +1314,7 @@ function getImageName(agentName) {
1129
1314
  */
1130
1315
  export function containerExists(containerName) {
1131
1316
  try {
1132
- execSync(`docker container inspect ${containerName}`, { stdio: 'pipe' });
1317
+ execSync(`docker container inspect ${containerName}`, { stdio: 'pipe', timeout: 5000 });
1133
1318
  return true;
1134
1319
  }
1135
1320
  catch {
@@ -1141,7 +1326,7 @@ export function containerExists(containerName) {
1141
1326
  */
1142
1327
  export function isContainerRunning(containerName) {
1143
1328
  try {
1144
- const status = execSync(`docker container inspect -f '{{.State.Running}}' ${containerName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
1329
+ const status = execSync(`docker container inspect -f '{{.State.Running}}' ${containerName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }).trim();
1145
1330
  return status === 'true';
1146
1331
  }
1147
1332
  catch {
@@ -1153,7 +1338,7 @@ export function isContainerRunning(containerName) {
1153
1338
  */
1154
1339
  export function getContainerId(containerName) {
1155
1340
  try {
1156
- const containerId = execSync(`docker container inspect -f '{{.Id}}' ${containerName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
1341
+ const containerId = execSync(`docker container inspect -f '{{.Id}}' ${containerName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }).trim();
1157
1342
  return containerId ? containerId.substring(0, 12) : null;
1158
1343
  }
1159
1344
  catch {
@@ -1189,7 +1374,7 @@ function buildDockerImage(agentDir, imageName, buildArgs = {}) {
1189
1374
  */
1190
1375
  function imageExists(imageName) {
1191
1376
  try {
1192
- execSync(`docker image inspect ${imageName}`, { stdio: 'pipe' });
1377
+ execSync(`docker image inspect ${imageName}`, { stdio: 'pipe', timeout: 5000 });
1193
1378
  return true;
1194
1379
  }
1195
1380
  catch {
@@ -1420,7 +1605,7 @@ function ensureDockerContainer(context, config, executor = 'claude-code') {
1420
1605
  // Container exists but is stopped - remove and recreate for fresh mounts
1421
1606
  console.debug(`[runners:docker] Removing stopped container ${containerName} to create fresh one`);
1422
1607
  try {
1423
- execSync(`docker rm -f ${containerName}`, { stdio: 'pipe' });
1608
+ execSync(`docker rm -f ${containerName}`, { stdio: 'pipe', timeout: 10000 });
1424
1609
  }
1425
1610
  catch {
1426
1611
  // Ignore removal errors
@@ -1564,7 +1749,7 @@ function writePromptFile(context) {
1564
1749
  * Uses docker exec for direct container access.
1565
1750
  * Uses a prompt file to avoid shell escaping issues.
1566
1751
  */
1567
- export function buildDevcontainerCommand(context, executor, promptFile, containerId, outputMode = 'interactive', permissionMode = 'safe', displayMode = 'terminal') {
1752
+ export function buildDevcontainerCommand(context, executor, promptFile, containerId, outputMode = 'interactive', permissionMode = 'safe', displayMode = 'terminal', mcpConfigFile) {
1568
1753
  // Calculate the relative path from agentDir to worktreePath for cd
1569
1754
  const relativePath = path.relative(context.agentDir, context.worktreePath);
1570
1755
  const cdCmd = relativePath ? `cd /workspace/${relativePath} && ` : '';
@@ -1582,7 +1767,11 @@ export function buildDevcontainerCommand(context, executor, promptFile, containe
1582
1767
  const permissionsFlag = skipPermissions ? '--dangerously-skip-permissions ' : '';
1583
1768
  // --effort high: skips the effort level prompt for automated agents (TKT-1134)
1584
1769
  const effortFlag = '--effort high ';
1585
- executorCmd = `claude ${bypassTrustFlag}${permissionsFlag}${effortFlag}${printFlag}"$(cat ${promptFile})"`;
1770
+ // TKT-053: Disable plan mode for background agents — prevents silent stalls
1771
+ const disallowPlanFlag = displayMode === 'background' ? '--disallowedTools EnterPlanMode ' : '';
1772
+ // Tool registry (TKT-083): pass MCP config to Claude Code via --mcp-config flag
1773
+ const mcpConfigFlag = mcpConfigFile ? `--mcp-config ${mcpConfigFile} ` : '';
1774
+ executorCmd = `claude ${bypassTrustFlag}${permissionsFlag}${effortFlag}${printFlag}${disallowPlanFlag}${mcpConfigFlag}"$(cat ${promptFile})"`;
1586
1775
  }
1587
1776
  else if (executor === 'codex') {
1588
1777
  // Use Codex adapter for mode validation and deterministic command building.
@@ -1634,11 +1823,12 @@ export async function runDevcontainer(context, executor, config, displayMode = '
1634
1823
  };
1635
1824
  }
1636
1825
  try {
1637
- // Check if Docker is running
1638
- if (!isDockerRunning()) {
1826
+ // Check if Docker is running (TKT-081: fast detection with diagnostic info)
1827
+ const dockerStatus = checkDockerDaemon();
1828
+ if (!dockerStatus.available) {
1639
1829
  return {
1640
1830
  success: false,
1641
- error: 'Docker is not running. Please start Docker Desktop and try again.',
1831
+ error: `Docker daemon is not available. ${dockerStatus.message}`,
1642
1832
  };
1643
1833
  }
1644
1834
  // Ensure GitHub token is available for git push operations
@@ -1672,6 +1862,16 @@ export async function runDevcontainer(context, executor, config, displayMode = '
1672
1862
  }
1673
1863
  // Write prompt to file in worktree (accessible by container)
1674
1864
  const { hostPath: promptHostPath, containerPath: promptFile } = writePromptFile(context);
1865
+ // Tool registry (TKT-083): generate MCP config file for container
1866
+ let mcpConfigContainerPath;
1867
+ if (context.hqPath && isClaudeExecutor(executor)) {
1868
+ const toolsResult = resolveToolsForSpawn(context.hqPath, context.toolPolicy, context.worktreePath);
1869
+ if (toolsResult.mcpConfigPath) {
1870
+ // Map host path to container path
1871
+ const relativeMcp = path.relative(context.agentDir, toolsResult.mcpConfigPath);
1872
+ mcpConfigContainerPath = `/workspace/${relativeMcp}`;
1873
+ }
1874
+ }
1675
1875
  // Inject fresh GitHub token into container (containers may be reused with stale/empty tokens)
1676
1876
  // This ensures git push works even if the container was created before token was available
1677
1877
  const githubToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
@@ -1688,7 +1888,7 @@ export async function runDevcontainer(context, executor, config, displayMode = '
1688
1888
  }
1689
1889
  // Build the docker exec command (just runs claude directly)
1690
1890
  // tmux session setup is handled by runDevcontainerInTmux, not buildDevcontainerCommand
1691
- const devcontainerCmd = buildDevcontainerCommand(context, executor, promptFile, containerId, config.outputMode, config.permissionMode, displayMode);
1891
+ const devcontainerCmd = buildDevcontainerCommand(context, executor, promptFile, containerId, config.outputMode, config.permissionMode, displayMode, mcpConfigContainerPath);
1692
1892
  // Execute based on display mode
1693
1893
  // When sessionManager is 'tmux', always use tmux inside container for session persistence
1694
1894
  // (allows reattach via `prlt session attach` even for background mode)
@@ -2333,13 +2533,12 @@ export async function runDocker(context, executor, config) {
2333
2533
  const prompt = buildPrompt(context);
2334
2534
  const containerName = `work-${context.ticketId}-${Date.now()}`;
2335
2535
  try {
2336
- // Check if docker is available
2337
- execSync('which docker', { stdio: 'pipe' });
2338
- // Check if Docker is running
2339
- if (!isDockerRunning()) {
2536
+ // Check if docker is available and daemon is responsive (TKT-081)
2537
+ const dockerStatus = checkDockerDaemon();
2538
+ if (!dockerStatus.available) {
2340
2539
  return {
2341
2540
  success: false,
2342
- error: 'Docker is not running. Please start Docker Desktop and try again.',
2541
+ error: `Docker daemon is not available. ${dockerStatus.message}`,
2343
2542
  };
2344
2543
  }
2345
2544
  // Build docker run command
@@ -2371,7 +2570,8 @@ export async function runDocker(context, executor, config) {
2371
2570
  // Non-Claude executors use their native command format from getExecutorCommand()
2372
2571
  dockerCmd += ` ${config.docker.image}`;
2373
2572
  if (isClaudeExecutor(executor)) {
2374
- dockerCmd += ` ${cmd} --print '${escapedPrompt}'`;
2573
+ // TKT-053: Disable plan mode — Docker runner is always detached (no user to approve)
2574
+ dockerCmd += ` ${cmd} --print --disallowedTools EnterPlanMode '${escapedPrompt}'`;
2375
2575
  }
2376
2576
  else {
2377
2577
  const argsStr = args.map(a => a === escapedPrompt ? `'${escapedPrompt}'` : a).join(' ');
@@ -2420,11 +2620,12 @@ export async function runOrchestratorInDocker(context, executor, config, options
2420
2620
  const containerName = `prlt-orchestrator-${(hqName).replace(/[^a-zA-Z0-9._-]/g, '-')}-${(orchestratorName).replace(/[^a-zA-Z0-9._-]/g, '-')}`;
2421
2621
  const imageName = `prlt-orchestrator-${(hqName).replace(/[^a-zA-Z0-9._-]/g, '-')}:latest`;
2422
2622
  try {
2423
- // Check Docker is running
2424
- if (!isDockerRunning()) {
2623
+ // Check Docker is running (TKT-081: fast detection with diagnostic info)
2624
+ const dockerStatus = checkDockerDaemon();
2625
+ if (!dockerStatus.available) {
2425
2626
  return {
2426
2627
  success: false,
2427
- error: 'Docker is not running. Please start Docker Desktop and try again.',
2628
+ error: `Docker daemon is not available. ${dockerStatus.message}`,
2428
2629
  };
2429
2630
  }
2430
2631
  // Check if container already exists and is running
@@ -2584,8 +2785,10 @@ export async function runOrchestratorInDocker(context, executor, config, options
2584
2785
  const skipPermissions = config.permissionMode === 'danger';
2585
2786
  const permissionsFlag = skipPermissions ? '--dangerously-skip-permissions ' : '';
2586
2787
  const effortFlag = skipPermissions ? '--effort high ' : '';
2788
+ // TKT-053: Disable plan mode for background agents — prevents silent stalls
2789
+ const disallowPlanFlag = displayMode === 'background' ? '--disallowedTools EnterPlanMode ' : '';
2587
2790
  const executorCmd = executor === 'claude-code'
2588
- ? `claude ${permissionsFlag}${effortFlag}"$(cat ${promptPath})"`
2791
+ ? `claude ${permissionsFlag}${effortFlag}${disallowPlanFlag}"$(cat ${promptPath})"`
2589
2792
  : `claude ${permissionsFlag}${effortFlag}"$(cat ${promptPath})"`;
2590
2793
  // Build tmux session name (reuses the same name as host tmux for consistency)
2591
2794
  const tmuxSessionName = options?.sessionName || containerName;
@@ -2602,7 +2805,7 @@ export async function runOrchestratorInDocker(context, executor, config, options
2602
2805
  const scriptContent = `#!/bin/bash
2603
2806
  cd /hq
2604
2807
  unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT
2605
- ${executor === 'claude-code' ? `claude ${permissionsFlag}${effortFlag}"$(cat ${promptPath})"` : `claude "$(cat ${promptPath})"`}
2808
+ ${executor === 'claude-code' ? `claude ${permissionsFlag}${effortFlag}${disallowPlanFlag}"$(cat ${promptPath})"` : `claude "$(cat ${promptPath})"`}
2606
2809
  echo ""
2607
2810
  echo "Orchestrator complete. Press Enter to close."
2608
2811
  exec bash
@@ -2712,19 +2915,125 @@ exec $SHELL
2712
2915
  }
2713
2916
  }
2714
2917
  // =============================================================================
2715
- // VM Runner
2918
+ // Sandbox Utilities
2919
+ // =============================================================================
2920
+ /**
2921
+ * Check if srt (sandbox-runtime) is installed on the host.
2922
+ */
2923
+ export function isSrtInstalled() {
2924
+ try {
2925
+ execSync('which srt', { stdio: 'pipe' });
2926
+ return true;
2927
+ }
2928
+ catch {
2929
+ return false;
2930
+ }
2931
+ }
2932
+ /**
2933
+ * Build the srt command with filesystem and network restrictions.
2934
+ *
2935
+ * Filesystem policy (read-restriction philosophy from claude-code-sandbox):
2936
+ * - Read/write: agent worktree directory
2937
+ * - Read-only: repo source (if different from worktree)
2938
+ * - Read-only: additional configured read paths
2939
+ * - Deny: home directory, system paths, other repos
2940
+ *
2941
+ * Network policy:
2942
+ * - Allow: configured domains (GitHub, Anthropic API, npm registries, etc.)
2943
+ * - Deny: everything else
2944
+ */
2945
+ export function buildSrtCommand(innerCommand, context, config) {
2946
+ const args = ['srt'];
2947
+ // Filesystem: always allow read/write to agent worktree
2948
+ args.push(`--fs-write=${context.worktreePath}`);
2949
+ // Allow read/write to the agent directory (parent of worktree, contains .devcontainer etc.)
2950
+ if (context.agentDir && context.agentDir !== context.worktreePath) {
2951
+ args.push(`--fs-write=${context.agentDir}`);
2952
+ }
2953
+ // Allow read/write to HQ scripts directory (for temp script files)
2954
+ if (context.hqPath) {
2955
+ const scriptsDir = path.join(context.hqPath, '.proletariat', 'scripts');
2956
+ args.push(`--fs-write=${scriptsDir}`);
2957
+ }
2958
+ // Allow read access to additional configured paths
2959
+ for (const readPath of config.sandbox.allowReadPaths) {
2960
+ args.push(`--fs-read=${readPath}`);
2961
+ }
2962
+ // Allow write access to additional configured paths
2963
+ for (const writePath of config.sandbox.allowWritePaths) {
2964
+ args.push(`--fs-write=${writePath}`);
2965
+ }
2966
+ // Allow read to temp directory (needed for script execution)
2967
+ args.push(`--fs-write=${os.tmpdir()}`);
2968
+ // Network: merge sandbox domains with firewall allowlist
2969
+ const allDomains = new Set([
2970
+ ...config.sandbox.networkDomains,
2971
+ ...config.firewall.allowlistDomains,
2972
+ ]);
2973
+ for (const domain of allDomains) {
2974
+ args.push(`--net-allow=${domain}`);
2975
+ }
2976
+ // The inner command to execute inside the sandbox
2977
+ args.push('--');
2978
+ args.push(innerCommand);
2979
+ return args.join(' ');
2980
+ }
2981
+ // =============================================================================
2982
+ // Sandbox Runner - srt-based sandbox on host
2983
+ // =============================================================================
2984
+ /**
2985
+ * Run command in an srt sandbox on the host machine.
2986
+ * Uses the same tmux session approach as the host runner, but wraps the
2987
+ * executor command with srt for filesystem and network isolation.
2988
+ *
2989
+ * Falls back to host runner with warning if srt is not installed.
2990
+ */
2991
+ export async function runSandbox(context, executor, config, displayMode = 'terminal') {
2992
+ // Check if srt is installed
2993
+ if (!isSrtInstalled()) {
2994
+ if (config.sandbox.fallbackToHost) {
2995
+ // Log warning via stderr (will be visible in terminal)
2996
+ process.stderr.write('\x1b[33m⚠️ srt (sandbox-runtime) not installed. Falling back to host execution.\n' +
2997
+ ' Install srt for filesystem + network isolation: https://github.com/anthropic-experimental/sandbox-runtime\x1b[0m\n');
2998
+ // Fall back to host runner
2999
+ return runHost(context, executor, config, displayMode);
3000
+ }
3001
+ return {
3002
+ success: false,
3003
+ error: 'srt (sandbox-runtime) is not installed.\n\n' +
3004
+ 'Install it from: https://github.com/anthropic-experimental/sandbox-runtime\n' +
3005
+ 'Or set sandbox.fallbackToHost to true in execution config to fall back to host.',
3006
+ };
3007
+ }
3008
+ // Delegate to host runner — the sandbox wrapping happens at the script level
3009
+ // We set a flag on context so the host runner knows to wrap with srt
3010
+ const sandboxContext = {
3011
+ ...context,
3012
+ executionEnvironment: 'sandbox',
3013
+ };
3014
+ return runHost(sandboxContext, executor, config, displayMode);
3015
+ }
3016
+ // =============================================================================
3017
+ // Cloud Runner (was VM Runner)
2716
3018
  // =============================================================================
2717
- export async function runVm(context, executor, config, host) {
2718
- const targetHost = host || config.vm.defaultHost;
3019
+ /**
3020
+ * Run command on a remote machine (cloud) via SSH.
3021
+ * Formerly 'runVm' — renamed to reflect the simplified environment hierarchy.
3022
+ * Uses cloud config with fallback to legacy vm config for backwards compatibility.
3023
+ */
3024
+ export async function runCloud(context, executor, config, host) {
3025
+ // Use cloud config, fall back to vm config for backwards compatibility
3026
+ const cloudConfig = config.cloud?.defaultHost ? config.cloud : config.vm;
3027
+ const targetHost = host || cloudConfig.defaultHost;
2719
3028
  if (!targetHost) {
2720
3029
  return {
2721
3030
  success: false,
2722
- error: 'No VM host specified. Use --host or configure execution.vm.default_host',
3031
+ error: 'No cloud host specified. Use --host or configure execution.cloud.default_host',
2723
3032
  };
2724
3033
  }
2725
3034
  const prompt = buildPrompt(context);
2726
- const user = config.vm.user;
2727
- const keyPath = config.vm.keyPath;
3035
+ const user = cloudConfig.user;
3036
+ const keyPath = cloudConfig.keyPath;
2728
3037
  const remoteWorkspace = `/workspace/${context.agentName}`;
2729
3038
  try {
2730
3039
  // Build SSH options
@@ -2733,7 +3042,7 @@ export async function runVm(context, executor, config, host) {
2733
3042
  sshOpts = `-i "${keyPath}"`;
2734
3043
  }
2735
3044
  // Sync worktree to remote
2736
- if (config.vm.syncMethod === 'rsync') {
3045
+ if (cloudConfig.syncMethod === 'rsync') {
2737
3046
  let rsyncCmd = `rsync -avz`;
2738
3047
  if (keyPath) {
2739
3048
  rsyncCmd += ` -e "ssh -i ${keyPath}"`;
@@ -2747,7 +3056,7 @@ export async function runVm(context, executor, config, host) {
2747
3056
  const gitPullCmd = `cd ${remoteWorkspace} && git fetch && git checkout ${context.branch}`;
2748
3057
  execSync(`ssh ${sshOpts} ${user}@${targetHost} "${gitPullCmd}"`, { stdio: 'pipe' });
2749
3058
  }
2750
- // Validate Codex mode: VM runner is always non-tty (SSH + nohup)
3059
+ // Validate Codex mode: Cloud runner is always non-tty (SSH + nohup)
2751
3060
  if (executor === 'codex') {
2752
3061
  const codexPermission = config.permissionMode;
2753
3062
  const modeError = validateCodexMode(codexPermission, 'non-tty');
@@ -2761,7 +3070,8 @@ export async function runVm(context, executor, config, host) {
2761
3070
  // Build the remote command based on executor type
2762
3071
  let remoteCmd;
2763
3072
  if (isClaudeExecutor(executor)) {
2764
- remoteCmd = `cd ${remoteWorkspace} && ${executorCmd} --print '${escapedPrompt}'`;
3073
+ // TKT-053: Disable plan mode VM runner is always nohup (no user to approve)
3074
+ remoteCmd = `cd ${remoteWorkspace} && ${executorCmd} --print --disallowedTools EnterPlanMode '${escapedPrompt}'`;
2765
3075
  }
2766
3076
  else {
2767
3077
  const argsStr = executorArgs.map(a => a === escapedPrompt ? `'${escapedPrompt}'` : a).join(' ');
@@ -2778,29 +3088,38 @@ export async function runVm(context, executor, config, host) {
2778
3088
  catch (error) {
2779
3089
  return {
2780
3090
  success: false,
2781
- error: error instanceof Error ? error.message : 'Failed to execute on VM',
3091
+ error: error instanceof Error ? error.message : 'Failed to execute on cloud',
2782
3092
  };
2783
3093
  }
2784
3094
  }
3095
+ /** @deprecated Use runCloud instead */
3096
+ export const runVm = runCloud;
2785
3097
  // =============================================================================
2786
3098
  // Runner Dispatcher
2787
3099
  // =============================================================================
2788
3100
  export async function runExecution(environment, context, executor, config = DEFAULT_EXECUTION_CONFIG, options) {
2789
- // Ensure tmux server has keychain access for OAuth (host only)
3101
+ // Ensure context knows its execution environment
3102
+ if (!context.executionEnvironment) {
3103
+ context.executionEnvironment = environment;
3104
+ }
3105
+ // Normalize environment (maps 'vm' -> 'cloud')
3106
+ const normalizedEnv = normalizeEnvironment(environment);
3107
+ // Ensure tmux server has keychain access for OAuth (host/sandbox only)
2790
3108
  // Docker uses claude-credentials volume, devcontainer runs inside container
2791
- if (environment === 'host') {
3109
+ if (normalizedEnv === 'host' || normalizedEnv === 'sandbox') {
2792
3110
  await ensureTmuxServerHasKeychainAccess();
2793
3111
  }
2794
- switch (environment) {
3112
+ switch (normalizedEnv) {
2795
3113
  case 'devcontainer':
2796
3114
  return runDevcontainer(context, executor, config, options?.displayMode, options?.sessionManager);
2797
3115
  case 'host':
2798
- // Host uses tmux for session persistence (same as devcontainer)
2799
3116
  return runHost(context, executor, config, options?.displayMode);
3117
+ case 'sandbox':
3118
+ return runSandbox(context, executor, config, options?.displayMode);
2800
3119
  case 'docker':
2801
3120
  return runDocker(context, executor, config);
2802
- case 'vm':
2803
- return runVm(context, executor, config, options?.host);
3121
+ case 'cloud':
3122
+ return runCloud(context, executor, config, options?.host);
2804
3123
  default:
2805
3124
  return { success: false, error: `Unknown execution environment: ${environment}` };
2806
3125
  }