@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
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import { CLI_NAMES } from './cli-names.mjs';
|
|
3
3
|
import { parseDispatcherCall } from './dispatcher-call.mjs';
|
|
4
4
|
import { ORDER_AGENTS } from './order-schema.mjs';
|
|
5
|
+
import { orderRuleMessage } from './order-rules.mjs';
|
|
6
|
+
import { orderDiagnosticMessage } from './order-diagnostics.mjs';
|
|
7
|
+
import { checkOutgoingRefusal } from './order-text-record.mjs';
|
|
5
8
|
|
|
6
9
|
function refusal(reason) {
|
|
7
|
-
|
|
10
|
+
const text = orderDiagnosticMessage('dispatcher-refusal', { reason });
|
|
11
|
+
checkOutgoingRefusal(text, 'refusal');
|
|
12
|
+
return { refusal: text };
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
function assembleRunCommand(agentType, taskFile) {
|
|
@@ -15,7 +20,7 @@ function assembleRunCommand(agentType, taskFile) {
|
|
|
15
20
|
/** Plan_63 D11: prompt templates and the gate must use the same command assembler. */
|
|
16
21
|
export function renderRunCommandTemplate(agentType) {
|
|
17
22
|
if (typeof agentType !== 'string' || !ORDER_AGENTS.includes(agentType)) {
|
|
18
|
-
throw new Error(
|
|
23
|
+
throw new Error(orderDiagnosticMessage('dispatcher-agent-input-known', { agentType }));
|
|
19
24
|
}
|
|
20
25
|
return assembleRunCommand(agentType, '<task-file path from the orchestrator>');
|
|
21
26
|
}
|
|
@@ -23,15 +28,16 @@ export function renderRunCommandTemplate(agentType) {
|
|
|
23
28
|
/** Builds the only permitted run command directly from the immutable order transcript. */
|
|
24
29
|
export function canonicalRunCommand(agentType, promptText) {
|
|
25
30
|
if (typeof agentType !== 'string' || !ORDER_AGENTS.includes(agentType)) {
|
|
26
|
-
return refusal(
|
|
31
|
+
return refusal(orderDiagnosticMessage('dispatcher-agent-input-known', { agentType }));
|
|
27
32
|
}
|
|
28
|
-
if (typeof promptText !== 'string') return refusal('prompt
|
|
33
|
+
if (typeof promptText !== 'string') return refusal(orderDiagnosticMessage('dispatcher-prompt-text-required'));
|
|
29
34
|
|
|
30
35
|
// Plan_63 D9, OW-054: the call cannot switch continuation or bypass order ownership.
|
|
31
36
|
const { inputs, problems } = parseDispatcherCall(agentType, promptText);
|
|
32
37
|
if (problems.length) {
|
|
33
|
-
const reasons = problems.map(({ line, reason }) => line === null ? reason
|
|
34
|
-
|
|
38
|
+
const reasons = problems.map(({ line, reason }) => line === null ? reason
|
|
39
|
+
: orderRuleMessage('dispatcher-call-refusal', { part: 'command-line', line, reason }));
|
|
40
|
+
return refusal(orderRuleMessage('dispatcher-call-refusal', { part: 'command', reasons: reasons.join('; ') }));
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
return { command: assembleRunCommand(agentType, inputs.get('task file')) };
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { canonicalRunCommand, sameCommand } from './dispatcher-command.mjs';
|
|
3
3
|
import { ownTranscriptPath, transcriptPrompt } from './dispatcher-order.mjs';
|
|
4
4
|
import { HANDBACK_TOOL } from './hook-definitions.mjs';
|
|
5
|
+
import { dispatcherDenial, dispatcherProtocolMessage } from './dispatcher-protocol.mjs';
|
|
6
|
+
import { orderDiagnosticMessage } from './order-diagnostics.mjs';
|
|
5
7
|
|
|
6
8
|
export { HANDBACK_TOOL };
|
|
7
9
|
|
|
@@ -10,7 +12,9 @@ export function gateOrder(payload) {
|
|
|
10
12
|
const prompt = transcriptPath ? transcriptPrompt(transcriptPath) : null;
|
|
11
13
|
if (!transcriptPath || !prompt) {
|
|
12
14
|
return {
|
|
13
|
-
refusal:
|
|
15
|
+
refusal: orderDiagnosticMessage('dispatcher-order-transcript-readable', {
|
|
16
|
+
transcriptPath: transcriptPath || '(none)',
|
|
17
|
+
}),
|
|
14
18
|
};
|
|
15
19
|
}
|
|
16
20
|
return canonicalRunCommand(payload.agent_type, prompt);
|
|
@@ -36,9 +40,7 @@ export function isFinalOutput(output) {
|
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
function denial(message, order) {
|
|
39
|
-
|
|
40
|
-
if (typeof order?.command !== 'string' || order.command.length === 0) return prefix;
|
|
41
|
-
return `${prefix}\n${order.command}\nRun it exactly, in the foreground; repeating it attaches to the same run.`;
|
|
43
|
+
return dispatcherDenial(message, order?.command);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
function deny(message, order, stateUpdate) {
|
|
@@ -57,43 +59,45 @@ export function decidePreToolUse({ payload, order, state }) {
|
|
|
57
59
|
// after the handback and wrote into a tree the caller believed settled; a substituted FAIL followed by a
|
|
58
60
|
// late run would repeat exactly that, so nothing — not even the canonical command — runs past delivery.
|
|
59
61
|
if (state?.handback === 'delivered') {
|
|
60
|
-
return deny('
|
|
62
|
+
return deny(dispatcherProtocolMessage('handback-delivered'));
|
|
61
63
|
}
|
|
62
64
|
if (toolName === 'Bash') {
|
|
63
65
|
if (order?.refusal) {
|
|
64
|
-
return deny('
|
|
66
|
+
return deny(dispatcherProtocolMessage('order-unreadable'), order);
|
|
65
67
|
}
|
|
66
68
|
if (order?.command && sameCommand(payload.tool_input?.command, order.command)
|
|
67
69
|
&& payload.tool_input?.run_in_background !== true) return { kind: 'pass' };
|
|
68
|
-
return deny('
|
|
70
|
+
return deny(dispatcherProtocolMessage('canonical-foreground'), order);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
if (toolName === HANDBACK_TOOL) {
|
|
72
|
-
if (order?.refusal) return allow(
|
|
74
|
+
if (order?.refusal) return allow(orderDiagnosticMessage('dispatcher-order-refusal', { refusal: order.refusal }));
|
|
73
75
|
if (typeof state?.runReceiptConflict === 'string' && state.runReceiptConflict.length > 0) {
|
|
74
|
-
return allow('
|
|
75
|
-
|
|
76
|
+
return allow(orderDiagnosticMessage('dispatcher-receipt-conflict', {
|
|
77
|
+
runReceipt: state.runReceipt, runReceiptConflict: state.runReceiptConflict,
|
|
78
|
+
}));
|
|
76
79
|
}
|
|
77
80
|
if (typeof state?.runnerOutput === 'string' && state.runnerOutput.length > 0) {
|
|
78
|
-
if (state.runnerFinal) return allow(
|
|
79
|
-
|
|
81
|
+
if (state.runnerFinal) return allow(orderDiagnosticMessage('dispatcher-runner-output',
|
|
82
|
+
{ output: state.runnerOutput }));
|
|
83
|
+
return deny(dispatcherProtocolMessage('verdict-pending'), order);
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
const attempts = Number.isInteger(state?.handbackAttempts) ? state.handbackAttempts : 0;
|
|
83
87
|
const handbackAttempts = attempts + 1;
|
|
84
88
|
if (handbackAttempts === 1) {
|
|
85
|
-
return deny('
|
|
89
|
+
return deny(dispatcherProtocolMessage('runner-verdict-required'), order,
|
|
86
90
|
{ handbackAttempts });
|
|
87
91
|
}
|
|
88
92
|
return {
|
|
89
93
|
kind: 'allow',
|
|
90
|
-
message: '
|
|
94
|
+
message: orderDiagnosticMessage('dispatcher-did-not-delegate', { command: order.command }),
|
|
91
95
|
stateUpdate: { handbackAttempts, handback: 'delivered' },
|
|
92
96
|
};
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
if (order?.refusal) {
|
|
96
|
-
return deny('
|
|
100
|
+
return deny(dispatcherProtocolMessage('order-unreadable'), order);
|
|
97
101
|
}
|
|
98
|
-
return deny('
|
|
102
|
+
return deny(dispatcherProtocolMessage('canonical-before-handback'), order);
|
|
99
103
|
}
|
|
@@ -7,6 +7,7 @@ import { readDispatcherPin } from './dispatcher-pin.mjs';
|
|
|
7
7
|
import { parseJsonText } from './json-file.mjs';
|
|
8
8
|
import { compareModelFamilies } from './model-family.mjs';
|
|
9
9
|
import { normalizeLedger, readLedgerFile, recordObservation } from './observation-ledger.mjs';
|
|
10
|
+
import { orderDiagnosticMessage } from './order-diagnostics.mjs';
|
|
10
11
|
|
|
11
12
|
export const DISPATCHER_MODEL_FILE = 'dispatcher-model.json';
|
|
12
13
|
|
|
@@ -46,13 +47,9 @@ export function modelObservation({ agentType, hostVersion, pin, models, complete
|
|
|
46
47
|
...(pin.family === null ? { pinReasons: pin.reasons } : {}),
|
|
47
48
|
...(incompleteMatch ? { complete: false } : {}),
|
|
48
49
|
};
|
|
49
|
-
const detail =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
? `${agentType} ran on ${parsed.join(', ')} matching the installed contract pin ${pin.family}`
|
|
53
|
-
: incompleteMatch
|
|
54
|
-
? `${agentType} model comparison is undetermined: the transcript has unreadable records`
|
|
55
|
-
: `${agentType} model comparison against the installed contract is undetermined`;
|
|
50
|
+
const detail = orderDiagnosticMessage('dispatcher-model-observation', {
|
|
51
|
+
verdict, agentType, models: parsed.join(', '), pinFamily: pin.family, incompleteMatch,
|
|
52
|
+
});
|
|
56
53
|
return { key: `${hostVersion || 'unknown'}|${agentType}`, verdict, detail, data };
|
|
57
54
|
}
|
|
58
55
|
|
|
@@ -107,6 +104,8 @@ export async function observeDispatcherModelStop({ brandRoot, stateDir, input, h
|
|
|
107
104
|
// D4: a diagnostic write failure must not block the stop or hide an observed quota violation.
|
|
108
105
|
}
|
|
109
106
|
return observation.verdict === 'violation'
|
|
110
|
-
?
|
|
107
|
+
? orderDiagnosticMessage('dispatcher-model-stop-warning', {
|
|
108
|
+
agentType: input.agent_type, models: observation.data.parsed.join(', '), pinFamily: observation.data.pinFamily,
|
|
109
|
+
})
|
|
111
110
|
: null;
|
|
112
111
|
}
|
|
@@ -10,6 +10,7 @@ import { CLI_NAMES } from './cli-names.mjs';
|
|
|
10
10
|
import { parseFrontmatter } from './frontmatter.mjs';
|
|
11
11
|
import { readOwnerRoots } from './install-owner-roots.mjs';
|
|
12
12
|
import { modelFamily } from './model-family.mjs';
|
|
13
|
+
import { orderDiagnosticMessage } from './order-diagnostics.mjs';
|
|
13
14
|
|
|
14
15
|
export function readDispatcherPin({ brandRoot, agentType }) {
|
|
15
16
|
const owners = readOwnerRoots({ brandRoot });
|
|
@@ -23,8 +24,8 @@ export function readDispatcherPin({ brandRoot, agentType }) {
|
|
|
23
24
|
try {
|
|
24
25
|
content = fs.readFileSync(file, 'utf8');
|
|
25
26
|
} catch (error) {
|
|
26
|
-
const problem = error.code === 'ENOENT' ? '
|
|
27
|
-
reasons.push(
|
|
27
|
+
const problem = error.code === 'ENOENT' ? 'missing' : 'unreadable';
|
|
28
|
+
reasons.push(orderDiagnosticMessage('dispatcher-pin-file-readable', { file, problem, cli: CLI_NAMES[0], root }));
|
|
28
29
|
continue;
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -35,16 +36,16 @@ export function readDispatcherPin({ brandRoot, agentType }) {
|
|
|
35
36
|
frontmatter = null;
|
|
36
37
|
}
|
|
37
38
|
if (frontmatter === null) {
|
|
38
|
-
reasons.push(
|
|
39
|
+
reasons.push(orderDiagnosticMessage('dispatcher-pin-frontmatter-readable', { file }));
|
|
39
40
|
continue;
|
|
40
41
|
}
|
|
41
42
|
if (frontmatter.name !== agentType) {
|
|
42
|
-
reasons.push(
|
|
43
|
+
reasons.push(orderDiagnosticMessage('dispatcher-pin-agent-name', { file, agentType }));
|
|
43
44
|
continue;
|
|
44
45
|
}
|
|
45
46
|
const family = modelFamily(frontmatter.model);
|
|
46
47
|
if (family === null) {
|
|
47
|
-
reasons.push(
|
|
48
|
+
reasons.push(orderDiagnosticMessage('dispatcher-pin-model-family', { file }));
|
|
48
49
|
continue;
|
|
49
50
|
}
|
|
50
51
|
pins.push({ root, family });
|
|
@@ -52,7 +53,7 @@ export function readDispatcherPin({ brandRoot, agentType }) {
|
|
|
52
53
|
|
|
53
54
|
if (new Set(pins.map(({ family }) => family)).size > 1) {
|
|
54
55
|
const disagreement = pins.map(({ root, family }) => `"${root}" -> ${family}`).join(', ');
|
|
55
|
-
reasons.push(
|
|
56
|
+
reasons.push(orderDiagnosticMessage('dispatcher-pin-owner-agreement', { disagreement }));
|
|
56
57
|
}
|
|
57
58
|
return reasons.length
|
|
58
59
|
? { family: null, reasons }
|
|
@@ -5,10 +5,63 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { renderRunCommandTemplate } from './dispatcher-command.mjs';
|
|
7
7
|
import { TASK_FILE_INPUT } from './dispatcher-call.mjs';
|
|
8
|
+
import { orderDiagnosticMessage } from './order-diagnostics.mjs';
|
|
9
|
+
import { checkedOrderText } from './order-text-record.mjs';
|
|
10
|
+
|
|
11
|
+
// Plan_79 D8: prompt and gate quote the same obligations, including corrective deny sentences.
|
|
12
|
+
export const DISPATCHER_PROTOCOL = Object.freeze({
|
|
13
|
+
'handback-delivered': Object.freeze({
|
|
14
|
+
data: Object.freeze([]),
|
|
15
|
+
message: () => 'the handback was already delivered; this dispatcher is finished and may not act further.',
|
|
16
|
+
}),
|
|
17
|
+
'order-unreadable': Object.freeze({
|
|
18
|
+
data: Object.freeze([]),
|
|
19
|
+
message: () => 'the order could not be read; nothing may run, so hand back now.',
|
|
20
|
+
}),
|
|
21
|
+
'canonical-foreground': Object.freeze({
|
|
22
|
+
data: Object.freeze([]),
|
|
23
|
+
message: () => 'only the canonical runner command may run in the foreground.',
|
|
24
|
+
}),
|
|
25
|
+
'verdict-pending': Object.freeze({
|
|
26
|
+
data: Object.freeze([]),
|
|
27
|
+
message: () => 'the run has started without a verdict; repeat the exact command to attach and wait.',
|
|
28
|
+
}),
|
|
29
|
+
'runner-verdict-required': Object.freeze({
|
|
30
|
+
data: Object.freeze([]),
|
|
31
|
+
message: () => 'the dispatcher handed back without a runner verdict; run the command first.',
|
|
32
|
+
}),
|
|
33
|
+
'canonical-before-handback': Object.freeze({
|
|
34
|
+
data: Object.freeze([]),
|
|
35
|
+
message: () => 'only the canonical runner command may run before handback.',
|
|
36
|
+
}),
|
|
37
|
+
'command-repeat': Object.freeze({
|
|
38
|
+
data: Object.freeze([]),
|
|
39
|
+
message: () => 'Run it exactly, in the foreground; repeating it attaches to the same run.',
|
|
40
|
+
}),
|
|
41
|
+
'denial': Object.freeze({
|
|
42
|
+
data: Object.freeze(['command']),
|
|
43
|
+
message: ({ reason, command }) => `codex-bridge dispatcher gate: ${reason}` +
|
|
44
|
+
(typeof command === 'string' && command.length > 0
|
|
45
|
+
? `\n${command}\n${dispatcherProtocolMessage('command-repeat')}` : ''),
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export function dispatcherProtocolMessage(key, values = {}) {
|
|
50
|
+
if (!Object.hasOwn(DISPATCHER_PROTOCOL, key)) {
|
|
51
|
+
throw new Error(orderDiagnosticMessage('dispatcher-protocol-key-known', { key }));
|
|
52
|
+
}
|
|
53
|
+
const entry = DISPATCHER_PROTOCOL[key];
|
|
54
|
+
return checkedOrderText('dispatcherProtocolMessage', key, values, entry.data, entry.message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// D8: command is data; the reason and command-repeat correction must already be checked text.
|
|
58
|
+
export function dispatcherDenial(reason, command) {
|
|
59
|
+
return dispatcherProtocolMessage('denial', { reason, command });
|
|
60
|
+
}
|
|
8
61
|
|
|
9
62
|
export function renderDispatcherProtocol(agentType) {
|
|
10
63
|
const command = renderRunCommandTemplate(agentType);
|
|
11
|
-
return `## Required dispatcher input
|
|
64
|
+
return String(`## Required dispatcher input
|
|
12
65
|
|
|
13
66
|
The call is exactly one line: \`${TASK_FILE_INPUT.label}: <task-file path from the orchestrator>\`.
|
|
14
67
|
The path must be absolute and name a file the orchestrator wrote with its file tool.
|
|
@@ -49,6 +102,16 @@ ${command}
|
|
|
49
102
|
\`\`\`
|
|
50
103
|
|
|
51
104
|
Replace the placeholder with the path from the call, nothing else.
|
|
105
|
+
The gate requires these instructions:
|
|
106
|
+
|
|
107
|
+
- ${dispatcherProtocolMessage('canonical-before-handback')}
|
|
108
|
+
- ${dispatcherProtocolMessage('canonical-foreground')}
|
|
109
|
+
- ${dispatcherProtocolMessage('command-repeat')}
|
|
110
|
+
|
|
111
|
+
If the order cannot be read, the gate's correction is:
|
|
112
|
+
|
|
113
|
+
- ${dispatcherProtocolMessage('order-unreadable')}
|
|
114
|
+
|
|
52
115
|
The first call starts the run and returns at once with \`RUN=<path> order-id=<id>\` and a
|
|
53
116
|
\`STARTED\` line. To get the verdict, run the identical command again. That call does not start
|
|
54
117
|
a second run or cost quota: it attaches to the run already in flight, prints
|
|
@@ -58,8 +121,14 @@ which is normal, not a hang. If the ceiling kills it, run the identical command
|
|
|
58
121
|
it attaches to the same run and keeps waiting.
|
|
59
122
|
|
|
60
123
|
Background execution (\`run_in_background\`, \`&\`, \`nohup\`) is prohibited, and so is inventing
|
|
61
|
-
a report from memory. Never add a flag
|
|
62
|
-
|
|
124
|
+
a report from memory. Never add a flag.
|
|
125
|
+
If you try to return before the runner's verdict, the gate's correction is:
|
|
126
|
+
|
|
127
|
+
- ${dispatcherProtocolMessage('runner-verdict-required')}
|
|
128
|
+
|
|
129
|
+
If the first call has only started the run, the gate's correction is:
|
|
130
|
+
|
|
131
|
+
- ${dispatcherProtocolMessage('verdict-pending')}
|
|
63
132
|
|
|
64
133
|
A runner refusal — an order id collision, a header problem or a grant problem — is the whole
|
|
65
134
|
answer: return it as \`FAIL\` with the runner's text. Never retry with a changed command or another
|
|
@@ -87,5 +156,8 @@ by checking \`status.json\` manually.
|
|
|
87
156
|
Inventing any outcome the runner did not print is equally prohibited. On 2026-08-13 a dispatcher said
|
|
88
157
|
\`FAIL — could not get the Codex run result because of an architectural environment limitation\` while
|
|
89
158
|
that run's \`status.json\` already said \`state=finished\`, \`status=OK\`.
|
|
90
|
-
|
|
159
|
+
After delivering the handback, the gate refuses every further action:
|
|
160
|
+
|
|
161
|
+
- ${dispatcherProtocolMessage('handback-delivered')}
|
|
162
|
+
`);
|
|
91
163
|
}
|
|
@@ -6,6 +6,7 @@ import { writeHomeJsonAtomic } from './atomic-json.mjs';
|
|
|
6
6
|
import { parseJsonText } from './json-file.mjs';
|
|
7
7
|
import { withHomeFileLock } from './file-lock.mjs';
|
|
8
8
|
import { stateDirWriter } from './home-write.mjs';
|
|
9
|
+
import { orderDiagnosticMessage } from './order-diagnostics.mjs';
|
|
9
10
|
|
|
10
11
|
// Plan_65 B5: every write goes through the registry id so purge accounts for records, locks and
|
|
11
12
|
// temporaries alike.
|
|
@@ -14,7 +15,7 @@ const ARTIFACT = 'dispatcher-state';
|
|
|
14
15
|
function assertIdentity(sessionId, agentId) {
|
|
15
16
|
if (typeof sessionId !== 'string' || sessionId.length === 0
|
|
16
17
|
|| typeof agentId !== 'string' || agentId.length === 0) {
|
|
17
|
-
throw new TypeError('
|
|
18
|
+
throw new TypeError(orderDiagnosticMessage('dispatcher-state-identity-required'));
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -57,7 +58,7 @@ function clockValue(ids) {
|
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
export async function updateDispatcherState(ids, mutate) {
|
|
60
|
-
if (typeof mutate !== 'function') throw new TypeError('
|
|
61
|
+
if (typeof mutate !== 'function') throw new TypeError(orderDiagnosticMessage('dispatcher-state-mutator-required'));
|
|
61
62
|
const file = dispatcherStatePath(ids);
|
|
62
63
|
const { sessionId, agentId } = ids;
|
|
63
64
|
const writer = stateDirWriter(ids.stateDir);
|
|
@@ -69,7 +70,7 @@ export async function updateDispatcherState(ids, mutate) {
|
|
|
69
70
|
const result = await mutate(current);
|
|
70
71
|
const candidate = result === undefined ? current : result;
|
|
71
72
|
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
72
|
-
throw new TypeError('
|
|
73
|
+
throw new TypeError(orderDiagnosticMessage('dispatcher-state-record-required'));
|
|
73
74
|
}
|
|
74
75
|
const record = {
|
|
75
76
|
...candidate,
|
|
@@ -1,55 +1,57 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reads which host roots own this home from the installation record, read-only, for hooks.
|
|
3
|
-
* Plan_67 D10 needs every recorded owner; the home image has no cli/ modules.
|
|
4
|
-
*/
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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
|
-
return { problem: '
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (parsed.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reads which host roots own this home from the installation record, read-only, for hooks.
|
|
3
|
+
* Plan_67 D10 needs every recorded owner; the home image has no cli/ modules.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { CLI_NAMES } from './cli-names.mjs';
|
|
8
|
+
import { homeArtifact } from './home-registry.mjs';
|
|
9
|
+
import { parseJsonText } from './json-file.mjs';
|
|
10
|
+
import { orderDiagnosticMessage } from './order-diagnostics.mjs';
|
|
11
|
+
|
|
12
|
+
const isObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
|
|
14
|
+
export function isFormat2(parsed) {
|
|
15
|
+
return isObject(parsed) && parsed.format === 2;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readOwnerRoots({ brandRoot }) {
|
|
19
|
+
const file = path.join(brandRoot, homeArtifact('install-record').primary[0]);
|
|
20
|
+
const detail = (problem) => orderDiagnosticMessage('install-record-owners-readable', { file, problem, cli: CLI_NAMES[0] });
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return error.code === 'ENOENT'
|
|
26
|
+
? { problem: 'missing', detail: detail('missing') }
|
|
27
|
+
: { problem: 'unreadable', detail: detail('unreadable') };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = parseJsonText(file, raw);
|
|
33
|
+
} catch {
|
|
34
|
+
return { problem: 'unreadable', detail: detail('invalid-json') };
|
|
35
|
+
}
|
|
36
|
+
if (!isFormat2(parsed)) {
|
|
37
|
+
return { problem: 'not-format-2', detail: detail('not-format-2') };
|
|
38
|
+
}
|
|
39
|
+
// Plan_67 R2: confirmation refuses ownerless records; enrollment must come first.
|
|
40
|
+
if (!isObject(parsed.owners) || Object.keys(parsed.owners).length === 0) {
|
|
41
|
+
return { problem: 'no-owners', detail: detail('no-owners') };
|
|
42
|
+
}
|
|
43
|
+
if (parsed.inventory !== 'complete') {
|
|
44
|
+
return { problem: 'inventory-incomplete', detail: detail('inventory-incomplete') };
|
|
45
|
+
}
|
|
46
|
+
if (Object.hasOwn(parsed, 'legacy')) {
|
|
47
|
+
return { problem: 'legacy-partition', detail: detail('legacy-partition') };
|
|
48
|
+
}
|
|
49
|
+
const roots = [];
|
|
50
|
+
for (const owner of Object.values(parsed.owners)) {
|
|
51
|
+
if (typeof owner?.root !== 'string' || owner.root.trim().length === 0) {
|
|
52
|
+
return { problem: 'malformed-owner', detail: detail('malformed-owner') };
|
|
53
|
+
}
|
|
54
|
+
roots.push(owner.root);
|
|
55
|
+
}
|
|
56
|
+
return { roots: roots.sort() };
|
|
57
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/** Parses blind advisor task choices so Plan_59 D5 rejects biased or ambiguous inputs before review. */
|
|
2
|
+
import { orderRuleMessage } from '../order-rules.mjs';
|
|
3
|
+
import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
|
|
4
|
+
|
|
2
5
|
const OPTION = /^\s*-\s+([a-z0-9][a-z0-9-]*):\s*(.*)$/;
|
|
3
6
|
const PREFERENCE = /recommend|prefer|рекоменд|предпочт|\(✓\)|★/iu;
|
|
4
7
|
|
|
5
8
|
function sections(text) {
|
|
6
|
-
if (typeof text !== 'string') throw new TypeError('
|
|
9
|
+
if (typeof text !== 'string') throw new TypeError(orderDiagnosticMessage('advisor-task-text-required'));
|
|
7
10
|
const found = { options: { header: null, lines: [] }, paths: { header: null, lines: [] } };
|
|
8
11
|
let current = null;
|
|
9
12
|
for (const [index, value] of text.split(/\r\n|\n|\r/).entries()) {
|
|
@@ -39,31 +42,37 @@ export function parseAdvisorTask(text) {
|
|
|
39
42
|
return { options: optionsFrom(options.lines), paths: pathsFrom(paths.lines) };
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
function at(line) {
|
|
43
|
-
return `line ${line.number} ${JSON.stringify(line.value)}`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
45
|
export function advisorTaskRefusal(text) {
|
|
47
46
|
const { options, paths } = sections(text);
|
|
48
|
-
if (!options.header) return '
|
|
47
|
+
if (!options.header) return orderRuleMessage('advisor-task-options', { violation: 'missing' });
|
|
49
48
|
const ids = new Set();
|
|
50
49
|
for (const line of options.lines) {
|
|
51
50
|
const preference = line.value.match(PREFERENCE);
|
|
52
51
|
if (preference) {
|
|
53
|
-
return
|
|
52
|
+
return orderRuleMessage('advisor-task-options', {
|
|
53
|
+
violation: 'preference', lineNo: line.number, line: line.value, preference: preference[0],
|
|
54
|
+
});
|
|
54
55
|
}
|
|
55
56
|
if (!/^\s*[-*+](?:\s|$)/.test(line.value)) continue;
|
|
56
57
|
const match = line.value.match(OPTION);
|
|
57
58
|
if (!match) {
|
|
58
|
-
return
|
|
59
|
+
return orderRuleMessage('advisor-task-options', {
|
|
60
|
+
violation: 'option-shape', lineNo: line.number, line: line.value,
|
|
61
|
+
});
|
|
59
62
|
}
|
|
60
|
-
if (ids.has(match[1])) return
|
|
63
|
+
if (ids.has(match[1])) return orderRuleMessage('advisor-task-options', {
|
|
64
|
+
violation: 'duplicate', lineNo: line.number, line: line.value, optionId: match[1],
|
|
65
|
+
});
|
|
61
66
|
ids.add(match[1]);
|
|
62
67
|
}
|
|
63
|
-
if (ids.size < 2) return
|
|
64
|
-
|
|
68
|
+
if (ids.size < 2) return orderRuleMessage('advisor-task-options', {
|
|
69
|
+
violation: 'too-few', lineNo: options.header.number, line: options.header.value,
|
|
70
|
+
});
|
|
71
|
+
if (!paths.header) return orderRuleMessage('advisor-task-paths', { violation: 'missing' });
|
|
65
72
|
if (!pathsFrom(paths.lines).length) {
|
|
66
|
-
return
|
|
73
|
+
return orderRuleMessage('advisor-task-paths', {
|
|
74
|
+
violation: 'empty', lineNo: paths.header.number, line: paths.header.value,
|
|
75
|
+
});
|
|
67
76
|
}
|
|
68
77
|
return null;
|
|
69
78
|
}
|
|
@@ -16,6 +16,7 @@ import { changedPaths, line, readText } from './paths.mjs';
|
|
|
16
16
|
import { splitRunChanges } from './environment.mjs';
|
|
17
17
|
import { scoutCoverage } from './verdict.mjs';
|
|
18
18
|
import { orderInputName } from '../order-schema.mjs';
|
|
19
|
+
import { unavailableRows } from './unavailable-rows.mjs';
|
|
19
20
|
|
|
20
21
|
/** What the run itself changed, and what the tooling around it changed while it worked. */
|
|
21
22
|
const runChanges = (runDir) =>
|
|
@@ -235,16 +236,6 @@ export function limitReply(ctx, meta) {
|
|
|
235
236
|
return rows;
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
/** Plan_60 D2: pre-start and post-start refusals share the same handoff contract. */
|
|
239
|
-
export function unavailableRows(signal) {
|
|
240
|
-
return [
|
|
241
|
-
'UNAVAILABLE — Codex is not available (signed out); ' +
|
|
242
|
-
'hand the task to the next executor, do not retry',
|
|
243
|
-
`Signal: ${line(signal, 170)}`,
|
|
244
|
-
'Operator check: codex --version; codex login status (sign in with codex login)',
|
|
245
|
-
];
|
|
246
|
-
}
|
|
247
|
-
|
|
248
239
|
export function unavailableReply(ctx, meta) {
|
|
249
240
|
const rows = unavailableRows(meta.reason);
|
|
250
241
|
if (ctx.agent === 'codex-build') {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Plan_60 D2: pre-start and post-start refusals share the same UNAVAILABLE handoff rows. */
|
|
2
|
+
import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
|
|
3
|
+
import { line } from './paths.mjs';
|
|
4
|
+
|
|
5
|
+
export function unavailableRows(signal) {
|
|
6
|
+
return [
|
|
7
|
+
orderDiagnosticMessage('codex-unavailable-handoff', { variant: 'handoff' }),
|
|
8
|
+
orderDiagnosticMessage('codex-unavailable-handoff', { variant: 'signal', signal: line(signal, 170) }),
|
|
9
|
+
orderDiagnosticMessage('codex-unavailable-handoff', { variant: 'operator-check' }),
|
|
10
|
+
];
|
|
11
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Plan_79 D4: owns diagnostics for the command-line flags the dispatcher passes to the runner. */
|
|
2
|
+
export const DIAGNOSTICS = Object.freeze({
|
|
3
|
+
'no-wait-grant-conflict': Object.freeze({
|
|
4
|
+
data: Object.freeze(['grant']),
|
|
5
|
+
category: 'cli-flags',
|
|
6
|
+
message: ({ grant }) =>
|
|
7
|
+
`--no-wait cannot be combined with a ${grant} grant: ` +
|
|
8
|
+
'checking an existing run must never authorize a new one.',
|
|
9
|
+
}),
|
|
10
|
+
'task-channel-conflict': Object.freeze({
|
|
11
|
+
data: Object.freeze([]),
|
|
12
|
+
category: 'cli-flags',
|
|
13
|
+
message: () => 'task text was supplied through both stdin and --task-file; choose exactly one channel',
|
|
14
|
+
}),
|
|
15
|
+
'unexpected-argument': Object.freeze({
|
|
16
|
+
data: Object.freeze(['value']),
|
|
17
|
+
category: 'cli-flags',
|
|
18
|
+
message: ({ value }) => `unexpected argument: ${value}`,
|
|
19
|
+
}),
|
|
20
|
+
'unknown-flag': Object.freeze({
|
|
21
|
+
data: Object.freeze(['name']),
|
|
22
|
+
category: 'cli-flags',
|
|
23
|
+
message: ({ name, help }) =>
|
|
24
|
+
`unknown flag --${name}: codex-bridge run takes only --agent, --task-file and --no-wait; ` +
|
|
25
|
+
'the order belongs in the task-file header' + (help ? `\n${help}` : ''),
|
|
26
|
+
}),
|
|
27
|
+
'boolean-flag-value': Object.freeze({
|
|
28
|
+
data: Object.freeze(['name', 'value']),
|
|
29
|
+
category: 'cli-flags',
|
|
30
|
+
message: ({ name, value }) =>
|
|
31
|
+
`--${name} takes no value, or one of 1/true/yes/0/false/no; got ${JSON.stringify(value)}. ` +
|
|
32
|
+
'A placeholder left in from the prompt template is not consent.',
|
|
33
|
+
}),
|
|
34
|
+
'missing-flag-value': Object.freeze({
|
|
35
|
+
data: Object.freeze(['key']),
|
|
36
|
+
category: 'cli-flags',
|
|
37
|
+
message: ({ key }) => `missing value for ${key}`,
|
|
38
|
+
}),
|
|
39
|
+
'flag-shell-safety': Object.freeze({
|
|
40
|
+
data: Object.freeze(['name', 'sequence']),
|
|
41
|
+
category: 'cli-flags',
|
|
42
|
+
message: ({ name, sequence }) =>
|
|
43
|
+
`--${name} contains forbidden shell sequence ${JSON.stringify(sequence)}; ` +
|
|
44
|
+
'put free text in the task file and pass only a short command-line value.',
|
|
45
|
+
}),
|
|
46
|
+
'agent-required': Object.freeze({
|
|
47
|
+
data: Object.freeze([]),
|
|
48
|
+
category: 'cli-flags',
|
|
49
|
+
message: () => '--agent is required',
|
|
50
|
+
}),
|
|
51
|
+
'agent-known': Object.freeze({
|
|
52
|
+
data: Object.freeze(['agent']),
|
|
53
|
+
category: 'cli-flags',
|
|
54
|
+
message: ({ agent }) => `unknown --agent ${agent}`,
|
|
55
|
+
}),
|
|
56
|
+
});
|