@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.
- package/dist/commands/caffeinate/index.d.ts +10 -0
- package/dist/commands/caffeinate/index.js +64 -0
- package/dist/commands/caffeinate/start.d.ts +14 -0
- package/dist/commands/caffeinate/start.js +86 -0
- package/dist/commands/caffeinate/status.d.ts +10 -0
- package/dist/commands/caffeinate/status.js +55 -0
- package/dist/commands/caffeinate/stop.d.ts +10 -0
- package/dist/commands/caffeinate/stop.js +47 -0
- package/dist/commands/commit.js +10 -8
- package/dist/commands/config/index.js +2 -3
- package/dist/commands/init.js +9 -1
- package/dist/commands/orchestrator/attach.js +64 -14
- package/dist/commands/orchestrator/start.d.ts +4 -4
- package/dist/commands/orchestrator/start.js +26 -16
- package/dist/commands/orchestrator/status.js +64 -23
- package/dist/commands/orchestrator/stop.js +44 -12
- package/dist/commands/session/attach.js +23 -0
- package/dist/commands/session/poke.js +1 -1
- package/dist/commands/work/index.js +4 -0
- package/dist/commands/work/linear.d.ts +24 -0
- package/dist/commands/work/linear.js +195 -0
- package/dist/commands/work/spawn.js +28 -19
- package/dist/commands/work/start.js +12 -2
- package/dist/hooks/init.js +8 -0
- package/dist/lib/caffeinate.d.ts +64 -0
- package/dist/lib/caffeinate.js +146 -0
- package/dist/lib/execution/codex-adapter.d.ts +96 -0
- package/dist/lib/execution/codex-adapter.js +148 -0
- package/dist/lib/execution/index.d.ts +1 -0
- package/dist/lib/execution/index.js +1 -0
- package/dist/lib/execution/runners.js +50 -6
- package/dist/lib/external-issues/index.d.ts +1 -1
- package/dist/lib/external-issues/index.js +1 -1
- package/dist/lib/external-issues/linear.d.ts +37 -0
- package/dist/lib/external-issues/linear.js +198 -0
- package/dist/lib/external-issues/types.d.ts +67 -0
- package/dist/lib/external-issues/types.js +41 -0
- package/dist/lib/init/index.d.ts +4 -0
- package/dist/lib/init/index.js +11 -1
- package/dist/lib/machine-config.d.ts +1 -0
- package/dist/lib/machine-config.js +6 -3
- package/dist/lib/pmo/storage/actions.js +3 -3
- package/dist/lib/pmo/storage/base.js +85 -6
- package/dist/lib/pmo/storage/epics.js +1 -1
- package/dist/lib/pmo/storage/tickets.js +2 -2
- package/dist/lib/pmo/storage/types.d.ts +2 -1
- package/oclif.manifest.json +4363 -4037
- 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
|
+
}
|
|
@@ -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
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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[]>;
|