@proletariat/cli 0.3.47 → 0.3.48

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 (48) 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/commit.js +10 -8
  10. package/dist/commands/config/index.js +2 -3
  11. package/dist/commands/init.js +9 -1
  12. package/dist/commands/orchestrator/attach.js +64 -14
  13. package/dist/commands/orchestrator/start.d.ts +4 -4
  14. package/dist/commands/orchestrator/start.js +26 -16
  15. package/dist/commands/orchestrator/status.js +64 -23
  16. package/dist/commands/orchestrator/stop.js +44 -12
  17. package/dist/commands/session/attach.js +23 -0
  18. package/dist/commands/session/poke.js +1 -1
  19. package/dist/commands/work/index.js +4 -0
  20. package/dist/commands/work/linear.d.ts +24 -0
  21. package/dist/commands/work/linear.js +195 -0
  22. package/dist/commands/work/spawn.js +28 -19
  23. package/dist/commands/work/start.js +12 -2
  24. package/dist/hooks/init.js +8 -0
  25. package/dist/lib/caffeinate.d.ts +64 -0
  26. package/dist/lib/caffeinate.js +146 -0
  27. package/dist/lib/execution/codex-adapter.d.ts +96 -0
  28. package/dist/lib/execution/codex-adapter.js +148 -0
  29. package/dist/lib/execution/index.d.ts +1 -0
  30. package/dist/lib/execution/index.js +1 -0
  31. package/dist/lib/execution/runners.js +50 -6
  32. package/dist/lib/external-issues/index.d.ts +1 -1
  33. package/dist/lib/external-issues/index.js +1 -1
  34. package/dist/lib/external-issues/linear.d.ts +37 -0
  35. package/dist/lib/external-issues/linear.js +198 -0
  36. package/dist/lib/external-issues/types.d.ts +67 -0
  37. package/dist/lib/external-issues/types.js +41 -0
  38. package/dist/lib/init/index.d.ts +4 -0
  39. package/dist/lib/init/index.js +11 -1
  40. package/dist/lib/machine-config.d.ts +1 -0
  41. package/dist/lib/machine-config.js +6 -3
  42. package/dist/lib/pmo/storage/actions.js +3 -3
  43. package/dist/lib/pmo/storage/base.js +85 -6
  44. package/dist/lib/pmo/storage/epics.js +1 -1
  45. package/dist/lib/pmo/storage/tickets.js +2 -2
  46. package/dist/lib/pmo/storage/types.d.ts +2 -1
  47. package/oclif.manifest.json +4363 -4037
  48. package/package.json +1 -1
@@ -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
+ };
@@ -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
+ }
@@ -8,3 +8,4 @@ export * from './runners.js';
8
8
  export * from './storage.js';
9
9
  export * from './config.js';
10
10
  export * from './devcontainer.js';
11
+ export * from './codex-adapter.js';
@@ -8,3 +8,4 @@ export * from './runners.js';
8
8
  export * from './storage.js';
9
9
  export * from './config.js';
10
10
  export * from './devcontainer.js';
11
+ export * from './codex-adapter.js';
@@ -11,6 +11,7 @@ import * as os from 'node:os';
11
11
  import { DEFAULT_EXECUTION_CONFIG, } from './types.js';
12
12
  import { getSetTitleCommands } from '../terminal.js';
13
13
  import { readDevcontainerJson } from './devcontainer.js';
14
+ import { getCodexCommand, resolveCodexExecutionContext, validateCodexMode } from './codex-adapter.js';
14
15
  // =============================================================================
15
16
  // Terminal Title Helpers
16
17
  // =============================================================================
@@ -167,11 +168,16 @@ export function getExecutorCommand(executor, prompt, skipPermissions = true) {
167
168
  }
168
169
  // Manual mode - will prompt for each action (still interactive, no -p)
169
170
  return { cmd: 'claude', args: [prompt] };
170
- case 'codex':
171
- // Map danger mode to Codex-native autonomy mode.
172
- return skipPermissions
173
- ? { cmd: 'codex', args: ['--yolo', '--prompt', prompt] }
174
- : { cmd: 'codex', args: ['--prompt', prompt] };
171
+ case 'codex': {
172
+ // Delegate to Codex adapter for deterministic mode mapping.
173
+ // getExecutorCommand is called without display/output context, so we use
174
+ // 'interactive' as default context (safe for validation — all permission modes
175
+ // are valid with interactive). Runners that need stricter validation should
176
+ // call the adapter directly with the actual execution context.
177
+ const codexPermission = skipPermissions ? 'danger' : 'safe';
178
+ const codexResult = getCodexCommand(prompt, codexPermission, 'interactive');
179
+ return { cmd: codexResult.cmd, args: codexResult.args };
180
+ }
175
181
  case 'aider':
