@proletariat/cli 0.3.56 → 0.3.58

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 (67) hide show
  1. package/README.md +69 -1
  2. package/bin/validate-better-sqlite3.cjs +44 -5
  3. package/dist/commands/dashboard.d.ts +38 -0
  4. package/dist/commands/dashboard.js +352 -0
  5. package/dist/commands/dashboard.js.map +1 -0
  6. package/dist/commands/media/add.d.ts +19 -0
  7. package/dist/commands/media/add.js +94 -0
  8. package/dist/commands/media/add.js.map +1 -0
  9. package/dist/commands/media/index.d.ts +14 -0
  10. package/dist/commands/media/index.js +85 -0
  11. package/dist/commands/media/index.js.map +1 -0
  12. package/dist/commands/media/list.d.ts +15 -0
  13. package/dist/commands/media/list.js +89 -0
  14. package/dist/commands/media/list.js.map +1 -0
  15. package/dist/commands/media/preprocess.d.ts +19 -0
  16. package/dist/commands/media/preprocess.js +91 -0
  17. package/dist/commands/media/preprocess.js.map +1 -0
  18. package/dist/commands/media/remove.d.ts +18 -0
  19. package/dist/commands/media/remove.js +101 -0
  20. package/dist/commands/media/remove.js.map +1 -0
  21. package/dist/commands/media/show.d.ts +17 -0
  22. package/dist/commands/media/show.js +122 -0
  23. package/dist/commands/media/show.js.map +1 -0
  24. package/dist/commands/orchestrator/start.js +6 -0
  25. package/dist/commands/orchestrator/start.js.map +1 -1
  26. package/dist/commands/repo/fix-remotes.d.ts +14 -0
  27. package/dist/commands/repo/fix-remotes.js +154 -0
  28. package/dist/commands/repo/fix-remotes.js.map +1 -0
  29. package/dist/commands/work/start.d.ts +1 -0
  30. package/dist/commands/work/start.js +42 -17
  31. package/dist/commands/work/start.js.map +1 -1
  32. package/dist/lib/agents/commands.js +5 -7
  33. package/dist/lib/agents/commands.js.map +1 -1
  34. package/dist/lib/agents/index.js +6 -7
  35. package/dist/lib/agents/index.js.map +1 -1
  36. package/dist/lib/database/index.d.ts +49 -1
  37. package/dist/lib/database/index.js +127 -0
  38. package/dist/lib/database/index.js.map +1 -1
  39. package/dist/lib/database/native-validation.d.ts +2 -0
  40. package/dist/lib/database/native-validation.js +30 -1
  41. package/dist/lib/database/native-validation.js.map +1 -1
  42. package/dist/lib/execution/runners.d.ts +24 -1
  43. package/dist/lib/execution/runners.js +309 -98
  44. package/dist/lib/execution/runners.js.map +1 -1
  45. package/dist/lib/execution/spawner.d.ts +2 -2
  46. package/dist/lib/execution/spawner.js +42 -23
  47. package/dist/lib/execution/spawner.js.map +1 -1
  48. package/dist/lib/execution/types.d.ts +2 -0
  49. package/dist/lib/execution/types.js.map +1 -1
  50. package/dist/lib/external-issues/shortcut.js +2 -1
  51. package/dist/lib/external-issues/shortcut.js.map +1 -1
  52. package/dist/lib/media/index.d.ts +91 -0
  53. package/dist/lib/media/index.js +475 -0
  54. package/dist/lib/media/index.js.map +1 -0
  55. package/dist/lib/repos/git.d.ts +44 -0
  56. package/dist/lib/repos/git.js +127 -0
  57. package/dist/lib/repos/git.js.map +1 -1
  58. package/dist/lib/repos/index.js +35 -2
  59. package/dist/lib/repos/index.js.map +1 -1
  60. package/dist/lib/work-source/config.d.ts +5 -0
  61. package/dist/lib/work-source/config.js +19 -0
  62. package/dist/lib/work-source/config.js.map +1 -1
  63. package/dist/lib/work-source/index.d.ts +1 -1
  64. package/dist/lib/work-source/index.js +1 -1
  65. package/dist/lib/work-source/index.js.map +1 -1
  66. package/oclif.manifest.json +3612 -3130
  67. package/package.json +1 -1
@@ -8,6 +8,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
+ import { fileURLToPath } from 'node:url';
11
12
  import { DEFAULT_EXECUTION_CONFIG, } from './types.js';
12
13
  import { getSetTitleCommands } from '../terminal.js';
13
14
  import { readDevcontainerJson, generateOrchestratorDockerfile } from './devcontainer.js';
@@ -387,35 +388,201 @@ export function runExecutorPreflight(environment, executor, options) {
387
388
  }
388
389
  return { ok: true };
389
390
  }
