@lyupro/codex-bridge 0.6.14 → 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 +33 -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 +15 -14
- 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/paths.mjs +6 -1
- 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/prompts.mjs +2 -2
- 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
|
@@ -1,204 +1,210 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Owns the dispatcher order registry for task-file headers.
|
|
3
|
-
* Plan_63 D9: the call carries only task file, so order entries belong here without runner flags.
|
|
4
|
-
* The D1/D3 `continue: none` regression still requires one schema for all header readers.
|
|
5
|
-
*/
|
|
6
|
-
import { isInputPlaceholder } from './order-values.mjs';
|
|
7
|
-
import { firstShellUnsafeSequence } from './shell-unsafe.mjs';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Owns the dispatcher order registry for task-file headers.
|
|
3
|
+
* Plan_63 D9: the call carries only task file, so order entries belong here without runner flags.
|
|
4
|
+
* The D1/D3 `continue: none` regression still requires one schema for all header readers.
|
|
5
|
+
*/
|
|
6
|
+
import { isInputPlaceholder } from './order-values.mjs';
|
|
7
|
+
import { firstShellUnsafeSequence } from './shell-unsafe.mjs';
|
|
8
|
+
import { orderRuleMessage, orderProblemLine, orderHeaderHelp } from './order-rules.mjs';
|
|
9
|
+
import { orderDiagnosticMessage } from './order-diagnostics.mjs';
|
|
10
|
+
|
|
11
|
+
const freezeEntries = (entries) => Object.freeze(entries.map((entry) => Object.freeze(entry)));
|
|
12
|
+
|
|
13
|
+
export const CONTINUATION_ORDER_INPUT = Object.freeze({
|
|
14
|
+
label: 'continue',
|
|
15
|
+
explanation:
|
|
16
|
+
'The run folder this pass is ordered to continue, followed by why the orchestrator is spending another pass. A continuation is assigned by the orchestrator; after a verdict, the dispatcher returns it and stops.',
|
|
17
|
+
example: '2026-08-05_092913_plan14-build — LIMIT at step 3, tests unwritten',
|
|
18
|
+
conditional: 'when this pass continues or repeats a named run',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const RETRY_ORDER_INPUT = Object.freeze({
|
|
22
|
+
label: 'retry',
|
|
23
|
+
explanation:
|
|
24
|
+
'The failed run this pass repeats, followed by why the orchestrator pays for the same pass again. A retry repeats that failed pass under its own order; it does not authorize the next pass.',
|
|
25
|
+
example: '2026-10-03_172017_cc-d66-advisor — model at capacity, same pass again',
|
|
26
|
+
conditional: 'when this pass continues or repeats a named run',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const REQUIRED_ORDER_INPUTS = Object.freeze({
|
|
30
|
+
'codex-scout': freezeEntries([
|
|
31
|
+
{
|
|
32
|
+
label: 'order id',
|
|
33
|
+
explanation: 'The label this order is known by. Repeating a call with the same label joins the run already in flight and costs no quota; a different piece of work needs a new label, never a reused one.',
|
|
34
|
+
example: 'plan-13-scout-20260804',
|
|
35
|
+
},
|
|
36
|
+
CONTINUATION_ORDER_INPUT,
|
|
37
|
+
RETRY_ORDER_INPUT,
|
|
38
|
+
]),
|
|
39
|
+
'codex-build': freezeEntries([
|
|
40
|
+
{
|
|
41
|
+
label: 'order id',
|
|
42
|
+
explanation: 'The label this order is known by. Repeating a call with the same label joins the run already in flight and costs no quota; a different piece of work needs a new label, never a reused one.',
|
|
43
|
+
example: 'plan-13-build-20260804',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: 'scope',
|
|
47
|
+
explanation: '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.',
|
|
48
|
+
example: 'src/home/lib/runner/**,tests/runner/**',
|
|
49
|
+
},
|
|
50
|
+
CONTINUATION_ORDER_INPUT,
|
|
51
|
+
RETRY_ORDER_INPUT,
|
|
52
|
+
]),
|
|
53
|
+
'codex-review': freezeEntries([
|
|
54
|
+
{
|
|
55
|
+
label: 'order id',
|
|
56
|
+
explanation: 'The label this order is known by. Repeating a call with the same label joins the run already in flight and costs no quota; a different piece of work needs a new label, never a reused one.',
|
|
57
|
+
example: 'plan-13-review-20260804',
|
|
58
|
+
},
|
|
59
|
+
CONTINUATION_ORDER_INPUT,
|
|
60
|
+
RETRY_ORDER_INPUT,
|
|
61
|
+
]),
|
|
62
|
+
// Plan_59 D7: the caller must name the phase and authorize the continued decision pass.
|
|
63
|
+
'codex-advisor': freezeEntries([
|
|
64
|
+
{
|
|
65
|
+
label: 'order id',
|
|
66
|
+
explanation: 'The label this design order is known by. Keep the same order id for scope and advise so phase 2 can settle the risks predicted in phase 1.',
|
|
67
|
+
example: 'plan-59-advisor-20260922',
|
|
68
|
+
},
|
|
69
|
+
CONTINUATION_ORDER_INPUT,
|
|
70
|
+
RETRY_ORDER_INPUT,
|
|
71
|
+
{
|
|
72
|
+
label: 'phase',
|
|
73
|
+
explanation: 'Pass scope first to predict risks and check the reading boundary, then advise with a continue: grant naming the scope run of the same order.',
|
|
74
|
+
example: 'scope',
|
|
75
|
+
},
|
|
76
|
+
]),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const ORDER_AGENTS = Object.freeze(Object.keys(REQUIRED_ORDER_INPUTS));
|
|
80
|
+
|
|
81
|
+
const OPTIONAL_ORDER_INPUTS = freezeEntries([
|
|
82
|
+
{
|
|
83
|
+
label: 'repository',
|
|
84
|
+
agents: ORDER_AGENTS,
|
|
85
|
+
explanation: 'The repository the runner works in. Pass its path as a labelled value so the dispatcher needs no cd.',
|
|
86
|
+
example: 'C:/work/codex-bridge',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'scope new',
|
|
90
|
+
agents: Object.freeze(['codex-build']),
|
|
91
|
+
explanation: 'Comma-separated globs for new files this build may create, relative to the repository root.',
|
|
92
|
+
example: 'src/home/lib/dispatcher-call.mjs,tests/dispatcher-call.test.mjs',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'slug',
|
|
96
|
+
agents: ORDER_AGENTS,
|
|
97
|
+
explanation: 'The readable name used for the run folder. It names this pass without replacing the order id.',
|
|
98
|
+
example: 'plan-76-parser',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: 'effort',
|
|
102
|
+
agents: ORDER_AGENTS,
|
|
103
|
+
explanation: 'The reasoning effort assigned to this pass. The runner checks which effort values it accepts.',
|
|
104
|
+
example: 'high',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
label: 'changeset',
|
|
108
|
+
agents: Object.freeze(['codex-review']),
|
|
109
|
+
explanation: 'The changes to review: uncommitted, base:<branch>, or commit:<sha>. Name only the ordered changeset.',
|
|
110
|
+
example: 'base:main',
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const schemas = new Map(ORDER_AGENTS.map((agentType) => [agentType,
|
|
115
|
+
freezeEntries([
|
|
116
|
+
...REQUIRED_ORDER_INPUTS[agentType],
|
|
117
|
+
...OPTIONAL_ORDER_INPUTS.filter((entry) => entry.agents.includes(agentType))
|
|
118
|
+
.map((entry) => ({ ...entry, optional: true })),
|
|
119
|
+
].map((entry) => ({
|
|
120
|
+
...entry,
|
|
121
|
+
required: !entry.conditional && !entry.optional,
|
|
122
|
+
conditional: Boolean(entry.conditional),
|
|
123
|
+
optional: Boolean(entry.optional),
|
|
124
|
+
}))),
|
|
125
|
+
]));
|
|
126
|
+
const NO_LABELS = Object.freeze([]);
|
|
127
|
+
|
|
128
|
+
export function orderLabelsFor(agentType) {
|
|
129
|
+
return schemas.get(agentType) ?? NO_LABELS;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const ALL_ORDER_LABELS = Object.freeze([...new Set(
|
|
133
|
+
ORDER_AGENTS.flatMap((agentType) => orderLabelsFor(agentType).map(({ label }) => label)),
|
|
134
|
+
)].sort());
|
|
135
|
+
|
|
136
|
+
// Plan_79 D6: formatted label names are schema data, not issued refusal text.
|
|
137
|
+
const inputNames = new Map(ALL_ORDER_LABELS.map((label) => [label, `\`${label}:\``]));
|
|
138
|
+
|
|
139
|
+
// Plan_63 D7: switching the order input channel changes only this function.
|
|
140
|
+
export function orderInputName(label) {
|
|
141
|
+
if (!inputNames.has(label)) throw new Error(orderDiagnosticMessage('order-input-name-known', { label }));
|
|
142
|
+
return inputNames.get(label);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function orderFromHeader(agentType, parsed) {
|
|
146
|
+
const order = new Map();
|
|
147
|
+
const problems = [];
|
|
148
|
+
if (!schemas.has(agentType)) {
|
|
149
|
+
problems.push({ lineNo: null, line: '',
|
|
150
|
+
reason: orderDiagnosticMessage('dispatcher-agent-known', { agent: agentType }) });
|
|
151
|
+
return { order, problems };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const labels = orderLabelsFor(agentType);
|
|
155
|
+
const accepted = new Set(labels.map(({ label }) => label));
|
|
156
|
+
for (const [label, value] of Object.entries(parsed.fields)) {
|
|
157
|
+
if (!ALL_ORDER_LABELS.includes(label)) continue;
|
|
158
|
+
// parseTaskHeader retains the first duplicate value; diagnostics must name that same line.
|
|
159
|
+
const entry = parsed.entries?.find((entry) => entry.label === label);
|
|
160
|
+
const location = entry ? { lineNo: entry.lineNo, line: entry.line }
|
|
161
|
+
: { lineNo: null, line: '' };
|
|
162
|
+
if (!accepted.has(label)) {
|
|
163
|
+
problems.push({ ...location,
|
|
164
|
+
reason: orderRuleMessage('agent-accepted-labels', { label, agent: agentType }) });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Grant values are validated by parseTaskHeader; re-reporting them would duplicate its problem.
|
|
168
|
+
if (label === 'continue' || label === 'retry') continue;
|
|
169
|
+
order.set(label, value);
|
|
170
|
+
if (isInputPlaceholder(value, label)) {
|
|
171
|
+
problems.push({ ...location, reason: orderRuleMessage('order-value-placeholder', { label }) });
|
|
172
|
+
}
|
|
173
|
+
// Plan_63 D5: the flag-era refusal (Plan_42) keeps its pair in the header — an order value is a short
|
|
174
|
+
// identifier, so a shell sequence in it is free text written in the wrong place.
|
|
175
|
+
const sequence = firstShellUnsafeSequence(value);
|
|
176
|
+
if (sequence !== null) {
|
|
177
|
+
problems.push({ ...location, reason: orderRuleMessage('header-shell-safety', {
|
|
178
|
+
labelName: orderInputName(label), sequence,
|
|
179
|
+
}) });
|
|
180
|
+
}
|
|
181
|
+
// Plan_63 D5: new paths cannot replace the existing-file boundary of a writing order.
|
|
182
|
+
if (agentType === 'codex-build' && label === 'scope'
|
|
183
|
+
&& !value.split(',').some((pattern) => pattern.trim())) {
|
|
184
|
+
problems.push({ ...location,
|
|
185
|
+
reason: orderRuleMessage('build-existing-scope-required', {
|
|
186
|
+
labelName: orderInputName(label), newLabelName: orderInputName('scope new'),
|
|
187
|
+
}) });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const { label, required, example } of labels) {
|
|
191
|
+
if (required && !Object.hasOwn(parsed.fields, label)) {
|
|
192
|
+
problems.push({ lineNo: null, line: '',
|
|
193
|
+
reason: orderRuleMessage('required-order-labels', { label, example }) });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { order, problems };
|
|
197
|
+
}
|
|
198
|
+
|
|
192
199
|
export function renderOrderProblems(agentType, problems) {
|
|
193
|
-
return [...problems.map(
|
|
194
|
-
? reason : `line ${lineNo}: ${reason}: ${line}`), renderOrderHeaderHelp(agentType)].join('\n');
|
|
200
|
+
return [...problems.map(orderProblemLine), renderOrderHeaderHelp(agentType)].join('\n');
|
|
195
201
|
}
|
|
196
202
|
|
|
197
203
|
export function renderOrderHeaderHelp(agentType) {
|
|
198
|
-
const labels = orderLabelsFor(agentType);
|
|
199
|
-
return [
|
|
200
|
-
...labels.filter(({ required }) => required),
|
|
201
|
-
...labels.filter(({ conditional }) => conditional),
|
|
202
|
-
...labels.filter(({ optional }) => optional),
|
|
203
|
-
]
|
|
204
|
-
}
|
|
204
|
+
const labels = orderLabelsFor(agentType);
|
|
205
|
+
return orderHeaderHelp([
|
|
206
|
+
...labels.filter(({ required }) => required),
|
|
207
|
+
...labels.filter(({ conditional }) => conditional),
|
|
208
|
+
...labels.filter(({ optional }) => optional),
|
|
209
|
+
]);
|
|
210
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Plan_79 D5/D6 records issued order text and journals unregistered text only inside test roots. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Plan_79 D9: EX_SOFTWARE is outside runner EXIT and Node's own 1-13;
|
|
6
|
+
// a journal fault must stop before a hook can swallow an exception or exit(0).
|
|
7
|
+
export const ORDER_TEXT_FAULT_EXIT = 70;
|
|
8
|
+
|
|
9
|
+
export function orderTextFaultDirectory(root) {
|
|
10
|
+
return path.join(path.dirname(root), path.basename(root) + '-faults');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const recorded = new Set();
|
|
14
|
+
|
|
15
|
+
export function recordOrderText(text) {
|
|
16
|
+
if (process.env.CODEX_BRIDGE_TEST_TMP && typeof text === 'string' && text.length) {
|
|
17
|
+
recorded.add(text);
|
|
18
|
+
}
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Plan_79 D6/D8: only catalog builders may pass their entry.message; the producer guard
|
|
23
|
+
// rejects caller-supplied renders after the B5b1 handwritten deny-frame acceptance probe.
|
|
24
|
+
export function checkedOrderText(builder, key, values, data, render) {
|
|
25
|
+
checkOrderTextParameters(builder, key, values, data);
|
|
26
|
+
return recordOrderText(render(values));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function recordedComposition(message) {
|
|
30
|
+
let remainder = message;
|
|
31
|
+
for (const text of [...recorded].sort((left, right) => right.length - left.length)) {
|
|
32
|
+
remainder = remainder.split(text).join('');
|
|
33
|
+
}
|
|
34
|
+
return /^[\s:;,.]*$/.test(remainder);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function journalViolation(root, entries) {
|
|
38
|
+
// Plan_79 B3j D5: retain die's caller and its caller even under a host's shallow limit.
|
|
39
|
+
const frameLimit = Error.stackTraceLimit;
|
|
40
|
+
let stack;
|
|
41
|
+
try {
|
|
42
|
+
Error.stackTraceLimit = Math.max(frameLimit || 0, 32);
|
|
43
|
+
// Skip this shared writer so the checker, builder/transport and caller keep their positions.
|
|
44
|
+
stack = new Error().stack.split('\n').slice(2).join('\n');
|
|
45
|
+
} finally {
|
|
46
|
+
Error.stackTraceLimit = frameLimit;
|
|
47
|
+
}
|
|
48
|
+
// Plan_79 B3j D5 acceptance: only the immediate builder/transport caller owns the refusal.
|
|
49
|
+
const caller = stack.split('\n')[2]?.trim() ?? null;
|
|
50
|
+
const directory = path.join(root, 'order-text-violations');
|
|
51
|
+
const journal = entries.map((entry) => ({ ...entry, pid: process.pid, caller, stack }));
|
|
52
|
+
try {
|
|
53
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
54
|
+
fs.appendFileSync(path.join(directory, `${process.pid}.jsonl`),
|
|
55
|
+
journal.map((entry) => JSON.stringify(entry) + '\n').join(''));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
journalFault(root, journal, error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Plan_79 D9 (review B10a): the module's only catch hands a failed write here, and this never
|
|
62
|
+
// returns. finally ends the process even when stderr or the marker fails, so neither an enclosing
|
|
63
|
+
// catch in a hook nor its exit(0) can hide the violation; the marker sits beside the test root.
|
|
64
|
+
function journalFault(root, journal, error) {
|
|
65
|
+
const lines = journal.map((entry) => JSON.stringify({ ...entry, error: error?.message }) + '\n');
|
|
66
|
+
try {
|
|
67
|
+
fs.writeSync(2, lines.map((line) => `order-text-record: journal write failed: ${line}`).join(''));
|
|
68
|
+
const faults = orderTextFaultDirectory(root);
|
|
69
|
+
fs.mkdirSync(faults, { recursive: true });
|
|
70
|
+
fs.appendFileSync(path.join(faults, `${process.pid}.jsonl`), lines.join(''));
|
|
71
|
+
} finally {
|
|
72
|
+
process.exit(ORDER_TEXT_FAULT_EXIT);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Plan_79 D6: check text inputs before a builder can launder them into recorded output.
|
|
77
|
+
export function checkOrderTextParameters(builder, key, values, data) {
|
|
78
|
+
const root = process.env.CODEX_BRIDGE_TEST_TMP;
|
|
79
|
+
if (!root) return values;
|
|
80
|
+
const entries = [];
|
|
81
|
+
for (const [parameter, value] of Object.entries(values)) {
|
|
82
|
+
if (data.includes(parameter) || value === undefined) continue;
|
|
83
|
+
if (typeof value === 'string' && recordedComposition(value)) continue;
|
|
84
|
+
// Never serialize/coerce an arbitrary object: even circular objects must be journaled.
|
|
85
|
+
entries.push({
|
|
86
|
+
kind: 'parameter', builder, key, parameter,
|
|
87
|
+
message: typeof value === 'string' ? value : undefined, valueType: typeof value,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (entries.length) journalViolation(root, entries);
|
|
91
|
+
return values;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function checkOutgoingRefusal(message, transport) {
|
|
95
|
+
const root = process.env.CODEX_BRIDGE_TEST_TMP;
|
|
96
|
+
if (!root) return message;
|
|
97
|
+
// Plan_79 D9: non-string text is a violation, without serializing arbitrary objects.
|
|
98
|
+
if (typeof message !== 'string') {
|
|
99
|
+
journalViolation(root, [{ kind: 'outgoing', transport, valueType: typeof message }]);
|
|
100
|
+
} else if (!recordedComposition(message)) {
|
|
101
|
+
journalViolation(root, [{ kind: 'outgoing', transport, message }]);
|
|
102
|
+
}
|
|
103
|
+
return message;
|
|
104
|
+
}
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Owns one responsibility: the grammar of a single order value.
|
|
3
|
-
* Plan_63 D9: call and header readers share this leaf without importing either registry.
|
|
4
|
-
*/
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
|
|
7
|
-
const PLACEHOLDER_VALUES = new Set(['todo', 'tbd', 'label', 'none', 'order id', 'scope', 'xxx']);
|
|
8
|
-
|
|
9
|
-
function cleanValue(value) {
|
|
10
|
-
|
|
11
|
-
return
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function isAbsoluteTaskFilePath(value) {
|
|
15
|
-
const cleaned = cleanValue(value);
|
|
16
|
-
return path.posix.isAbsolute(cleaned) || path.win32.isAbsolute(cleaned);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isPlaceholder(value) {
|
|
20
|
-
const cleaned = cleanValue(value);
|
|
21
|
-
if (!cleaned) return true;
|
|
22
|
-
if (cleaned.startsWith('<') && cleaned.endsWith('>')) return true;
|
|
23
|
-
return PLACEHOLDER_VALUES.has(cleaned.replace(/[.,;:]+$/, '').trim().toLowerCase());
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Plan_59: scope is a real phase value, while it remains a placeholder for existing inputs. Plan_63 D5: none is a
|
|
27
|
-
// real reasoning effort the flag channel always accepted, so the header channel must not refuse it as a template.
|
|
28
|
-
const REAL_VALUES = Object.freeze({ phase: 'scope', effort: 'none' });
|
|
29
|
-
export const isInputPlaceholder = (value, label) =>
|
|
30
|
-
!(Object.hasOwn(REAL_VALUES, label) && cleanValue(value) === REAL_VALUES[label]) && isPlaceholder(value);
|
|
31
|
-
|
|
32
|
-
// Plan_75 D5, 2026-10-03 20:42: header parsing must reuse the existing grant value grammar.
|
|
33
|
-
export function splitGrantValue(value) {
|
|
34
|
-
if (isPlaceholder(value)) return null;
|
|
35
|
-
|
|
36
|
-
for (const separator of [/\s+—\s+/, /\s+-\s+/, /\s*:\s*/]) {
|
|
37
|
-
const match = value.match(new RegExp(`^(.+?)${separator.source}(.+)$`));
|
|
38
|
-
if (!match) continue;
|
|
39
|
-
const run = cleanValue(match[1]);
|
|
40
|
-
const reason = cleanValue(match[2]);
|
|
41
|
-
if (isPlaceholder(run) || isPlaceholder(reason)) return null;
|
|
42
|
-
return { run, reason };
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Owns one responsibility: the grammar of a single order value.
|
|
3
|
+
* Plan_63 D9: call and header readers share this leaf without importing either registry.
|
|
4
|
+
*/
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const PLACEHOLDER_VALUES = new Set(['todo', 'tbd', 'label', 'none', 'order id', 'scope', 'xxx']);
|
|
8
|
+
|
|
9
|
+
function cleanValue(value) {
|
|
10
|
+
const text = typeof value === 'string' ? value : '';
|
|
11
|
+
return text.trim().replace(/^([`'\"])(.*)\1$/, '$2').trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isAbsoluteTaskFilePath(value) {
|
|
15
|
+
const cleaned = cleanValue(value);
|
|
16
|
+
return path.posix.isAbsolute(cleaned) || path.win32.isAbsolute(cleaned);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isPlaceholder(value) {
|
|
20
|
+
const cleaned = cleanValue(value);
|
|
21
|
+
if (!cleaned) return true;
|
|
22
|
+
if (cleaned.startsWith('<') && cleaned.endsWith('>')) return true;
|
|
23
|
+
return PLACEHOLDER_VALUES.has(cleaned.replace(/[.,;:]+$/, '').trim().toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Plan_59: scope is a real phase value, while it remains a placeholder for existing inputs. Plan_63 D5: none is a
|
|
27
|
+
// real reasoning effort the flag channel always accepted, so the header channel must not refuse it as a template.
|
|
28
|
+
const REAL_VALUES = Object.freeze({ phase: 'scope', effort: 'none' });
|
|
29
|
+
export const isInputPlaceholder = (value, label) =>
|
|
30
|
+
!(Object.hasOwn(REAL_VALUES, label) && cleanValue(value) === REAL_VALUES[label]) && isPlaceholder(value);
|
|
31
|
+
|
|
32
|
+
// Plan_75 D5, 2026-10-03 20:42: header parsing must reuse the existing grant value grammar.
|
|
33
|
+
export function splitGrantValue(value) {
|
|
34
|
+
if (isPlaceholder(value)) return null;
|
|
35
|
+
|
|
36
|
+
for (const separator of [/\s+—\s+/, /\s+-\s+/, /\s*:\s*/]) {
|
|
37
|
+
const match = value.match(new RegExp(`^(.+?)${separator.source}(.+)$`));
|
|
38
|
+
if (!match) continue;
|
|
39
|
+
const run = cleanValue(match[1]);
|
|
40
|
+
const reason = cleanValue(match[2]);
|
|
41
|
+
if (isPlaceholder(run) || isPlaceholder(reason)) return null;
|
|
42
|
+
return { run, reason };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|