176
182
  return { cmd: 'aider', args: ['--message', prompt] };
177
183
  case 'custom':
@@ -382,6 +388,15 @@ export async function runHost(context, executor, config, displayMode = 'terminal
382
388
  const prompt = buildPrompt(context);
383
389
  // Terminal - use sandboxed setting
384
390
  const skipPermissions = !config.sandboxed;
391
+ // Validate Codex mode combination before proceeding
392
+ if (executor === 'codex') {
393
+ const codexPermission = config.sandboxed ? 'safe' : 'danger';
394
+ const codexContext = resolveCodexExecutionContext(displayMode, config.outputMode);
395
+ const modeError = validateCodexMode(codexPermission, codexContext);
396
+ if (modeError) {
397
+ return { success: false, error: modeError.message };
398
+ }
399
+ }
385
400
  const { cmd, args } = getExecutorCommand(executor, prompt, skipPermissions);
386
401
  // Write command to temp script to avoid shell escaping issues
387
402
  // Use HQ .proletariat/scripts if available, otherwise fallback to home dir
@@ -1246,8 +1261,21 @@ export function buildDevcontainerCommand(context, executor, promptFile, containe
1246
1261
  const effortFlag = '--effort high ';
1247
1262
  executorCmd = `claude ${bypassTrustFlag}${permissionsFlag}${effortFlag}${printFlag}"$(cat ${promptFile})"`;
1248
1263
  }
1264
+ else if (executor === 'codex') {
1265
+ // Use Codex adapter for mode validation and deterministic command building.
1266
+ // Validates that the permission/display combination is supported before building.
1267
+ const codexPermission = sandboxed ? 'safe' : 'danger';
1268
+ const codexContext = resolveCodexExecutionContext(displayMode, outputMode);
1269
+ const modeError = validateCodexMode(codexPermission, codexContext);
1270
+ if (modeError) {
1271
+ throw modeError;
1272
+ }
1273
+ const codexResult = getCodexCommand('PLACEHOLDER', codexPermission, codexContext);
1274
+ const argsStr = codexResult.args.map(a => a === 'PLACEHOLDER' ? `"$(cat ${promptFile})"` : a).join(' ');
1275
+ executorCmd = `${codexResult.cmd} ${argsStr}`;
1276
+ }
1249
1277
  else {
1250
- // Non-Claude executors: use getExecutorCommand() to get correct command and args
1278
+ // Non-Claude, non-Codex executors: use getExecutorCommand() to get correct command and args
1251
1279
  const { cmd, args } = getExecutorCommand(executor, `PLACEHOLDER`, !sandboxed);
1252
1280
  // Replace the placeholder prompt with a file read for shell safety
1253
1281
  const argsStr = args.map(a => a === 'PLACEHOLDER' ? `"$(cat ${promptFile})"` : a).join(' ');
@@ -1987,6 +2015,14 @@ export async function runDocker(context, executor, config) {
1987
2015
  if (config.docker.cpus) {
1988
2016
  dockerCmd += ` --cpus ${config.docker.cpus}`;
1989
2017
  }
2018
+ // Validate Codex mode: Docker runner is always non-tty (detached with -d)
2019
+ if (executor === 'codex') {
2020
+ const codexPermission = config.sandboxed ? 'safe' : 'danger';
2021
+ const modeError = validateCodexMode(codexPermission, 'non-tty');
2022
+ if (modeError) {
2023
+ return { success: false, error: modeError.message };
2024
+ }
2025
+ }
1990
2026
  // Build executor command using getExecutorCommand() for correct invocation
1991
2027
  const escapedPrompt = prompt.replace(/'/g, "'\\''");
1992
2028
  const { cmd, args } = getExecutorCommand(executor, escapedPrompt, !config.sandboxed);
@@ -2049,6 +2085,14 @@ export async function runVm(context, executor, config, host) {
2049
2085
  const gitPullCmd = `cd ${remoteWorkspace} && git fetch && git checkout ${context.branch}`;
2050
2086
  execSync(`ssh ${sshOpts} ${user}@${targetHost} "${gitPullCmd}"`, { stdio: 'pipe' });
2051
2087
  }
2088
+ // Validate Codex mode: VM runner is always non-tty (SSH + nohup)
2089
+ if (executor === 'codex') {
2090
+ const codexPermission = config.sandboxed ? 'safe' : 'danger';
2091
+ const modeError = validateCodexMode(codexPermission, 'non-tty');
2092
+ if (modeError) {
2093
+ return { success: false, error: modeError.message };
2094
+ }
2095
+ }
2052
2096
  // Execute on remote using executor-appropriate command
2053
2097
  const escapedPrompt = prompt.replace(/'/g, "'\\''");
2054
2098
  const { cmd: executorCmd, args: executorArgs } = getExecutorCommand(executor, escapedPrompt, !config.sandboxed);
@@ -4,7 +4,7 @@
4
4
  * Shared contract for normalizing external issues (Linear, Jira) into
5
5
  * a canonical IssueEnvelope format with deterministic spawn context mapping.
6
6
  */
7
- export { type IssueSource, type IssueEnvelope, type IssueSpawnContext, type IssueValidationError, type IssueValidationErrorCode, type IssueValidationResult, type ExternalIssueAdapter, type ExternalIssueErrorCode, ISSUE_SOURCES, ExternalIssueError, } from './types.js';
7
+ export { type IssueSource, type IssueEnvelope, type IssueSpawnContext, type IssueValidationError, type IssueValidationErrorCode, type IssueValidationResult, type ExternalIssueAdapter, type ExternalIssueErrorCode, type ExternalIssueAdapterErrorCode, type NormalizedIssueEnvelope, type IssueSourceMetadata, ISSUE_SOURCES, ExternalIssueError, ExternalIssueAdapterError, toNormalizedEnvelope, } from './types.js';
8
8
  export { validateIssueEnvelope, validateOrThrow, } from './validation.js';
9
9
  export { mapToSpawnContext, } from './mapper.js';
10
10
  export { LinearIssueAdapter, JiraIssueAdapter, } from './adapters.js';
@@ -5,7 +5,7 @@
5
5
  * a canonical IssueEnvelope format with deterministic spawn context mapping.
6
6
  */
7
7
  // Types and interfaces
8
- export { ISSUE_SOURCES, ExternalIssueError, } from './types.js';
8
+ export { ISSUE_SOURCES, ExternalIssueError, ExternalIssueAdapterError, toNormalizedEnvelope, } from './types.js';
9
9
  // Validation
10
10
  export { validateIssueEnvelope, validateOrThrow, } from './validation.js';
11
11
  // Mapper
@@ -0,0 +1,37 @@
1
+ import { type IssueEnvelope, type NormalizedIssueEnvelope } from './types.js';
2
+ export interface LinearAdapterConfig {
3
+ apiKey?: string;
4
+ team?: string;
5
+ apiUrl?: string;
6
+ }
7
+ /**
8
+ * Normalize a raw Linear API issue node into a canonical IssueEnvelope.
9
+ */
10
+ export declare function normalizeLinearIssue(rawIssue: unknown): IssueEnvelope;
11
+ /**
12
+ * Normalize a raw Linear issue into a PMO-ready NormalizedIssueEnvelope.
13
+ */
14
+ export declare function normalizeLinearIssueToEnvelope(rawIssue: unknown): NormalizedIssueEnvelope;
15
+ /**
16
+ * Build a PMO ticket description from a NormalizedIssueEnvelope.
17
+ */
18
+ export declare function buildLinearTicketDescription(envelope: NormalizedIssueEnvelope): string;
19
+ /**
20
+ * Build ticket metadata from a NormalizedIssueEnvelope for traceability.
21
+ */
22
+ export declare function buildLinearMetadata(envelope: NormalizedIssueEnvelope): Record<string, string>;
23
+ /**
24
+ * Build a spawn context message from a NormalizedIssueEnvelope.
25
+ */
26
+ export declare function buildLinearSpawnContextMessage(envelope: NormalizedIssueEnvelope, additionalMessage?: string): string;
27
+ /**
28
+ * Build a CLI command string for selecting a specific Linear issue.
29
+ */
30
+ export declare function buildLinearIssueChoiceCommand(issueIdentifier: string, projectId?: string): string;
31
+ /**
32
+ * Fetch and normalize Linear issues into NormalizedIssueEnvelopes.
33
+ */
34
+ export declare function listLinearIssues(configInput: LinearAdapterConfig, options?: {
35
+ limit?: number;
36
+ fetchImpl?: typeof fetch;
37
+ }): Promise<NormalizedIssueEnvelope[]>;