391
+ const INTEGRATION_COMMANDS = [
392
+ {
393
+ provider: 'asana',
394
+ displayName: 'Asana',
395
+ commands: [
396
+ 'prlt asana connect — authenticate with Asana',
397
+ 'prlt asana sync --ticket TKT-XXX --create-missing --project <gid> — sync a PMO ticket to Asana',
398
+ 'prlt asana import — import Asana tasks into PMO',
399
+ ],
400
+ },
401
+ {
402
+ provider: 'linear',
403
+ displayName: 'Linear',
404
+ commands: [
405
+ 'prlt linear connect — authenticate with Linear',
406
+ 'prlt linear sync --ticket TKT-XXX --create-missing — sync a PMO ticket to Linear',
407
+ 'prlt linear import — import Linear issues into PMO',
408
+ ],
409
+ },
410
+ {
411
+ provider: 'jira',
412
+ displayName: 'Jira',
413
+ commands: [
414
+ 'prlt jira connect — authenticate with Jira',
415
+ 'prlt jira sync --ticket TKT-XXX --create-missing — sync a PMO ticket to Jira',
416
+ 'prlt jira import — import Jira issues into PMO',
417
+ ],
418
+ },
419
+ {
420
+ provider: 'shortcut',
421
+ displayName: 'Shortcut',
422
+ commands: [
423
+ 'prlt shortcut connect — authenticate with Shortcut',
424
+ 'prlt shortcut sync --ticket TKT-XXX --create-missing — sync a PMO ticket to Shortcut',
425
+ 'prlt shortcut import — import Shortcut stories into PMO',
426
+ ],
427
+ },
428
+ {
429
+ provider: 'monday',
430
+ displayName: 'Monday.com',
431
+ commands: [
432
+ 'prlt monday connect — authenticate with Monday.com',
433
+ 'prlt monday sync --ticket TKT-XXX --create-missing — sync a PMO ticket to Monday.com',
434
+ ],
435
+ },
436
+ ];
390
437
  /**
391
- * Build the system prompt for orchestrator sessions.
392
- * This is injected via Claude Code's --system-prompt flag so the orchestrator
393
- * knows its role immediately without relying on CLAUDE.md.
438
+ * Build the integration commands section for agent prompts.
439
+ * Only includes integrations that are actually connected/configured.
440
+ * Returns empty string if no integrations are connected.
394
441
  */
