@lyupro/codex-bridge 0.6.9 → 0.6.11
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/CHANGELOG.md +132 -0
- package/README.md +15 -4
- package/cli/dispatcher-model-check.mjs +59 -0
- package/cli/doctor-host-side.mjs +2 -1
- package/cli/doctor-installation.mjs +111 -111
- package/cli/doctor.mjs +13 -5
- package/cli/handback-witness-check.mjs +24 -18
- package/cli/hosts.mjs +2 -10
- package/cli/install-owners.mjs +1 -4
- package/cli/install-record.mjs +2 -1
- package/cli/inventory-transition.mjs +1 -1
- package/cli/lifecycle-lock-inspect.mjs +3 -2
- package/cli/lifecycle-lock.mjs +19 -132
- package/cli/manifest.mjs +6 -6
- package/cli/read.mjs +20 -0
- package/cli/settings-merge.mjs +3 -29
- package/package.json +2 -2
- package/scripts/home-write-inventory.mjs +12 -2
- package/src/agents/codex-advisor.md +37 -121
- package/src/agents/codex-build.md +27 -135
- package/src/agents/codex-review.md +19 -111
- package/src/agents/codex-scout.md +21 -130
- package/src/home/hooks/dispatcher-gate.mjs +14 -9
- package/src/home/hooks/live-runs.mjs +0 -41
- package/src/home/hooks/order-gate.mjs +80 -47
- package/src/home/hooks/reply-guard.mjs +52 -65
- package/src/home/hooks/reply-identity.mjs +64 -0
- package/src/home/hooks/reply-parser.mjs +1 -1
- package/src/home/hooks/reply-verdicts.mjs +31 -27
- package/src/home/hooks/reply-witness.mjs +48 -0
- package/src/home/lib/claude-layout.mjs +14 -0
- package/src/home/lib/command-path.mjs +32 -0
- package/src/home/lib/dispatcher-call.mjs +171 -0
- package/src/home/lib/dispatcher-command.mjs +18 -54
- package/src/home/lib/dispatcher-gate.mjs +4 -0
- package/src/home/lib/dispatcher-model.mjs +112 -0
- package/src/home/lib/dispatcher-order.mjs +21 -22
- package/src/home/lib/dispatcher-pin.mjs +60 -0
- package/src/home/lib/dispatcher-protocol.mjs +91 -0
- package/src/home/lib/dispatcher-stop.mjs +11 -2
- package/src/home/lib/handback-witness.mjs +137 -48
- package/src/home/lib/home-registry.mjs +7 -0
- package/src/home/lib/install-owner-roots.mjs +53 -0
- package/src/home/lib/kernel-lock.mjs +171 -0
- package/src/home/lib/meta/chain.mjs +19 -8
- package/src/home/lib/meta/context-only.mjs +43 -0
- package/src/home/lib/meta/events.mjs +14 -2
- package/src/home/lib/meta/launch-rows.mjs +23 -0
- package/src/home/lib/meta/paths.mjs +1 -1
- package/src/home/lib/meta/reply.mjs +39 -4
- package/src/home/lib/meta/run-state.mjs +3 -0
- package/src/home/lib/meta/verdict.mjs +16 -8
- package/src/home/lib/model-family.mjs +38 -0
- package/src/home/lib/observation-ledger.mjs +145 -0
- package/src/home/lib/order-schema.mjs +204 -0
- package/src/home/lib/order-values.mjs +46 -0
- package/src/home/lib/run-codex.mjs +6 -3
- package/src/home/lib/runner/advise-carry.mjs +2 -1
- package/src/home/lib/runner/args.mjs +140 -220
- package/src/home/lib/runner/attach.mjs +24 -17
- package/src/home/lib/runner/build-evidence.mjs +29 -0
- package/src/home/lib/runner/codex-availability.mjs +50 -0
- package/src/home/lib/runner/codex-cmd.mjs +2 -19
- package/src/home/lib/runner/continuation.mjs +174 -140
- package/src/home/lib/runner/exit-codes.mjs +13 -0
- package/src/home/lib/runner/flag-baseline.mjs +105 -0
- package/src/home/lib/runner/flag-scan.mjs +165 -0
- package/src/home/lib/runner/git-state.mjs +0 -33
- package/src/home/lib/runner/launcher.mjs +237 -277
- package/src/home/lib/runner/order-claim.mjs +84 -0
- package/src/home/lib/runner/order-options.mjs +30 -0
- package/src/home/lib/runner/order-owner.mjs +19 -0
- package/src/home/lib/runner/pass-gate.mjs +111 -0
- package/src/home/lib/runner/preflight.mjs +26 -28
- package/src/home/lib/runner/prompts.mjs +7 -2
- package/src/home/lib/runner/question-kind.mjs +41 -0
- package/src/home/lib/runner/run-env.mjs +2 -1
- package/src/home/lib/runner/sandbox-probe.mjs +1 -1
- package/src/home/lib/runner/scope-check.mjs +31 -12
- package/src/home/lib/runner/task-input.mjs +34 -12
- package/src/home/lib/runner/worker-admission.mjs +43 -0
- package/src/home/lib/runner/worker.mjs +19 -18
- package/src/home/lib/subagent-model-env.mjs +44 -0
- package/src/home/lib/task-header.mjs +158 -0
- package/src/home/lib/untyped-stop.mjs +30 -0
- package/src/home/lib/write-meta.mjs +17 -5
- package/src/home/lib/required-inputs.mjs +0 -261
- /package/{cli → src/home/lib}/frontmatter.mjs +0 -0
package/cli/install-owners.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { createHash } from 'node:crypto';
|
|
13
13
|
import { normalizeRepoPath } from '../src/home/lib/runner/project-dir.mjs';
|
|
14
|
+
import { isFormat2 } from '../src/home/lib/install-owner-roots.mjs';
|
|
14
15
|
import {
|
|
15
16
|
fileEntry,
|
|
16
17
|
INSTALL_METHOD_COPY,
|
|
@@ -20,10 +21,6 @@ import {
|
|
|
20
21
|
|
|
21
22
|
const isObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
22
23
|
|
|
23
|
-
export function isFormat2(parsed) {
|
|
24
|
-
return isObject(parsed) && parsed.format === 2;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
24
|
export function imageFingerprint(image) {
|
|
28
25
|
const brand = image.fingerprints?.brand;
|
|
29
26
|
if (brand === undefined) return null;
|
package/cli/install-record.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { createHomeWriter } from '../src/home/lib/home-write.mjs';
|
|
5
5
|
import { readJsonFile } from '../src/home/lib/json-file.mjs';
|
|
6
6
|
import { HOOK_DEFINITIONS } from '../src/home/lib/hook-definitions.mjs';
|
|
7
|
-
import { isFormat2
|
|
7
|
+
import { isFormat2 } from '../src/home/lib/install-owner-roots.mjs';
|
|
8
|
+
import { ownerView, validateFormat2, withOwner, withoutOwner } from './install-owners.mjs';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The record's own spelling of the field saying how the package was installed, and its one value.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fs from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { askYesNo, isInteractive } from './terminal-question.mjs';
|
|
8
|
-
import { isFormat2 } from '
|
|
8
|
+
import { isFormat2 } from '../src/home/lib/install-owner-roots.mjs';
|
|
9
9
|
import { readInstallRecordFile } from './install-record.mjs';
|
|
10
10
|
import { normalizedRulesOwner, readRulesRegistry } from './rules-owners.mjs';
|
|
11
11
|
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import fsp from 'node:fs/promises';
|
|
7
7
|
import net from 'node:net';
|
|
8
|
+
import { tryHoldSocket } from '../src/home/lib/kernel-lock.mjs';
|
|
8
9
|
import {
|
|
9
|
-
holderLiveness, lifecycleLockAddress, lifecycleLockStrategy, parseHolder,
|
|
10
|
+
holderLiveness, lifecycleLockAddress, lifecycleLockStrategy, parseHolder,
|
|
10
11
|
} from './lifecycle-lock.mjs';
|
|
11
12
|
|
|
12
13
|
const refusedCodes = new Set(['ECONNREFUSED', 'ENOENT']);
|
|
@@ -81,7 +82,7 @@ export async function inspectLifecycleLock(homeRoot, {
|
|
|
81
82
|
const answer = await readSocketAnswer(target.address, answerTimeoutMs, createConnection);
|
|
82
83
|
if (answer.kind === 'answer') return result({ state: 'held', holder: answer.holder });
|
|
83
84
|
if (answer.kind !== 'refused') return result({ state: 'unverified', reason: answer.reason });
|
|
84
|
-
const release = await tryHoldSocket(target.address, createServer);
|
|
85
|
+
const release = await tryHoldSocket(target.address, createServer, 'lifecycle lock');
|
|
85
86
|
if (release === 'in-use') return result({ state: 'held', holder: null });
|
|
86
87
|
await release();
|
|
87
88
|
return result({ state: 'free' });
|
package/cli/lifecycle-lock.mjs
CHANGED
|
@@ -17,12 +17,14 @@
|
|
|
17
17
|
* the holder must fail, or the platform did not give exclusivity and the call refuses rather than
|
|
18
18
|
* switch protocols. What the holder answers a waiter is diagnosis only — it never grants anything.
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
21
|
-
import fs from 'node:fs';
|
|
20
|
+
import { randomUUID } from 'node:crypto';
|
|
22
21
|
import fsp from 'node:fs/promises';
|
|
23
22
|
import net from 'node:net';
|
|
24
23
|
import path from 'node:path';
|
|
25
24
|
import { createHomeWriter } from '../src/home/lib/home-write.mjs';
|
|
25
|
+
import {
|
|
26
|
+
acquireKernelLock, directoryDigest, kernelLockAddress, kernelLockStrategy,
|
|
27
|
+
} from '../src/home/lib/kernel-lock.mjs';
|
|
26
28
|
import { parseJsonText } from '../src/home/lib/json-file.mjs';
|
|
27
29
|
import { processIdentity } from '../src/home/lib/process-identity.mjs';
|
|
28
30
|
|
|
@@ -46,25 +48,23 @@ function validateOptions(homeRoot, { command, hostRoot, waitMs, retryMs, answerT
|
|
|
46
48
|
|
|
47
49
|
function statIdentity(homeRoot) {
|
|
48
50
|
try {
|
|
49
|
-
|
|
50
|
-
if (!stat.isDirectory()) throw new Error(`lifecycle lock home is not a directory: ${homeRoot}`);
|
|
51
|
-
return createHash('sha256').update(`${stat.dev}:${stat.ino}`).digest('hex');
|
|
51
|
+
return directoryDigest(homeRoot);
|
|
52
52
|
} catch (error) {
|
|
53
|
-
if (error
|
|
54
|
-
|
|
53
|
+
if (error.message === `${homeRoot} is not a directory`) {
|
|
54
|
+
throw new Error(`lifecycle lock home is not a directory: ${homeRoot}`);
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export function lifecycleLockStrategy(platform = process.platform) {
|
|
59
|
-
|
|
60
|
-
return platform === 'linux' ? 'abstract-socket' : 'file';
|
|
61
|
+
return kernelLockStrategy(platform) ?? 'file';
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
function lockAddress(homeRoot, digest, platform) {
|
|
64
65
|
const strategy = lifecycleLockStrategy(platform);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return { strategy, address: path.join(homeRoot, LOCK_NAME) };
|
|
66
|
+
const address = kernelLockAddress('lifecycle', digest, platform);
|
|
67
|
+
return { strategy, address: address ?? path.join(homeRoot, LOCK_NAME) };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export function lifecycleLockAddress(homeRoot, { platform = process.platform } = {}) {
|
|
@@ -87,73 +87,6 @@ async function prepareHome(homeRoot) {
|
|
|
87
87
|
return { writer, digest };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function listen(server, address) {
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
const listening = () => { server.removeListener('error', failed); resolve(); };
|
|
93
|
-
const failed = (error) => { server.removeListener('listening', listening); reject(error); };
|
|
94
|
-
server.once('listening', listening);
|
|
95
|
-
server.once('error', failed);
|
|
96
|
-
try {
|
|
97
|
-
server.listen(address);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
server.removeListener('listening', listening);
|
|
100
|
-
server.removeListener('error', failed);
|
|
101
|
-
reject(error);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function closeServer(server) {
|
|
107
|
-
if (!server) return Promise.resolve();
|
|
108
|
-
return new Promise((resolve, reject) => {
|
|
109
|
-
try {
|
|
110
|
-
server.close((error) => {
|
|
111
|
-
if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') reject(error);
|
|
112
|
-
else resolve();
|
|
113
|
-
});
|
|
114
|
-
} catch (error) {
|
|
115
|
-
if (error.code === 'ERR_SERVER_NOT_RUNNING') resolve();
|
|
116
|
-
else reject(error);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function closeQuietly(server) {
|
|
122
|
-
try { await closeServer(server); } catch { /* Preserve the original failure. */ }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function selfCheck(server, address, createServer) {
|
|
126
|
-
let probe;
|
|
127
|
-
let detail;
|
|
128
|
-
try {
|
|
129
|
-
probe = createServer();
|
|
130
|
-
await listen(probe, address);
|
|
131
|
-
detail = 'a second listener succeeded';
|
|
132
|
-
} catch (error) {
|
|
133
|
-
if (!error || error.code !== 'EADDRINUSE') detail = (error && (error.code || error.message)) || String(error);
|
|
134
|
-
}
|
|
135
|
-
if (detail === undefined) {
|
|
136
|
-
await closeQuietly(probe);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
await Promise.all([closeQuietly(probe), closeQuietly(server)]);
|
|
140
|
-
throw new Error(`lifecycle lock self-check failed: ${detail}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function tryHoldSocket(address, createServer = net.createServer) {
|
|
144
|
-
const server = createServer((socket) => socket.destroy());
|
|
145
|
-
try {
|
|
146
|
-
await listen(server, address);
|
|
147
|
-
await selfCheck(server, address, createServer);
|
|
148
|
-
let releasePromise;
|
|
149
|
-
return () => (releasePromise ??= closeServer(server));
|
|
150
|
-
} catch (error) {
|
|
151
|
-
await closeQuietly(server);
|
|
152
|
-
if (error?.code === 'EADDRINUSE') return 'in-use';
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
90
|
function validHolder(value) {
|
|
158
91
|
return value && Number.isInteger(value.pid) && value.pid > 0
|
|
159
92
|
&& typeof value.command === 'string' && value.command.length > 0
|
|
@@ -188,29 +121,6 @@ function busyMessage(homeRoot, holder, hint = '') {
|
|
|
188
121
|
return new Error(`lifecycle lock busy for ${homeRoot}: ${details}; nothing was changed${hint}`);
|
|
189
122
|
}
|
|
190
123
|
|
|
191
|
-
function readSocketHolder(address, timeoutMs) {
|
|
192
|
-
return new Promise((resolve) => {
|
|
193
|
-
let data = '';
|
|
194
|
-
let settled = false;
|
|
195
|
-
const socket = net.createConnection(address);
|
|
196
|
-
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
197
|
-
const finish = (holder) => {
|
|
198
|
-
if (settled) return;
|
|
199
|
-
settled = true;
|
|
200
|
-
clearTimeout(timer);
|
|
201
|
-
socket.destroy();
|
|
202
|
-
resolve(holder);
|
|
203
|
-
};
|
|
204
|
-
socket.on('data', (chunk) => {
|
|
205
|
-
data += chunk.toString('utf8');
|
|
206
|
-
const newline = data.indexOf('\n');
|
|
207
|
-
if (newline !== -1) finish(parseHolder(data.slice(0, newline)));
|
|
208
|
-
});
|
|
209
|
-
socket.once('end', () => finish(parseHolder(data)));
|
|
210
|
-
socket.once('error', () => finish(null));
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
124
|
function lockResult(token, strategy, releaseAction) {
|
|
215
125
|
let releasePromise;
|
|
216
126
|
return {
|
|
@@ -226,36 +136,13 @@ function lockResult(token, strategy, releaseAction) {
|
|
|
226
136
|
async function acquireSocketLock(homeRoot, holder, {
|
|
227
137
|
address, strategy, waitMs, retryMs, answerTimeoutMs, createServer,
|
|
228
138
|
}) {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
for (;;) {
|
|
238
|
-
const server = createServer(onConnection);
|
|
239
|
-
try {
|
|
240
|
-
await listen(server, address);
|
|
241
|
-
holder.acquiredAt = new Date().toISOString();
|
|
242
|
-
await selfCheck(server, address, createServer);
|
|
243
|
-
server.unref();
|
|
244
|
-
return lockResult(holder.token, strategy, async () => {
|
|
245
|
-
const closing = closeServer(server);
|
|
246
|
-
for (const socket of accepted) socket.destroy();
|
|
247
|
-
await closing;
|
|
248
|
-
});
|
|
249
|
-
} catch (error) {
|
|
250
|
-
await closeQuietly(server);
|
|
251
|
-
if (!error || error.code !== 'EADDRINUSE') throw error;
|
|
252
|
-
const remaining = deadline - Date.now();
|
|
253
|
-
if (remaining <= 0) break;
|
|
254
|
-
await wait(Math.min(retryMs, remaining));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
throw busyMessage(homeRoot, await readSocketHolder(address, answerTimeoutMs));
|
|
139
|
+
const result = await acquireKernelLock({
|
|
140
|
+
address, holder, waitMs, retryMs, answerTimeoutMs, createServer,
|
|
141
|
+
parseAnswer: parseHolder,
|
|
142
|
+
label: 'lifecycle lock',
|
|
143
|
+
});
|
|
144
|
+
if (!result.held) throw busyMessage(homeRoot, result.holder);
|
|
145
|
+
return lockResult(holder.token, strategy, result.release);
|
|
259
146
|
}
|
|
260
147
|
|
|
261
148
|
async function acquireFileLock(homeRoot, writer, holder, { waitMs, retryMs }) {
|
package/cli/manifest.mjs
CHANGED
|
@@ -4,14 +4,13 @@ import path from 'node:path';
|
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { HOOK_DEFINITIONS } from '../src/home/lib/hook-definitions.mjs';
|
|
7
|
-
import {
|
|
8
|
-
renderRequiredInputSummary,
|
|
9
|
-
renderRequiredInputs,
|
|
10
|
-
} from '../src/home/lib/required-inputs.mjs';
|
|
7
|
+
import { renderRequiredInputSummary } from '../src/home/lib/dispatcher-call.mjs';
|
|
11
8
|
import { renderNoSelfExecution } from '../src/home/lib/no-self-execution.mjs';
|
|
9
|
+
import { renderDispatcherProtocol } from '../src/home/lib/dispatcher-protocol.mjs';
|
|
10
|
+
import { ORDER_AGENTS } from '../src/home/lib/order-schema.mjs';
|
|
12
11
|
import { renderStopSummary } from '../src/home/lib/stop-contract.mjs';
|
|
13
12
|
import { readJsonFile } from '../src/home/lib/json-file.mjs';
|
|
14
|
-
import { normalizeFrontmatter } from '
|
|
13
|
+
import { normalizeFrontmatter } from '../src/home/lib/frontmatter.mjs';
|
|
15
14
|
import {
|
|
16
15
|
INSTALL_RECORD_NAME,
|
|
17
16
|
RULES_NAME,
|
|
@@ -77,7 +76,8 @@ export function replacePlaceholders(content, installationRoot) {
|
|
|
77
76
|
const replaced = content
|
|
78
77
|
.replaceAll('{{CODEX_BRIDGE_DIR}}', posix(path.resolve(installationRoot)))
|
|
79
78
|
.replaceAll('{{CODEX_REQUIRED_INPUTS_SUMMARY}}', renderRequiredInputSummary(agentType))
|
|
80
|
-
.replaceAll('{{
|
|
79
|
+
.replaceAll('{{CODEX_DISPATCHER_PROTOCOL}}', ORDER_AGENTS.includes(agentType)
|
|
80
|
+
? renderDispatcherProtocol(agentType) : '{{CODEX_DISPATCHER_PROTOCOL}}')
|
|
81
81
|
.replaceAll('{{CODEX_NO_SELF_EXECUTION}}', renderNoSelfExecution())
|
|
82
82
|
.replaceAll('{{CODEX_STOP_SUMMARY}}', renderStopSummary());
|
|
83
83
|
return normalizeFrontmatter(replaced);
|
package/cli/read.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** Renders a run's structured Codex transport as a readable event-by-event report. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readJsonFileSync } from '../src/home/lib/json-file.mjs';
|
|
2
4
|
import { readEvents } from '../src/home/lib/meta/events.mjs';
|
|
3
5
|
import { safeSlice } from '../src/home/lib/meta/paths.mjs';
|
|
4
6
|
import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
|
|
@@ -92,6 +94,24 @@ export function read({ run, cwd = process.cwd(), runsRootPath = runsRoot() } = {
|
|
|
92
94
|
// run from before the event stream existed has no file, while a run killed before the CLI
|
|
93
95
|
// spoke has an empty one. Reporting both as "predates this change" would hide the second.
|
|
94
96
|
if (!eventData.hasStream) {
|
|
97
|
+
let state;
|
|
98
|
+
try {
|
|
99
|
+
state = readJsonFileSync(path.join(lookup.runDir, 'status.json'))?.state;
|
|
100
|
+
} catch {
|
|
101
|
+
// The 2026-09-06 refusal needs its state; unreadable status keeps the legacy explanation.
|
|
102
|
+
}
|
|
103
|
+
if (state === 'aborted_pre_start') {
|
|
104
|
+
return result(
|
|
105
|
+
1,
|
|
106
|
+
`Run ${lookup.runDir} was refused before Codex started (state aborted_pre_start), so it has no events.jsonl.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (state === 'running') {
|
|
110
|
+
return result(
|
|
111
|
+
1,
|
|
112
|
+
`Run ${lookup.runDir} has no events.jsonl yet: Codex has not started; repeat the command shortly.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
95
115
|
return result(1, `Run ${lookup.runDir} has no events.jsonl; it predates the event stream.`);
|
|
96
116
|
}
|
|
97
117
|
if (!eventData.hasEvents) {
|
package/cli/settings-merge.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/** Merges and removes named codex-bridge hooks without disturbing host settings. */
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { existsSync, statSync } from 'node:fs';
|
|
5
4
|
import fs from 'node:fs/promises';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { randomUUID } from 'node:crypto';
|
|
8
7
|
import { readJsonFileWithRaw } from '../src/home/lib/json-file.mjs';
|
|
8
|
+
import { envValue, resolveCommandOnPath } from '../src/home/lib/command-path.mjs';
|
|
9
9
|
|
|
10
10
|
const LEGACY_SPEC = { event: 'SubagentStop', matcher: '*' };
|
|
11
11
|
const settingsRuns = new AsyncLocalStorage();
|
|
@@ -18,34 +18,8 @@ export function shortCommandFor(name) {
|
|
|
18
18
|
return `codex-bridge hook ${name}`;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function envValue(env, name) {
|
|
22
|
-
const key = Object.keys(env).find((entry) => entry.toLowerCase() === name);
|
|
23
|
-
return key ? env[key] : '';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function pathValue(env) {
|
|
27
|
-
return envValue(env, 'path');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function resolveCommand(name, env) {
|
|
31
|
-
const extensions = process.platform === 'win32'
|
|
32
|
-
? (env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')
|
|
33
|
-
: [''];
|
|
34
|
-
for (const directory of pathValue(env).split(path.delimiter).filter(Boolean)) {
|
|
35
|
-
for (const extension of extensions) {
|
|
36
|
-
const candidate = path.join(path.resolve(directory), `${name}${extension}`);
|
|
37
|
-
try {
|
|
38
|
-
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
|
|
39
|
-
} catch {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
21
|
export function commandReachable(name = 'codex-bridge', env = process.env) {
|
|
48
|
-
return Boolean(
|
|
22
|
+
return Boolean(resolveCommandOnPath(name, env));
|
|
49
23
|
}
|
|
50
24
|
|
|
51
25
|
/**
|
|
@@ -63,7 +37,7 @@ export function reachableCommandVersion(name = 'codex-bridge', env = process.env
|
|
|
63
37
|
}
|
|
64
38
|
|
|
65
39
|
export function runReachableCommand(name, args, env = process.env) {
|
|
66
|
-
const shimPath =
|
|
40
|
+
const shimPath = resolveCommandOnPath(name, env);
|
|
67
41
|
if (!shimPath) return null;
|
|
68
42
|
// Node 24 warned with DEP0190 during update (2026-09-07): invoke cmd explicitly for npm shims.
|
|
69
43
|
// Live probes covered plain, bin with space, Program Files (x86), weird & name, and caret^dir.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyupro/codex-bridge",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Claude Code-to-Codex delegation with 7 guards,
|
|
3
|
+
"version": "0.6.11",
|
|
4
|
+
"description": "Claude Code-to-Codex delegation with 7 guards, 3199 tests, zero runtime dependencies, and zero build steps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"codex",
|
|
@@ -37,13 +37,23 @@ export const HOME_WRITE_INVENTORY = Object.freeze([
|
|
|
37
37
|
outside('src/home/lib/retention.mjs', 'fs-binding-escape', 1, 'The injected filesystem falls back to fs while cleaning run folders under the runs root, outside package home.'),
|
|
38
38
|
outside('src/home/lib/retention.mjs', 'unlinkSync', 1, 'Removes transport files inside run folders under the runs root, outside package home.'),
|
|
39
39
|
outside('src/home/lib/runner/git-state.mjs', 'openSync', 1, 'Read-only open with the r flag, hashing an untracked file of the worked repository in chunks (Plan_73 B4b).'),
|
|
40
|
+
outside('src/home/lib/runner/build-evidence.mjs', 'writeFileSync', 11, 'Writes the before and after tree evidence of a build run, flags.txt and flags-coverage.txt inside a run folder.'),
|
|
40
41
|
outside('src/home/lib/runner/codex-cmd.mjs', 'createWriteStream', 2, 'Appends stderr.log and events.jsonl inside a run folder.'),
|
|
42
|
+
outside('src/home/lib/runner/flag-baseline.mjs', 'mkdirSync', 1, 'Creates the start-content copy folder inside a run folder (Plan_60 D3a).'),
|
|
43
|
+
outside('src/home/lib/runner/flag-baseline.mjs', 'rmSync', 1, 'Removes the start-content copies inside a run folder after the flag scan; the manifest stays.'),
|
|
44
|
+
outside('src/home/lib/runner/flag-baseline.mjs', 'writeFileSync', 2, 'Writes start-content copies and the flag baseline manifest inside a run folder.'),
|
|
45
|
+
outside('src/home/lib/runner/flag-scan.mjs', 'mkdirSync', 1,
|
|
46
|
+
'Creates the temporary HEAD-content folder inside a run folder (Plan_60 D3a).'),
|
|
47
|
+
outside('src/home/lib/runner/flag-scan.mjs', 'unlinkSync', 1,
|
|
48
|
+
'Removes a temporary HEAD-content file immediately after its flag comparison.'),
|
|
49
|
+
outside('src/home/lib/runner/flag-scan.mjs', 'writeFileSync', 1,
|
|
50
|
+
'Writes temporary HEAD bytes inside a run folder for the added-line flag comparison.'),
|
|
41
51
|
outside('src/home/lib/runner/launcher.mjs', 'mkdirSync', 2, 'Creates project and run folders under the runs root.'),
|
|
42
|
-
outside('src/home/lib/runner/launcher.mjs', 'writeFileSync',
|
|
52
|
+
outside('src/home/lib/runner/launcher.mjs', 'writeFileSync', 7, 'Writes run prompt, environment, scope and schema into a run folder.'),
|
|
43
53
|
outside('src/home/lib/runner/project-dir.mjs', 'mkdirSync', 2, 'Creates the runs root and a project folder under the runs root.'),
|
|
44
54
|
outside('src/home/lib/runner/project-dir.mjs', 'writeFileSync', 1, 'Writes the project ownership marker under the runs root.'),
|
|
45
55
|
outside('src/home/lib/runner/run-context.mjs', 'writeFileSync', 1, 'Writes a worker reply inside a run folder.'),
|
|
46
56
|
outside('src/home/lib/runner/worker-order.mjs', 'writeFileSync', 1, 'Writes worker.json inside a run folder.'),
|
|
47
|
-
outside('src/home/lib/runner/worker.mjs', 'writeFileSync',
|
|
57
|
+
outside('src/home/lib/runner/worker.mjs', 'writeFileSync', 1, 'Writes report.md inside a run folder.'),
|
|
48
58
|
outside('src/home/lib/write-meta.mjs', 'writeFileSync', 1, 'Writes meta.json inside a run folder under the runs root.'),
|
|
49
59
|
].map((entry) => Object.freeze(entry)));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: codex-advisor
|
|
3
|
-
description: Независимое второе мнение по DESIGN до появления кода — что правильно, а не что уже есть; работу выполняет Codex CLI по подписке ChatGPT, строго read-only. Два этапа: сначала `scope` — достаточно ли переданного списка файлов и какие риски ожидаются; затем `advise`
|
|
3
|
+
description: Независимое второе мнение по DESIGN до появления кода — что правильно, а не что уже есть; работу выполняет Codex CLI по подписке ChatGPT, строго read-only. Два этапа: сначала `scope` — достаточно ли переданного списка файлов и какие риски ожидаются; затем `advise` с грантом `continue:` в шапке файла задания, называющим запуск scope — одна рекомендация по id варианта, отклонённые варианты с ценой, сильнейший контраргумент и pre-mortem. Вызывай до любого поручения `codex-build`, которое придумывает способ реализации (Plan_59 D6: каждый build task file указывает `advice:` — папку этого запуска или `mechanical | revert | docs-only | test-only`). Полный результат — в ~/.claude/codex-runs/, в чате — не более пяти строк и путь. {{CODEX_REQUIRED_INPUTS_SUMMARY}} {{CODEX_STOP_SUMMARY}}
|
|
4
4
|
model: haiku
|
|
5
5
|
tools: Bash
|
|
6
6
|
---
|
|
@@ -14,119 +14,29 @@ ChatGPT subscription. All design reading and reasoning must happen on the Codex
|
|
|
14
14
|
line you send to chat costs Claude tokens. Therefore: do not read files, do not retell the report,
|
|
15
15
|
and do not reason about the design yourself.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
- `
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
know which option the requester favors (D5). Biased or malformed options are refused before
|
|
38
|
-
quota is spent.
|
|
39
|
-
- `## Paths` — at least one repository-relative path per list line. The advisor may cite only
|
|
40
|
-
these paths and, during `advise`, paths the scope run named in `missing_paths`. Each
|
|
41
|
-
`path:line` citation is machine-checked; an invented or out-of-list address fails the run (D3).
|
|
42
|
-
- Scope patterns are globs relative to the repository root. A pattern that matches nothing there is
|
|
43
|
-
refused before the run starts. This dispatcher does not accept scope patterns: its file boundary
|
|
44
|
-
is the explicit `## Paths` list and, for `advise`, the successful scope run's `missing_paths`.
|
|
45
|
-
- Every input listed under **Required dispatcher inputs** above, passed on exactly as given:
|
|
46
|
-
`order id` as `--order-id`, `task file` as `--task-file`, and `phase` as `--phase`. Use the same
|
|
47
|
-
order id for both phases; never invent or edit it. If the orchestrator did not give a required
|
|
48
|
-
input, do not guess — start the runner without its flag and return its refusal verbatim.
|
|
49
|
-
- `continue` — when the task text contains a line beginning with the `continue:` label, pass the
|
|
50
|
-
bare `--continue` flag even when its run name or reason looks malformed. Do not inspect, repair,
|
|
51
|
-
or swallow this grant; pass it through and let the runner issue the refusal. A continuation is
|
|
52
|
-
assigned by the orchestrator, never chosen by you. After the verdict, return the exact attaching
|
|
53
|
-
output and stop; do not issue or invent another continuation. If no such grant line is present,
|
|
54
|
-
the flag must not be present. The advise phase requires this grant to continue the scope run.
|
|
55
|
-
- Optional: `slug: <short-name>` for the run folder; by default, the slug is taken from the order
|
|
56
|
-
id. Optional: `effort: <none|low|medium|high|xhigh|max>` only when the orchestrator named a
|
|
57
|
-
depth.
|
|
58
|
-
|
|
59
|
-
## When the host refuses the command
|
|
60
|
-
|
|
61
|
-
If the host refuses to run `codex-bridge run` — a permission prompt, a classifier denial, anything
|
|
62
|
-
that stops the command — that refusal is your final answer. Report `FAIL`, name your own order id,
|
|
63
|
-
and state the one correction: the operator runs `codex-bridge install`, which grants the permission
|
|
64
|
-
rule this package needs.
|
|
65
|
-
|
|
66
|
-
You are forbidden to look for a way around it. Specifically, and without exception:
|
|
67
|
-
|
|
68
|
-
- never call `run-codex.mjs`, or any file inside the installed package, by path;
|
|
69
|
-
- never start the runner through `node`, `npx`, `sh`, `bash` or any other interpreter;
|
|
70
|
-
- never retry the same call in PowerShell because Bash refused it, or the reverse;
|
|
71
|
-
- never split the call over more than one line, and never add a pipe, a semicolon or a redirect;
|
|
72
|
-
- never advise the operator to grant a permission rule on an internal file — a rule on anything but
|
|
73
|
-
the package command undoes the very design that makes this call permission-stable.
|
|
74
|
-
|
|
75
|
-
Every one of those forms was removed on purpose: a host matches a permission rule against the
|
|
76
|
-
beginning of the final command line, so an interpreter, a path or a continuation makes the call
|
|
77
|
-
unmatchable by construction. Reaching for one does not rescue the run; it guarantees the refusal.
|
|
78
|
-
|
|
79
|
-
## The only thing you do
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
codex-bridge run --agent codex-advisor --repo "<repository-path or .>" --phase "<phase from the orchestrator>" --slug "<slug>" --order-id "<order id from the orchestrator>" --task-file "<task-file path from the orchestrator>"
|
|
83
|
-
```
|
|
17
|
+
{{CODEX_DISPATCHER_PROTOCOL}}
|
|
18
|
+
|
|
19
|
+
## Role notes
|
|
20
|
+
|
|
21
|
+
The advisor performs two passes of one order: `phase: scope` first, then `phase: advise`
|
|
22
|
+
as the continuation of that same order's successful advisor scope run. The orchestrator
|
|
23
|
+
writes both phases in the task-file header; the `advise` header includes a `continue:`
|
|
24
|
+
grant naming the scope run. Never choose or change the phase or grant yourself.
|
|
25
|
+
|
|
26
|
+
The task file must use these headings and list formats:
|
|
27
|
+
|
|
28
|
+
- `## Question` — free-text design question.
|
|
29
|
+
- `## Options` — at least two lines of the form `- <option_id>: <description>`. Each id must
|
|
30
|
+
match `[a-z0-9][a-z0-9-]*`; ids must be unique. Do not include preference markers or wording
|
|
31
|
+
such as `recommend`, `prefer`, `рекоменд`, `предпочт`, `(✓)`, or `★`. The advisor must not
|
|
32
|
+
know which option the requester favors (D5). Biased or malformed options are refused before
|
|
33
|
+
quota is spent.
|
|
34
|
+
- `## Paths` — at least one repository-relative path per list line. The advisor may cite only
|
|
35
|
+
these paths and, during `advise`, paths the scope run named in `missing_paths`. Each
|
|
36
|
+
`path:line` citation is machine-checked; an invented or out-of-list address fails the run (D3).
|
|
84
37
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`1/true/yes/0/false/no` there). The grant line names a run folder and a reason, and the runner parses
|
|
88
|
-
it, so a malformed grant is passed through for the runner to refuse. If
|
|
89
|
-
there is no grant line, do not add the flag. Add `--effort "<value>"` only if the orchestrator
|
|
90
|
-
named a depth, and only with one of `none|low|medium|high|xhigh|max`. Never add free text or any
|
|
91
|
-
flags beyond those two conditional flags.
|
|
92
|
-
|
|
93
|
-
The first `scope` call starts the run and returns at once with `RUN=<path> order-id=<id>` and a
|
|
94
|
-
`STARTED` line. To get its verdict, run the identical command a second time — same order id, slug,
|
|
95
|
-
phase and flags. That second call attaches to the existing run and prints `ATTACH=<path>` before
|
|
96
|
-
blocking for the verdict. Do not start another scope run to attach.
|
|
97
|
-
|
|
98
|
-
After an `OK` scope verdict, the orchestrator may run `advise` with `--continue`, the same order id,
|
|
99
|
-
and the continuation grant it supplied. That call continues the scope run; it is not a new order.
|
|
100
|
-
Attach to that advise run by repeating its identical command if it returns `STARTED`.
|
|
101
|
-
|
|
102
|
-
If the runner refuses with an order id collision — the id already belongs to a run whose task
|
|
103
|
-
differs — that refusal is the whole answer: return `FAIL` with the runner's text. Never retry under
|
|
104
|
-
a different id of your own choosing; the order id is the orchestrator's.
|
|
105
|
-
|
|
106
|
-
Background execution (`run_in_background`, `&`, `nohup`) is prohibited, and so is inventing a
|
|
107
|
-
report from memory. If the attaching call is killed by a time ceiling, run the identical command again: it attaches to the same
|
|
108
|
-
run and keeps waiting. Never add a flag — the host allows exactly the one command your
|
|
109
|
-
order yields and refuses every other. A real run takes 20-25 minutes, which is normal, not a hang.
|
|
110
|
-
Give the ordinary attaching call `timeout: 1800000` (30 minutes).
|
|
111
|
-
|
|
112
|
-
**Never change `--order-id` or `--slug` to get a fresh run.** The order id is issued by the
|
|
113
|
-
orchestrator and is what makes a repeat harmless; changing it turns a repeat into another paid run.
|
|
114
|
-
|
|
115
|
-
The runner does the rest: validates the task file and phase, creates the run folder and artifacts,
|
|
116
|
-
starts Codex in a read-only sandbox, records status, and prints ready-made response lines. Invalid
|
|
117
|
-
or biased `Options`/`Paths`, `advise` without `--continue`, `advise` without an `OK` advisor scope
|
|
118
|
-
run, and an undeclared phase are free refusals; no ChatGPT quota is spent.
|
|
119
|
-
|
|
120
|
-
**Your response = the exact stdout of the attaching call**: the `ATTACH=<path>` line and the status
|
|
121
|
-
block below it. Do not add or remove anything: no preamble, explanations, apologies, or retelling
|
|
122
|
-
of findings. The report is in the file; the orchestrator will read it if needed. A `STARTED` result
|
|
123
|
-
is not final.
|
|
124
|
-
|
|
125
|
-
The only allowed final response is this exact stdout. Wording such as "the run has started,
|
|
126
|
-
waiting for completion," "I will wait for a notification," or "Monitor started in the background"
|
|
127
|
-
is prohibited: the subagent ceases to exist immediately after responding, nobody can wait, and the
|
|
128
|
-
orchestrator gets a promise instead of a result. Inventing any outcome the runner did not print is
|
|
129
|
-
equally prohibited.
|
|
38
|
+
The advisor's file boundary is the explicit `## Paths` list and, for `advise`, the
|
|
39
|
+
successful scope run's `missing_paths`.
|
|
130
40
|
|
|
131
41
|
## What you return
|
|
132
42
|
|
|
@@ -142,12 +52,16 @@ path.
|
|
|
142
52
|
- `FAIL` — the result is empty, invalid, the return code is nonzero, or the run was abandoned.
|
|
143
53
|
- `LIMIT` — the result is empty and the log signals a ChatGPT quota limit. The quota is spent and
|
|
144
54
|
the task was not completed; do not restart it.
|
|
55
|
+
- `UNAVAILABLE` — Codex is missing or signed out (checked before start, or recognised from the CLI's own
|
|
56
|
+
sign-in refusal after start). The task was not attempted or could not run; this is not a task failure and
|
|
57
|
+
not a reason to restart — return the block as is, the orchestrator hands the task to the next executor.
|
|
145
58
|
|
|
146
|
-
The script return code mirrors the status: `0` / `1` / `3`. A nonzero code is not a reason to
|
|
59
|
+
The script return code mirrors the status: `0` / `1` / `3` / `5`. A nonzero code is not a reason to
|
|
147
60
|
retry, change the command, or investigate on your own.
|
|
61
|
+
Code `5` means `UNAVAILABLE`.
|
|
148
62
|
|
|
149
63
|
For `scope`, `OK — scope: sufficient; …` means the listed paths cover the question; continue with
|
|
150
|
-
|
|
64
|
+
the `advise` phase only when the orchestrator's task-file header grants that continuation. `OK — scope: insufficient;
|
|
151
65
|
N paths named` followed by a `Missing:` line is also an `OK` run, not a `LIMIT`: the orchestrator
|
|
152
66
|
may add context or continue with `advise`. `LIMIT` alone means the ChatGPT quota is spent.
|
|
153
67
|
|
|
@@ -157,16 +71,18 @@ For `advise`, expect `OK — recommend <option_id>: …`, followed by `Rejected:
|
|
|
157
71
|
verbatim; compose nothing.
|
|
158
72
|
|
|
159
73
|
The run folder contains its status and artifacts. An abandoned run is not a reason to start over
|
|
160
|
-
yourself: the orchestrator decides whether to continue, and without the
|
|
161
|
-
|
|
162
|
-
|
|
74
|
+
yourself: the orchestrator decides whether to continue, and without the orchestrator's `continue:` or `retry:` grant in the task-file header, the runner will reject that repeat run itself.
|
|
75
|
+
The orchestrator repeats a failed scope or a failed advise with a `retry:` grant naming that failed
|
|
76
|
+
run, under the same order id. A successful scope is never continued as scope:
|
|
163
77
|
the runner refuses a continued scope, because it would spend the continuation that advise needs.
|
|
164
78
|
|
|
165
79
|
## Codex is unavailable
|
|
166
80
|
|
|
167
|
-
The runner
|
|
168
|
-
|
|
169
|
-
|
|
81
|
+
The runner looks `codex` up on PATH and asks `codex login status` before starting. A missing binary
|
|
82
|
+
or the measured "Not logged in" answer prints a ready-made `UNAVAILABLE` block (first line
|
|
83
|
+
`UNAVAILABLE — `, exit 5, no run folder). An unclear probe prints an ordinary refusal starting
|
|
84
|
+
`Codex CLI unavailable:` (exit 1). Return that output verbatim.
|
|
85
|
+
Performing the task manually instead of Codex is prohibited for any reason it fails.
|
|
170
86
|
|
|
171
87
|
## What a violation looks like
|
|
172
88
|
|