@lyupro/codex-bridge 0.6.12 → 0.6.14
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 +105 -0
- package/README.md +10 -8
- package/cli/command-registry.mjs +59 -18
- package/cli/doctor-installation.mjs +132 -111
- package/cli/doctor-load-witness.mjs +75 -0
- package/cli/doctor-runs.mjs +41 -6
- package/cli/doctor-sandbox.mjs +64 -0
- package/cli/doctor.mjs +26 -8
- package/cli/guidance.mjs +42 -0
- package/cli/host-inspection.mjs +1 -0
- package/cli/hosts.mjs +4 -3
- package/cli/install-record.mjs +4 -2
- package/cli/manifest.mjs +6 -3
- package/cli/read.mjs +1 -2
- package/cli/removal-outcomes.mjs +5 -2
- package/cli/removal-plan.mjs +4 -1
- package/cli/remove-layout.mjs +4 -4
- package/cli/run-lookup.mjs +14 -2
- package/cli/runs-inventory.mjs +10 -2
- package/cli/runs-move-copy.mjs +123 -0
- package/cli/runs-move-history.mjs +78 -0
- package/cli/runs-move-remove.mjs +92 -0
- package/cli/runs-move.mjs +96 -0
- package/cli/stop.mjs +1 -2
- package/cli/uninstall-purge.mjs +1 -0
- package/cli/uninstall.mjs +2 -0
- package/cli/update.mjs +12 -7
- package/package.json +4 -2
- package/scripts/home-write-inventory.mjs +17 -2
- package/scripts/isolated-roots.mjs +2 -1
- package/src/{agents → claude/agents}/codex-advisor.md +8 -7
- package/src/{agents → claude/agents}/codex-build.md +7 -6
- package/src/{agents → claude/agents}/codex-review.md +6 -5
- package/src/{agents → claude/agents}/codex-scout.md +7 -6
- package/src/{commands → claude/commands}/usage.md +9 -5
- package/src/claude/rules/core.md +29 -0
- package/src/{rules → codex/rules}/codex-bridge.rules +1 -1
- package/src/home/guidance/advisor.md +42 -0
- package/src/home/guidance/budget.md +37 -0
- package/src/home/guidance/concurrency.md +17 -0
- package/src/home/guidance/order.md +66 -0
- package/src/home/guidance/records.md +32 -0
- package/src/home/hooks/live-runs.mjs +2 -1
- package/src/home/hooks/load-witness.mjs +23 -0
- package/src/home/lib/claude-layout.mjs +1 -0
- package/src/home/lib/codex-home.mjs +8 -0
- package/src/home/lib/home-registry.mjs +21 -0
- package/src/home/lib/hook-definitions.mjs +2 -0
- package/src/home/lib/host-version.mjs +33 -14
- package/src/home/lib/load-witness.mjs +113 -0
- package/src/home/lib/meta/reply.mjs +1 -1
- package/src/home/lib/runner/codex-availability.mjs +11 -2
- package/src/home/lib/runner/exit-codes.mjs +1 -1
- package/src/home/lib/runner/launcher.mjs +10 -4
- package/src/home/lib/runner/preflight.mjs +12 -4
- package/src/home/lib/runner/retired-roots.mjs +81 -0
- package/src/home/lib/runner/run-autocommit.mjs +109 -0
- package/src/home/lib/runner/runs-root.mjs +47 -7
- package/src/home/lib/runner/sandbox-diagnosis.mjs +180 -0
- package/src/home/lib/runner/sandbox-probe.mjs +29 -1
- package/src/home/lib/runner/task-input.mjs +30 -0
- package/src/home/lib/runner/worker.mjs +5 -0
- /package/src/{commands → claude/commands}/env.md +0 -0
- /package/src/{commands → claude/commands}/model.md +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copies a run store into a caller-created staging folder and proves every byte arrived.
|
|
3
|
+
*
|
|
4
|
+
* Plan_77 D1: the 2026-10-07 VaultForge journal cost $16.57 instead of $0.3-0.8
|
|
5
|
+
* because records lived in another project's git tree. Every record must survive.
|
|
6
|
+
* A 2026-10-08 Windows probe (Plan_77 B5a) preserved trailing dots with ordinary Node
|
|
7
|
+
* absolute paths; keep names verbatim, including tradeforge.loc (do not cd into codex-runs).
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { normalizeRepoPath } from '../src/home/lib/runner/project-dir.mjs';
|
|
13
|
+
|
|
14
|
+
export function sha256File(file) {
|
|
15
|
+
const digest = createHash('sha256');
|
|
16
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
17
|
+
const descriptor = fs.openSync(file, 'r');
|
|
18
|
+
try {
|
|
19
|
+
for (;;) {
|
|
20
|
+
const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, null);
|
|
21
|
+
if (!bytes) break;
|
|
22
|
+
digest.update(buffer.subarray(0, bytes));
|
|
23
|
+
}
|
|
24
|
+
return digest.digest('hex');
|
|
25
|
+
} finally {
|
|
26
|
+
fs.closeSync(descriptor);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function entryStat(file) {
|
|
31
|
+
const stat = fs.lstatSync(file);
|
|
32
|
+
if (stat.isSymbolicLink() || (!stat.isFile() && !stat.isDirectory())) {
|
|
33
|
+
throw new Error(`Unsupported run store entry: ${file}`);
|
|
34
|
+
}
|
|
35
|
+
return stat;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A read-only inventory also serves the dry run; empty folders count as records. */
|
|
39
|
+
export function inspectRunStore(root) {
|
|
40
|
+
if (!entryStat(root).isDirectory()) throw new Error(`Run store is not a directory: ${root}`);
|
|
41
|
+
const entries = new Map();
|
|
42
|
+
const totals = { files: 0, directories: 0, bytes: 0 };
|
|
43
|
+
function walk(relative) {
|
|
44
|
+
for (const name of fs.readdirSync(path.join(root, relative)).sort()) {
|
|
45
|
+
const child = path.join(relative, name);
|
|
46
|
+
const stat = entryStat(path.join(root, child));
|
|
47
|
+
const directory = stat.isDirectory();
|
|
48
|
+
entries.set(child, { directory, size: stat.size });
|
|
49
|
+
if (directory) {
|
|
50
|
+
totals.directories += 1;
|
|
51
|
+
walk(child);
|
|
52
|
+
} else {
|
|
53
|
+
totals.files += 1;
|
|
54
|
+
totals.bytes += stat.size;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
walk('');
|
|
59
|
+
return { entries, ...totals };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Plan_77 D3: reject unsafe destinations before creating any copy. */
|
|
63
|
+
export function checkRunStoreDestination({ from, to }) {
|
|
64
|
+
const source = normalizeRepoPath(from);
|
|
65
|
+
const destination = normalizeRepoPath(to);
|
|
66
|
+
if (source === destination || source.startsWith(`${destination}/`) || destination.startsWith(`${source}/`)) {
|
|
67
|
+
throw new Error(`Run store paths overlap: ${from} and ${to}.`);
|
|
68
|
+
}
|
|
69
|
+
let stat;
|
|
70
|
+
try {
|
|
71
|
+
stat = fs.lstatSync(to);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code === 'ENOENT') return false;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.readdirSync(to).length) {
|
|
77
|
+
throw new Error(`Run store destination is not an empty directory: ${to}.`);
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function verifyPaths(expected, actual, root) {
|
|
83
|
+
for (const relative of [...new Set([...expected.keys(), ...actual.keys()])].sort()) {
|
|
84
|
+
if (!expected.has(relative) || !actual.has(relative)
|
|
85
|
+
|| expected.get(relative).directory !== actual.get(relative).directory) {
|
|
86
|
+
throw new Error(`Run store verification failed at ${path.join(root, relative)}: paths differ.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function copyRunStore({ from, to, hash = sha256File }) {
|
|
92
|
+
const destinationExists = checkRunStoreDestination({ from, to });
|
|
93
|
+
const original = inspectRunStore(from);
|
|
94
|
+
if (!destinationExists) throw new Error(`Run store staging folder must already exist: ${to}.`);
|
|
95
|
+
try {
|
|
96
|
+
for (const [relative, entry] of original.entries) {
|
|
97
|
+
const src = path.join(from, relative);
|
|
98
|
+
const dst = path.join(to, relative);
|
|
99
|
+
entryStat(src); // Never follow a link introduced between the inventory and the copy.
|
|
100
|
+
if (entry.directory) fs.mkdirSync(dst);
|
|
101
|
+
else fs.copyFileSync(src, dst, fs.constants.COPYFILE_EXCL);
|
|
102
|
+
}
|
|
103
|
+
const source = inspectRunStore(from);
|
|
104
|
+
const destination = inspectRunStore(to);
|
|
105
|
+
verifyPaths(original.entries, source.entries, from);
|
|
106
|
+
verifyPaths(source.entries, destination.entries, to);
|
|
107
|
+
for (const [relative, entry] of source.entries) {
|
|
108
|
+
if (entry.directory) continue;
|
|
109
|
+
if (entry.size !== destination.entries.get(relative).size
|
|
110
|
+
|| hash(path.join(from, relative)) !== hash(path.join(to, relative))) {
|
|
111
|
+
throw new Error(`Run store verification failed at ${relative}: size or hash differs.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Check the path sets again after hashing, before declaring this copy verified.
|
|
115
|
+
verifyPaths(source.entries, inspectRunStore(from).entries, from);
|
|
116
|
+
verifyPaths(source.entries, inspectRunStore(to).entries, to);
|
|
117
|
+
return { files: source.files, directories: source.directories, bytes: source.bytes };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// Plan_77 F1: the caller guarantees this staging folder was exclusively created by this process.
|
|
120
|
+
fs.rmSync(to, { recursive: true, force: true });
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gives the moved run store the git history its records had in the repository they left.
|
|
3
|
+
*
|
|
4
|
+
* Plan_77 D4: split only a clone so the old repository's working tree, index and refs stay intact.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
export function runGit(cwd, args) {
|
|
12
|
+
const { status, stdout, stderr } = spawnSync('git', ['-C', cwd, ...args], {
|
|
13
|
+
encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024,
|
|
14
|
+
});
|
|
15
|
+
return { status, stdout, stderr };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function gitFailure(step, result) {
|
|
19
|
+
const firstLine = (result.stderr || '').trim().split(/\r?\n/)[0];
|
|
20
|
+
return new Error(`Git ${step} failed (exit ${result.status}): ${firstLine}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function gitStep(git, cwd, args, statuses = [0]) {
|
|
24
|
+
let result;
|
|
25
|
+
try {
|
|
26
|
+
result = git(cwd, args);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw gitFailure(args[0], { status: null, stderr: error.message });
|
|
29
|
+
}
|
|
30
|
+
if (!statuses.includes(result.status)) throw gitFailure(args[0], result);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** D4: dry runs use exactly the same read-only eligibility checks, without creating a clone. */
|
|
35
|
+
export function inspectRunHistory({ from, git = runGit }) {
|
|
36
|
+
const repository = gitStep(git, from, ['rev-parse', '--show-toplevel'], [0, 128]);
|
|
37
|
+
if (repository.status !== 0) {
|
|
38
|
+
if (/not a git repository/i.test(repository.stderr || '')) return { imported: false, reason: 'not in git' };
|
|
39
|
+
throw gitFailure('rev-parse', repository);
|
|
40
|
+
}
|
|
41
|
+
const top = repository.stdout.trim();
|
|
42
|
+
const prefix = path.relative(top, from).split(path.sep).join('/');
|
|
43
|
+
if (!prefix) return { imported: false, reason: 'store is a repository of its own' };
|
|
44
|
+
const commits = Number(gitStep(git, top, ['rev-list', '--count', 'HEAD', '--', prefix]).stdout.trim());
|
|
45
|
+
if (commits === 0) return { imported: false, reason: 'no history' };
|
|
46
|
+
return { top, prefix, commits };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function importRunHistory({ from, to, git = runGit, tmpdir = os.tmpdir() }) {
|
|
50
|
+
const history = inspectRunHistory({ from, git });
|
|
51
|
+
if (history.imported === false) return history;
|
|
52
|
+
const gitDir = path.join(to, '.git');
|
|
53
|
+
const hadGit = fs.existsSync(gitDir);
|
|
54
|
+
const clone = fs.mkdtempSync(path.join(tmpdir, 'codex-bridge-runs-history-'));
|
|
55
|
+
let initializing = false;
|
|
56
|
+
try {
|
|
57
|
+
gitStep(git, tmpdir, ['clone', '--quiet', '--no-checkout', history.top, clone]);
|
|
58
|
+
// D4: subtree requires the prefix on disk; materialize only that prefix in the disposable clone.
|
|
59
|
+
gitStep(git, clone, ['checkout', '--quiet', 'HEAD', '--', history.prefix]);
|
|
60
|
+
gitStep(git, clone, ['subtree', 'split', '--quiet', `--prefix=${history.prefix}`, '-b', 'codex-runs-history']);
|
|
61
|
+
initializing = true;
|
|
62
|
+
gitStep(git, to, ['init', '--quiet']);
|
|
63
|
+
gitStep(git, to, ['fetch', '--quiet', clone, 'codex-runs-history']);
|
|
64
|
+
// D4: mixed reset imports the index and history while keeping the verified on-disk records.
|
|
65
|
+
gitStep(git, to, ['reset', '--quiet', 'FETCH_HEAD']);
|
|
66
|
+
gitStep(git, to, ['add', '-A']);
|
|
67
|
+
if (gitStep(git, to, ['diff', '--cached', '--quiet'], [0, 1]).status === 1) {
|
|
68
|
+
gitStep(git, to, ['commit', '--quiet', '-m', 'Import run records moved by codex-bridge (Plan_77)']);
|
|
69
|
+
}
|
|
70
|
+
return { imported: true, commits: Number(gitStep(git, to, ['rev-list', '--count', 'HEAD']).stdout.trim()) };
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// D4: roll back only metadata created by this import, never the source repository or records.
|
|
73
|
+
if (initializing && !hadGit) fs.rmSync(gitDir, { recursive: true, force: true });
|
|
74
|
+
throw error;
|
|
75
|
+
} finally {
|
|
76
|
+
fs.rmSync(clone, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes the old run store once the operator agrees and every file in it has an identical copy in the new one.
|
|
3
|
+
*
|
|
4
|
+
* Plan_77 D3/B5c: an older package can still write to the old root after the D7 switch.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { inspectRunStore, sha256File } from './runs-move-copy.mjs';
|
|
9
|
+
import { askYesNo, isInteractive } from './terminal-question.mjs';
|
|
10
|
+
|
|
11
|
+
/** Returns the first 20 differing relative paths and the total count, including empty folders. */
|
|
12
|
+
export function oldStoreDifferences({ from, to, hash = sha256File }) {
|
|
13
|
+
const source = path.resolve(from);
|
|
14
|
+
const destination = path.resolve(to);
|
|
15
|
+
const relative = path.relative(source, destination);
|
|
16
|
+
const reverse = path.relative(destination, source);
|
|
17
|
+
const inside = (value) => value === '' || (!path.isAbsolute(value)
|
|
18
|
+
&& value !== '..' && !value.startsWith(`..${path.sep}`));
|
|
19
|
+
if (inside(relative) || inside(reverse)) throw new Error(`Run store paths overlap: ${from} and ${to}.`);
|
|
20
|
+
const oldEntries = inspectRunStore(from).entries;
|
|
21
|
+
const newEntries = inspectRunStore(to).entries;
|
|
22
|
+
const paths = [];
|
|
23
|
+
let count = 0;
|
|
24
|
+
for (const [entryPath, oldEntry] of oldEntries) {
|
|
25
|
+
const newEntry = newEntries.get(entryPath);
|
|
26
|
+
if (!newEntry || oldEntry.directory !== newEntry.directory
|
|
27
|
+
|| (!oldEntry.directory && (oldEntry.size !== newEntry.size
|
|
28
|
+
|| hash(path.join(from, entryPath)) !== hash(path.join(to, entryPath))))) {
|
|
29
|
+
count += 1;
|
|
30
|
+
if (paths.length < 20) paths.push(entryPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { paths, count };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function differencesResult(from, { paths, count }) {
|
|
37
|
+
return { exitCode: 1,
|
|
38
|
+
output: `The old folder ${from} has ${count} entries the new store does not hold identically:\n${paths.join('\n')}\nNothing was removed.` };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function removeOldStore({ from, to, ask = askYesNo, interactive = isInteractive,
|
|
42
|
+
questionOptions = {}, hash }) {
|
|
43
|
+
let quarantine;
|
|
44
|
+
const restore = () => {
|
|
45
|
+
if (!quarantine) return '';
|
|
46
|
+
if (!fs.existsSync(from)) {
|
|
47
|
+
try {
|
|
48
|
+
fs.renameSync(quarantine, from);
|
|
49
|
+
quarantine = undefined;
|
|
50
|
+
return '';
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return `\nCould not restore ${quarantine} to ${from}: ${error.message}. Records remain in ${quarantine}.`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return `\nThe old records remain in ${quarantine}; ${from} also exists. Both folders were kept.`;
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
const differences = oldStoreDifferences({ from, to, hash });
|
|
59
|
+
if (differences.count) return differencesResult(from, differences);
|
|
60
|
+
if (!interactive(questionOptions)) {
|
|
61
|
+
return { exitCode: 0,
|
|
62
|
+
output: `The old folder ${from} is still there; run codex-bridge runs move in a terminal to remove it.` };
|
|
63
|
+
}
|
|
64
|
+
const answer = await ask(`Remove the old folder ${from}? Every file in it has an identical copy in ${to}. `
|
|
65
|
+
+ 'It stays in the history of the git repository that holds it until that project commits the removal.', questionOptions);
|
|
66
|
+
if (answer === 'cancel') return { exitCode: 130, output: 'Cancelled; nothing was removed.' };
|
|
67
|
+
if (answer !== 'yes') return { exitCode: 0, output: 'The old folder was kept.' };
|
|
68
|
+
// Plan_77 F1: isolate the old tree before comparing; never delete a newly recreated from.
|
|
69
|
+
const isolated = `${from}.removing-${process.pid}-${Date.now()}`;
|
|
70
|
+
try {
|
|
71
|
+
fs.renameSync(from, isolated);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (['EPERM', 'EBUSY', 'EACCES'].includes(error.code)) {
|
|
74
|
+
return { exitCode: 1,
|
|
75
|
+
output: `A process still uses a file in ${from}; nothing was removed. Close it and repeat.` };
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
quarantine = isolated;
|
|
80
|
+
const confirmed = oldStoreDifferences({ from: quarantine, to, hash });
|
|
81
|
+
if (confirmed.count) {
|
|
82
|
+
const result = differencesResult(from, confirmed);
|
|
83
|
+
result.output += restore();
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
fs.rmSync(quarantine, { recursive: true });
|
|
87
|
+
quarantine = undefined;
|
|
88
|
+
return { exitCode: 0, output: `Removed ${from}.` };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return { exitCode: 1, output: `${error.message}\nNothing was removed.${restore()}` };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moves the run records from the retired default root into the package home, on the operator's signal.
|
|
3
|
+
*
|
|
4
|
+
* Plan_77 D3/D7: projects are closed by the operator; refuse live runs and prove
|
|
5
|
+
* the copy and import D4 history before writing the D7 move record that switches roots.
|
|
6
|
+
* The 2026-10-07 VaultForge $16.57 journal incident is why records leave foreign git.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { runsRootResolution } from '../src/home/lib/runner/runs-root.mjs';
|
|
11
|
+
import { resolveBrandHome } from '../src/home/lib/brand-home.mjs';
|
|
12
|
+
import { writeRunsMoveRecord } from '../src/home/lib/runner/retired-roots.mjs';
|
|
13
|
+
import { allLiveRuns } from '../src/home/hooks/live-runs.mjs';
|
|
14
|
+
import { checkRunStoreDestination, copyRunStore, inspectRunStore } from './runs-move-copy.mjs';
|
|
15
|
+
import { importRunHistory, inspectRunHistory, runGit } from './runs-move-history.mjs';
|
|
16
|
+
import { oldStoreDifferences } from './runs-move-remove.mjs';
|
|
17
|
+
|
|
18
|
+
export function runsMove({ dryRun = false, resolution = runsRootResolution(), liveRuns = allLiveRuns,
|
|
19
|
+
stateDir, importHistory = importRunHistory, git = runGit } = {}) {
|
|
20
|
+
const { source, root, legacyRoot, homeRoot } = resolution;
|
|
21
|
+
const refuse = (output) => ({ exitCode: 1, output, oldStore: null, homeRoot });
|
|
22
|
+
if (source === 'CODEX_RUNS_ROOT') {
|
|
23
|
+
return refuse(`CODEX_RUNS_ROOT is set to ${root}; unset it to move the default run store.`);
|
|
24
|
+
}
|
|
25
|
+
if (source === 'moved') {
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(legacyRoot) && fs.lstatSync(legacyRoot).isDirectory()) {
|
|
28
|
+
return { exitCode: 0, oldStore: dryRun ? null : legacyRoot, homeRoot,
|
|
29
|
+
output: `Run records already live in ${homeRoot}; the old folder ${legacyRoot} still exists.` };
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return refuse(error.message);
|
|
33
|
+
}
|
|
34
|
+
return refuse(`Run records already live in ${homeRoot}.`);
|
|
35
|
+
}
|
|
36
|
+
if (source === 'default') return refuse(`No run store to move: ${legacyRoot} does not exist.`);
|
|
37
|
+
let liveRefusal = null;
|
|
38
|
+
try {
|
|
39
|
+
const live = liveRuns(legacyRoot);
|
|
40
|
+
if (live.length) {
|
|
41
|
+
liveRefusal = `Runs are still live in ${legacyRoot}:\n${live.map(({ dir }) => dir).join('\n')}. `
|
|
42
|
+
+ 'Wait for them or stop them with codex-bridge stop, then repeat.';
|
|
43
|
+
// Like purge --dry-run: a preview names the refusal and still shows the move, since some
|
|
44
|
+
// project is nearly always running and the operator plans the move from these numbers.
|
|
45
|
+
if (!dryRun) return refuse(liveRefusal);
|
|
46
|
+
}
|
|
47
|
+
checkRunStoreDestination({ from: legacyRoot, to: homeRoot });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return refuse(liveRefusal ? `${liveRefusal}\n${error.message}` : error.message);
|
|
50
|
+
}
|
|
51
|
+
const staging = `${homeRoot}.moving-${process.pid}-${Date.now()}`;
|
|
52
|
+
let ownsStaging = false;
|
|
53
|
+
let published = false;
|
|
54
|
+
try {
|
|
55
|
+
if (!dryRun) {
|
|
56
|
+
fs.lstatSync(legacyRoot);
|
|
57
|
+
fs.mkdirSync(path.dirname(homeRoot), { recursive: true });
|
|
58
|
+
fs.mkdirSync(staging);
|
|
59
|
+
ownsStaging = true;
|
|
60
|
+
}
|
|
61
|
+
const counts = dryRun ? inspectRunStore(legacyRoot) : copyRunStore({ from: legacyRoot, to: staging });
|
|
62
|
+
const megabytes = (counts.bytes / (1024 * 1024)).toFixed(2);
|
|
63
|
+
if (dryRun) {
|
|
64
|
+
const history = inspectRunHistory({ from: legacyRoot, git });
|
|
65
|
+
const description = history.imported === false ? history.reason : `would import ${history.commits} commits`;
|
|
66
|
+
const preview = `Would copy ${counts.files} files (${megabytes} MB) in ${counts.directories} folders from ${legacyRoot} to ${homeRoot}; history: ${description}. Dry run: nothing changed.`;
|
|
67
|
+
return { exitCode: liveRefusal ? 1 : 0, oldStore: null, homeRoot,
|
|
68
|
+
output: liveRefusal ? `${liveRefusal}\n${preview}` : preview };
|
|
69
|
+
}
|
|
70
|
+
const history = importHistory({ from: legacyRoot, to: staging, git });
|
|
71
|
+
// Plan_77 F1: a writer may start or change records during copy/history import.
|
|
72
|
+
const live = liveRuns(legacyRoot);
|
|
73
|
+
const differences = oldStoreDifferences({ from: legacyRoot, to: staging });
|
|
74
|
+
if (live.length || differences.count) {
|
|
75
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
76
|
+
ownsStaging = false;
|
|
77
|
+
return refuse(`A run started or a record changed during the move: ${[
|
|
78
|
+
...live.map(({ dir }) => dir), ...differences.paths,
|
|
79
|
+
].slice(0, 20).join(', ')}. Nothing was switched; repeat when no project is running.`);
|
|
80
|
+
}
|
|
81
|
+
// Plan_77 F1: publish only our exclusive staging folder; never roll back a competing store.
|
|
82
|
+
if (checkRunStoreDestination({ from: legacyRoot, to: homeRoot })) fs.rmdirSync(homeRoot);
|
|
83
|
+
fs.renameSync(staging, homeRoot);
|
|
84
|
+
ownsStaging = false;
|
|
85
|
+
published = true;
|
|
86
|
+
const description = history.imported ? `imported ${history.commits} commits` : history.reason;
|
|
87
|
+
// D7: publishing the record is the last write, after copy verification and D4 history import.
|
|
88
|
+
writeRunsMoveRecord(stateDir ?? resolution.stateDir ?? resolveBrandHome().stateDir, { root: legacyRoot });
|
|
89
|
+
return { exitCode: 0, oldStore: legacyRoot, homeRoot,
|
|
90
|
+
output: `Moved ${counts.files} files (${megabytes} MB) from ${legacyRoot} to ${homeRoot}; history: ${description}. New runs write to ${homeRoot}. The old folder is untouched.` };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (ownsStaging) fs.rmSync(staging, { recursive: true, force: true });
|
|
93
|
+
if (published) fs.rmSync(homeRoot, { recursive: true, force: true });
|
|
94
|
+
return refuse(`${error.message}\nNothing was switched; the old folder is untouched.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
package/cli/stop.mjs
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
processAlive,
|
|
11
11
|
} from '../src/home/lib/process-identity.mjs';
|
|
12
12
|
import { markAbandoned, readJson } from '../src/home/lib/write-meta.mjs';
|
|
13
|
-
import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
|
|
14
13
|
import { resolveRunFolder } from './run-lookup.mjs';
|
|
15
14
|
|
|
16
15
|
const EXIT_POLL_MS = 25;
|
|
@@ -52,7 +51,7 @@ function result(exitCode, output) {
|
|
|
52
51
|
export async function stop({
|
|
53
52
|
run,
|
|
54
53
|
cwd = process.cwd(),
|
|
55
|
-
runsRootPath
|
|
54
|
+
runsRootPath,
|
|
56
55
|
commandRunner,
|
|
57
56
|
} = {}) {
|
|
58
57
|
const lookup = resolveRunFolder({ command: 'stop', run, cwd, runsRootPath });
|
package/cli/uninstall-purge.mjs
CHANGED
|
@@ -82,6 +82,7 @@ async function purgeInRun(options, ticket) {
|
|
|
82
82
|
detached: true, imageMembers: plan.imageMembers, authorization,
|
|
83
83
|
});
|
|
84
84
|
await removeEmpty(host.commandsDir);
|
|
85
|
+
await removeEmpty(host.rulesDir);
|
|
85
86
|
if (record) await removeOutside(writer, legacyInstallRecordPath(host));
|
|
86
87
|
await removeEmpty(host.agentsDir);
|
|
87
88
|
await removeEmptyLayout(host.legacyAgentsDir);
|
package/cli/uninstall.mjs
CHANGED
|
@@ -109,6 +109,7 @@ no host is recorded as using it.`,
|
|
|
109
109
|
const plan = await buildHomeRemovalPlan({ command: 'uninstall', host, packageRoot: options.packageRoot,
|
|
110
110
|
imagePolicy });
|
|
111
111
|
await removeEmpty(host.commandsDir);
|
|
112
|
+
await removeEmpty(host.rulesDir);
|
|
112
113
|
await removeEmpty(host.agentsDir);
|
|
113
114
|
await removeEmptyLayout(host.legacyAgentsDir);
|
|
114
115
|
await removeEmptyLayout(host.legacyCommandsDir);
|
|
@@ -221,6 +222,7 @@ using ${host.brandRoot}.`);
|
|
|
221
222
|
const { outcomes } = await executeHomePlan(host, plan,
|
|
222
223
|
{ detached: hostSide.detached, imageMembers: plan.imageMembers });
|
|
223
224
|
await removeEmpty(host.commandsDir);
|
|
225
|
+
await removeEmpty(host.rulesDir);
|
|
224
226
|
if (detachedRecord) await removeOutside(writer, legacyInstallRecordPath(host));
|
|
225
227
|
await removeEmpty(host.agentsDir);
|
|
226
228
|
await removeEmptyLayout(host.legacyAgentsDir);
|
package/cli/update.mjs
CHANGED
|
@@ -73,8 +73,11 @@ function displayFile(file) {
|
|
|
73
73
|
return `${entry.root}/${entry.path}`;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function classifyPlanned(state) {
|
|
77
|
-
|
|
76
|
+
function classifyPlanned(state, { adoptUnrecorded = false } = {}) {
|
|
77
|
+
// A planned path the record does not name but someone already filled is not ours to overwrite:
|
|
78
|
+
// install refuses it without --force, and update used to call it 'new' and copy over it — for
|
|
79
|
+
// the host rules file (Plan_64 B2c) that would silently replace the operator's own text.
|
|
80
|
+
if (!state.recorded) return state.exists && !state.matchesPackage && !adoptUnrecorded ? 'unowned' : 'new';
|
|
78
81
|
if (!state.exists) return 'missing';
|
|
79
82
|
if (state.matchesPackage) return 'up-to-date';
|
|
80
83
|
if (state.recordedFingerprint === state.fingerprint) return 'outdated';
|
|
@@ -93,7 +96,7 @@ function appliedOutput(states) {
|
|
|
93
96
|
[count('outdated'), 'updated'],
|
|
94
97
|
[count('new'), 'added'],
|
|
95
98
|
[count('missing'), 'restored'],
|
|
96
|
-
[states.filter((state) => state.status
|
|
99
|
+
[states.filter((state) => ['modified', 'unowned'].includes(state.status) && state.item).length, 'overwritten'],
|
|
97
100
|
[states.filter((state) => state.status === 'orphaned' && state.exists).length, 'removed'],
|
|
98
101
|
].filter(([total]) => total > 0).map(([total, label]) => `${total} ${label}`);
|
|
99
102
|
return parts.length ? `Updated codex-bridge: ${parts.join(', ')}.` : 'Updated codex-bridge.';
|
|
@@ -103,7 +106,7 @@ function conflictOutput(conflicts, legacy, dryRun) {
|
|
|
103
106
|
const heading = dryRun ? 'Update would stop for these paths:' : 'Update stopped for these paths:';
|
|
104
107
|
const lines = conflicts.map((state) => ` ${state.relative} (${state.status})`);
|
|
105
108
|
if (legacy) lines.push('Installation record has no fingerprints; differing files are treated as modified.');
|
|
106
|
-
lines.push('Run update again with --force to overwrite modified files and restore missing files.');
|
|
109
|
+
lines.push('Run update again with --force to overwrite modified or unowned files and restore missing files.');
|
|
107
110
|
return [heading, ...lines].join('\n');
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -112,7 +115,7 @@ function dryRunOutput(states, hookStates, legacy, oldHooks) {
|
|
|
112
115
|
for (const state of states) {
|
|
113
116
|
if (state.status === 'outdated') lines.push(`Would update ${state.relative}.`);
|
|
114
117
|
if (state.status === 'new') lines.push(`Would create ${state.relative}.`);
|
|
115
|
-
if (state.status
|
|
118
|
+
if (['modified', 'unowned'].includes(state.status) && state.item) lines.push(`Would overwrite ${state.relative}.`);
|
|
116
119
|
if (state.status === 'missing') lines.push(`Would restore ${state.relative}.`);
|
|
117
120
|
if (state.status === 'orphaned' && state.exists) lines.push(`Would remove ${state.relative}.`);
|
|
118
121
|
if (state.status === 'modified' && !state.item) lines.push(`Would preserve modified orphan ${state.relative}.`);
|
|
@@ -228,7 +231,9 @@ async function updateInRun({
|
|
|
228
231
|
matchesPackage: ruleExists && await targetMatches(rule, host.brandRoot),
|
|
229
232
|
fingerprint: ruleExists ? await fileFingerprint(rule.target) : null,
|
|
230
233
|
};
|
|
231
|
-
|
|
234
|
+
// Records written before the Codex rules file was recorded name no rules, yet the file under the
|
|
235
|
+
// package's own name was written by this package: adopting it is the migration, not a collision.
|
|
236
|
+
ruleState.status = classifyPlanned(ruleState, { adoptUnrecorded: true });
|
|
232
237
|
const orphanStates = await Promise.all(record.files
|
|
233
238
|
.filter((file) => !planned.has(recordFileKey(file)))
|
|
234
239
|
.map(async (file) => {
|
|
@@ -245,7 +250,7 @@ async function updateInRun({
|
|
|
245
250
|
return { ...state, status: classifyOrphan(state) };
|
|
246
251
|
}));
|
|
247
252
|
const states = [...plannedStates, ruleState, ...orphanStates];
|
|
248
|
-
const conflicts = states.filter((state) =>
|
|
253
|
+
const conflicts = states.filter((state) => ['modified', 'unowned', 'missing'].includes(state.status));
|
|
249
254
|
const legacy = !record.fingerprints;
|
|
250
255
|
if (conflicts.length && !force) {
|
|
251
256
|
return { exitCode: 1, output: conflictOutput(conflicts, legacy, dryRun) };
|
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
|
|
3
|
+
"version": "0.6.14",
|
|
4
|
+
"description": "Claude Code-to-Codex delegation with 8 guards, 3654 tests, zero runtime dependencies, and zero build steps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"codex",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"cli",
|
|
35
35
|
"src",
|
|
36
36
|
"scripts",
|
|
37
|
+
"!scripts/release-check",
|
|
37
38
|
"README.md",
|
|
38
39
|
"LICENSE",
|
|
39
40
|
"CHANGELOG.md"
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"scripts": {
|
|
42
43
|
"dev:install": "node bin/codex-bridge.mjs update --force",
|
|
43
44
|
"test": "node scripts/run-tests.mjs",
|
|
45
|
+
"release-check:rules-load-claude": "node scripts/release-check/rules-load-claude.mjs",
|
|
44
46
|
"check:size": "node scripts/check-file-size.mjs",
|
|
45
47
|
"check:open-work": "node scripts/open-work/check.mjs --strict"
|
|
46
48
|
},
|
|
@@ -16,6 +16,18 @@ export const HOME_WRITE_INVENTORY = Object.freeze([
|
|
|
16
16
|
outside('cli/probe-rig.mjs', 'mkdir', 2, 'Creates hook and agent directories inside the throwaway probe rig.'),
|
|
17
17
|
outside('cli/probe-rig.mjs', 'writeFile', 3, 'Writes hook, agent, and settings fixtures inside the throwaway probe rig.'),
|
|
18
18
|
outside('cli/prune.mjs', 'rmSync', 1, 'Deletes the explicitly selected run or archived transport target.'),
|
|
19
|
+
outside('cli/runs-move-copy.mjs', 'mkdirSync', 1, 'Recreates run folders inside the staging copy this command created (Plan_77 B5a/F1); run records are operator data outside the home writer.'),
|
|
20
|
+
outside('cli/runs-move-copy.mjs', 'copyFileSync', 1, 'Copies the run store into the runs folder and removes only the copy it created when verification fails (Plan_77 B5a); run records are operator data outside the home writer.'),
|
|
21
|
+
outside('cli/runs-move-copy.mjs', 'rmSync', 1, 'Copies the run store into the runs folder and removes only the copy it created when verification fails (Plan_77 B5a); run records are operator data outside the home writer.'),
|
|
22
|
+
outside('cli/runs-move-copy.mjs', 'openSync', 1, 'Copies the run store into the runs folder and removes only the copy it created when verification fails (Plan_77 B5a); run records are operator data outside the home writer.'),
|
|
23
|
+
outside('cli/runs-move-history.mjs', 'mkdtempSync', 1, 'Creates a temporary history clone in the system temp folder, keeping the source repository untouched (Plan_77 D4/B5b).'),
|
|
24
|
+
outside('cli/runs-move-history.mjs', 'rmSync', 2, 'Removes the temporary history clone in the system temp folder and metadata this import created on failure (Plan_77 D4/B5b).'),
|
|
25
|
+
outside('cli/runs-move.mjs', 'mkdirSync', 2, 'Creates the home parent and the exclusive staging folder of this process for the run store copy (Plan_77 F1).'),
|
|
26
|
+
outside('cli/runs-move.mjs', 'rmSync', 3, 'Removes only the staging folder this command created, or the store it just renamed into place when the move record fails (Plan_77 F1).'),
|
|
27
|
+
outside('cli/runs-move.mjs', 'rmdirSync', 1, 'Removes an empty pre-existing runs folder non-recursively before publishing the staging copy (Plan_77 F1).'),
|
|
28
|
+
outside('cli/runs-move.mjs', 'renameSync', 1, 'Publishes the verified staging copy as the runs folder (Plan_77 F1).'),
|
|
29
|
+
outside('cli/runs-move-remove.mjs', 'rmSync', 1, 'Removes the isolated old run store after the operator agrees and every file in it is proven identical in the new store (Plan_77 D3/B5c/F1).'),
|
|
30
|
+
outside('cli/runs-move-remove.mjs', 'renameSync', 2, 'Isolates the old run store before the last comparison and restores it when a difference appears (Plan_77 F1).'),
|
|
19
31
|
outside('cli/remove-layout.mjs', 'rmdir', 1, 'Removes empty host package directories (agents, commands, their legacy layout); home folders go through the adapter (Plan_65 B15b2).'),
|
|
20
32
|
outside('cli/rules-owners.mjs', 'rename', 1, 'Publishes the shared host rules ownership registry.'),
|
|
21
33
|
outside('cli/rules-owners.mjs', 'rm', 2, 'Removes a failed temporary copy or the host rules ownership registry.'),
|
|
@@ -32,10 +44,12 @@ export const HOME_WRITE_INVENTORY = Object.freeze([
|
|
|
32
44
|
outside('src/home/lib/file-lock.mjs', 'rm', 1, 'Raw route of the lock, taken only after writer.assertOutside proves the lock is beyond the home (Codex rules registry).'),
|
|
33
45
|
outside('src/home/lib/heartbeat.mjs', 'writeFileSync', 1, 'Heartbeat stamp inside a run folder under the runs root.'),
|
|
34
46
|
outside('src/home/lib/host-version.mjs', 'open', 1, 'Read-only open of a run transcript tail with the r flag.'),
|
|
47
|
+
outside('src/home/lib/load-witness.mjs', 'openSync', 1, 'Read-only open with the r flag, reading at most the shared core byte ceiling plus one sentinel byte (Plan_64 D15 / review B8); all witness state writes use the registered load-witness artifact.'),
|
|
35
48
|
outside('src/home/lib/meta/run-state.mjs', 'writeFileSync', 3, 'Writes status.json and meta.json inside run folders under the runs root.'),
|
|
36
49
|
outside('src/home/lib/run-codex.mjs', 'appendFileSync', 1, 'Appends a crash diagnostic to stderr.log inside a run folder.'),
|
|
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
|
|
38
|
-
outside('src/home/lib/retention.mjs', 'unlinkSync', 1, 'Removes transport files inside run folders under the runs root, outside
|
|
50
|
+
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, which is operator data outside the home writer even when it is runs/ of the home (Plan_77).'),
|
|
51
|
+
outside('src/home/lib/retention.mjs', 'unlinkSync', 1, 'Removes transport files inside run folders under the runs root, which is operator data outside the home writer even when it is runs/ of the home (Plan_77).'),
|
|
52
|
+
outside('src/home/lib/runner/sandbox-diagnosis.mjs', 'openSync', 1, 'Read-only open with the r flag, reading a bounded tail of the Codex sandbox log under CODEX_HOME (Plan_78 B1).'),
|
|
39
53
|
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
54
|
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.'),
|
|
41
55
|
outside('src/home/lib/runner/codex-cmd.mjs', 'createWriteStream', 2, 'Appends stderr.log and events.jsonl inside a run folder.'),
|
|
@@ -53,6 +67,7 @@ export const HOME_WRITE_INVENTORY = Object.freeze([
|
|
|
53
67
|
outside('src/home/lib/runner/project-dir.mjs', 'mkdirSync', 2, 'Creates the runs root and a project folder under the runs root.'),
|
|
54
68
|
outside('src/home/lib/runner/project-dir.mjs', 'writeFileSync', 1, 'Writes the project ownership marker under the runs root.'),
|
|
55
69
|
outside('src/home/lib/runner/run-context.mjs', 'writeFileSync', 1, 'Writes a worker reply inside a run folder.'),
|
|
70
|
+
outside('src/home/lib/runner/run-autocommit.mjs', 'appendFileSync', 1, 'Appends an autocommit failure line to stderr.log inside the run folder (Plan_77 B6).'),
|
|
56
71
|
outside('src/home/lib/runner/worker-order.mjs', 'writeFileSync', 1, 'Writes worker.json inside a run folder.'),
|
|
57
72
|
outside('src/home/lib/runner/worker.mjs', 'writeFileSync', 1, 'Writes report.md inside a run folder.'),
|
|
58
73
|
outside('src/home/lib/write-meta.mjs', 'writeFileSync', 1, 'Writes meta.json inside a run folder under the runs root.'),
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* stood in the way of the seeded-file migration the release was about.
|
|
9
9
|
*
|
|
10
10
|
* `CODEX_RUNS_ROOT` joined before a failure rather than after one (Plan_65 B12): the runs root
|
|
11
|
-
* falls back to the
|
|
11
|
+
* falls back to the resolver's default (the package home's `runs/`, or the old
|
|
12
|
+
* `~/.claude/codex-runs` while a move is pending), so a runner test that did not name it
|
|
12
13
|
* filed its runs among real ones, and the purge liveness checks would read real live runs as the
|
|
13
14
|
* fixture's — a verdict that depends on what the operator happens to be running.
|
|
14
15
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: codex-advisor
|
|
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`). Полный результат — в
|
|
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`). Полный результат — в корень прогонов (`codex-bridge doctor` → `runsRoot`; по умолчанию `~/.lyupro/.codex-bridge/runs/`), в чате — не более пяти строк и путь. {{CODEX_REQUIRED_INPUTS_SUMMARY}} {{CODEX_STOP_SUMMARY}}
|
|
4
4
|
model: haiku
|
|
5
5
|
tools: Bash
|
|
6
6
|
---
|
|
@@ -42,7 +42,7 @@ successful scope run's `missing_paths`.
|
|
|
42
42
|
|
|
43
43
|
Return the attaching output exactly as printed by the runner, including its ready-made short
|
|
44
44
|
result and report path. Do not compose, summarize, or improve the result yourself. The full report
|
|
45
|
-
lives under `~/.
|
|
45
|
+
lives under the runs root (`codex-bridge doctor` → `runsRoot`; by default `~/.lyupro/.codex-bridge/runs/`); the orchestrator receives no more than five lines plus that
|
|
46
46
|
path.
|
|
47
47
|
|
|
48
48
|
## The script determines status, not you
|
|
@@ -52,7 +52,7 @@ path.
|
|
|
52
52
|
- `FAIL` — the result is empty, invalid, the return code is nonzero, or the run was abandoned.
|
|
53
53
|
- `LIMIT` — the result is empty and the log signals a ChatGPT quota limit. The quota is spent and
|
|
54
54
|
the task was not completed; do not restart it.
|
|
55
|
-
- `UNAVAILABLE` — Codex is
|
|
55
|
+
- `UNAVAILABLE` — Codex is signed out (checked before start, or recognised from the CLI's own
|
|
56
56
|
sign-in refusal after start). The task was not attempted or could not run; this is not a task failure and
|
|
57
57
|
not a reason to restart — return the block as is, the orchestrator hands the task to the next executor.
|
|
58
58
|
|
|
@@ -78,10 +78,11 @@ the runner refuses a continued scope, because it would spend the continuation th
|
|
|
78
78
|
|
|
79
79
|
## Codex is unavailable
|
|
80
80
|
|
|
81
|
-
The runner looks `codex` up on PATH and
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
`
|
|
81
|
+
The runner looks `codex` up on PATH and retries once after ~1 s. If the lookup still fails, it prints
|
|
82
|
+
an ordinary refusal starting `Codex CLI readiness unconfirmed:` (exit 1, no run folder).
|
|
83
|
+
When found, it asks `codex login status` before starting. The measured "Not logged in" answer prints
|
|
84
|
+
a ready-made `UNAVAILABLE` block (first line `UNAVAILABLE — `, exit 5, no run folder). Any other unclear
|
|
85
|
+
probe prints an ordinary refusal starting `Codex CLI unavailable:` (exit 1). Return that output verbatim.
|
|
85
86
|
Performing the task manually instead of Codex is prohibited for any reason it fails.
|
|
86
87
|
|
|
87
88
|
## What a violation looks like
|