@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.
- 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/claude/index.js +21 -21
- package/dist/commands/claude/open.js +1 -1
- package/dist/commands/commit.js +10 -8
- package/dist/commands/config/index.js +4 -5
- package/dist/commands/execution/config.d.ts +2 -2
- package/dist/commands/execution/config.js +18 -18
- package/dist/commands/execution/list.js +2 -2
- package/dist/commands/execution/view.js +2 -2
- package/dist/commands/init.js +9 -1
- package/dist/commands/orchestrator/attach.js +64 -14
- package/dist/commands/orchestrator/start.d.ts +5 -5
- package/dist/commands/orchestrator/start.js +45 -35
- package/dist/commands/orchestrator/status.js +64 -23
- package/dist/commands/orchestrator/stop.js +44 -12
- package/dist/commands/qa/index.js +12 -12
- package/dist/commands/session/attach.js +23 -0
- package/dist/commands/session/poke.js +1 -1
- package/dist/commands/staff/add.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 +218 -0
- package/dist/commands/work/revise.js +8 -8
- package/dist/commands/work/spawn.js +29 -20
- package/dist/commands/work/start.js +22 -12
- package/dist/commands/work/watch.js +3 -3
- package/dist/hooks/init.js +8 -0
- package/dist/lib/agents/index.js +2 -2
- package/dist/lib/caffeinate.d.ts +64 -0
- package/dist/lib/caffeinate.js +146 -0
- package/dist/lib/database/drizzle-schema.d.ts +7 -7
- package/dist/lib/database/drizzle-schema.js +1 -1
- package/dist/lib/execution/codex-adapter.d.ts +96 -0
- package/dist/lib/execution/codex-adapter.js +148 -0
- package/dist/lib/execution/config.d.ts +6 -6
- package/dist/lib/execution/config.js +17 -10
- package/dist/lib/execution/devcontainer.d.ts +3 -3
- package/dist/lib/execution/devcontainer.js +3 -3
- package/dist/lib/execution/index.d.ts +1 -0
- package/dist/lib/execution/index.js +1 -0
- package/dist/lib/execution/runners.d.ts +2 -2
- package/dist/lib/execution/runners.js +69 -26
- package/dist/lib/execution/spawner.js +3 -3
- package/dist/lib/execution/storage.d.ts +2 -2
- package/dist/lib/execution/storage.js +3 -3
- package/dist/lib/execution/types.d.ts +2 -2
- package/dist/lib/execution/types.js +1 -1
- 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 +43 -0
- package/dist/lib/external-issues/linear.js +261 -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/schema.d.ts +1 -1
- package/dist/lib/pmo/schema.js +1 -1
- package/dist/lib/pmo/storage/actions.js +3 -3
- package/dist/lib/pmo/storage/base.js +116 -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/dist/lib/repos/index.js +1 -1
- package/oclif.manifest.json +3052 -2721
- 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 (
|
|
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
|
-
|
|
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.
|
|
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`));
|
package/dist/hooks/init.js
CHANGED
|
@@ -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)
|
package/dist/lib/agents/index.js
CHANGED
|
@@ -223,7 +223,7 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
-
// Create devcontainer config for
|
|
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
|
|
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
|
-
|
|
3552
|
-
name: "
|
|
3551
|
+
permissionMode: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
3552
|
+
name: "permission_mode";
|
|
3553
3553
|
tableName: "agent_work";
|
|
3554
|
-
dataType: "
|
|
3555
|
-
columnType: "
|
|
3556
|
-
data:
|
|
3557
|
-
driverParam:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
134
|
-
|
|
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
|
-
/**
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
config.
|
|
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
|
|
495
|
-
if (options.
|
|
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
|
-
|
|
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
|
-
|
|
552
|
+
permissionMode: resolvedPermissionMode,
|
|
546
553
|
createPR,
|
|
547
554
|
};
|
|
548
555
|
}
|