@lyupro/codex-bridge 0.6.15 → 0.6.16
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 +23 -0
- package/README.md +1 -1
- package/cli/command-registry.mjs +1 -1
- package/cli/guidance-render.mjs +57 -0
- package/cli/guidance-topics.mjs +8 -0
- package/cli/guidance.mjs +9 -10
- package/package.json +2 -2
- package/scripts/home-write-inventory.mjs +2 -0
- package/scripts/run-tests.mjs +44 -0
- package/src/claude/rules/core.md +5 -4
- package/src/home/guidance/order.md +12 -13
- package/src/home/guidance/records.md +3 -1
- package/src/home/hooks/dispatcher-gate.mjs +8 -2
- package/src/home/hooks/order-gate.mjs +16 -15
- package/src/home/lib/dispatcher-call.mjs +27 -24
- package/src/home/lib/dispatcher-command.mjs +12 -6
- package/src/home/lib/dispatcher-gate.mjs +20 -16
- package/src/home/lib/dispatcher-model.mjs +7 -8
- package/src/home/lib/dispatcher-pin.mjs +7 -6
- package/src/home/lib/dispatcher-protocol.mjs +76 -4
- package/src/home/lib/dispatcher-state.mjs +4 -3
- package/src/home/lib/install-owner-roots.mjs +57 -55
- package/src/home/lib/meta/advisor-task.mjs +21 -12
- package/src/home/lib/meta/reply.mjs +1 -10
- package/src/home/lib/meta/unavailable-rows.mjs +11 -0
- package/src/home/lib/order-diagnostics/cli-flags.mjs +56 -0
- package/src/home/lib/order-diagnostics/filesystem.mjs +18 -0
- package/src/home/lib/order-diagnostics/host-state.mjs +263 -0
- package/src/home/lib/order-diagnostics/internal.mjs +82 -0
- package/src/home/lib/order-diagnostics/run-state.mjs +58 -0
- package/src/home/lib/order-diagnostics.mjs +48 -0
- package/src/home/lib/order-rules/call.mjs +130 -0
- package/src/home/lib/order-rules/header-grants.mjs +175 -0
- package/src/home/lib/order-rules/header.mjs +206 -0
- package/src/home/lib/order-rules/task-document.mjs +80 -0
- package/src/home/lib/order-rules.mjs +77 -0
- package/src/home/lib/order-schema.mjs +206 -200
- package/src/home/lib/order-text-record.mjs +104 -0
- package/src/home/lib/order-values.mjs +46 -46
- package/src/home/lib/runner/advise-carry.mjs +9 -7
- package/src/home/lib/runner/args.mjs +23 -26
- package/src/home/lib/runner/codex-availability.mjs +10 -6
- package/src/home/lib/runner/continuation.mjs +158 -174
- package/src/home/lib/runner/launcher.mjs +19 -17
- package/src/home/lib/runner/order-claim.mjs +15 -9
- package/src/home/lib/runner/order-options.mjs +3 -2
- package/src/home/lib/runner/order-owner.mjs +9 -7
- package/src/home/lib/runner/pass-gate.mjs +14 -18
- package/src/home/lib/runner/preflight.mjs +25 -35
- package/src/home/lib/runner/question-kind.mjs +4 -2
- package/src/home/lib/runner/retired-roots.mjs +18 -9
- package/src/home/lib/runner/runs-root.mjs +3 -3
- package/src/home/lib/runner/sandbox-diagnosis.mjs +17 -17
- package/src/home/lib/runner/sandbox-probe.mjs +18 -27
- package/src/home/lib/runner/scope-check.mjs +30 -23
- package/src/home/lib/runner/task-file.mjs +6 -4
- package/src/home/lib/runner/task-input.mjs +12 -16
- package/src/home/lib/runner/worker-admission.mjs +5 -2
- package/src/home/lib/subagent-model-env.mjs +5 -6
- package/src/home/lib/task-header.mjs +159 -158
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.16] - 2026-10-14
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `codex-bridge guidance order` now prints every rule the runner, the order gate and the dispatcher enforce on a
|
|
12
|
+
task file, under "Rules the runner enforces", straight from the registry that also builds each refusal. A rule
|
|
13
|
+
could previously be enforced without being described anywhere, and an orchestrator learned it only from a
|
|
14
|
+
refusal. The topic's own text keeps the examples and the meaning of the grants, not copies of these rules.
|
|
15
|
+
- The rules core installed into the Claude Code host now says to run `codex-bridge guidance order` before every
|
|
16
|
+
task file. Run `codex-bridge update` to refresh the installed core.
|
|
17
|
+
- The `records` topic describes `task.md` (the assembled input Codex receives), `raw.log` (optional transport
|
|
18
|
+
evidence) and the required fields of `review.json`. The list of shell-unsafe sequences in `order` is printed
|
|
19
|
+
from the code that refuses them, not copied by hand.
|
|
20
|
+
- The dispatcher prompts and the dispatcher gate take their instructions from one catalog, so the reason a call is
|
|
21
|
+
denied is a phrase the dispatcher prompt already contains. Installed prompts change wording after
|
|
22
|
+
`codex-bridge update`; the gate's refusal texts are unchanged.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- The test-time check that every refusal text comes from the registry can no longer lose a violation when its
|
|
27
|
+
journal cannot be written: the process stops with exit code 70 and leaves a marker the test runner fails on.
|
|
28
|
+
This affects only the package's own test suite.
|
|
29
|
+
|
|
7
30
|
## [0.6.15] - 2026-10-12
|
|
8
31
|
|
|
9
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ Run records in `~/.lyupro/.codex-bridge/runs/` (or `~/.claude/codex-runs/` befor
|
|
|
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 **4262 automated tests: 4259 passing and 3 skipped**.
|
|
66
66
|
|
|
67
67
|
## Verify
|
|
68
68
|
|
package/cli/command-registry.mjs
CHANGED
|
@@ -283,7 +283,7 @@ export const COMMANDS = [
|
|
|
283
283
|
usage: ['codex-bridge guidance [<topic>]'],
|
|
284
284
|
section: 'public',
|
|
285
285
|
async handler(argv, io) {
|
|
286
|
-
const result = guidance(argv);
|
|
286
|
+
const result = await guidance(argv);
|
|
287
287
|
if (result.exitCode === 2) io.error(result.output);
|
|
288
288
|
else io.log(result.output);
|
|
289
289
|
return result.exitCode;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Plan_79 B1: guidance derives owned values from the installed home, never a second list. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { GUIDANCE_TOPICS } from './guidance-topics.mjs';
|
|
5
|
+
|
|
6
|
+
const codeList = (values) => values.map((value) =>
|
|
7
|
+
value.includes('`') ? '`` ' + value + ' ``' : '`' + value + '`').join(', ');
|
|
8
|
+
|
|
9
|
+
function renderError(topic, placeholder, message) {
|
|
10
|
+
return new Error(`Cannot render guidance topic "${topic}" placeholder "${placeholder}": ${message}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function renderGuidance(text, { root, topic }) {
|
|
14
|
+
const values = new Map();
|
|
15
|
+
for (const [placeholder, name] of text.matchAll(/\{\{([^{}]*)\}\}/g)) {
|
|
16
|
+
if (values.has(placeholder)) continue;
|
|
17
|
+
try {
|
|
18
|
+
// The 2026-08-26 seeded-config incident requires file URLs rooted in the installed home.
|
|
19
|
+
if (name === 'shell-unsafe-sequences') {
|
|
20
|
+
const { SHELL_UNSAFE_SEQUENCES } = await import(pathToFileURL(path.join(root, 'lib', 'shell-unsafe.mjs')).href);
|
|
21
|
+
values.set(placeholder, codeList(SHELL_UNSAFE_SEQUENCES));
|
|
22
|
+
} else if (name === 'review-required-fields') {
|
|
23
|
+
const { SCHEMAS } = await import(pathToFileURL(path.join(root, 'lib', 'runner', 'schemas.mjs')).href);
|
|
24
|
+
const review = SCHEMAS['codex-review'];
|
|
25
|
+
values.set(placeholder, `Top-level fields: ${codeList(review.required)}; finding fields: ${codeList(review.properties.findings.items.required)}`);
|
|
26
|
+
} else if (name.startsWith('order-rules:')) {
|
|
27
|
+
const ruleTopic = name.slice('order-rules:'.length);
|
|
28
|
+
if (ruleTopic !== 'all' && !Object.hasOwn(GUIDANCE_TOPICS, ruleTopic)) {
|
|
29
|
+
throw new Error(`unknown order rule topic "${ruleTopic}"`);
|
|
30
|
+
}
|
|
31
|
+
const { ORDER_RULES } = await import(pathToFileURL(path.join(root, 'lib', 'order-rules.mjs')).href);
|
|
32
|
+
const rules = Object.entries(ORDER_RULES);
|
|
33
|
+
const items = (owner) => rules.filter(([, rule]) => rule.topic === owner)
|
|
34
|
+
.map(([key, rule]) => `- \`${key}\`: ${rule.rule}`).join('\n');
|
|
35
|
+
const block = ruleTopic === 'all'
|
|
36
|
+
? Object.keys(GUIDANCE_TOPICS).map((owner) => {
|
|
37
|
+
const list = items(owner);
|
|
38
|
+
return list ? `Topic \`${owner}\`:\n${list}` : '';
|
|
39
|
+
}).filter(Boolean).join('\n\n')
|
|
40
|
+
: items(ruleTopic);
|
|
41
|
+
if (!block) throw new Error('no order rules to render');
|
|
42
|
+
values.set(placeholder, block);
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error('unknown placeholder');
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw renderError(topic, placeholder, error.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const rendered = text.replace(/\{\{([^{}]*)\}\}/g, (placeholder) => values.get(placeholder));
|
|
51
|
+
const unresolved = rendered.indexOf('{{');
|
|
52
|
+
if (unresolved !== -1) {
|
|
53
|
+
const placeholder = rendered.slice(unresolved).split(/\r?\n/, 1)[0];
|
|
54
|
+
throw renderError(topic, placeholder, 'unresolved placeholder');
|
|
55
|
+
}
|
|
56
|
+
return rendered;
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Defines the registered guidance topics and their summaries. */
|
|
2
|
+
export const GUIDANCE_TOPICS = Object.freeze({
|
|
3
|
+
order: 'Task file, header labels, and scope.',
|
|
4
|
+
advisor: 'Scope and advise phases, and the continue grant.',
|
|
5
|
+
budget: 'Run deadline and slicing.',
|
|
6
|
+
records: 'Finding and reading runs.',
|
|
7
|
+
concurrency: 'Parallel and writing runs.',
|
|
8
|
+
});
|
package/cli/guidance.mjs
CHANGED
|
@@ -2,14 +2,8 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { resolveBrandHome } from '../src/home/lib/brand-home.mjs';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
order: 'Task file, header labels, and scope.',
|
|
8
|
-
advisor: 'Scope and advise phases, and the continue grant.',
|
|
9
|
-
budget: 'Run deadline and slicing.',
|
|
10
|
-
records: 'Finding and reading runs.',
|
|
11
|
-
concurrency: 'Parallel and writing runs.',
|
|
12
|
-
});
|
|
5
|
+
import { renderGuidance } from './guidance-render.mjs';
|
|
6
|
+
import { GUIDANCE_TOPICS } from './guidance-topics.mjs';
|
|
13
7
|
|
|
14
8
|
const topicList = () => [
|
|
15
9
|
'Topics:',
|
|
@@ -18,7 +12,7 @@ const topicList = () => [
|
|
|
18
12
|
'Usage: codex-bridge guidance [<topic>]',
|
|
19
13
|
].join('\n');
|
|
20
14
|
|
|
21
|
-
export function guidance(argv, { env = process.env, homedir } = {}) {
|
|
15
|
+
export async function guidance(argv, { env = process.env, homedir } = {}) {
|
|
22
16
|
if (argv.length === 0) return { exitCode: 0, output: topicList() };
|
|
23
17
|
if (argv.length > 1) {
|
|
24
18
|
return { exitCode: 2, output: `codex-bridge guidance accepts at most one topic.\n${topicList()}` };
|
|
@@ -31,12 +25,17 @@ export function guidance(argv, { env = process.env, homedir } = {}) {
|
|
|
31
25
|
// The 2026-08-26 seeded-config incident forbids reading installed files from the clone.
|
|
32
26
|
const { root } = resolveBrandHome({ homedir, env });
|
|
33
27
|
const file = path.join(root, 'guidance', `${topic}.md`);
|
|
28
|
+
let text;
|
|
34
29
|
try {
|
|
35
|
-
|
|
30
|
+
text = fs.readFileSync(file, 'utf8');
|
|
36
31
|
} catch (error) {
|
|
37
32
|
return {
|
|
38
33
|
exitCode: 1,
|
|
39
34
|
output: `Cannot read guidance file ${file}: ${error.message}\nrun codex-bridge update`,
|
|
40
35
|
};
|
|
41
36
|
}
|
|
37
|
+
return renderGuidance(text, { root, topic }).then(
|
|
38
|
+
(output) => ({ exitCode: 0, output }),
|
|
39
|
+
(error) => ({ exitCode: 1, output: `${error.message}\nrun codex-bridge update` }),
|
|
40
|
+
);
|
|
42
41
|
}
|
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 8 guards,
|
|
3
|
+
"version": "0.6.16",
|
|
4
|
+
"description": "Claude Code-to-Codex delegation with 8 guards, 4262 tests, zero runtime dependencies, and zero build steps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"codex",
|
|
@@ -46,6 +46,8 @@ export const HOME_WRITE_INVENTORY = Object.freeze([
|
|
|
46
46
|
outside('src/home/lib/host-version.mjs', 'open', 1, 'Read-only open of a run transcript tail with the r flag.'),
|
|
47
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.'),
|
|
48
48
|
outside('src/home/lib/meta/run-state.mjs', 'writeFileSync', 3, 'Writes status.json and meta.json inside run folders under the runs root.'),
|
|
49
|
+
outside('src/home/lib/order-text-record.mjs', 'mkdirSync', 2, 'Plan_79 D5/D6/D9 creates the outgoing/parameter journal under CODEX_BRIDGE_TEST_TMP and, only on journal failure, its sibling fault directory outside the throwaway root.'),
|
|
50
|
+
outside('src/home/lib/order-text-record.mjs', 'appendFileSync', 2, 'Plan_79 D5/D6/D9 appends outgoing/parameter violations under CODEX_BRIDGE_TEST_TMP and, only on journal failure, the same entries plus the error to a sibling fault marker.'),
|
|
49
51
|
outside('src/home/lib/run-codex.mjs', 'appendFileSync', 1, 'Appends a crash diagnostic to stderr.log inside a run folder.'),
|
|
50
52
|
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
53
|
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).'),
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -31,6 +31,41 @@ import { auditWrites } from './home-write-audit.mjs';
|
|
|
31
31
|
import { HOME_WRITE_INVENTORY } from './home-write-inventory.mjs';
|
|
32
32
|
import { readmeText, suiteCountMismatch } from './suite-count.mjs';
|
|
33
33
|
import { snapshotRoot, addedEntries } from './root-guard/root-entries.mjs';
|
|
34
|
+
import { orderTextFaultDirectory } from '../src/home/lib/order-text-record.mjs';
|
|
35
|
+
|
|
36
|
+
// Plan_79 B5b2b D5/D6: migration is complete, so every journal line is fatal.
|
|
37
|
+
export function inspectOutgoingRefusals(directory, report) {
|
|
38
|
+
let fatal = 0;
|
|
39
|
+
if (fs.existsSync(directory)) {
|
|
40
|
+
for (const file of fs.readdirSync(directory)) {
|
|
41
|
+
const lines = fs.readFileSync(path.join(directory, file), 'utf8').split(/\r?\n/).filter(Boolean);
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
report(`run-tests: refusal text outside the registry: ${line}\n`);
|
|
44
|
+
fatal++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return fatal;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Plan_79 D9: a child whose fault exit is expected must still leave runner-visible evidence.
|
|
52
|
+
export function inspectOrderTextFaults(directory, report) {
|
|
53
|
+
let fatal = 0;
|
|
54
|
+
if (fs.existsSync(directory)) {
|
|
55
|
+
for (const file of fs.readdirSync(directory)) {
|
|
56
|
+
const lines = fs.readFileSync(path.join(directory, file), 'utf8').split(/\r?\n/).filter(Boolean);
|
|
57
|
+
if (!lines.length) lines.push(`empty fault marker: ${file}`);
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
report(`run-tests: refusal check could not journal: ${line}\n`);
|
|
60
|
+
fatal++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return fatal;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Importing the journal checker in isolated tests must not launch another suite.
|
|
68
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
34
69
|
|
|
35
70
|
// One pattern only: on 2026-09-25 three files passed as three arguments ran just the first, and a
|
|
36
71
|
// verification reported green for two files it never touched. Refuse before any root is created.
|
|
@@ -76,6 +111,13 @@ try {
|
|
|
76
111
|
});
|
|
77
112
|
const additions = addedEntries(before, snapshotRoot(repositoryRoot));
|
|
78
113
|
process.exitCode = result.status ?? 1;
|
|
114
|
+
// Plan_79 D5: inspect before cleanup, even when every expected refusal test passed.
|
|
115
|
+
const refusalJournal = path.join(testTmpRoot, 'order-text-violations');
|
|
116
|
+
const fatalRefusals = inspectOutgoingRefusals(refusalJournal,
|
|
117
|
+
(line) => process.stderr.write(line));
|
|
118
|
+
const fatalFaults = inspectOrderTextFaults(orderTextFaultDirectory(testTmpRoot),
|
|
119
|
+
(line) => process.stderr.write(line));
|
|
120
|
+
if ((fatalRefusals || fatalFaults) && process.exitCode === 0) process.exitCode = 1;
|
|
79
121
|
for (const name of additions) {
|
|
80
122
|
process.stderr.write(`run-tests: the suite left ${name} in the repository root (or something else created it while the suite ran)\n`);
|
|
81
123
|
}
|
|
@@ -96,5 +138,7 @@ try {
|
|
|
96
138
|
} finally {
|
|
97
139
|
for (const [, directory] of created) fs.rmSync(directory, { recursive: true, force: true });
|
|
98
140
|
fs.rmSync(testTmpRoot, { recursive: true, force: true });
|
|
141
|
+
fs.rmSync(orderTextFaultDirectory(testTmpRoot), { recursive: true, force: true });
|
|
99
142
|
if (tapFile) fs.rmSync(path.dirname(tapFile), { recursive: true, force: true });
|
|
100
143
|
}
|
|
144
|
+
}
|
package/src/claude/rules/core.md
CHANGED
|
@@ -9,10 +9,11 @@ code: a `scope` phase, then `advise`).
|
|
|
9
9
|
|
|
10
10
|
- The call is one line, `task file: <absolute path>`. Labels go in the task-file header, free text in its body. A
|
|
11
11
|
refused call costs nothing: read the refusal, fix the task file, call again.
|
|
12
|
-
- Before
|
|
13
|
-
`advisor` (two phases, the `continue:` grant),
|
|
14
|
-
|
|
15
|
-
show its output to the operator — never guess
|
|
12
|
+
- Before every task file, run `codex-bridge guidance order` and follow it: it lists the enforced rules.
|
|
13
|
+
Other topics, via `codex-bridge guidance <topic>`: `advisor` (two phases, the `continue:` grant),
|
|
14
|
+
`budget` (run deadline, slicing), `records` (finding and reading runs), `concurrency` (parallel and writing
|
|
15
|
+
runs); `codex-bridge guidance` lists them. If it fails, stop and show its output to the operator — never guess
|
|
16
|
+
the contract.
|
|
16
17
|
- Every `codex-build` task file carries exactly one `advice:` line.
|
|
17
18
|
- A writing run has a hard deadline: slice the task before the launch, not after a failure. After a deadline `FAIL`,
|
|
18
19
|
look at `git status` before deciding anything.
|
|
@@ -4,14 +4,11 @@ Write a task file that gives each dispatcher the complete order and the build's
|
|
|
4
4
|
## Call and header
|
|
5
5
|
|
|
6
6
|
The call is exactly one line: `task file: <task-file path from the orchestrator>`.
|
|
7
|
-
The path must be absolute and name a file the orchestrator wrote with its file tool.
|
|
8
7
|
The dispatcher never creates, reads or rewrites it. The whole order lives in that file's header.
|
|
9
8
|
The runner checks the header, never the dispatcher. Free text belongs in the task file.
|
|
10
9
|
|
|
11
10
|
Start at the first line with consecutive lowercase `label: value` lines, then a blank line and the task body.
|
|
12
|
-
Labels must use their exact spelling, start at column one and have a non-empty value; duplicates are refused.
|
|
13
11
|
The header ends at the first line that is not a recognized label line, including a blank line.
|
|
14
|
-
Do not put grants or advice below the header, even in a code fence; quote examples with `>` or reword them inside a sentence.
|
|
15
12
|
|
|
16
13
|
| Agent | Required labels | Conditional labels | Optional labels |
|
|
17
14
|
| --- | --- | --- | --- |
|
|
@@ -20,22 +17,20 @@ Do not put grants or advice below the header, even in a code fence; quote exampl
|
|
|
20
17
|
| codex-review | `order id` | `continue`, `retry` | `repository`, `slug`, `effort`, `changeset` |
|
|
21
18
|
| codex-advisor | `order id`, `phase` | `continue`, `retry` | `repository`, `slug`, `effort` |
|
|
22
19
|
|
|
23
|
-
Both grant labels are conditional when this pass continues or repeats a named run
|
|
24
|
-
`continue` names the continued run; `retry` repeats a failed pass and does not authorize the next pass.
|
|
25
|
-
Each value names a bare run folder (not a path, `.` or `..`) and a non-placeholder reason, separated by ` — `, ` - ` or `:`.
|
|
20
|
+
Both grant labels are conditional when this pass continues or repeats a named run: `continue` names the continued run; `retry` repeats a failed pass and does not authorize the next pass.
|
|
26
21
|
For advisor, pass `scope` first, then `advise` with a `continue:` grant naming the scope run of the same order.
|
|
27
22
|
|
|
28
23
|
`repository` is the repository path; `slug` names this pass without replacing the order id.
|
|
29
24
|
The runner checks accepted `effort` values. `changeset` is `uncommitted`, `base:<branch>` or `commit:<sha>`.
|
|
30
|
-
|
|
31
|
-
`phase: scope` and `effort: none` are real values.
|
|
25
|
+
Forbidden shell sequences are {{shell-unsafe-sequences}}.
|
|
26
|
+
`phase: scope` and `effort: none` are real values, not placeholders.
|
|
32
27
|
|
|
33
28
|
## File boundary
|
|
34
29
|
|
|
35
30
|
`scope`: Comma-separated globs relative to the repository root, listing every file the run may touch — including each caller of what changes, not only the file being edited. Anything outside the list fails the run.
|
|
36
31
|
|
|
37
32
|
`scope new`: Comma-separated globs for new files this build may create, relative to the repository root.
|
|
38
|
-
`scope`
|
|
33
|
+
Paths listed in `scope new` are already inside the run's boundary, so they are not repeated in `scope`.
|
|
39
34
|
<!-- service-folders:begin -->
|
|
40
35
|
Never name service folders in scope: `.git/`, `.claude/`, `.codex/`, `.omx/`, `.omc/`, `node_modules/`.
|
|
41
36
|
<!-- service-folders:end -->
|
|
@@ -52,15 +47,19 @@ advice: docs-only
|
|
|
52
47
|
|
|
53
48
|
## Advice
|
|
54
49
|
|
|
55
|
-
Every build task header contains exactly one `advice:` line.
|
|
56
|
-
whose `meta.json` identifies a `codex-advisor` advise pass with status `OK`, or exactly `mechanical`, `revert`,
|
|
57
|
-
`docs-only`, or `test-only`. Anything else is refused before the run starts.
|
|
50
|
+
Every build task header contains exactly one `advice:` line.
|
|
58
51
|
|
|
59
52
|
## Verify
|
|
60
53
|
|
|
61
|
-
Put the verification command in the task file under a `## Verify` heading
|
|
54
|
+
Put the verification command in the task file under a `## Verify` heading.
|
|
62
55
|
The dispatcher never reads that file and never puts the command on the command line.
|
|
63
56
|
|
|
57
|
+
## Rules the runner enforces
|
|
58
|
+
|
|
59
|
+
These are the rules a pre-start refusal names.
|
|
60
|
+
|
|
61
|
+
{{order-rules:all}}
|
|
62
|
+
|
|
64
63
|
## Free refusals
|
|
65
64
|
|
|
66
65
|
A pre-start refusal creates no run folder and spends no quota; the same order id remains reusable for the corrected order.
|
|
@@ -16,10 +16,12 @@ A started run records its lifecycle and output in its run folder:
|
|
|
16
16
|
|
|
17
17
|
- `meta.json`: agent, phase and final status; use it to identify an advisor advise pass before citing it in `advice:`.
|
|
18
18
|
- `status.json`: lifecycle state (`running`, `finished`, `failed` or `abandoned`) and runner PID.
|
|
19
|
+
- `task.md`: the task the runner assembled for the executor, not a byte copy of the orchestrator's task file.
|
|
19
20
|
- `result.json`: structured scout, build or advisor result; build includes `summary`, `changes`, `verify_command`, `verify_passed`, `leftovers` and `report_markdown`.
|
|
20
|
-
- `review.json`: the review agent's structured result instead of `result.json`.
|
|
21
|
+
- `review.json`: the review agent's structured result instead of `result.json`. {{review-required-fields}}.
|
|
21
22
|
- `report.md`: the full report; the runner expands a build result's `report_markdown` into this file.
|
|
22
23
|
- `events.jsonl` and `stderr.log`: transport evidence for progress, deadline or startup failures.
|
|
24
|
+
- `raw.log`: transport evidence when present; retention may remove it.
|
|
23
25
|
|
|
24
26
|
A failed or interrupted run may have no complete result. Read the recorded status and available evidence rather than treating a missing report as an empty successful run.
|
|
25
27
|
|
|
@@ -20,14 +20,20 @@ import {
|
|
|
20
20
|
} from '../lib/dispatcher-state.mjs';
|
|
21
21
|
import { recordInterceptedAttempt, witnessHostVersion } from '../lib/handback-witness.mjs';
|
|
22
22
|
import { observeSessionHost } from '../lib/host-observations.mjs';
|
|
23
|
+
import { orderDiagnosticMessage } from '../lib/order-diagnostics.mjs';
|
|
24
|
+
import { checkOutgoingRefusal } from '../lib/order-text-record.mjs';
|
|
23
25
|
|
|
24
26
|
function emit(decision, toolInput) {
|
|
25
27
|
const hookSpecificOutput = {
|
|
26
28
|
hookEventName: 'PreToolUse',
|
|
27
29
|
permissionDecision: decision.kind,
|
|
28
30
|
};
|
|
29
|
-
if (decision.kind === 'deny')
|
|
31
|
+
if (decision.kind === 'deny') {
|
|
32
|
+
checkOutgoingRefusal(decision.reason, 'deny');
|
|
33
|
+
hookSpecificOutput.permissionDecisionReason = decision.reason;
|
|
34
|
+
}
|
|
30
35
|
if (decision.kind === 'allow') {
|
|
36
|
+
checkOutgoingRefusal(decision.message, 'handback');
|
|
31
37
|
hookSpecificOutput.updatedInput = { ...toolInput, message: decision.message };
|
|
32
38
|
}
|
|
33
39
|
process.stdout.write(`${JSON.stringify({ hookSpecificOutput })}\n`);
|
|
@@ -93,7 +99,7 @@ async function main() {
|
|
|
93
99
|
} catch (error) {
|
|
94
100
|
decision = {
|
|
95
101
|
kind: 'deny',
|
|
96
|
-
reason:
|
|
102
|
+
reason: orderDiagnosticMessage('dispatcher-gate-internal-error', { message: errorMessage(error) }),
|
|
97
103
|
};
|
|
98
104
|
}
|
|
99
105
|
|
|
@@ -30,6 +30,9 @@ import { runsRoot } from '../lib/runner/runs-root.mjs';
|
|
|
30
30
|
import { parseTaskDocument } from '../lib/runner/task-file.mjs';
|
|
31
31
|
import { parseTaskHeader, taskHeaderRefusal } from '../lib/task-header.mjs';
|
|
32
32
|
import { envModelProblem } from '../lib/subagent-model-env.mjs';
|
|
33
|
+
import { orderRuleMessage } from '../lib/order-rules.mjs';
|
|
34
|
+
import { orderDiagnosticMessage } from '../lib/order-diagnostics.mjs';
|
|
35
|
+
import { checkOutgoingRefusal } from '../lib/order-text-record.mjs';
|
|
33
36
|
|
|
34
37
|
const GUARDED = new Set(Object.keys(AGENTS));
|
|
35
38
|
/**
|
|
@@ -41,10 +44,15 @@ const SUBAGENT_TOOL_NAMES = new Set(SUBAGENT_TOOLS);
|
|
|
41
44
|
|
|
42
45
|
let systemMessage;
|
|
43
46
|
const pass = () => {
|
|
44
|
-
if (systemMessage)
|
|
47
|
+
if (systemMessage) {
|
|
48
|
+
checkOutgoingRefusal(systemMessage, 'system-message');
|
|
49
|
+
process.stdout.write(JSON.stringify({ systemMessage }));
|
|
50
|
+
}
|
|
45
51
|
process.exit(0);
|
|
46
52
|
};
|
|
47
53
|
const deny = (reason) => {
|
|
54
|
+
checkOutgoingRefusal(reason, 'deny');
|
|
55
|
+
if (systemMessage) checkOutgoingRefusal(systemMessage, 'system-message');
|
|
48
56
|
process.stdout.write(JSON.stringify({
|
|
49
57
|
...(systemMessage ? { systemMessage } : {}),
|
|
50
58
|
hookSpecificOutput: {
|
|
@@ -75,11 +83,8 @@ if (!GUARDED.has(toolInput.subagent_type)) pass();
|
|
|
75
83
|
// Plan_67 D6: one installed model contract; refuse even matching aliases before prompt exits.
|
|
76
84
|
const agentType = toolInput.subagent_type;
|
|
77
85
|
if (Object.hasOwn(toolInput, 'model')) {
|
|
78
|
-
deny(
|
|
79
|
-
|
|
80
|
-
+ `its installed agent file. Call ${agentType} without model. `
|
|
81
|
-
+ 'The run folder was not created; quota was not spent.',
|
|
82
|
-
);
|
|
86
|
+
deny([orderRuleMessage('agent-call-model', { agentType }),
|
|
87
|
+
orderDiagnosticMessage('run-not-created')].join(' '));
|
|
83
88
|
}
|
|
84
89
|
try {
|
|
85
90
|
const hostVersion = await transcriptHostVersion(input.transcript_path).catch(() => null);
|
|
@@ -93,9 +98,9 @@ try {
|
|
|
93
98
|
const latch = modelLatch({ ledger: readDispatcherModel({ stateDir: BRAND_STATE_DIR }), agentType });
|
|
94
99
|
if (latch.active) {
|
|
95
100
|
const { data, at } = latch.violation;
|
|
96
|
-
systemMessage =
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
systemMessage = orderDiagnosticMessage('dispatcher-model-latch', {
|
|
102
|
+
agentType, models: data.parsed.join(', '), pinFamily: data.pinFamily, hostVersion: data.hostVersion, at,
|
|
103
|
+
});
|
|
99
104
|
}
|
|
100
105
|
} catch {
|
|
101
106
|
// D4 (a): a corrupt ledger must not turn a diagnostic warning into a refusal.
|
|
@@ -111,11 +116,7 @@ if (typeof toolInput.prompt !== 'string') pass();
|
|
|
111
116
|
const teammateField = ['name', 'team_name']
|
|
112
117
|
.find((key) => typeof toolInput[key] === 'string' && toolInput[key].trim());
|
|
113
118
|
if (teammateField) {
|
|
114
|
-
deny(
|
|
115
|
-
`Order gate denied the Agent call because it passes \`${teammateField}\`: a dispatcher launched as a ` +
|
|
116
|
-
'teammate escapes the dispatcher gate, whose agent_type becomes the teammate name. Call ' +
|
|
117
|
-
`${toolInput.subagent_type} with subagent_type only, without name or team_name.`,
|
|
118
|
-
);
|
|
119
|
+
deny(orderRuleMessage('agent-call-teammate', { field: teammateField, agentType: toolInput.subagent_type }));
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
// Plan_76 D1: exact registry labels prevent the scope-new and repository-prose incident of 2026-10-04.
|
|
@@ -144,7 +145,7 @@ const headerRefusal = taskHeaderRefusal(parsed);
|
|
|
144
145
|
if (headerRefusal) deny(headerRefusal);
|
|
145
146
|
const { order, problems } = orderFromHeader(toolInput.subagent_type, parsed);
|
|
146
147
|
if (problems.length) {
|
|
147
|
-
deny(
|
|
148
|
+
deny([renderOrderProblems(toolInput.subagent_type, problems), orderDiagnosticMessage('run-not-created')].join('\n'));
|
|
148
149
|
}
|
|
149
150
|
const orderId = order.get('order id');
|
|
150
151
|
// Plan_63 D9 / OW-054: only a validated header grant reaches the runner's grant rules.
|
|
@@ -15,10 +15,12 @@ import {
|
|
|
15
15
|
isInputPlaceholder,
|
|
16
16
|
} from './order-values.mjs';
|
|
17
17
|
import { firstShellUnsafeSequence } from './shell-unsafe.mjs';
|
|
18
|
+
import { orderRuleMessage } from './order-rules.mjs';
|
|
19
|
+
import { orderDiagnosticMessage } from './order-diagnostics.mjs';
|
|
18
20
|
|
|
19
21
|
export const TASK_FILE_INPUT = Object.freeze({
|
|
20
22
|
label: 'task file',
|
|
21
|
-
explanation: '
|
|
23
|
+
explanation: orderRuleMessage('dispatcher-task-file-help'),
|
|
22
24
|
example: 'C:/Users/me/AppData/Local/Temp/claude/<session>/scratchpad/task-plan-13.md',
|
|
23
25
|
});
|
|
24
26
|
|
|
@@ -28,7 +30,7 @@ export function callInputsFor(agentType) {
|
|
|
28
30
|
|
|
29
31
|
const knownLabels = new Set(ALL_ORDER_LABELS);
|
|
30
32
|
|
|
31
|
-
const freeTextReason = '
|
|
33
|
+
const freeTextReason = orderRuleMessage('dispatcher-call-shape', { violation: 'free-text' });
|
|
32
34
|
|
|
33
35
|
function normaliseLabel(label) {
|
|
34
36
|
return label.toLowerCase().replace(/^[-*] /, '').replace(/[*`]/g, '')
|
|
@@ -51,8 +53,7 @@ function shellRewrittenSequence(value) {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
function shellRewrittenReason(label, sequence) {
|
|
54
|
-
return
|
|
55
|
-
+ 'write the path with forward slashes and without `$`';
|
|
56
|
+
return orderRuleMessage('dispatcher-call-shape', { violation: 'rewritten', label, sequence });
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/** Returns the accepted label values and every refusal; one reader for the gate, the command and the transcript. */
|
|
@@ -64,7 +65,7 @@ export function parseDispatcherCall(agentType, promptText) {
|
|
|
64
65
|
const acceptedLines = new Map();
|
|
65
66
|
|
|
66
67
|
if (!ORDER_AGENTS.includes(agentType)) {
|
|
67
|
-
problems.push({ line: null, text: '', reason:
|
|
68
|
+
problems.push({ line: null, text: '', reason: orderDiagnosticMessage('dispatcher-agent-input-known', { agentType }) });
|
|
68
69
|
return { inputs, problems };
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -78,13 +79,13 @@ export function parseDispatcherCall(agentType, promptText) {
|
|
|
78
79
|
const value = tail.trim();
|
|
79
80
|
|
|
80
81
|
if (labels.has(label) && !value) {
|
|
81
|
-
problems.push({ line, text, reason:
|
|
82
|
+
problems.push({ line, text, reason: orderRuleMessage('dispatcher-call-shape', { violation: 'empty', label }) });
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
if (labels.has(label) && /^[ \t]+/.test(tail)) {
|
|
85
86
|
if (inputs.has(label)) {
|
|
86
87
|
const first = acceptedLines.get(label).line;
|
|
87
|
-
problems.push({ line, text, reason:
|
|
88
|
+
problems.push({ line, text, reason: orderRuleMessage('dispatcher-call-shape', { violation: 'duplicate', label, first, line }) });
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
90
91
|
inputs.set(label, value);
|
|
@@ -95,29 +96,30 @@ export function parseDispatcherCall(agentType, promptText) {
|
|
|
95
96
|
const normalised = label === null ? null : normaliseLabel(label);
|
|
96
97
|
let reason = freeTextReason;
|
|
97
98
|
if (labels.has(normalised)) {
|
|
98
|
-
reason =
|
|
99
|
+
reason = orderRuleMessage('dispatcher-call-shape', { violation: 'spelling', label: normalised });
|
|
99
100
|
} else if (knownLabels.has(normalised)) {
|
|
100
|
-
reason =
|
|
101
|
+
reason = orderRuleMessage('dispatcher-call-shape', { violation: 'header', label: normalised });
|
|
101
102
|
}
|
|
102
103
|
problems.push({ line, text, reason });
|
|
103
104
|
});
|
|
104
105
|
|
|
105
106
|
for (const entry of entries) {
|
|
106
107
|
if (!entry.conditional && !entry.optional && !inputs.has(entry.label)) {
|
|
107
|
-
problems.push({ line: null, text: '', label: entry.label,
|
|
108
|
+
problems.push({ line: null, text: '', label: entry.label,
|
|
109
|
+
reason: orderRuleMessage('dispatcher-call-shape', { violation: 'missing', label: entry.label }) });
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
for (const [label, value] of inputs) {
|
|
111
113
|
const location = acceptedLines.get(label);
|
|
112
114
|
if (isInputPlaceholder(value, label)) {
|
|
113
|
-
problems.push({ ...location, reason:
|
|
115
|
+
problems.push({ ...location, reason: orderRuleMessage('dispatcher-call-shape', { violation: 'placeholder', label }) });
|
|
114
116
|
}
|
|
115
117
|
if (label === 'task file' && !isAbsoluteTaskFilePath(value)) {
|
|
116
|
-
problems.push({ ...location, reason: '
|
|
118
|
+
problems.push({ ...location, reason: orderRuleMessage('dispatcher-call-shape', { violation: 'absolute' }) });
|
|
117
119
|
}
|
|
118
120
|
const sequence = unsafeSequence(value);
|
|
119
121
|
if (sequence !== null) {
|
|
120
|
-
const reason =
|
|
122
|
+
const reason = orderRuleMessage('dispatcher-call-shape', { violation: 'unsafe', label, sequence });
|
|
121
123
|
problems.push({ ...location, reason });
|
|
122
124
|
} else {
|
|
123
125
|
const expanding = shellRewrittenSequence(value);
|
|
@@ -129,17 +131,20 @@ export function parseDispatcherCall(agentType, promptText) {
|
|
|
129
131
|
|
|
130
132
|
/** Names each refused line and the agent's header template, so one free refusal carries its own repair. */
|
|
131
133
|
export function renderCallRefusal(agentType, problems) {
|
|
132
|
-
const lines = ['
|
|
134
|
+
const lines = [orderRuleMessage('dispatcher-call-refusal', { part: 'intro' })];
|
|
133
135
|
const entries = callInputsFor(agentType);
|
|
134
136
|
for (const { line, text, reason, label } of problems) {
|
|
135
|
-
const entry = line === null && reason ===
|
|
137
|
+
const entry = line === null && reason === orderRuleMessage('dispatcher-call-shape', { violation: 'missing', label })
|
|
136
138
|
? entries.find((entry) => entry.label === label) : undefined;
|
|
137
|
-
const detail = entry ?
|
|
138
|
-
|
|
139
|
+
const detail = entry ? orderRuleMessage('dispatcher-call-refusal', {
|
|
140
|
+
part: 'detail', explanation: entry.explanation, label: entry.label, example: entry.example,
|
|
141
|
+
}) : '';
|
|
142
|
+
lines.push(line === null ? orderRuleMessage('dispatcher-call-refusal', { part: 'problem', reason, detail })
|
|
143
|
+
: orderRuleMessage('dispatcher-call-refusal', { part: 'line', line, text, reason }));
|
|
139
144
|
}
|
|
140
|
-
lines.push('
|
|
141
|
-
'
|
|
142
|
-
...renderOrderHeaderHelp(agentType).split('\n'), '
|
|
145
|
+
lines.push(orderRuleMessage('dispatcher-call-refusal', { part: 'call' }),
|
|
146
|
+
orderRuleMessage('dispatcher-call-refusal', { part: 'header' }),
|
|
147
|
+
...renderOrderHeaderHelp(agentType).split('\n'), orderRuleMessage('dispatcher-call-refusal', { part: 'free-text' }));
|
|
143
148
|
return lines.join('\n');
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -159,13 +164,11 @@ function conditionFor(entry) {
|
|
|
159
164
|
|
|
160
165
|
/** Keeps the caller's short prompt aligned with the header registry, including grant conditions. */
|
|
161
166
|
export function renderRequiredInputSummary(agentType) {
|
|
162
|
-
if (!ORDER_AGENTS.includes(agentType)) return '';
|
|
167
|
+
if (!ORDER_AGENTS.includes(agentType)) return orderRuleMessage('dispatcher-call-summary', { variant: 'unknown-agent' });
|
|
163
168
|
const entries = headerInputsFor(agentType);
|
|
164
169
|
const required = entries.filter(({ required }) => required).map(({ label }) => `\`${label}\``);
|
|
165
170
|
const conditional = entries.filter(({ conditional }) => conditional)
|
|
166
171
|
.map((entry) => `\`${entry.label}\` (${conditionFor(entry)})`);
|
|
167
172
|
const optional = entries.filter(({ optional }) => optional).map(({ label }) => `\`${label}\``);
|
|
168
|
-
return
|
|
169
|
-
+ `conditional labels: ${conditional.join(', ')}; optional labels: ${optional.join(', ')}. `
|
|
170
|
-
+ 'Free text belongs in the task file.';
|
|
173
|
+
return orderRuleMessage('dispatcher-call-summary', { variant: 'labels', required, conditional, optional });
|
|
171
174
|
}
|