@proletariat/cli 0.3.47 → 0.3.49

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 (74) hide show
  1. package/dist/commands/caffeinate/index.d.ts +10 -0
  2. package/dist/commands/caffeinate/index.js +64 -0
  3. package/dist/commands/caffeinate/start.d.ts +14 -0
  4. package/dist/commands/caffeinate/start.js +86 -0
  5. package/dist/commands/caffeinate/status.d.ts +10 -0
  6. package/dist/commands/caffeinate/status.js +55 -0
  7. package/dist/commands/caffeinate/stop.d.ts +10 -0
  8. package/dist/commands/caffeinate/stop.js +47 -0
  9. package/dist/commands/claude/index.js +21 -21
  10. package/dist/commands/claude/open.js +1 -1
  11. package/dist/commands/commit.js +10 -8
  12. package/dist/commands/config/index.js +4 -5
  13. package/dist/commands/execution/config.d.ts +2 -2
  14. package/dist/commands/execution/config.js +18 -18
  15. package/dist/commands/execution/list.js +2 -2
  16. package/dist/commands/execution/view.js +2 -2
  17. package/dist/commands/init.js +9 -1
  18. package/dist/commands/orchestrator/attach.js +64 -14
  19. package/dist/commands/orchestrator/start.d.ts +5 -5
  20. package/dist/commands/orchestrator/start.js +45 -35
  21. package/dist/commands/orchestrator/status.js +64 -23
  22. package/dist/commands/orchestrator/stop.js +44 -12
  23. package/dist/commands/qa/index.js +12 -12
  24. package/dist/commands/session/attach.js +23 -0
  25. package/dist/commands/session/poke.js +1 -1
  26. package/dist/commands/staff/add.js +1 -1
  27. package/dist/commands/work/index.js +4 -0
  28. package/dist/commands/work/linear.d.ts +24 -0
  29. package/dist/commands/work/linear.js +218 -0
  30. package/dist/commands/work/revise.js +8 -8
  31. package/dist/commands/work/spawn.js +29 -20
  32. package/dist/commands/work/start.js +22 -12
  33. package/dist/commands/work/watch.js +3 -3
  34. package/dist/hooks/init.js +8 -0
  35. package/dist/lib/agents/index.js +2 -2
  36. package/dist/lib/caffeinate.d.ts +64 -0
  37. package/dist/lib/caffeinate.js +146 -0
  38. package/dist/lib/database/drizzle-schema.d.ts +7 -7
  39. package/dist/lib/database/drizzle-schema.js +1 -1
  40. package/dist/lib/execution/codex-adapter.d.ts +96 -0
  41. package/dist/lib/execution/codex-adapter.js +148 -0
  42. package/dist/lib/execution/config.d.ts +6 -6
  43. package/dist/lib/execution/config.js +17 -10
  44. package/dist/lib/execution/devcontainer.d.ts +3 -3
  45. package/dist/lib/execution/devcontainer.js +3 -3
  46. package/dist/lib/execution/index.d.ts +1 -0
  47. package/dist/lib/execution/index.js +1 -0
  48. package/dist/lib/execution/runners.d.ts +2 -2
  49. package/dist/lib/execution/runners.js +69 -26
  50. package/dist/lib/execution/spawner.js +3 -3
  51. package/dist/lib/execution/storage.d.ts +2 -2
  52. package/dist/lib/execution/storage.js +3 -3
  53. package/dist/lib/execution/types.d.ts +2 -2
  54. package/dist/lib/execution/types.js +1 -1
  55. package/dist/lib/external-issues/index.d.ts +1 -1
  56. package/dist/lib/external-issues/index.js +1 -1
  57. package/dist/lib/external-issues/linear.d.ts +43 -0
  58. package/dist/lib/external-issues/linear.js +261 -0
  59. package/dist/lib/external-issues/types.d.ts +67 -0
  60. package/dist/lib/external-issues/types.js +41 -0
  61. package/dist/lib/init/index.d.ts +4 -0
  62. package/dist/lib/init/index.js +11 -1
  63. package/dist/lib/machine-config.d.ts +1 -0
  64. package/dist/lib/machine-config.js +6 -3
  65. package/dist/lib/pmo/schema.d.ts +1 -1
  66. package/dist/lib/pmo/schema.js +1 -1
  67. package/dist/lib/pmo/storage/actions.js +3 -3
  68. package/dist/lib/pmo/storage/base.js +116 -6
  69. package/dist/lib/pmo/storage/epics.js +1 -1
  70. package/dist/lib/pmo/storage/tickets.js +2 -2
  71. package/dist/lib/pmo/storage/types.d.ts +2 -1
  72. package/dist/lib/repos/index.js +1 -1
  73. package/oclif.manifest.json +3052 -2721
  74. package/package.json +1 -1