395
- export function buildOrchestratorSystemPrompt(context) {
396
- const hqName = context.hqName || 'workspace';
397
- let prompt = `You are an orchestrator for the **${hqName}** project. `;
398
- prompt += `Use \`prlt\` to view what's running — board, sessions, tickets, PRs. `;
399
- prompt += `Do not implement any work yourself. `;
400
- prompt += `Your job is to review, plan, investigate, delegate (via \`prlt work start\`), and review completed work.\n\n`;
442
+ function buildIntegrationCommandsSection(connectedIntegrations) {
443
+ if (!connectedIntegrations || connectedIntegrations.length === 0)
444
+ return '';
445
+ const connected = INTEGRATION_COMMANDS.filter(ic => connectedIntegrations.includes(ic.provider));
446
+ if (connected.length === 0)
447
+ return '';
448
+ let section = `## Integration Commands\n\n`;
449
+ section += `The following external integrations are connected. Use these prlt commands to interact with them.\n\n`;
450
+ for (const integration of connected) {
451
+ section += `### ${integration.displayName}\n`;
452
+ for (const cmd of integration.commands) {
453
+ section += `- \`${cmd.split(' — ')[0]}\` — ${cmd.split(' — ')[1] || ''}\n`;
454
+ }
455
+ section += '\n';
456
+ }
457
+ 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`;
458
+ return section;
459
+ }
460
+ const ORCHESTRATOR_COMMAND_REGISTRY = [
461
+ {
462
+ title: 'Agent Lifecycle',
463
+ commands: [
464
+ { 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' },
465
+ { cmd: 'prlt session list', desc: 'List running sessions', checkPath: 'session/list' },
466
+ { cmd: 'prlt session inspect <agent>', desc: 'Inspect session details', checkPath: 'session/inspect' },
467
+ { cmd: 'prlt session poke <agent> \'message\'', desc: 'Send message to agent', checkPath: 'session/poke' },
468
+ { cmd: 'prlt session peek <agent> --lines 200', desc: 'Read agent output', checkPath: 'session/peek' },
469
+ { cmd: 'prlt session health', desc: 'Check health of all sessions', checkPath: 'session/health' },
470
+ { cmd: 'prlt session restart <agent>', desc: 'Restart a stuck agent', checkPath: 'session/restart' },
471
+ { cmd: 'prlt session exec <agent> -- git status', desc: 'Run command in agent context', checkPath: 'session/exec' },
472
+ { cmd: 'prlt session prune', desc: 'Clean up dead sessions', checkPath: 'session/prune' },
473
+ ],
474
+ },
475
+ {
476
+ title: 'Board Management',
477
+ commands: [
478
+ { cmd: 'prlt board view', desc: 'View the board', checkPath: 'board/view' },
479
+ { cmd: 'prlt ticket list', desc: 'List tickets', checkPath: 'ticket/list' },
480
+ { cmd: 'prlt ticket show <id>', desc: 'Show ticket details', checkPath: 'ticket/show' },
481
+ { cmd: 'prlt ticket create --title \'x\' --description \'y\'', desc: 'Create a ticket', checkPath: 'ticket/create' },
482
+ { cmd: 'prlt ticket edit <id> --title \'...\' --add-ac \'...\'', desc: 'Edit ticket fields', checkPath: 'ticket/edit' },
483
+ ],
484
+ },
485
+ {
486
+ title: 'PR Workflow',
487
+ commands: [
488
+ { cmd: 'gh pr list', desc: 'List open PRs' },
489
+ { cmd: 'gh pr view <num>', desc: 'View PR details' },
490
+ { cmd: 'gh pr checks <num>', desc: 'Check CI status' },
491
+ { cmd: 'gh pr merge <num> --squash', desc: 'Merge PR (squash only)' },
492
+ ],
493
+ },
494
+ ];
495
+ const ORCHESTRATOR_ANTI_PATTERNS = [
496
+ { bad: 'docker exec <container> ...', good: 'prlt session exec', checkPath: 'session/exec' },
497
+ { bad: 'tmux send-keys ...', good: 'prlt session poke', checkPath: 'session/poke' },
498
+ { bad: 'tmux capture-pane ...', good: 'prlt session peek', checkPath: 'session/peek' },
499
+ { bad: 'Direct git operations on agent worktrees', good: 'prlt session exec', checkPath: 'session/exec' },
500
+ ];
501
+ /**
502
+ * Resolve the commands directory for dynamic command availability checks.
503
+ * Looks for compiled command files under dist/commands/.
504
+ */
505
+ let _commandsDir = null;
506
+ function getCommandsDir() {
507
+ if (_commandsDir === null) {
508
+ const currentFile = fileURLToPath(import.meta.url);
509
+ // From dist/lib/execution/runners.js → dist/commands/
510
+ _commandsDir = path.resolve(path.dirname(currentFile), '..', '..', 'commands');
511
+ }
512
+ return _commandsDir;
513
+ }
514
+ function isCommandAvailable(checkPath) {
515
+ const dir = getCommandsDir();
516
+ // Check for compiled .js file or directory (which would contain index.js)
517
+ return fs.existsSync(path.join(dir, `${checkPath}.js`)) || fs.existsSync(path.join(dir, checkPath));
518
+ }
519
+ /**
520
+ * Build the dynamic command reference section for the orchestrator prompt.
521
+ * Only includes commands that are actually available in this build.
522
+ */
523
+ function buildOrchestratorCommandReference() {
524
+ let ref = '';
525
+ for (const category of ORCHESTRATOR_COMMAND_REGISTRY) {
526
+ const available = category.commands.filter(c => !c.checkPath || isCommandAvailable(c.checkPath));
527
+ if (available.length === 0)
528
+ continue;
529
+ ref += `### ${category.title}\n`;
530
+ for (const cmd of available) {
531
+ ref += `- \`${cmd.cmd}\` — ${cmd.desc}\n`;
532
+ }
533
+ ref += '\n';
534
+ }
535
+ return ref;
536
+ }
537
+ /**
538
+ * Build the anti-patterns section for the orchestrator prompt.
539
+ * Only includes anti-patterns where the prlt replacement is available.
540
+ */
541
+ function buildOrchestratorAntiPatterns() {
542
+ const available = ORCHESTRATOR_ANTI_PATTERNS.filter(ap => !ap.checkPath || isCommandAvailable(ap.checkPath));
543
+ if (available.length === 0)
544
+ return '';
545
+ let section = `## Anti-Patterns — NEVER DO\n\n`;
546
+ for (const ap of available) {
547
+ section += `- \`${ap.bad}\` → use \`${ap.good}\` instead\n`;
548
+ }
549
+ section += `\n`;
550
+ return section;
551
+ }
552
+ /**
553
+ * Build the shared orchestrator prompt body (role, runtime, commands, anti-patterns).
554
+ * Used by both buildOrchestratorSystemPrompt and buildOrchestratorPrompt.
555
+ */
556
+ function buildOrchestratorBody(hqName, context) {
557
+ let prompt = '';
558
+ // Runtime declaration
559
+ prompt += `## prlt Is Your Orchestration Runtime\n\n`;
560
+ prompt += `prlt is your orchestration runtime. NEVER use raw docker exec, tmux send-keys, or direct container access. `;
561
+ prompt += `All orchestration goes through prlt. Every agent interaction, session management, and board operation `;
562
+ prompt += `has a dedicated prlt command. Using raw infrastructure commands bypasses session tracking, breaks `;
563
+ prompt += `health monitoring, and creates orphaned processes.\n\n`;
564
+ // Role
401
565
  prompt += `## Your Role\n`;
402
566
  prompt += `- Plan and prioritize work across the board\n`;
403
567
  prompt += `- Delegate implementation to agents via \`prlt work start\`\n`;
404
568
  prompt += `- Monitor agent progress and review completed work\n`;
405
569
  prompt += `- Merge completed PRs via \`gh pr merge --squash\`\n`;
406
570
  prompt += `- Never write code or make changes to source files yourself\n\n`;
407
- prompt += `## Discovering State\n`;
408
- prompt += `Always discover current state dynamically — do NOT rely on static context files:\n`;
409
- prompt += `- **Board**: \`prlt board view\`, \`prlt ticket list\`, \`prlt ticket show <id>\`\n`;
410
- prompt += `- **Agents**: \`prlt session list\`, \`prlt session peek <session>\`, \`prlt work status\`\n`;
411
- prompt += `- **PRs/CI**: \`gh pr list\`, \`gh pr view <num>\`, \`gh pr checks <num>\`\n`;
412
- prompt += `- All prlt MCP tools are also available\n\n`;
571
+ // Command reference (dynamically generated)
572
+ prompt += `## Command Reference\n\n`;
573
+ prompt += buildOrchestratorCommandReference();
574
+ // Spawning agents (detailed example)
413
575
  prompt += `## Spawning Agents\n`;
414
576
  prompt += `\`\`\`\n`;
415
577
  prompt += `script -q /dev/null prlt work start TKT-XXXX --ephemeral --skip-permissions --create-pr --display background --action implement --run-on-host --yes\n`;
416
578
  prompt += `\`\`\`\n`;
417
579
  prompt += `- Review: \`--action review-comment\`\n`;
418
580
  prompt += `- Fix: \`--action review-fix\`\n\n`;
581
+ // Anti-patterns (dynamically generated)
582
+ prompt += buildOrchestratorAntiPatterns();
583
+ // Integration commands (only for connected integrations)
584
+ prompt += buildIntegrationCommandsSection(context.connectedIntegrations);
585
+ // Workflow
419
586
  prompt += `## Workflow\n`;
420
587
  prompt += `- Squash merge only: \`gh pr merge --squash\`\n`;
421
588
  prompt += `- After merging: subsequent PRs from parallel agents will need rebase\n`;
@@ -437,6 +604,19 @@ export function buildOrchestratorSystemPrompt(context) {
437
604
  }
438
605
  return prompt;
439
606
  }
