@lyupro/codex-bridge 0.5.3 → 0.5.4
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 +71 -0
- package/README.md +152 -152
- package/bin/codex-bridge.mjs +188 -174
- package/cli/doctor-format.mjs +23 -0
- package/cli/doctor-hooks.mjs +114 -0
- package/cli/doctor-installation.mjs +111 -0
- package/cli/doctor-runs.mjs +55 -0
- package/cli/doctor.mjs +150 -399
- package/cli/host-contract.mjs +135 -0
- package/cli/hosts.mjs +4 -3
- package/cli/install.mjs +31 -8
- package/cli/probe-contract.mjs +174 -0
- package/package.json +49 -49
- package/scripts/run-tests.mjs +4 -1
- package/src/agents/codex-build.md +265 -265
- package/src/agents/codex-review.md +219 -219
- package/src/agents/codex-scout.md +232 -232
- package/src/home/lib/brand-home.mjs +39 -0
- package/src/home/lib/meta/reply.mjs +27 -0
- package/src/home/lib/run-config.mjs +18 -6
- package/src/home/lib/runner/codex-args.mjs +154 -0
- package/src/home/lib/runner/codex-cmd.mjs +268 -398
- package/src/home/lib/runner/launcher.mjs +18 -30
- package/src/home/lib/runner/worker-order.mjs +56 -0
- package/src/home/lib/write-meta.mjs +11 -3
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records whether the installed Claude Code host still honours hook refusals.
|
|
3
|
+
*
|
|
4
|
+
* Every guard here refuses work with `permissionDecision: "deny"`, and the sibling key of that same
|
|
5
|
+
* object died quietly: a neighbouring package established that `"allow"` stopped being honoured
|
|
6
|
+
* somewhere between host 2.1.119 and 2.1.231, with no error printed on the way. Nothing in this
|
|
7
|
+
* package would notice `deny` going the same way — the suite checks that a hook RETURNED a refusal,
|
|
8
|
+
* never that the host APPLIED it. The record is bound to a host version because that is what
|
|
9
|
+
* changes underneath an installation: the host updates itself, the package does not.
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { CLI_NAMES } from '../src/home/lib/cli-names.mjs';
|
|
16
|
+
import { HOOK_DEFINITIONS } from '../src/home/lib/hook-definitions.mjs';
|
|
17
|
+
import { readJsonFile } from '../src/home/lib/json-file.mjs';
|
|
18
|
+
|
|
19
|
+
export const HOST_CONTRACT_RECORD_NAME = '.host-contract.json';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The command that runs the probe, kept here so every message gains it in one edit.
|
|
23
|
+
*
|
|
24
|
+
* It stayed unnamed in the messages until the command existed: a package that sends an operator to
|
|
25
|
+
* a command answering `unknown doctor option` has spent its credibility on the first line they
|
|
26
|
+
* read. The advice arrived in the same change as the command, and only in the states a probe can
|
|
27
|
+
* actually change — a message that advises a run which would tell you nothing new is noise.
|
|
28
|
+
*
|
|
29
|
+
* Derived rather than spelled out: Plan_17 §5 found installer and hook lists drifting apart once
|
|
30
|
+
* the same name lived in two places. `CLI_NAMES[0]` is the canonical spelling.
|
|
31
|
+
*/
|
|
32
|
+
export const PROBE_COMMAND = `${CLI_NAMES[0]} doctor --probe-contract`;
|
|
33
|
+
|
|
34
|
+
export function hostContractPath(host) {
|
|
35
|
+
if (!host?.brandRoot) throw new Error('host has no brand installation root');
|
|
36
|
+
return path.join(host.brandRoot, HOST_CONTRACT_RECORD_NAME);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function readHostContract(host) {
|
|
40
|
+
const target = hostContractPath(host);
|
|
41
|
+
try {
|
|
42
|
+
const record = await readJsonFile(target);
|
|
43
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)
|
|
44
|
+
|| typeof record.version !== 'string' || !record.version) return null;
|
|
45
|
+
return {
|
|
46
|
+
version: record.version,
|
|
47
|
+
checkedAt: record.checkedAt,
|
|
48
|
+
result: record.result,
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function writeHostContract(host, { version, result, now = new Date() }) {
|
|
56
|
+
if (typeof version !== 'string' || !version) {
|
|
57
|
+
throw new Error('host contract version must be a non-empty string');
|
|
58
|
+
}
|
|
59
|
+
if (result !== 'honored' && result !== 'ignored') {
|
|
60
|
+
throw new Error('host contract result must be honored or ignored');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const target = hostContractPath(host);
|
|
64
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
65
|
+
const record = { version, result, checkedAt: now.toISOString() };
|
|
66
|
+
await fs.mkdir(host.brandRoot, { recursive: true });
|
|
67
|
+
try {
|
|
68
|
+
await fs.writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
69
|
+
await fs.rename(temporary, target);
|
|
70
|
+
} finally {
|
|
71
|
+
await fs.rm(temporary, { force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parseHostVersion(text) {
|
|
76
|
+
if (typeof text !== 'string') return null;
|
|
77
|
+
return text.trim().match(/^(\d+\.\d+\.\d+)(?:\s|$)/)?.[1] ?? null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function detectHostVersion({ run = spawnSync } = {}) {
|
|
81
|
+
try {
|
|
82
|
+
const result = run('claude', ['--version'], {
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
shell: false,
|
|
85
|
+
timeout: 5000,
|
|
86
|
+
});
|
|
87
|
+
if (!result || result.error || result.status !== 0) return null;
|
|
88
|
+
return parseHostVersion(result.stdout);
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function contractStatus({ record, version }) {
|
|
95
|
+
// Loose equality on purpose: a caller that forgot the field hands over `undefined`, and the
|
|
96
|
+
// stale branch below would then report "the current host is undefined" as if it had measured it.
|
|
97
|
+
if (version == null) {
|
|
98
|
+
return {
|
|
99
|
+
state: 'unknown-host',
|
|
100
|
+
message: 'The host version could not be read, so the refusal contract cannot be judged.',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (!record) {
|
|
104
|
+
return {
|
|
105
|
+
state: 'unverified',
|
|
106
|
+
message: `The refusal contract has never been probed on this machine (host ${version}); run ${PROBE_COMMAND}.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (record.version !== version) {
|
|
110
|
+
return {
|
|
111
|
+
state: 'stale',
|
|
112
|
+
message: `The refusal contract was probed on host ${record.version}, but the current host is ${version}; run ${PROBE_COMMAND}.`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (record.result === 'ignored') {
|
|
116
|
+
const guards = HOOK_DEFINITIONS
|
|
117
|
+
.filter((definition) => definition.event === 'PreToolUse')
|
|
118
|
+
.map((definition) => definition.name)
|
|
119
|
+
.join(', ');
|
|
120
|
+
return {
|
|
121
|
+
state: 'ignored',
|
|
122
|
+
message: `DANGER: Host ${version} does not honour hook refusals, so every guard is inert: ${guards}. Re-run ${PROBE_COMMAND} once the host updates.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (record.result === 'honored') {
|
|
126
|
+
return {
|
|
127
|
+
state: 'verified',
|
|
128
|
+
message: `Host ${version} honours hook refusals (verified ${record.checkedAt}).`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
state: 'unverified',
|
|
133
|
+
message: `The refusal contract record is invalid (host ${version}); run ${PROBE_COMMAND}.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
package/cli/hosts.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { resolveBrandHome } from '../src/home/lib/brand-home.mjs';
|
|
5
6
|
|
|
6
7
|
function repositoryRoot(start) {
|
|
7
8
|
let current = path.resolve(start);
|
|
@@ -35,9 +36,9 @@ export function resolveHost({
|
|
|
35
36
|
// Plan_25 moves package-owned runtime files out of foreign Claude settings, whose absolute
|
|
36
37
|
// hook paths caused the package-file-layout incident. Keep the brand root overrideable so tests
|
|
37
38
|
// and later installation steps never need to know the operator's home directory.
|
|
38
|
-
const resolvedBrandRoot =
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const resolvedBrandRoot = brandRoot
|
|
40
|
+
? path.resolve(brandRoot)
|
|
41
|
+
: resolveBrandHome({ homedir, env: process.env }).root;
|
|
41
42
|
const brandPaths = {
|
|
42
43
|
brandRoot: resolvedBrandRoot,
|
|
43
44
|
brandHooksDir: path.join(resolvedBrandRoot, 'hooks'),
|
package/cli/install.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from './settings-merge.mjs';
|
|
27
27
|
import { addRulesOwner, readRulesRegistry } from './rules-owners.mjs';
|
|
28
28
|
import { addPermissionRules } from './permissions.mjs';
|
|
29
|
+
import { contractStatus, detectHostVersion, readHostContract } from './host-contract.mjs';
|
|
29
30
|
import { readRunConfig, retentionNotice } from '../src/home/lib/run-config.mjs';
|
|
30
31
|
|
|
31
32
|
const WARNING = '\u001b[33m';
|
|
@@ -56,6 +57,10 @@ function retentionOutput(line, output) {
|
|
|
56
57
|
return `${output}\n${line}`;
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
function contractOutput(status, output) {
|
|
61
|
+
return status.state === 'verified' ? output : `${output}\n${status.message}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
async function migrateLegacySeed(host, seed) {
|
|
60
65
|
if (await targetExists(seed.target)) return;
|
|
61
66
|
const legacyName = path.basename(seed.target) === 'config.json' ? 'run-config.json' : path.basename(seed.target);
|
|
@@ -73,10 +78,22 @@ async function migrateLegacySeed(host, seed) {
|
|
|
73
78
|
await fs.rm(legacy, { force: true });
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
async function installInRun({
|
|
81
|
+
async function installInRun({
|
|
82
|
+
host,
|
|
83
|
+
dryRun = false,
|
|
84
|
+
force = false,
|
|
85
|
+
packageRoot,
|
|
86
|
+
env = process.env,
|
|
87
|
+
contractRecord,
|
|
88
|
+
hostVersion,
|
|
89
|
+
} = {}) {
|
|
77
90
|
// Validate the shared registry before writes; package removal on a broken registry left the host without its watchdog.
|
|
78
91
|
await readRulesRegistry(host);
|
|
79
92
|
const configuredRetentionLine = retentionLine(host);
|
|
93
|
+
const hostContract = contractStatus({
|
|
94
|
+
record: contractRecord === undefined ? await readHostContract(host) : contractRecord,
|
|
95
|
+
version: hostVersion === undefined ? detectHostVersion() : hostVersion,
|
|
96
|
+
});
|
|
80
97
|
const plan = await buildInstallPlan(host, packageRoot);
|
|
81
98
|
const rule = { ...rulesPlan(host, packageRoot), processing: 'copy' };
|
|
82
99
|
const currentPackage = await packageInfo(packageRoot);
|
|
@@ -112,7 +129,7 @@ async function installInRun({ host, dryRun = false, force = false, packageRoot,
|
|
|
112
129
|
const files = conflicts.map(({ item }) => ` ${item.relativeToRoot || item.name}`).join('\n');
|
|
113
130
|
return {
|
|
114
131
|
exitCode: 1,
|
|
115
|
-
output: retentionOutput(configuredRetentionLine, `Conflicting files:\n${files}\nRun install again with --force to overwrite them.`),
|
|
132
|
+
output: contractOutput(hostContract, retentionOutput(configuredRetentionLine, `Conflicting files:\n${files}\nRun install again with --force to overwrite them.`)),
|
|
116
133
|
};
|
|
117
134
|
}
|
|
118
135
|
|
|
@@ -124,7 +141,10 @@ async function installInRun({ host, dryRun = false, force = false, packageRoot,
|
|
|
124
141
|
if (!changedFiles.length && !changedRule && inspectedHooks.every((state) => state.present)
|
|
125
142
|
&& recordHasHooks(record, targets) && sameRecord) {
|
|
126
143
|
if (!dryRun) await addRulesOwner(host);
|
|
127
|
-
return {
|
|
144
|
+
return {
|
|
145
|
+
exitCode: 0,
|
|
146
|
+
output: contractOutput(hostContract, retentionOutput(configuredRetentionLine, 'codex-bridge is already installed; nothing to do.')),
|
|
147
|
+
};
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
if (dryRun) {
|
|
@@ -140,7 +160,7 @@ async function installInRun({ host, dryRun = false, force = false, packageRoot,
|
|
|
140
160
|
: `Would register ${definition.event} hook for matcher ${definition.matcher} with ${registration.form} command.`);
|
|
141
161
|
});
|
|
142
162
|
lines.push('Would write installation record in the brand root.');
|
|
143
|
-
return { exitCode: 0, output: retentionOutput(configuredRetentionLine, lines.join('\n')) };
|
|
163
|
+
return { exitCode: 0, output: contractOutput(hostContract, retentionOutput(configuredRetentionLine, lines.join('\n'))) };
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
// Claim ownership of the shared rules file before writing anything. Claiming it last meant a
|
|
@@ -196,10 +216,13 @@ async function installInRun({ host, dryRun = false, force = false, packageRoot,
|
|
|
196
216
|
});
|
|
197
217
|
return {
|
|
198
218
|
exitCode: 0,
|
|
199
|
-
output:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
219
|
+
output: contractOutput(
|
|
220
|
+
hostContract,
|
|
221
|
+
retentionOutput(
|
|
222
|
+
configuredRetentionLine,
|
|
223
|
+
`Installed ${plan.length + 1} files and registered the ${targets.map(({ definition }) => definition.event).join(' and ')} hooks.`
|
|
224
|
+
+ `\nGranted ${permissionResult.added} permission rule(s) in ${host.settingsPath} so the host runs the package command without asking.`,
|
|
225
|
+
),
|
|
203
226
|
),
|
|
204
227
|
};
|
|
205
228
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measures whether the installed host applies a PreToolUse refusal, not merely whether the hook
|
|
3
|
+
* returns one. The sibling `allow` decision silently stopped working between hosts 2.1.119 and
|
|
4
|
+
* 2.1.231, so a missing marker is trusted only when the hook fired and the host completed cleanly.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { detectHostVersion, writeHostContract } from './host-contract.mjs';
|
|
11
|
+
|
|
12
|
+
export const PROBE_MARKER = 'codex-bridge-contract-probe';
|
|
13
|
+
|
|
14
|
+
const MARKER_NAME = `${PROBE_MARKER}.marker`;
|
|
15
|
+
const JOURNAL_NAME = 'probe-journal.log';
|
|
16
|
+
|
|
17
|
+
const exists = async (target) => {
|
|
18
|
+
try {
|
|
19
|
+
await fs.access(target);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function judgeProbe({ markerExists, hookFired, hostResult }) {
|
|
27
|
+
if (markerExists) {
|
|
28
|
+
return { result: 'ignored', reason: 'The marker command ran despite the hook refusal.' };
|
|
29
|
+
}
|
|
30
|
+
if (!hostResult) {
|
|
31
|
+
return { result: null, reason: 'The host could not be started.' };
|
|
32
|
+
}
|
|
33
|
+
if (hostResult.error) {
|
|
34
|
+
const timedOut = hostResult.error.code === 'ETIMEDOUT';
|
|
35
|
+
return {
|
|
36
|
+
result: null,
|
|
37
|
+
reason: timedOut ? 'The host timed out before the probe completed.' : 'The host process failed to start.',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (hostResult.signal) {
|
|
41
|
+
return { result: null, reason: `The host was terminated by signal ${hostResult.signal}.` };
|
|
42
|
+
}
|
|
43
|
+
if (hostResult.status !== 0) {
|
|
44
|
+
return { result: null, reason: `The host exited with status ${String(hostResult.status)}.` };
|
|
45
|
+
}
|
|
46
|
+
if (!hookFired) {
|
|
47
|
+
return { result: null, reason: 'The probe hook never received the marker command.' };
|
|
48
|
+
}
|
|
49
|
+
return { result: 'honored', reason: 'The hook fired and the refused marker command did not run.' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const hookSource = `#!/usr/bin/env node
|
|
53
|
+
import fs from 'node:fs';
|
|
54
|
+
import { fileURLToPath } from 'node:url';
|
|
55
|
+
|
|
56
|
+
const marker = ${JSON.stringify(PROBE_MARKER)};
|
|
57
|
+
const journalPath = fileURLToPath(new URL('../${JOURNAL_NAME}', import.meta.url));
|
|
58
|
+
let input;
|
|
59
|
+
try {
|
|
60
|
+
input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
61
|
+
} catch {
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof input?.tool_input?.command === 'string' && input.tool_input.command.includes(marker)) {
|
|
66
|
+
fs.appendFileSync(journalPath, \`${PROBE_MARKER}\\n\`, 'utf8');
|
|
67
|
+
process.stdout.write(JSON.stringify({
|
|
68
|
+
hookSpecificOutput: {
|
|
69
|
+
hookEventName: 'PreToolUse',
|
|
70
|
+
permissionDecision: 'deny',
|
|
71
|
+
permissionDecisionReason: 'Contract probe refused its marker command.',
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
// Forward slashes on purpose: the host runs this hook command line through a POSIX shell, and a
|
|
78
|
+
// Windows path handed over verbatim arrives carrying backslashes that shell may swallow. The
|
|
79
|
+
// doubled form this replacement first carried matched nothing at all on a real path.
|
|
80
|
+
const shellQuote = (value) => `"${value.replaceAll('\\', '/').replaceAll('"', '\\"')}"`;
|
|
81
|
+
|
|
82
|
+
export async function buildRig(dir) {
|
|
83
|
+
const claudeDir = path.join(dir, '.claude');
|
|
84
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
85
|
+
const hookPath = path.join(hooksDir, 'probe-hook.mjs');
|
|
86
|
+
const markerPath = path.join(dir, MARKER_NAME);
|
|
87
|
+
const journalPath = path.join(claudeDir, JOURNAL_NAME);
|
|
88
|
+
const command = `touch ${MARKER_NAME}`;
|
|
89
|
+
const prompt = `Run exactly this Bash command and nothing else: ${command}`;
|
|
90
|
+
const settings = {
|
|
91
|
+
hooks: {
|
|
92
|
+
PreToolUse: [{
|
|
93
|
+
matcher: 'Bash',
|
|
94
|
+
hooks: [{ type: 'command', command: `${shellQuote(process.execPath)} ${shellQuote(hookPath)}` }],
|
|
95
|
+
}],
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await fs.mkdir(hooksDir, { recursive: true });
|
|
100
|
+
await fs.writeFile(hookPath, hookSource, 'utf8');
|
|
101
|
+
await fs.writeFile(
|
|
102
|
+
path.join(claudeDir, 'settings.json'),
|
|
103
|
+
`${JSON.stringify(settings, null, 2)}\n`,
|
|
104
|
+
'utf8',
|
|
105
|
+
);
|
|
106
|
+
return { dir, markerPath, journalPath, command, prompt };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function probeContract({
|
|
110
|
+
host,
|
|
111
|
+
version = detectHostVersion(),
|
|
112
|
+
runHost = spawnSync,
|
|
113
|
+
rigRoot = os.tmpdir(),
|
|
114
|
+
now = new Date(),
|
|
115
|
+
}) {
|
|
116
|
+
if (version == null) {
|
|
117
|
+
return {
|
|
118
|
+
state: 'inconclusive',
|
|
119
|
+
result: null,
|
|
120
|
+
version: null,
|
|
121
|
+
message: 'The host version could not be read, so no contract probe was run.',
|
|
122
|
+
recorded: false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await fs.mkdir(rigRoot, { recursive: true });
|
|
127
|
+
const dir = await fs.mkdtemp(path.join(rigRoot, `${PROBE_MARKER}-`));
|
|
128
|
+
try {
|
|
129
|
+
const rig = await buildRig(dir);
|
|
130
|
+
let hostResult;
|
|
131
|
+
try {
|
|
132
|
+
hostResult = runHost('claude', [
|
|
133
|
+
'--setting-sources', 'project',
|
|
134
|
+
'--allowedTools', 'Bash',
|
|
135
|
+
'-p', rig.prompt,
|
|
136
|
+
], {
|
|
137
|
+
cwd: rig.dir,
|
|
138
|
+
encoding: 'utf8',
|
|
139
|
+
shell: false,
|
|
140
|
+
timeout: 120000,
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
hostResult = { error };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const verdict = judgeProbe({
|
|
147
|
+
markerExists: await exists(rig.markerPath),
|
|
148
|
+
hookFired: await exists(rig.journalPath),
|
|
149
|
+
hostResult,
|
|
150
|
+
});
|
|
151
|
+
if (verdict.result == null) {
|
|
152
|
+
return {
|
|
153
|
+
state: 'inconclusive',
|
|
154
|
+
result: null,
|
|
155
|
+
version,
|
|
156
|
+
message: verdict.reason,
|
|
157
|
+
recorded: false,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await writeHostContract(host, { version, result: verdict.result, now });
|
|
162
|
+
return {
|
|
163
|
+
state: 'probed',
|
|
164
|
+
result: verdict.result,
|
|
165
|
+
version,
|
|
166
|
+
message: verdict.reason,
|
|
167
|
+
recorded: true,
|
|
168
|
+
};
|
|
169
|
+
} finally {
|
|
170
|
+
// Windows can retain a just-written hook tree briefly; cleanup failure must not erase a valid
|
|
171
|
+
// measurement or prevent the result from being recorded.
|
|
172
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
173
|
+
}
|
|
174
|
+
}
|
package/package.json
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@lyupro/codex-bridge",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Claude Code-to-Codex delegation with 6 guards,
|
|
5
|
-
"keywords": [
|
|
6
|
-
"claude-code",
|
|
7
|
-
"codex",
|
|
8
|
-
"codex-cli",
|
|
9
|
-
"subagent",
|
|
10
|
-
"ai-agents",
|
|
11
|
-
"orchestration",
|
|
12
|
-
"hooks"
|
|
13
|
-
],
|
|
14
|
-
"license": "MIT",
|
|
15
|
-
"author": "Lyu Pro <lyupro.dev@gmail.com> (https://lyupro.com)",
|
|
16
|
-
"repository": {
|
|
17
|
-
"type": "git",
|
|
18
|
-
"url": "git+https://github.com/lyupro/codex-bridge.git"
|
|
19
|
-
},
|
|
20
|
-
"bugs": {
|
|
21
|
-
"url": "https://github.com/lyupro/codex-bridge/issues"
|
|
22
|
-
},
|
|
23
|
-
"homepage": "https://github.com/lyupro/codex-bridge#readme",
|
|
24
|
-
"type": "module",
|
|
25
|
-
"bin": {
|
|
26
|
-
"codex-bridge": "./bin/codex-bridge.mjs",
|
|
27
|
-
"codexb": "./bin/codex-bridge.mjs"
|
|
28
|
-
},
|
|
29
|
-
"engines": {
|
|
30
|
-
"node": ">=24.0.0"
|
|
31
|
-
},
|
|
32
|
-
"files": [
|
|
33
|
-
"bin",
|
|
34
|
-
"cli",
|
|
35
|
-
"src",
|
|
36
|
-
"scripts",
|
|
37
|
-
"README.md",
|
|
38
|
-
"LICENSE",
|
|
39
|
-
"CHANGELOG.md"
|
|
40
|
-
],
|
|
41
|
-
"scripts": {
|
|
42
|
-
"dev:install": "node bin/codex-bridge.mjs update --force",
|
|
43
|
-
"test": "node scripts/run-tests.mjs",
|
|
44
|
-
"check:size": "node scripts/check-file-size.mjs"
|
|
45
|
-
},
|
|
46
|
-
"publishConfig": {
|
|
47
|
-
"access": "public"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@lyupro/codex-bridge",
|
|
3
|
+
"version": "0.5.4",
|
|
4
|
+
"description": "Claude Code-to-Codex delegation with 6 guards, 809 tests, zero runtime dependencies, and zero build steps.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"codex",
|
|
8
|
+
"codex-cli",
|
|
9
|
+
"subagent",
|
|
10
|
+
"ai-agents",
|
|
11
|
+
"orchestration",
|
|
12
|
+
"hooks"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Lyu Pro <lyupro.dev@gmail.com> (https://lyupro.com)",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/lyupro/codex-bridge.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/lyupro/codex-bridge/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/lyupro/codex-bridge#readme",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"codex-bridge": "./bin/codex-bridge.mjs",
|
|
27
|
+
"codexb": "./bin/codex-bridge.mjs"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin",
|
|
34
|
+
"cli",
|
|
35
|
+
"src",
|
|
36
|
+
"scripts",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"CHANGELOG.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"dev:install": "node bin/codex-bridge.mjs update --force",
|
|
43
|
+
"test": "node scripts/run-tests.mjs",
|
|
44
|
+
"check:size": "node scripts/check-file-size.mjs"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -25,6 +25,8 @@ const created = ISOLATED_ROOTS.map((name) => [
|
|
|
25
25
|
name,
|
|
26
26
|
fs.mkdtempSync(path.join(os.tmpdir(), `codex-bridge-test-${name.toLowerCase()}-`)),
|
|
27
27
|
]);
|
|
28
|
+
// Fixtures created temporary trees no one removed, so keep them under one disposable root.
|
|
29
|
+
const testTmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-bridge-test-tmp-'));
|
|
28
30
|
const pattern = process.argv[2] || 'tests/**/*.test.mjs';
|
|
29
31
|
// Only a whole-suite run can judge the README's number; a single file legitimately reports two.
|
|
30
32
|
const wholeSuite = !process.argv[2];
|
|
@@ -43,7 +45,7 @@ const reporters = tapFile
|
|
|
43
45
|
try {
|
|
44
46
|
const result = spawnSync(process.execPath, ['--test', ...reporters, pattern], {
|
|
45
47
|
stdio: 'inherit',
|
|
46
|
-
env: { ...process.env, ...Object.fromEntries(created) },
|
|
48
|
+
env: { ...process.env, ...Object.fromEntries(created), CODEX_BRIDGE_TEST_TMP: testTmpRoot },
|
|
47
49
|
});
|
|
48
50
|
process.exitCode = result.status ?? 1;
|
|
49
51
|
if (tapFile && process.exitCode === 0) {
|
|
@@ -61,5 +63,6 @@ try {
|
|
|
61
63
|
}
|
|
62
64
|
} finally {
|
|
63
65
|
for (const [, directory] of created) fs.rmSync(directory, { recursive: true, force: true });
|
|
66
|
+
fs.rmSync(testTmpRoot, { recursive: true, force: true });
|
|
64
67
|
if (tapFile) fs.rmSync(path.dirname(tapFile), { recursive: true, force: true });
|
|
65
68
|
}
|