@@ -161,7 +161,7 @@ export default class WorkWatch extends PMOCommand {
161
161
  if (!flags.mode) {
162
162
  if (hasDevcontainer) {
163
163
  const envChoices = [
164
- { name: '🐳 devcontainer (sandboxed, recommended)', value: 'devcontainer' },
164
+ { name: '🐳 devcontainer (isolated, recommended)', value: 'devcontainer' },
165
165
  { name: '💻 host (runs directly on your machine)', value: 'host' },
166
166
  ];
167
167
  if (jsonMode) {
@@ -244,13 +244,13 @@ export default class WorkWatch extends PMOCommand {
244
244
  const promptResult = await promptExecutionSettings(db, {
245
245
  displayMode: this.displayMode,
246
246
  environment: this.environment,
247
- skipPermissions: flags['skip-permissions'] ? true : undefined,
247
+ permissionMode: flags['skip-permissions'] ? 'danger' : undefined,
248
248
  createPR: flags['create-pr'] ? true : undefined,
249
249
  log: (msg) => this.log(styles.header(msg)),
250
250
  jsonMode: jsonMode ? { flags, commandName: 'work watch' } : undefined,
251
251
  });
252
252
  this.executionConfig = promptResult.executionConfig;
253
- this.skipPermissions = promptResult.skipPermissions;
253
+ this.skipPermissions = promptResult.permissionMode === 'danger';
254
254
  this.createPR = promptResult.createPR;
255
255
  this.log('');
256
256
  this.log(styles.header(`👁️ Watching column "${this.columnName}" for new tickets`));
@@ -18,6 +18,14 @@ const hook = async function ({ id, argv, config }) {
18
18
  if (process.env.OCLIF_COMPILATION || process.argv[1]?.includes('oclif')) {
19
19
  return;
20
20
  }
21
+ // Skip when in test environments that provide their own HQ
22
+ if (process.env.PRLT_HQ_PATH && process.env.PRLT_TEST_ENV) {
23
+ return;
24
+ }
25
+ // Skip init redirect when explicitly disabled (e.g., e2e test isolation)
26
+ if (process.env.PRLT_SKIP_INIT_REDIRECT === '1') {
27
+ return;
28
+ }
21
29
  // Skip when --help or --version flags are present - these should always be available
22
30
  // Check both process.argv (production CLI) and the oclif-provided argv
23
31
  // (programmatic invocation via @oclif/test runCommand)
@@ -223,7 +223,7 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
223
223
  }
224
224
  }
225
225
  }
226
- // Create devcontainer config for sandboxed execution
226
+ // Create devcontainer config for isolated execution
227
227
  // Note: Agent metadata is stored in SQLite (agents table), not in config files
228
228
  // Always create devcontainer config (even if no repos were created) so agent rebuild works
229
229
  if (!options?.skipDevcontainer) {
@@ -368,7 +368,7 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
368
368
  }
369
369
  }
370
370
  }
371
- // Create devcontainer config for sandboxed execution
371
+ // Create devcontainer config for isolated execution
372
372
  // Note: Agent metadata is stored in SQLite (agents table), not in config files
373
373
  if (!options?.skipDevcontainer) {
374
374
  console.log(styles.muted(` Creating devcontainer config...`));
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Caffeinate management for macOS
3
+ *
4
+ * Manages a background `caffeinate` process to prevent macOS from sleeping.
5
+ * PID state is stored in a runtime file for reliable stop/status operations.
6
+ */
7
+ export interface CaffeinateState {
8
+ pid: number;
9
+ startedAt: string;
10
+ flags: string[];
11
+ duration?: number;
12
+ }
13
+ /**
14
+ * Ensure the platform is macOS. Throws a descriptive error on other platforms.
15
+ */
16
+ export declare function assertMacOS(): void;
17
+ /**
18
+ * Check whether the platform is macOS.
19
+ */
20
+ export declare function isMacOS(): boolean;
21
+ /**
22
+ * Read the stored caffeinate state from the PID file.
23
+ * Returns null if no state exists or the file is invalid.
24
+ */
25
+ export declare function readState(): CaffeinateState | null;
26
+ /**
27
+ * Write caffeinate state to the PID file.
28
+ */
29
+ export declare function writeState(state: CaffeinateState): void;
30
+ /**
31
+ * Remove the PID file.
32
+ */
33
+ export declare function clearState(): void;
34
+ /**
35
+ * Check whether a process with the given PID is still running.
36
+ */
37
+ export declare function isProcessRunning(pid: number): boolean;
38
+ /**
39
+ * Get the status of the managed caffeinate process.
40
+ * Cleans up stale state if the process is no longer running.
41
+ */
42
+ export declare function getStatus(): {
43
+ running: boolean;
44
+ state: CaffeinateState | null;
45
+ };
46
+ /**
47
+ * Start a background caffeinate process.
48
+ *
49
+ * @param flags - Additional flags to pass to caffeinate (e.g. ['-d', '-i'])
50
+ * @param duration - Optional duration in seconds (passed as -t flag)
51
+ * @returns The state of the started process
52
+ * @throws If already running (idempotent guard) or on non-macOS
53
+ */
54
+ export declare function startCaffeinate(flags?: string[], duration?: number): CaffeinateState;
55
+ /**
56
+ * Stop the managed caffeinate process.
57
+ *
58
+ * @returns true if a process was stopped, false if nothing was running
59
+ */
60
+ export declare function stopCaffeinate(): boolean;
61
+ export declare const _internals: {
62
+ RUNTIME_DIR: string;
63
+ PID_FILE: string;
64
+ };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Caffeinate management for macOS
3
+ *
4
+ * Manages a background `caffeinate` process to prevent macOS from sleeping.
5
+ * PID state is stored in a runtime file for reliable stop/status operations.
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as os from 'node:os';
9
+ import * as path from 'node:path';
10
+ import { spawn } from 'node:child_process';
11
+ const RUNTIME_DIR = path.join(os.tmpdir(), 'prlt');
12
+ const PID_FILE = path.join(RUNTIME_DIR, 'caffeinate.pid');
13
+ /**
14
+ * Ensure the platform is macOS. Throws a descriptive error on other platforms.
15
+ */
16
+ export function assertMacOS() {
17
+ if (process.platform !== 'darwin') {
18
+ throw new Error(`caffeinate is only supported on macOS (current platform: ${process.platform})`);
19
+ }
20
+ }
21
+ /**
22
+ * Check whether the platform is macOS.
23
+ */
24
+ export function isMacOS() {
25
+ return process.platform === 'darwin';
26
+ }
27
+ /**
28
+ * Read the stored caffeinate state from the PID file.
29
+ * Returns null if no state exists or the file is invalid.
30
+ */
31
+ export function readState() {
32
+ try {
33
+ const data = fs.readFileSync(PID_FILE, 'utf-8');
34
+ return JSON.parse(data);
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ /**
41
+ * Write caffeinate state to the PID file.
42
+ */
43
+ export function writeState(state) {
44
+ fs.mkdirSync(RUNTIME_DIR, { recursive: true });
45
+ fs.writeFileSync(PID_FILE, JSON.stringify(state, null, 2));
46
+ }
47
+ /**
48
+ * Remove the PID file.
49
+ */
50
+ export function clearState() {
51
+ try {
52
+ fs.unlinkSync(PID_FILE);
53
+ }
54
+ catch {
55
+ // File may not exist - that's fine
56
+ }
57
+ }
58
+ /**
59
+ * Check whether a process with the given PID is still running.
60
+ */
61
+ export function isProcessRunning(pid) {
62
+ try {
63
+ process.kill(pid, 0);
64
+ return true;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ /**
71
+ * Get the status of the managed caffeinate process.
72
+ * Cleans up stale state if the process is no longer running.
73
+ */
74
+ export function getStatus() {
75
+ const state = readState();
76
+ if (!state) {
77
+ return { running: false, state: null };
78
+ }
79
+ if (!isProcessRunning(state.pid)) {
80
+ // Process died externally - clean up stale PID file
81
+ clearState();
82
+ return { running: false, state: null };
83
+ }
84
+ return { running: true, state };
85
+ }
86
+ /**
87
+ * Start a background caffeinate process.
88
+ *
89
+ * @param flags - Additional flags to pass to caffeinate (e.g. ['-d', '-i'])
90
+ * @param duration - Optional duration in seconds (passed as -t flag)
91
+ * @returns The state of the started process
92
+ * @throws If already running (idempotent guard) or on non-macOS
93
+ */
94
+ export function startCaffeinate(flags = [], duration) {
95
+ assertMacOS();
96
+ // Idempotent: if already running, return existing state
97
+ const { running, state: existingState } = getStatus();
98
+ if (running && existingState) {
99
+ return existingState;
100
+ }
101
+ const args = [...flags];
102
+ if (duration !== undefined && duration > 0) {
103
+ args.push('-t', String(duration));
104
+ }
105
+ const child = spawn('caffeinate', args, {
106
+ detached: true,
107
+ stdio: 'ignore',
108
+ });
109
+ child.unref();
110
+ if (!child.pid) {
111
+ throw new Error('Failed to start caffeinate process');
112
+ }
113
+ const state = {
114
+ pid: child.pid,
115
+ startedAt: new Date().toISOString(),
116
+ flags: args,
117
+ ...(duration !== undefined && duration > 0 ? { duration } : {}),
118
+ };
119
+ writeState(state);
120
+ return state;
121
+ }
122
+ /**
123
+ * Stop the managed caffeinate process.
124
+ *
125
+ * @returns true if a process was stopped, false if nothing was running
126
+ */
127
+ export function stopCaffeinate() {
128
+ const { running, state } = getStatus();
129
+ if (!running || !state) {
130
+ clearState(); // Clean up any stale file
131
+ return false;
132
+ }
133
+ try {
134
+ process.kill(state.pid, 'SIGTERM');
135
+ }
136
+ catch {
137
+ // Process may have already exited
138
+ }
139
+ clearState();
140
+ return true;
141
+ }
142
+ // Exported for testing
143
+ export const _internals = {
144
+ RUNTIME_DIR,
145
+ PID_FILE,
146
+ };
@@ -3548,19 +3548,19 @@ export declare const pmoAgentWork: import("drizzle-orm/sqlite-core").SQLiteTable
3548
3548
  identity: undefined;
3549
3549
  generated: undefined;
3550
3550
  }, object>;
3551
- sandboxed: import("drizzle-orm/sqlite-core").SQLiteColumn<{
3552
- name: "sandboxed";
3551
+ permissionMode: import("drizzle-orm/sqlite-core").SQLiteColumn<{
3552
+ name: "permission_mode";
3553
3553
  tableName: "agent_work";
3554
- dataType: "boolean";
3555
- columnType: "SQLiteBoolean";
3556
- data: boolean;
3557
- driverParam: number;
3554
+ dataType: "string";
3555
+ columnType: "SQLiteText";
3556
+ data: string;
3557
+ driverParam: string;
3558
3558
  notNull: true;
3559
3559
  hasDefault: true;
3560
3560
  isPrimaryKey: false;
3561
3561
  isAutoincrement: false;
3562
3562
  hasRuntimeDefault: false;
3563
- enumValues: undefined;
3563
+ enumValues: [string, ...string[]];
3564
3564
  baseColumn: never;
3565
3565
  identity: undefined;
3566
3566
  generated: undefined;
@@ -428,7 +428,7 @@ export const pmoAgentWork = sqliteTable('agent_work', {
428
428
  executor: text('executor').notNull(),
429
429
  environment: text('environment').notNull().default('host'),
430
430
  displayMode: text('display_mode').notNull().default('terminal'),
431
- sandboxed: integer('sandboxed', { mode: 'boolean' }).notNull().default(false),
431
+ permissionMode: text('permission_mode').notNull().default('safe'),
432
432
  status: text('status').notNull().default('starting'),
433
433
  branch: text('branch'),
434
434
  pid: text('pid'),
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Codex Runtime Adapter
3
+ *
4
+ * Explicitly maps permission modes and execution contexts to Codex CLI invocations.
5
+ * Validates that the requested combination is supported and returns a deterministic
6
+ * command configuration.
7
+ *
8
+ * ## Codex Mode Mapping
9
+ *
10
+ * Codex has two permission modes:
11
+ * - `--yolo` (danger): Execute commands autonomously without approval prompts
12
+ * - (default) (safe): Prompt the user for approval before running commands
13
+ *
14
+ * Codex execution contexts:
15
+ * - Interactive terminal: User is watching in a TTY (terminal tab, foreground tmux)
16
+ * - Background/detached: Running in a tmux session, no direct TTY attached at start
17
+ * - Non-TTY: Piped output, CI, Docker detached — no TTY available
18
+ *
19
+ * ## Supported Combinations
20
+ *
21
+ * | Permission | Context | Supported | Notes |
22
+ * |------------|---------------|-----------|------------------------------------------|
23
+ * | danger | interactive | Yes | `codex --yolo --prompt "..."` |
24
+ * | danger | background | Yes | `codex --yolo --prompt "..."` |
25
+ * | danger | non-tty | Yes | `codex --yolo --prompt "..."` |
26
+ * | safe | interactive | Yes | `codex --prompt "..."` (user can approve)|
27
+ * | safe | background | No | Cannot prompt for approval in background |
28
+ * | safe | non-tty | No | Cannot prompt for approval without TTY |
29
+ */
30
+ import type { DisplayMode, OutputMode, PermissionMode } from './types.js';
31
+ /**
32
+ * The execution context determines whether Codex can interact with a user.
33
+ *
34
+ * - interactive: Running in a TTY where the user can see prompts and respond
35
+ * - background: Running detached (tmux background, no terminal tab open)
36
+ * - non-tty: No TTY at all (piped, Docker -d, CI runner)
37
+ */
38
+ export type CodexExecutionContext = 'interactive' | 'background' | 'non-tty';
39
+ /**
40
+ * Error thrown when an unsupported Codex mode combination is requested.
41
+ */
42
+ export declare class CodexModeError extends Error {
43
+ readonly permissionMode: PermissionMode;
44
+ readonly executionContext: CodexExecutionContext;
45
+ constructor(permissionMode: PermissionMode, executionContext: CodexExecutionContext);
46
+ }
47
+ /**
48
+ * The resolved command and arguments for invoking Codex.
49
+ */
50
+ export interface CodexCommandResult {
51
+ cmd: 'codex';
52
+ args: string[];
53
+ /** Whether --yolo (autonomous mode) is active */
54
+ yolo: boolean;
55
+ /** The resolved execution context */
56
+ executionContext: CodexExecutionContext;
57
+ }
58
+ /**
59
+ * Derive the Codex execution context from proletariat display mode and output mode.
60
+ *
61
+ * Maps proletariat's display/output dimensions to Codex's execution context:
62
+ * - terminal + interactive → interactive (user is watching, TTY available)
63
+ * - foreground + interactive → interactive
64
+ * - terminal + print → non-tty (output is piped, no interaction)
65
+ * - background + any → background (detached tmux, no direct TTY)
66
+ * - foreground + print → non-tty
67
+ */
68
+ export declare function resolveCodexExecutionContext(displayMode: DisplayMode, outputMode: OutputMode): CodexExecutionContext;
69
+ /**
70
+ * Check whether a Codex mode combination is supported.
71
+ * Returns null if valid, or a CodexModeError if the combination is unsupported.
72
+ */
73
+ export declare function validateCodexMode(permissionMode: PermissionMode, executionContext: CodexExecutionContext): CodexModeError | null;
74
+ /**
75
+ * Build the Codex CLI command for a given permission mode and execution context.
76
+ *
77
+ * This is the single source of truth for Codex invocation. All runners should
78
+ * use this function (via getCodexCommand or getCodexCommandFromConfig) rather
79
+ * than building Codex CLI args inline.
80
+ *
81
+ * @throws CodexModeError if the combination is unsupported
82
+ */
83
+ export declare function getCodexCommand(prompt: string, permissionMode: PermissionMode, executionContext: CodexExecutionContext): CodexCommandResult;
84
+ /**
85
+ * Build the Codex CLI command from proletariat config values.
86
+ *
87
+ * Convenience wrapper that resolves execution context from display/output mode
88
+ * before delegating to getCodexCommand().
89
+ *
90
+ * @param prompt - The prompt text for Codex
91
+ * @param sandboxed - If true, use safe mode; if false, use danger mode
92
+ * @param displayMode - How the agent output is displayed
93
+ * @param outputMode - Whether output is interactive or print
94
+ * @throws CodexModeError if the resolved combination is unsupported
95
+ */
96
+ export declare function getCodexCommandFromConfig(prompt: string, sandboxed: boolean, displayMode?: DisplayMode, outputMode?: OutputMode): CodexCommandResult;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Codex Runtime Adapter
3
+ *
4
+ * Explicitly maps permission modes and execution contexts to Codex CLI invocations.
5
+ * Validates that the requested combination is supported and returns a deterministic
6
+ * command configuration.
7
+ *
8
+ * ## Codex Mode Mapping
9
+ *
10
+ * Codex has two permission modes:
11
+ * - `--yolo` (danger): Execute commands autonomously without approval prompts
12
+ * - (default) (safe): Prompt the user for approval before running commands
13
+ *
14
+ * Codex execution contexts:
15
+ * - Interactive terminal: User is watching in a TTY (terminal tab, foreground tmux)
16
+ * - Background/detached: Running in a tmux session, no direct TTY attached at start
17
+ * - Non-TTY: Piped output, CI, Docker detached — no TTY available
18
+ *
19
+ * ## Supported Combinations
20
+ *
21
+ * | Permission | Context | Supported | Notes |
22
+ * |------------|---------------|-----------|------------------------------------------|
23
+ * | danger | interactive | Yes | `codex --yolo --prompt "..."` |
24
+ * | danger | background | Yes | `codex --yolo --prompt "..."` |
25
+ * | danger | non-tty | Yes | `codex --yolo --prompt "..."` |
26
+ * | safe | interactive | Yes | `codex --prompt "..."` (user can approve)|
27
+ * | safe | background | No | Cannot prompt for approval in background |
28
+ * | safe | non-tty | No | Cannot prompt for approval without TTY |
29
+ */
30
+ // =============================================================================
31
+ // Codex Adapter Errors
32
+ // =============================================================================
33
+ /**
34
+ * Error thrown when an unsupported Codex mode combination is requested.
35
+ */
36
+ export class CodexModeError extends Error {
37
+ permissionMode;
38
+ executionContext;
39
+ constructor(permissionMode, executionContext) {
40
+ const message = buildModeErrorMessage(permissionMode, executionContext);
41
+ super(message);
42
+ this.name = 'CodexModeError';
43
+ this.permissionMode = permissionMode;
44
+ this.executionContext = executionContext;
45
+ }
46
+ }
47
+ function buildModeErrorMessage(permissionMode, executionContext) {
48
+ if (permissionMode === 'safe' && executionContext === 'background') {
49
+ return (`Codex safe mode cannot run in background: Codex needs a TTY to prompt for approval. ` +
50
+ `Either use danger mode (--yolo) for background execution, or run in a terminal where you can interact with Codex.`);
51
+ }
52
+ if (permissionMode === 'safe' && executionContext === 'non-tty') {
53
+ return (`Codex safe mode requires an interactive terminal: Codex needs a TTY to prompt for approval. ` +
54
+ `Either use danger mode (--yolo) for non-interactive execution, or run in a terminal where you can interact with Codex.`);
55
+ }
56
+ return `Unsupported Codex mode combination: permission=${permissionMode}, context=${executionContext}`;
57
+ }
58
+ // =============================================================================
59
+ // Context Resolution
60
+ // =============================================================================
61
+ /**
62
+ * Derive the Codex execution context from proletariat display mode and output mode.
63
+ *
64
+ * Maps proletariat's display/output dimensions to Codex's execution context:
65
+ * - terminal + interactive → interactive (user is watching, TTY available)
66
+ * - foreground + interactive → interactive
67
+ * - terminal + print → non-tty (output is piped, no interaction)
68
+ * - background + any → background (detached tmux, no direct TTY)
69
+ * - foreground + print → non-tty
70
+ */
71
+ export function resolveCodexExecutionContext(displayMode, outputMode) {
72
+ // Background display is always background context regardless of output mode
73
+ if (displayMode === 'background') {
74
+ return 'background';
75
+ }
76
+ // Print mode means output is captured/piped, no interactive TTY
77
+ if (outputMode === 'print') {
78
+ return 'non-tty';
79
+ }
80
+ // Terminal or foreground with interactive output = interactive
81
+ return 'interactive';
82
+ }
83
+ // =============================================================================
84
+ // Mode Validation
85
+ // =============================================================================
86
+ /**
87
+ * Check whether a Codex mode combination is supported.
88
+ * Returns null if valid, or a CodexModeError if the combination is unsupported.
89
+ */
90
+ export function validateCodexMode(permissionMode, executionContext) {
91
+ // Danger mode works in all contexts — no user interaction needed
92
+ if (permissionMode === 'danger') {
93
+ return null;
94
+ }
95
+ // Safe mode requires an interactive terminal for approval prompts
96
+ if (executionContext === 'interactive') {
97
+ return null;
98
+ }
99
+ return new CodexModeError(permissionMode, executionContext);
100
+ }
101
+ // =============================================================================
102
+ // Command Builder
103
+ // =============================================================================
104
+ /**
105
+ * Build the Codex CLI command for a given permission mode and execution context.
106
+ *
107
+ * This is the single source of truth for Codex invocation. All runners should
108
+ * use this function (via getCodexCommand or getCodexCommandFromConfig) rather
109
+ * than building Codex CLI args inline.
110
+ *
111
+ * @throws CodexModeError if the combination is unsupported
112
+ */
113
+ export function getCodexCommand(prompt, permissionMode, executionContext) {
114
+ // Validate the combination
115
+ const error = validateCodexMode(permissionMode, executionContext);
116
+ if (error) {
117
+ throw error;
118
+ }
119
+ const yolo = permissionMode === 'danger';
120
+ const args = [];
121
+ if (yolo) {
122
+ args.push('--yolo');
123
+ }
124
+ args.push('--prompt', prompt);
125
+ return {
126
+ cmd: 'codex',
127
+ args,
128
+ yolo,
129
+ executionContext,
130
+ };
131
+ }
132
+ /**
133
+ * Build the Codex CLI command from proletariat config values.
134
+ *
135
+ * Convenience wrapper that resolves execution context from display/output mode
136
+ * before delegating to getCodexCommand().
137
+ *
138
+ * @param prompt - The prompt text for Codex
139
+ * @param sandboxed - If true, use safe mode; if false, use danger mode
140
+ * @param displayMode - How the agent output is displayed
141
+ * @param outputMode - Whether output is interactive or print
142
+ * @throws CodexModeError if the resolved combination is unsupported
143
+ */
144
+ export function getCodexCommandFromConfig(prompt, sandboxed, displayMode = 'terminal', outputMode = 'interactive') {
145
+ const permissionMode = sandboxed ? 'safe' : 'danger';
146
+ const executionContext = resolveCodexExecutionContext(displayMode, outputMode);
147
+ return getCodexCommand(prompt, permissionMode, executionContext);
148
+ }
@@ -5,7 +5,7 @@
5
5
  * Uses the workspace_settings table (not pmo_settings - execution is workspace-level).
6
6
  */
7
7
  import Database from 'better-sqlite3';
8
- import { ExecutionConfig, TerminalApp, Shell, DisplayMode, OutputMode, ExecutionEnvironment, AuthMethod } from './types.js';
8
+ import { ExecutionConfig, TerminalApp, Shell, DisplayMode, OutputMode, ExecutionEnvironment, AuthMethod, PermissionMode } from './types.js';
9
9
  import { type JsonFlags } from '../prompt-json.js';
10
10
  declare const CONFIG_KEYS: {
11
11
  terminalApp: string;
@@ -15,7 +15,7 @@ declare const CONFIG_KEYS: {
15
15
  defaultExecutor: string;
16
16
  autoExecute: string;
17
17
  outputMode: string;
18
- sandboxed: string;
18
+ permissionMode: string;
19
19
  tmuxSession: string;
20
20
  tmuxLayout: string;
21
21
  tmuxControlMode: string;
@@ -130,8 +130,8 @@ export interface ExecutionPromptOptions {
130
130
  environment: ExecutionEnvironment;
131
131
  /** Skip output mode prompt and use this value instead */
132
132
  outputMode?: OutputMode;
133
- /** Skip permission prompt and use this value instead */
134
- skipPermissions?: boolean;
133
+ /** Permission mode override (skips prompt) */
134
+ permissionMode?: PermissionMode;
135
135
  /** Skip PR prompt and use this value instead */
136
136
  createPR?: boolean;
137
137
  /** Force re-prompt for terminal preferences */
@@ -147,8 +147,8 @@ export interface ExecutionPromptOptions {
147
147
  export interface ExecutionPromptResult {
148
148
  /** Execution config with terminal/shell/output settings */
149
149
  executionConfig: ExecutionConfig;
150
- /** Whether to skip permission checks */
151
- skipPermissions: boolean;
150
+ /** Permission mode for agent execution */
151
+ permissionMode: PermissionMode;
152
152
  /** Whether to create PR when work is ready */
153
153
  createPR: boolean;
154
154
  }
@@ -19,7 +19,7 @@ const CONFIG_KEYS = {
19
19
  defaultExecutor: 'execution.default_executor',
20
20
  autoExecute: 'execution.auto_execute',
21
21
  outputMode: 'execution.output_mode',
22
- sandboxed: 'execution.sandboxed',
22
+ permissionMode: 'execution.permission_mode',
23
23
  tmuxSession: 'execution.tmux.session',
24
24
  tmuxLayout: 'execution.tmux.layout',
25
25
  tmuxControlMode: 'execution.tmux.control_mode',
@@ -95,10 +95,17 @@ export function loadExecutionConfig(db) {
95
95
  if (outputMode) {
96
96
  config.outputMode = outputMode;
97
97
  }
98
- // Load sandboxed preference
99
- const sandboxed = getSetting(db, CONFIG_KEYS.sandboxed);
100
- if (sandboxed !== null) {
101
- config.sandboxed = sandboxed === 'true';
98
+ // Load permission mode preference
99
+ const permissionMode = getSetting(db, CONFIG_KEYS.permissionMode);
100
+ if (permissionMode) {
101
+ config.permissionMode = permissionMode;
102
+ }
103
+ else {
104
+ // Backward compat: read legacy 'execution.sandboxed' setting
105
+ const legacySandboxed = getSetting(db, 'execution.sandboxed');
106
+ if (legacySandboxed !== null) {
107
+ config.permissionMode = legacySandboxed === 'true' ? 'safe' : 'danger';
108
+ }
102
109
  }
103
110
  // Load auth method preference
104
111
  const authMethod = getSetting(db, CONFIG_KEYS.authMethod);
@@ -491,8 +498,8 @@ export async function promptExecutionSettings(db, options) {
491
498
  }
492
499
  executionConfig.outputMode = outputMode;
493
500
  // Prompt for permissions mode (unless flag override is provided)
494
- let skipPermissions = options.skipPermissions ?? false;
495
- if (options.skipPermissions === undefined) {
501
+ let resolvedPermissionMode = options.permissionMode ?? 'safe';
502
+ if (options.permissionMode === undefined) {
496
503
  const containerNote = (environment === 'devcontainer' || environment === 'docker')
497
504
  ? ' (container provides additional isolation)'
498
505
  : '';
@@ -504,7 +511,7 @@ export async function promptExecutionSettings(db, options) {
504
511
  if (isJsonMode && jsonMode) {
505
512
  outputPromptAsJson(buildPromptConfig('list', 'permissionMode', `Permission mode for Claude Code${containerNote}:`, permissionChoices, 'safe'), createMetadata(jsonMode.commandName, jsonMode.flags));
506
513
  }
507
- const { permissionMode } = await inquirer.prompt([
514
+ const { permissionMode: selectedMode } = await inquirer.prompt([
508
515
  {
509
516
  type: 'list',
510
517
  name: 'permissionMode',
@@ -513,7 +520,7 @@ export async function promptExecutionSettings(db, options) {
513
520
  default: 'safe',
514
521
  },
515
522
  ]);
516
- skipPermissions = permissionMode === 'danger';
523
+ resolvedPermissionMode = selectedMode;
517
524
  }
518
525
  // Prompt for PR creation when work is complete (unless flag override is provided)
519
526
  let createPR = options.createPR ?? false;
@@ -542,7 +549,7 @@ export async function promptExecutionSettings(db, options) {
542
549
  }
543
550
  return {
544
551
  executionConfig,
545
- skipPermissions,
552
+ permissionMode: resolvedPermissionMode,
546
553
  createPR,
547
554
  };
548
555
  }