607
+ /**
608
+ * Build the system prompt for orchestrator sessions.
609
+ * This is injected via Claude Code's --system-prompt flag so the orchestrator
610
+ * knows its role immediately without relying on CLAUDE.md.
611
+ */
612
+ export function buildOrchestratorSystemPrompt(context) {
613
+ const hqName = context.hqName || 'workspace';
614
+ let prompt = `You are an orchestrator for the **${hqName}** project. `;
615
+ prompt += `Do not implement any work yourself. `;
616
+ prompt += `Your job is to review, plan, investigate, delegate (via \`prlt work start\`), and review completed work.\n\n`;
617
+ prompt += buildOrchestratorBody(hqName, context);
618
+ return prompt;
619
+ }
440
620
  function buildOrchestratorPrompt(context) {
441
621
  // Full prompt including role context — used for non-Claude executors that
442
622
  // don't support --system-prompt. For Claude Code, runHost() splits this into
@@ -444,43 +624,7 @@ function buildOrchestratorPrompt(context) {
444
624
  const hqName = context.hqName || 'workspace';
445
625
  let prompt = `# Orchestrator: ${hqName}\n\n`;
446
626
  prompt += `You are the orchestrator for the **${hqName}** workspace using the prlt ecosystem.\n\n`;
447
- prompt += `## Your Role\n`;
448
- prompt += `- Plan and prioritize work across the board\n`;
449
- prompt += `- Delegate implementation to agents via \`prlt work start\`\n`;
450
- prompt += `- Monitor agent progress and review completed work\n`;
451
- prompt += `- Merge completed PRs via \`gh pr merge --squash\`\n`;
452
- prompt += `- Never write code or make changes to source files yourself\n\n`;
453
- prompt += `## Discovering State\n`;
454
- prompt += `Always discover current state dynamically — do NOT rely on static context files:\n`;
455
- prompt += `- **Board**: \`prlt board view\`, \`prlt ticket list\`, \`prlt ticket show <id>\`\n`;
456
- prompt += `- **Agents**: \`prlt session list\`, \`prlt session peek <session>\`, \`prlt work status\`\n`;
457
- prompt += `- **PRs/CI**: \`gh pr list\`, \`gh pr view <num>\`, \`gh pr checks <num>\`\n`;
458
- prompt += `- All prlt MCP tools are also available\n\n`;
459
- prompt += `## Spawning Agents\n`;
460
- prompt += `\`\`\`\n`;
461
- prompt += `script -q /dev/null prlt work start TKT-XXXX --ephemeral --skip-permissions --create-pr --display background --action implement --run-on-host --yes\n`;
462
- prompt += `\`\`\`\n`;
463
- prompt += `- Review: \`--action review-comment\`\n`;
464
- prompt += `- Fix: \`--action review-fix\`\n\n`;
465
- prompt += `## Workflow\n`;
466
- prompt += `- Squash merge only: \`gh pr merge --squash\`\n`;
467
- prompt += `- After merging: subsequent PRs from parallel agents will need rebase\n`;
468
- prompt += `- Kill stale sessions after their PRs are merged\n\n`;
469
- // Load .orchestrator-context.md from HQ root if it exists
470
- if (context.hqPath) {
471
- const contextFilePath = path.join(context.hqPath, '.orchestrator-context.md');
472
- if (fs.existsSync(contextFilePath)) {
473
- try {
474
- const contextContent = fs.readFileSync(contextFilePath, 'utf-8').trim();
475
- if (contextContent) {
476
- prompt += `## Workspace Context\n\n${contextContent}\n\n`;
477
- }
478
- }
479
- catch {
480
- // Ignore read errors
481
- }
482
- }
483
- }
627
+ prompt += buildOrchestratorBody(hqName, context);
484
628
  // Include user's custom prompt or action content
485
629
  if (context.actionPrompt) {
486
630
  prompt += `## Instructions\n\n${context.actionPrompt}\n`;
@@ -533,6 +677,11 @@ function buildPrompt(context) {
533
677
  }
534
678
  // Note: Branch setup (fetch + checkout/create) is now handled programmatically
535
679
  // in work/start.ts before the agent spawns, so no prompt instructions needed
680
+ // Integration commands (only for connected integrations)
681
+ const integrationSection = buildIntegrationCommandsSection(context.connectedIntegrations);
682
+ if (integrationSection) {
683
+ prompt += `\n${integrationSection}`;
684
+ }
536
685
  // Additional instructions from --message flag (appended to any action)
537
686
  if (context.customMessage) {
538
687
  prompt += `\n## Additional Instructions\n\n${context.customMessage}\n`;
@@ -647,7 +796,8 @@ export async function runHost(context, executor, config, displayMode = 'terminal
647
796
  }
648
797
  // Build the executor command using getExecutorCommand() output
649
798
  // For Claude Code, we also support outputMode and additional flags
650
- // For non-Claude executors, we use the command as-is from getExecutorCommand()
799
+ // For Codex, we use the codex adapter for deterministic command building (TKT-080)
800
+ // For other executors, we use the command as-is from getExecutorCommand()
651
801
  let executorInvocation;
652
802
  if (isClaudeExecutor(executor)) {
653
803
  // Build flags based on config - Claude-specific flags
@@ -658,13 +808,26 @@ export async function runHost(context, executor, config, displayMode = 'terminal
658
808
  const effortFlag = skipPermissions ? '--effort high ' : '';
659
809
  // Orchestrator sessions inject their role via --system-prompt
660
810
  const systemPromptFlag = systemPromptPath ? '--system-prompt "$(cat "$SYSTEM_PROMPT_PATH")" ' : '';
661
- executorInvocation = `${cmd} ${permissionsFlag}${effortFlag}${printFlag}${systemPromptFlag}"$(cat "$PROMPT_PATH")"`;
811
+ // TKT-053: Disable plan mode for background agents — prevents silent stalls
812
+ // when there's no user to approve the plan mode transition
813
+ const disallowPlanFlag = displayMode === 'background' ? '--disallowedTools EnterPlanMode ' : '';
814
+ executorInvocation = `${cmd} ${permissionsFlag}${effortFlag}${printFlag}${disallowPlanFlag}${systemPromptFlag}"$(cat "$PROMPT_PATH")"`;
815
+ }
816
+ else if (executor === 'codex') {
817
+ // TKT-080: Use Codex adapter for deterministic command building.
818
+ // Uses PLACEHOLDER pattern for reliable prompt replacement (same as devcontainer runner).
819
+ const codexPermission = config.permissionMode;
820
+ const codexContext = resolveCodexExecutionContext(displayMode, config.outputMode);
821
+ const codexResult = getCodexCommand('PLACEHOLDER', codexPermission, codexContext);
822
+ const argsStr = codexResult.args.map(a => a === 'PLACEHOLDER' ? '"$(cat "$PROMPT_PATH")"' : a).join(' ');
823
+ executorInvocation = `${codexResult.cmd} ${argsStr}`;
662
824
  }
663
825
  else {
664
- // Non-Claude executors: build command from getExecutorCommand() args
665
- // Replace the prompt in args with a file read to avoid shell escaping
666
- const argsWithFile = args.map(a => a === prompt ? '"$(cat "$PROMPT_PATH")"' : `"${a}"`);
667
- executorInvocation = `${cmd} ${argsWithFile.join(' ')}`;
826
+ // Non-Claude, non-Codex executors: build command from getExecutorCommand() args
827
+ // Use PLACEHOLDER for reliable prompt replacement instead of fragile string comparison
828
+ const { cmd: execCmd, args: execArgs } = getExecutorCommand(executor, 'PLACEHOLDER', skipPermissions);
829
+ const argsWithFile = execArgs.map(a => a === 'PLACEHOLDER' ? '"$(cat "$PROMPT_PATH")"' : `"${a}"`);
830
+ executorInvocation = `${execCmd} ${argsWithFile.join(' ')}`;
668
831
  }
669
832
  // Build script that runs executor and keeps shell open after completion
670
833
  const setTitleCmds = getSetTitleCommands(windowTitle);
@@ -976,30 +1139,71 @@ export function getGitHubToken() {
976
1139
  export function isGitHubTokenAvailable() {
977
1140
  return getGitHubToken() !== null;
978
1141
  }
979
- // =============================================================================
980
- // Docker Status Check
981
- // =============================================================================
982
1142
  /**
983
- * Check if Docker daemon is running.
984
- * Returns true if Docker is available and responsive.
985
- * Uses retry logic to handle slow Docker Desktop startup.
1143
+ * Check Docker daemon health with fast detection (TKT-081).
1144
+ *
1145
+ * Uses `docker ps` with a 5-second timeout to quickly detect:
1146
+ * - Docker not installed
1147
+ * - Docker installed but daemon unresponsive (stuck on license, initializing, 500 errors)
1148
+ * - Docker ready
1149
+ *
1150
+ * Total worst-case time: ~5 seconds (single attempt with timeout).
986
1151
  */
987
- export function isDockerRunning() {
988
- const maxRetries = 3;
989
- const timeout = 10000; // 10 seconds
990
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
991
- try {
992
- execSync('docker info', { stdio: 'pipe', timeout });
993
- return true;
1152
+ export function checkDockerDaemon() {
1153
+ // First: is docker even installed?
1154
+ try {
1155
+ execSync('which docker', { stdio: 'pipe', timeout: 3000 });
1156
+ }
1157
+ catch {
1158
+ return {
1159
+ available: false,
1160
+ reason: 'not-installed',
1161
+ message: 'Docker is not installed.',
1162
+ };
1163
+ }
1164
+ // Second: is the daemon responsive? Use `docker ps` — it's lightweight and
1165
+ // fails fast when the daemon returns 500s or hangs on GUI prompts.
1166
+ const timeout = 5000; // 5 seconds — enough for a healthy daemon, fast fail otherwise
1167
+ try {
1168
+ execSync('docker ps -q --no-trunc', { stdio: 'pipe', timeout });
1169
+ return {
1170
+ available: true,
1171
+ reason: 'ready',
1172
+ message: 'Docker daemon is ready.',
1173
+ };
1174
+ }
1175
+ catch (error) {
1176
+ // Parse the error to give actionable feedback
1177
+ const stderr = error?.stderr?.toString() || '';
1178
+ const isTimeout = error?.killed === true;
1179
+ let message;
1180
+ if (isTimeout) {
1181
+ message = 'Docker daemon is not responding (timed out after 5s). Docker Desktop may be initializing or stuck — check for license/login prompts.';
994
1182
  }
995
- catch {
996
- if (attempt === maxRetries) {
997
- return false;
998
- }
999
- // Brief pause before retry
1183
+ else if (stderr.includes('500') || stderr.includes('Internal Server Error')) {
1184
+ message = 'Docker daemon is returning errors (500). Docker Desktop needs attention — check for license/login prompts.';
1000
1185
  }
1186
+ else if (stderr.includes('connect') || stderr.includes('Cannot connect') || stderr.includes('Is the docker daemon running')) {
1187
+ message = 'Docker daemon is not running. Start Docker Desktop and try again.';
1188
+ }
1189
+ else {
1190
+ message = `Docker daemon is not ready: ${stderr.trim() || 'unknown error'}. Check Docker Desktop status.`;
1191
+ }
1192
+ return {
1193
+ available: false,
1194
+ reason: 'daemon-not-ready',
1195
+ message,
1196
+ };
1001
1197
  }
1002
- return false;
1198
+ }
1199
+ /**
1200
+ * Check if Docker daemon is running.
1201
+ * Returns true if Docker is available and responsive.
1202
+ *
1203
+ * For detailed diagnostics, use checkDockerDaemon() instead.
1204
+ */
1205
+ export function isDockerRunning() {
1206
+ return checkDockerDaemon().available;
1003
1207
  }
1004
1208
  /**
1005
1209
  * Check if the devcontainer CLI is installed.
@@ -1056,7 +1260,7 @@ function getImageName(agentName) {
1056
1260
  */
1057
1261
  export function containerExists(containerName) {
1058
1262
  try {
1059
- execSync(`docker container inspect ${containerName}`, { stdio: 'pipe' });
1263
+ execSync(`docker container inspect ${containerName}`, { stdio: 'pipe', timeout: 5000 });
1060
1264
  return true;
1061
1265
  }
1062
1266
  catch {
@@ -1068,7 +1272,7 @@ export function containerExists(containerName) {
1068
1272
  */
1069
1273
  export function isContainerRunning(containerName) {
1070
1274
  try {
1071
- const status = execSync(`docker container inspect -f '{{.State.Running}}' ${containerName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
1275
+ const status = execSync(`docker container inspect -f '{{.State.Running}}' ${containerName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }).trim();
1072
1276
  return status === 'true';
1073
1277
  }
1074
1278
  catch {
@@ -1080,7 +1284,7 @@ export function isContainerRunning(containerName) {
1080
1284
  */
1081
1285
  export function getContainerId(containerName) {
1082
1286
  try {
1083
- const containerId = execSync(`docker container inspect -f '{{.Id}}' ${containerName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
1287
+ const containerId = execSync(`docker container inspect -f '{{.Id}}' ${containerName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }).trim();
1084
1288
  return containerId ? containerId.substring(0, 12) : null;
1085
1289
  }
1086
1290
  catch {
@@ -1116,7 +1320,7 @@ function buildDockerImage(agentDir, imageName, buildArgs = {}) {
1116
1320
  */
1117
1321
  function imageExists(imageName) {
1118
1322
  try {
1119
- execSync(`docker image inspect ${imageName}`, { stdio: 'pipe' });
1323
+ execSync(`docker image inspect ${imageName}`, { stdio: 'pipe', timeout: 5000 });
1120
1324
  return true;
1121
1325
  }
1122
1326
  catch {
@@ -1347,7 +1551,7 @@ function ensureDockerContainer(context, config, executor = 'claude-code') {
1347
1551
  // Container exists but is stopped - remove and recreate for fresh mounts
1348
1552
  console.debug(`[runners:docker] Removing stopped container ${containerName} to create fresh one`);
1349
1553
  try {
1350
- execSync(`docker rm -f ${containerName}`, { stdio: 'pipe' });
1554
+ execSync(`docker rm -f ${containerName}`, { stdio: 'pipe', timeout: 10000 });
1351
1555
  }
1352
1556
  catch {
1353
1557
  // Ignore removal errors
@@ -1509,7 +1713,9 @@ export function buildDevcontainerCommand(context, executor, promptFile, containe
1509
1713
  const permissionsFlag = skipPermissions ? '--dangerously-skip-permissions ' : '';
1510
1714
  // --effort high: skips the effort level prompt for automated agents (TKT-1134)
1511
1715
  const effortFlag = '--effort high ';
1512
- executorCmd = `claude ${bypassTrustFlag}${permissionsFlag}${effortFlag}${printFlag}"$(cat ${promptFile})"`;
1716
+ // TKT-053: Disable plan mode for background agents — prevents silent stalls
1717
+ const disallowPlanFlag = displayMode === 'background' ? '--disallowedTools EnterPlanMode ' : '';
1718
+ executorCmd = `claude ${bypassTrustFlag}${permissionsFlag}${effortFlag}${printFlag}${disallowPlanFlag}"$(cat ${promptFile})"`;
1513
1719
  }
1514
1720
  else if (executor === 'codex') {
1515
1721
  // Use Codex adapter for mode validation and deterministic command building.
@@ -1561,11 +1767,12 @@ export async function runDevcontainer(context, executor, config, displayMode = '
1561
1767
  };
1562
1768
  }
1563
1769
  try {
1564
- // Check if Docker is running
1565
- if (!isDockerRunning()) {
1770
+ // Check if Docker is running (TKT-081: fast detection with diagnostic info)
1771
+ const dockerStatus = checkDockerDaemon();
1772
+ if (!dockerStatus.available) {
1566
1773
  return {
1567
1774
  success: false,
1568
- error: 'Docker is not running. Please start Docker Desktop and try again.',
1775
+ error: `Docker daemon is not available. ${dockerStatus.message}`,
1569
1776
  };
1570
1777
  }
1571
1778
  // Ensure GitHub token is available for git push operations
@@ -2260,13 +2467,12 @@ export async function runDocker(context, executor, config) {
2260
2467
  const prompt = buildPrompt(context);
2261
2468
  const containerName = `work-${context.ticketId}-${Date.now()}`;
2262
2469
  try {
2263
- // Check if docker is available
2264
- execSync('which docker', { stdio: 'pipe' });
2265
- // Check if Docker is running
2266
- if (!isDockerRunning()) {
2470
+ // Check if docker is available and daemon is responsive (TKT-081)
2471
+ const dockerStatus = checkDockerDaemon();
2472
+ if (!dockerStatus.available) {
2267
2473
  return {
2268
2474
  success: false,
2269
- error: 'Docker is not running. Please start Docker Desktop and try again.',
2475
+ error: `Docker daemon is not available. ${dockerStatus.message}`,
2270
2476
  };
2271
2477
  }
2272
2478
  // Build docker run command
@@ -2298,7 +2504,8 @@ export async function runDocker(context, executor, config) {
2298
2504
  // Non-Claude executors use their native command format from getExecutorCommand()
2299
2505
  dockerCmd += ` ${config.docker.image}`;
2300
2506
  if (isClaudeExecutor(executor)) {
2301
- dockerCmd += ` ${cmd} --print '${escapedPrompt}'`;
2507
+ // TKT-053: Disable plan mode — Docker runner is always detached (no user to approve)
2508
+ dockerCmd += ` ${cmd} --print --disallowedTools EnterPlanMode '${escapedPrompt}'`;
2302
2509
  }
2303
2510
  else {
2304
2511
  const argsStr = args.map(a => a === escapedPrompt ? `'${escapedPrompt}'` : a).join(' ');
@@ -2347,11 +2554,12 @@ export async function runOrchestratorInDocker(context, executor, config, options
2347
2554
  const containerName = `prlt-orchestrator-${(hqName).replace(/[^a-zA-Z0-9._-]/g, '-')}-${(orchestratorName).replace(/[^a-zA-Z0-9._-]/g, '-')}`;
2348
2555
  const imageName = `prlt-orchestrator-${(hqName).replace(/[^a-zA-Z0-9._-]/g, '-')}:latest`;
2349
2556
  try {
2350
- // Check Docker is running
2351
- if (!isDockerRunning()) {
2557
+ // Check Docker is running (TKT-081: fast detection with diagnostic info)
2558
+ const dockerStatus = checkDockerDaemon();
2559
+ if (!dockerStatus.available) {
2352
2560
  return {
2353
2561
  success: false,
2354
- error: 'Docker is not running. Please start Docker Desktop and try again.',
2562
+ error: `Docker daemon is not available. ${dockerStatus.message}`,
2355
2563
  };
2356
2564
  }
2357
2565
  // Check if container already exists and is running
@@ -2511,8 +2719,10 @@ export async function runOrchestratorInDocker(context, executor, config, options
2511
2719
  const skipPermissions = config.permissionMode === 'danger';
2512
2720
  const permissionsFlag = skipPermissions ? '--dangerously-skip-permissions ' : '';
2513
2721
  const effortFlag = skipPermissions ? '--effort high ' : '';
2722
+ // TKT-053: Disable plan mode for background agents — prevents silent stalls
2723
+ const disallowPlanFlag = displayMode === 'background' ? '--disallowedTools EnterPlanMode ' : '';
2514
2724
  const executorCmd = executor === 'claude-code'
2515
- ? `claude ${permissionsFlag}${effortFlag}"$(cat ${promptPath})"`
2725
+ ? `claude ${permissionsFlag}${effortFlag}${disallowPlanFlag}"$(cat ${promptPath})"`
2516
2726
  : `claude ${permissionsFlag}${effortFlag}"$(cat ${promptPath})"`;
2517
2727
  // Build tmux session name (reuses the same name as host tmux for consistency)
2518
2728
  const tmuxSessionName = options?.sessionName || containerName;
@@ -2529,7 +2739,7 @@ export async function runOrchestratorInDocker(context, executor, config, options
2529
2739
  const scriptContent = `#!/bin/bash
2530
2740
  cd /hq
2531
2741
  unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT
2532
- ${executor === 'claude-code' ? `claude ${permissionsFlag}${effortFlag}"$(cat ${promptPath})"` : `claude "$(cat ${promptPath})"`}
2742
+ ${executor === 'claude-code' ? `claude ${permissionsFlag}${effortFlag}${disallowPlanFlag}"$(cat ${promptPath})"` : `claude "$(cat ${promptPath})"`}
2533
2743
  echo ""
2534
2744
  echo "Orchestrator complete. Press Enter to close."
2535
2745
  exec bash
@@ -2688,7 +2898,8 @@ export async function runVm(context, executor, config, host) {
2688
2898
  // Build the remote command based on executor type
2689
2899
  let remoteCmd;
2690
2900
  if (isClaudeExecutor(executor)) {
2691
- remoteCmd = `cd ${remoteWorkspace} && ${executorCmd} --print '${escapedPrompt}'`;
2901
+ // TKT-053: Disable plan mode VM runner is always nohup (no user to approve)
2902
+ remoteCmd = `cd ${remoteWorkspace} && ${executorCmd} --print --disallowedTools EnterPlanMode '${escapedPrompt}'`;
2692
2903
  }
2693
2904
  else {
2694
2905
  const argsStr = executorArgs.map(a => a === escapedPrompt ? `'${escapedPrompt}'` : a).join(' ');