@lyupro/codex-bridge 0.6.5 → 0.6.6
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 +20 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/home/lib/meta/advice-status.mjs +3 -7
- package/src/home/lib/runner/advise-carry.mjs +50 -0
- package/src/home/lib/runner/attach.mjs +11 -7
- package/src/home/lib/runner/continuation.mjs +3 -1
- package/src/home/lib/runner/launcher.mjs +14 -13
- package/src/home/lib/runner/prompts.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.6.6] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- The advise phase is handed the risks it is judged on. `--continue` only grants permission to
|
|
12
|
+
continue: Codex starts a fresh session, and the advise prompt never listed the `predicted_risks` or
|
|
13
|
+
`missing_paths` of the scope run — it even forbade reading those paths — while the judge read them
|
|
14
|
+
from the scope run folder on its own. Every advise run therefore failed with
|
|
15
|
+
`risk_outcomes … count 0`. The launcher now takes one snapshot of the scope result into the advise
|
|
16
|
+
run's `advisor-task.json` (field `scope`) before the run folder exists; the prompt's
|
|
17
|
+
"Scope phase results" section is rendered from that snapshot and the judge reads only it. A scope
|
|
18
|
+
result that cannot be carried is refused before the run starts, spending no quota.
|
|
19
|
+
- Repeating a `--continue` call returns the verdict of the run it started. The launcher starts a run
|
|
20
|
+
and returns, and only a repeated identical call attaches and returns the verdict — but a
|
|
21
|
+
continuation refused to attach, and the repeat was then rejected as reusing a spent grant. No
|
|
22
|
+
continuation of any agent could bring its verdict back to chat: a dispatcher reported `FAIL` while
|
|
23
|
+
its run was still working. A `--continue` call now first attaches to the run of the same order id
|
|
24
|
+
whose `continued_from` names the grant; with no such run a new pass starts as before. The grant stays
|
|
25
|
+
single-use.
|
|
26
|
+
|
|
7
27
|
## [0.6.5] - 2026-09-23
|
|
8
28
|
|
|
9
29
|
### Added
|
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ If a global install and a clone coexist, you have two package copies. `codex-bri
|
|
|
62
62
|
- **Every run leaves an audit folder.** The verbatim task, scope, before/after git state, events, report, verdict, and reason remain together.
|
|
63
63
|
- **Zero runtime dependencies and zero build steps.** The package is plain `.mjs` on the Node.js standard library.
|
|
64
64
|
|
|
65
|
-
The current suite contains **
|
|
65
|
+
The current suite contains **1501 automated tests: 1500 passing and 1 skipped**.
|
|
66
66
|
|
|
67
67
|
## Verify
|
|
68
68
|
|
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 6 guards,
|
|
3
|
+
"version": "0.6.6",
|
|
4
|
+
"description": "Claude Code-to-Codex delegation with 6 guards, 1501 tests, zero runtime dependencies, and zero build steps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"codex",
|
|
@@ -32,14 +32,10 @@ export function adviceGap(runDir, result, eventData) {
|
|
|
32
32
|
|
|
33
33
|
let scopeResult;
|
|
34
34
|
if (phase === 'advise') {
|
|
35
|
-
if (
|
|
36
|
-
reasons.push('
|
|
35
|
+
if (!task?.scope || !Array.isArray(task.scope.predicted_risks)) {
|
|
36
|
+
reasons.push('advisor-task.json#scope is missing the phase-1 predicted_risks');
|
|
37
37
|
} else {
|
|
38
|
-
|
|
39
|
-
scopeResult = readJson(scopeFile);
|
|
40
|
-
if (!scopeResult || typeof scopeResult !== 'object' || !Array.isArray(scopeResult.predicted_risks)) {
|
|
41
|
-
reasons.push('phase-1 result.json or its predicted_risks is missing');
|
|
42
|
-
}
|
|
38
|
+
scopeResult = { predicted_risks: task.scope.predicted_risks, missing_paths: task.scope.missing_paths };
|
|
43
39
|
}
|
|
44
40
|
}
|
|
45
41
|
if (reasons.length) return contractReason(reasons);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Owns the scope snapshot shared by advise readers because run 2026-09-23_141341_plan59-a1-stdin-hang left them with different inputs. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readJsonFileSync } from '../json-file.mjs';
|
|
4
|
+
import { parseAdvisorTask } from '../meta/advisor-task.mjs';
|
|
5
|
+
import { die } from './args.mjs';
|
|
6
|
+
|
|
7
|
+
export function advisorTaskArtifact({ taskText, phase, runsRoot, grantRun }) {
|
|
8
|
+
const task = parseAdvisorTask(taskText);
|
|
9
|
+
if (phase !== 'advise') return task;
|
|
10
|
+
if (typeof runsRoot !== 'string' || !runsRoot || typeof grantRun !== 'string' || !grantRun) {
|
|
11
|
+
throw new Error('Cannot carry advise scope result: runsRoot and grantRun are required.');
|
|
12
|
+
}
|
|
13
|
+
const scopeFile = path.join(runsRoot, grantRun, 'result.json');
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = readJsonFileSync(scopeFile);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error(`Cannot read advise scope result at ${scopeFile}: ${error.message}`);
|
|
19
|
+
}
|
|
20
|
+
if (!result || typeof result !== 'object' || Array.isArray(result) ||
|
|
21
|
+
!Array.isArray(result.predicted_risks) || !Array.isArray(result.missing_paths) ||
|
|
22
|
+
result.predicted_risks.some((risk) => !risk || typeof risk.id !== 'string' || typeof risk.risk !== 'string') ||
|
|
23
|
+
result.missing_paths.some((entry) => typeof entry !== 'string')) {
|
|
24
|
+
throw new Error(`Malformed advise scope result at ${scopeFile}: expected predicted_risks and missing_paths arrays.`);
|
|
25
|
+
}
|
|
26
|
+
return { ...task, scope: { run: grantRun, predicted_risks: result.predicted_risks, missing_paths: result.missing_paths } };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Called before makeRunDir: a scope result that cannot be carried is known before any quota is
|
|
30
|
+
// spent, so it refuses like every other preflight gate instead of leaving a registered run behind.
|
|
31
|
+
export function advisorTaskOrRefuse(input) {
|
|
32
|
+
try {
|
|
33
|
+
return advisorTaskArtifact(input);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return die(`${error.message} The run folder was not created; quota was not spent.`, 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function adviseSection(artifact) {
|
|
40
|
+
if (!artifact?.scope) return '';
|
|
41
|
+
return [
|
|
42
|
+
'## Scope phase results',
|
|
43
|
+
'',
|
|
44
|
+
'Predicted risks:',
|
|
45
|
+
...artifact.scope.predicted_risks.map(({ id, risk }) => `- ${id}: ${risk}`),
|
|
46
|
+
'',
|
|
47
|
+
'Paths you may additionally read and cite:',
|
|
48
|
+
...artifact.scope.missing_paths.map((entry) => `- ${entry}`),
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
@@ -5,6 +5,7 @@ import { readJsonFileSync } from '../json-file.mjs';
|
|
|
5
5
|
import { workerMayBeAlive } from '../meta/run-liveness.mjs';
|
|
6
6
|
import { processAlive } from '../process-identity.mjs';
|
|
7
7
|
import { exitCodeFor, writeFailure, chainRuns } from '../write-meta.mjs';
|
|
8
|
+
import { sameRun } from './continuation.mjs';
|
|
8
9
|
import { conflictingOrderOwner, runsForOrder } from './order-owner.mjs';
|
|
9
10
|
|
|
10
11
|
// The attach call is the only process that waits for a worker it did not spawn. Keeping that
|
|
@@ -93,18 +94,21 @@ export async function waitForReply(runDir, workerPid, status) {
|
|
|
93
94
|
* path only covers a live run that has not recorded a verdict yet.
|
|
94
95
|
*
|
|
95
96
|
* A repeat that arrives after the verdict is answered from disk rather than refused, for the same
|
|
96
|
-
* reason. `--continue`
|
|
97
|
-
* the
|
|
97
|
+
* reason. `--continue` may attach to a run created from its grant: the 2026-09-23 dispatcher
|
|
98
|
+
* incident showed that repeating the identical continuation command must return to that run.
|
|
98
99
|
*/
|
|
99
|
-
export async function attach({ runsRoot, repo, slug, taskHash, orderId, chain, isContinue, noWait } = {}) {
|
|
100
|
-
if (isContinue) return null;
|
|
100
|
+
export async function attach({ runsRoot, repo, slug, taskHash, orderId, chain, grantRun, isContinue, noWait } = {}) {
|
|
101
101
|
const runs = chain || chainRuns(runsRoot, repo, slug, taskHash, orderId);
|
|
102
102
|
const runRecords = runs
|
|
103
103
|
.map((run) => ({ run, status: readJsonFile(path.join(runsRoot, run, 'status.json')) }))
|
|
104
104
|
.filter(({ status }) => status);
|
|
105
105
|
const sameOrder = runsForOrder(runRecords, orderId);
|
|
106
|
+
const attachRuns = isContinue
|
|
107
|
+
? sameOrder.filter(({ status }) => grantRun && status.continued_from && sameRun(runsRoot, status.continued_from, grantRun))
|
|
108
|
+
: sameOrder;
|
|
109
|
+
if (isContinue && !attachRuns.length) return null;
|
|
106
110
|
|
|
107
|
-
const owner = conflictingOrderOwner(runRecords, orderId, taskHash);
|
|
111
|
+
const owner = isContinue ? null : conflictingOrderOwner(runRecords, orderId, taskHash);
|
|
108
112
|
if (owner) {
|
|
109
113
|
const ownerDir = path.join(runsRoot, owner.run);
|
|
110
114
|
console.log(
|
|
@@ -120,8 +124,8 @@ export async function attach({ runsRoot, repo, slug, taskHash, orderId, chain, i
|
|
|
120
124
|
// order, and while it was in flight a repeat used to be answered by the first run's reply.txt —
|
|
121
125
|
// a stale verdict presented as this pass's answer. Found by the Plan_11-2 checklist, 2026-08-04.
|
|
122
126
|
let candidate = null;
|
|
123
|
-
for (let i =
|
|
124
|
-
const entry =
|
|
127
|
+
for (let i = attachRuns.length - 1; i >= 0; i -= 1) {
|
|
128
|
+
const entry = attachRuns[i];
|
|
125
129
|
const dir = path.join(runsRoot, entry.run);
|
|
126
130
|
if (fs.existsSync(path.join(dir, 'reply.txt'))) {
|
|
127
131
|
announceSavedReply(dir, orderId, entry.status.started_at);
|
|
@@ -9,7 +9,9 @@ import path from 'node:path';
|
|
|
9
9
|
import { CONTINUATION_INPUT, parseContinuationGrant } from '../required-inputs.mjs';
|
|
10
10
|
import { readJson } from '../write-meta.mjs';
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// One comparison of run names for the grant gate and for attach, so a repeat and a grant can never
|
|
13
|
+
// disagree about whether two spellings name the same folder.
|
|
14
|
+
export const sameRun = (runsRootPath, left, right) => {
|
|
13
15
|
const a = path.resolve(runsRootPath, left);
|
|
14
16
|
const b = path.resolve(runsRootPath, right);
|
|
15
17
|
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
@@ -27,7 +27,7 @@ import { parseArgs, die } from './args.mjs';
|
|
|
27
27
|
import { settleTaskInput } from './task-input.mjs';
|
|
28
28
|
import { parseContinuationGrant } from '../required-inputs.mjs';
|
|
29
29
|
import { continuationRefusal } from './continuation.mjs';
|
|
30
|
-
import {
|
|
30
|
+
import { advisorTaskOrRefuse, adviseSection } from './advise-carry.mjs';
|
|
31
31
|
import { schemaFor } from './schemas.mjs';
|
|
32
32
|
import { INSTRUCTIONS } from './prompts.mjs';
|
|
33
33
|
import { git, headSha, branchName, worktreeSnapshot, reviewScope } from './git-state.mjs';
|
|
@@ -146,12 +146,18 @@ export async function launcher(argv = process.argv.slice(2)) {
|
|
|
146
146
|
const chain = chainRuns(projectRunsRoot, repoRoot, opts.slug, taskHash, opts.orderId);
|
|
147
147
|
const startedChain = startedRuns(projectRunsRoot, chain);
|
|
148
148
|
const continuationGrant = parseContinuationGrant(taskText);
|
|
149
|
+
const attachExistingRun = () => attach({
|
|
150
|
+
runsRoot: projectRunsRoot, repo: repoRoot, slug: opts.slug, taskHash, orderId: opts.orderId, chain,
|
|
151
|
+
grantRun: continuationGrant?.run, isContinue: opts.continue, noWait: opts.noWait,
|
|
152
|
+
});
|
|
153
|
+
let attachedExitCode = opts.continue ? await attachExistingRun() : null;
|
|
154
|
+
if (attachedExitCode !== null) return attachedExitCode;
|
|
149
155
|
|
|
150
156
|
// The 2026-08-10_220535_plan25-2-install-table-two-roots incident exposed this ordering:
|
|
151
157
|
// a grant without its flag must refuse before attach can print an older run's verdict.
|
|
152
158
|
// markAbandoned() ran before this gate, so a dead runner already has meta.json with FAIL.
|
|
153
159
|
// A run without a verdict can therefore only still be in flight; ordinary repeats go through
|
|
154
|
-
// attach()
|
|
160
|
+
// attach() after this gate. Repeated --continue calls attach first because their grant is spent.
|
|
155
161
|
const continuationError = continuationRefusal(
|
|
156
162
|
projectRunsRoot,
|
|
157
163
|
startedChain,
|
|
@@ -164,20 +170,13 @@ export async function launcher(argv = process.argv.slice(2)) {
|
|
|
164
170
|
const scopeRun = continuationGrant &&
|
|
165
171
|
scopeRunRefusal({ agent: opts.agent, phase: opts.phase, runsRoot: projectRunsRoot, grantRun: continuationGrant.run });
|
|
166
172
|
if (scopeRun) die(scopeRun, 1);
|
|
173
|
+
// Plan_59 D22: the advise snapshot is read before the folder exists, so a broken scope result refuses for free.
|
|
174
|
+
const advisorTask = opts.agent === 'codex-advisor' ? advisorTaskOrRefuse({ taskText, phase: opts.phase, runsRoot: projectRunsRoot, grantRun: continuationGrant?.run }) : null;
|
|
167
175
|
|
|
168
176
|
// One order produced six Codex runs on 2026-08-03 because the caller's time ceiling made it
|
|
169
177
|
// restart the synchronous launcher. A live same-order run is now the repeat target: attach
|
|
170
178
|
// before creating a folder, probing Codex or spending another token.
|
|
171
|
-
|
|
172
|
-
runsRoot: projectRunsRoot,
|
|
173
|
-
repo: repoRoot,
|
|
174
|
-
slug: opts.slug,
|
|
175
|
-
taskHash,
|
|
176
|
-
orderId: opts.orderId,
|
|
177
|
-
chain,
|
|
178
|
-
isContinue: opts.continue,
|
|
179
|
-
noWait: opts.noWait,
|
|
180
|
-
});
|
|
179
|
+
if (!opts.continue) attachedExitCode = await attachExistingRun();
|
|
181
180
|
if (attachedExitCode !== null) return attachedExitCode;
|
|
182
181
|
|
|
183
182
|
if (startedChain.length && !opts.continue) {
|
|
@@ -274,7 +273,7 @@ export async function launcher(argv = process.argv.slice(2)) {
|
|
|
274
273
|
if (opts.agent === 'codex-scout') {
|
|
275
274
|
fs.writeFileSync(path.join(runDir, 'questions.json'), `${JSON.stringify(questions, null, 2)}\n`);
|
|
276
275
|
}
|
|
277
|
-
if (
|
|
276
|
+
if (advisorTask) fs.writeFileSync(path.join(runDir, 'advisor-task.json'), `${JSON.stringify(advisorTask, null, 2)}\n`);
|
|
278
277
|
|
|
279
278
|
if (opts.agent === 'codex-build') {
|
|
280
279
|
fs.writeFileSync(path.join(runDir, 'scope.txt'), `${opts.scopePatterns.join('\n')}\n`);
|
|
@@ -289,6 +288,8 @@ export async function launcher(argv = process.argv.slice(2)) {
|
|
|
289
288
|
// and what may be edited. Both also go to disk as questions.json / scope.txt, so the verdict
|
|
290
289
|
// is computed from the same list Codex was handed, not from a second reading of the wording.
|
|
291
290
|
const sections = [`## Operator task (verbatim)\n\n${taskText}`];
|
|
291
|
+
const scopeResults = adviseSection(advisorTask);
|
|
292
|
+
if (scopeResults) sections.push(scopeResults);
|
|
292
293
|
if (questions.length) {
|
|
293
294
|
sections.push(
|
|
294
295
|
[
|
|
@@ -16,7 +16,7 @@ const BODIES = {
|
|
|
16
16
|
return `Judge a DESIGN before code exists.
|
|
17
17
|
|
|
18
18
|
Rules for both phases:
|
|
19
|
-
- Read ONLY the paths listed under \`## Paths\`, plus \`git log\` / \`git show\` on those paths.
|
|
19
|
+
- Read ONLY the paths listed under \`## Paths\` and, in the advise phase, under \`## Scope phase results\`, plus \`git log\` / \`git show\` on those paths.
|
|
20
20
|
- Write nothing. Do your own reading and checks; do not spawn or delegate to other agents.
|
|
21
21
|
- Every claim about code must carry a \`path:line\` address inside those paths. Addresses are
|
|
22
22
|
machine-checked for existence; do not invent addresses or treat an unchecked claim as evidence.
|
|
@@ -32,7 +32,7 @@ Scope phase:
|
|
|
32
32
|
(facts accepted from the task without checking, never empty), together with predicted_risks.
|
|
33
33
|
` : `
|
|
34
34
|
Advise phase:
|
|
35
|
-
- Settle every predicted risk
|
|
35
|
+
- Settle every predicted risk listed under \`## Scope phase results\` in risk_outcomes: its risk_id, an outcome of
|
|
36
36
|
confirmed or refuted, and an address supporting that outcome.
|
|
37
37
|
- Give ONE recommendation by task option_id, or none-of-these with a described unlisted_option.
|
|
38
38
|
Put every rejected task option in rejected with its cost; for none-of-these this means EVERY
|