@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
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from '../write-meta.mjs';
|
|
18
18
|
import { TASK_HASH_SCHEME } from '../meta/chain.mjs';
|
|
19
19
|
import { orderInputName } from '../order-schema.mjs';
|
|
20
|
+
import { orderLabelReason, orderRuleMessage } from '../order-rules.mjs';
|
|
21
|
+
import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
|
|
20
22
|
import { EXIT } from './exit-codes.mjs';
|
|
21
23
|
import { passGate } from './pass-gate.mjs';
|
|
22
24
|
import { setRun } from './run-context.mjs';
|
|
@@ -53,7 +55,10 @@ const RUNNER_ENTRY = fileURLToPath(new URL('../run-codex.mjs', import.meta.url))
|
|
|
53
55
|
const stamp = () => {
|
|
54
56
|
const d = new Date();
|
|
55
57
|
const p = (n) => String(n).padStart(2, '0');
|
|
56
|
-
return
|
|
58
|
+
return [
|
|
59
|
+
[d.getFullYear(), p(d.getMonth() + 1), p(d.getDate())].join('-'),
|
|
60
|
+
[p(d.getHours()), p(d.getMinutes()), p(d.getSeconds())].join(''),
|
|
61
|
+
].join('_');
|
|
57
62
|
};
|
|
58
63
|
|
|
59
64
|
/**
|
|
@@ -69,7 +74,7 @@ export function makeRunDir(base) {
|
|
|
69
74
|
return dir;
|
|
70
75
|
} catch (err) {
|
|
71
76
|
if (err.code !== 'EEXIST') throw err;
|
|
72
|
-
dir =
|
|
77
|
+
dir = [base, n].join('-');
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
}
|
|
@@ -108,16 +113,14 @@ export async function launcher(argv = process.argv.slice(2)) {
|
|
|
108
113
|
// lists are empty and the check passes. Only paths named in `scope new:` may be absent.
|
|
109
114
|
const scopeRefusal = validateScope(repoRoot, opts.scopePatterns, opts.scopeNewPatterns);
|
|
110
115
|
if (scopeRefusal) {
|
|
111
|
-
die(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
`Action: ${scopeRefusal.action}. The run folder was not created; quota was not spent.`,
|
|
115
|
-
);
|
|
116
|
+
die([orderLabelReason(orderInputName(scopeRefusal.label), orderRuleMessage('scope-pattern-refusal', {
|
|
117
|
+
pattern: scopeRefusal.pattern, reason: scopeRefusal.reason, action: scopeRefusal.action,
|
|
118
|
+
})), orderDiagnosticMessage('run-not-created')].join(' '));
|
|
116
119
|
}
|
|
117
120
|
// Plan_77 D6: project-dir.mjs:84 recursively recreated a retired CODEX_RUNS_ROOT.
|
|
118
121
|
// Refuse before that mkdir, using the same resolution that checked the advice header.
|
|
119
122
|
const stale = staleOverrideRefusal(resolution);
|
|
120
|
-
if (stale) die(
|
|
123
|
+
if (stale) die([stale, orderDiagnosticMessage('run-not-created')].join('\n'), EXIT.FAIL);
|
|
121
124
|
const projectRunsRoot = resolveProjectRunsDir(resolution.root, repoRoot).dir;
|
|
122
125
|
// Plan_60 D4c, 2026-09-24: two launchers of one order both billed. The claim comes before markAbandoned, or a
|
|
123
126
|
// contender could close a run whose worker is admitting itself and start a second paid one.
|
|
@@ -134,10 +137,9 @@ export async function launcher(argv = process.argv.slice(2)) {
|
|
|
134
137
|
if (isGitRepo) {
|
|
135
138
|
const drift = abandonedBranchDrift(projectRunsRoot, repoRoot, branchName(repoRoot));
|
|
136
139
|
if (drift) {
|
|
137
|
-
die(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
);
|
|
140
|
+
die(orderDiagnosticMessage('repository-detached-after-abandoned-run', {
|
|
141
|
+
run: drift.run, branch: drift.branch,
|
|
142
|
+
}));
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
|
|
@@ -302,8 +304,8 @@ export async function launcher(argv = process.argv.slice(2)) {
|
|
|
302
304
|
const { reply } = writeFailure(
|
|
303
305
|
runDir,
|
|
304
306
|
opts.agent,
|
|
305
|
-
|
|
306
|
-
['
|
|
307
|
+
orderDiagnosticMessage('cmd-argument-transport', { argument: unsafe }),
|
|
308
|
+
[orderDiagnosticMessage('codex-not-started')],
|
|
307
309
|
true,
|
|
308
310
|
);
|
|
309
311
|
console.log(reply);
|
|
@@ -335,9 +337,9 @@ export async function launcher(argv = process.argv.slice(2)) {
|
|
|
335
337
|
const started = await new Promise((resolve) => {
|
|
336
338
|
worker.once('spawn', () => resolve(true));
|
|
337
339
|
worker.once('error', (err) => {
|
|
338
|
-
const { reply } = writeFailure(runDir, opts.agent,
|
|
339
|
-
'
|
|
340
|
-
|
|
340
|
+
const { reply } = writeFailure(runDir, opts.agent,
|
|
341
|
+
orderDiagnosticMessage('run-worker-start-failure', { message: err.message }),
|
|
342
|
+
[orderDiagnosticMessage('codex-not-started')], true);
|
|
341
343
|
console.log(reply);
|
|
342
344
|
resolve(false);
|
|
343
345
|
});
|
|
@@ -10,17 +10,19 @@ import {
|
|
|
10
10
|
} from '../kernel-lock.mjs';
|
|
11
11
|
import { parseJsonText } from '../json-file.mjs';
|
|
12
12
|
import { chainRuns, readJson } from '../write-meta.mjs';
|
|
13
|
+
import { orderRuleMessage } from '../order-rules.mjs';
|
|
14
|
+
import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
|
|
13
15
|
|
|
14
16
|
// Plan_60 D4/D4c: preparation takes seconds; expiry must refuse rather than permit double billing.
|
|
15
17
|
export const ORDER_CLAIM_TIMING = Object.freeze({ waitMs: 60_000, retryMs: 100, answerTimeoutMs: 1_000 });
|
|
16
18
|
|
|
17
19
|
export function orderClaimAddress(projectRunsRoot, orderId, platform = process.platform) {
|
|
18
20
|
if (typeof orderId !== 'string' || orderId.length === 0) {
|
|
19
|
-
throw new TypeError('
|
|
21
|
+
throw new TypeError(orderDiagnosticMessage('order-claim-input-valid', { violation: 'order-id' }));
|
|
20
22
|
}
|
|
21
23
|
// Plan_60 A4 r5: owner matching is case-sensitive; retries must retain the exact order id.
|
|
22
24
|
const digest = directoryDigest(projectRunsRoot, { suffix: orderId });
|
|
23
|
-
if (digest === null) throw new Error(
|
|
25
|
+
if (digest === null) throw new Error(orderDiagnosticMessage('project-runs-folder-required', { projectRunsRoot }));
|
|
24
26
|
if (kernelLockStrategy(platform) === null) return null;
|
|
25
27
|
return kernelLockAddress('order', digest, platform);
|
|
26
28
|
}
|
|
@@ -30,7 +32,7 @@ export async function acquireOrderClaim({
|
|
|
30
32
|
timing = ORDER_CLAIM_TIMING, createServer,
|
|
31
33
|
}) {
|
|
32
34
|
if (role !== 'launcher' && role !== 'worker') {
|
|
33
|
-
throw new TypeError('
|
|
35
|
+
throw new TypeError(orderDiagnosticMessage('order-claim-input-valid', { violation: 'role' }));
|
|
34
36
|
}
|
|
35
37
|
const address = orderClaimAddress(projectRunsRoot, orderId, platform);
|
|
36
38
|
if (address === null) return { claimed: false, unsupported: true, release: async () => {} };
|
|
@@ -60,13 +62,16 @@ export function parseClaimHolder(line) {
|
|
|
60
62
|
// Plan_60 D4/D4c: a holder answer diagnoses contention but cannot authorize anything.
|
|
61
63
|
export function orderClaimHolderText(holder) {
|
|
62
64
|
return holder === null
|
|
63
|
-
? '
|
|
64
|
-
:
|
|
65
|
+
? orderRuleMessage('order-launch-exclusive', { variant: 'holder-unverified' })
|
|
66
|
+
: orderRuleMessage('order-launch-exclusive', {
|
|
67
|
+
variant: 'holder', role: holder.role, pid: holder.pid, acquiredAt: holder.acquiredAt,
|
|
68
|
+
});
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
export function orderClaimBusyText(orderId, holder) {
|
|
68
|
-
return
|
|
69
|
-
|
|
72
|
+
return [orderRuleMessage('order-launch-exclusive', {
|
|
73
|
+
variant: 'busy', orderId, holderDescription: orderClaimHolderText(holder),
|
|
74
|
+
}), orderDiagnosticMessage('run-not-created')].join(' ');
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
/**
|
|
@@ -79,6 +84,7 @@ export function unclaimedRaceRefusal({ projectRunsRoot, repoRoot, slug, taskHash
|
|
|
79
84
|
const appeared = chain.find((run) => !before.has(run)
|
|
80
85
|
&& readJson(path.join(projectRunsRoot, run, 'status.json'))?.order_id === orderId);
|
|
81
86
|
if (!appeared) return null;
|
|
82
|
-
return
|
|
83
|
-
|
|
87
|
+
return [orderRuleMessage('order-launch-exclusive', {
|
|
88
|
+
variant: 'race', orderId, run: appeared,
|
|
89
|
+
}), orderDiagnosticMessage('run-not-created')].join(' ');
|
|
84
90
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Normalises one order's values into runner options, whatever channel carried them (Plan_63 D5). */
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { orderRuleMessage } from '../order-rules.mjs';
|
|
3
4
|
|
|
4
5
|
export function orderOptions(agentType, order, { cwd = process.cwd() } = {}) {
|
|
5
6
|
const options = { orderId: (order.get('order id') ?? '').trim() };
|
|
@@ -10,7 +11,7 @@ export function orderOptions(agentType, order, { cwd = process.cwd() } = {}) {
|
|
|
10
11
|
options.effort = order.get('effort');
|
|
11
12
|
if (!options.effort || /\s/.test(options.effort)) {
|
|
12
13
|
problems.push({ label: 'effort',
|
|
13
|
-
reason:
|
|
14
|
+
reason: orderRuleMessage('effort-value', { value: options.effort }) });
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
options.repo = path.resolve(order.get('repository') || cwd);
|
|
@@ -18,7 +19,7 @@ export function orderOptions(agentType, order, { cwd = process.cwd() } = {}) {
|
|
|
18
19
|
// Plan_29 incident: a generic slug let an honest order inherit a days-old chain.
|
|
19
20
|
if (!/[A-Za-z0-9]/.test(options.slug)) {
|
|
20
21
|
problems.push({ label: order.has('slug') ? 'slug' : 'order id',
|
|
21
|
-
reason:
|
|
22
|
+
reason: orderRuleMessage('run-slug', { value: options.slug }) });
|
|
22
23
|
}
|
|
23
24
|
options.changeset = order.get('changeset') || 'uncommitted';
|
|
24
25
|
const declaredScopePatterns = (order.get('scope') || '').split(',').map((p) => p.trim()).filter(Boolean);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Keeps the runner and producer hook on one definition of which task owns an order id. */
|
|
2
2
|
import { TASK_HASH_SCHEME } from '../meta/chain.mjs';
|
|
3
3
|
import { orderInputName } from '../order-schema.mjs';
|
|
4
|
+
import { orderRuleMessage } from '../order-rules.mjs';
|
|
4
5
|
|
|
5
6
|
const byStartThenName = (left, right) => {
|
|
6
7
|
const leftAt = String(left.status.started_at || '');
|
|
@@ -34,14 +35,15 @@ export function ownerPredatesHashScheme(owner) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export function orderOwnerConflictText(owner, ownerDir, orderId) {
|
|
37
|
-
const identity = `Order id collision: ${JSON.stringify(String(orderId ?? ''))} already belongs to run folder ${ownerDir} ` +
|
|
38
|
-
`(slug ${owner.status.slug}, started_at ${owner.status.started_at})`;
|
|
39
38
|
// Plan_75 D5, 2026-10-03: an old hash cannot prove that a repeated order is a different task.
|
|
40
39
|
if (ownerPredatesHashScheme(owner)) {
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
return orderRuleMessage('order-id-owner', {
|
|
41
|
+
violation: 'predates-hash-scheme', orderId, ownerDir,
|
|
42
|
+
slug: owner.status.slug, startedAt: owner.status.started_at, hashScheme: TASK_HASH_SCHEME,
|
|
43
|
+
});
|
|
44
44
|
}
|
|
45
|
-
return
|
|
46
|
-
'
|
|
45
|
+
return orderRuleMessage('order-id-owner', {
|
|
46
|
+
violation: 'different-task', orderId, ownerDir,
|
|
47
|
+
slug: owner.status.slug, startedAt: owner.status.started_at, labelName: orderInputName('order id'),
|
|
48
|
+
});
|
|
47
49
|
}
|
|
@@ -15,6 +15,8 @@ import { continuationRefusal, retryRefusal, readyGrantLines } from './continuati
|
|
|
15
15
|
import { advisorTaskOrRefuse } from './advise-carry.mjs';
|
|
16
16
|
import { scopeRunRefusal } from './preflight.mjs';
|
|
17
17
|
import { orderInputName } from '../order-schema.mjs';
|
|
18
|
+
import { orderRuleMessage } from '../order-rules.mjs';
|
|
19
|
+
import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
|
|
18
20
|
|
|
19
21
|
export async function passGate({ opts, taskText, header, projectRunsRoot, repoRoot, beforeWait }) {
|
|
20
22
|
// A second pass at the same task is a real need — after a timeout or a LIMIT the work has
|
|
@@ -30,10 +32,9 @@ export async function passGate({ opts, taskText, header, projectRunsRoot, repoRo
|
|
|
30
32
|
const grant = header.grant;
|
|
31
33
|
// OW-040, 2026-09-30: only a retry repeats failed scope without spending advise's continuation.
|
|
32
34
|
if (opts.agent === 'codex-advisor' && opts.phase === 'scope' && opts.continue && grant?.kind !== 'retry') {
|
|
33
|
-
die(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'scope run that failed is repeated with a `retry:` grant instead. The run folder was not created; quota was not spent.');
|
|
35
|
+
die([orderRuleMessage('advisor-scope-continuation', {
|
|
36
|
+
phaseLabel: orderInputName('phase'), continueLabel: orderInputName('continue'),
|
|
37
|
+
}), orderDiagnosticMessage('run-not-created')].join(' '));
|
|
37
38
|
}
|
|
38
39
|
let continuationGrant = grant?.kind === 'continue' ? grant : null;
|
|
39
40
|
const retryOf = grant?.kind === 'retry' ? grant.run : null;
|
|
@@ -60,8 +61,9 @@ export async function passGate({ opts, taskText, header, projectRunsRoot, repoRo
|
|
|
60
61
|
// A run written before phases existed has none; for every agent but the advisor that is 'default'.
|
|
61
62
|
const phaseOf = (value) => String(value ?? '').trim() || 'default';
|
|
62
63
|
if (namedStatus.agent !== opts.agent || phaseOf(namedStatus.phase) !== phaseOf(opts.phase)) {
|
|
63
|
-
die(
|
|
64
|
-
|
|
64
|
+
die([orderRuleMessage('retry-pass-identity', {
|
|
65
|
+
agent: namedStatus.agent, phase: namedStatus.phase,
|
|
66
|
+
}), orderDiagnosticMessage('run-not-created')].join(' '));
|
|
65
67
|
}
|
|
66
68
|
// Plan_75 D4, TradeForge capacity incident: retry advise against its original successful scope.
|
|
67
69
|
continuationGrant = String(namedStatus.continued_from ?? '').trim()
|
|
@@ -93,18 +95,12 @@ export async function passGate({ opts, taskText, header, projectRunsRoot, repoRo
|
|
|
93
95
|
const last = startedChain[startedChain.length - 1];
|
|
94
96
|
const lastSlug = String(readJson(path.join(projectRunsRoot, last, 'status.json'))?.slug || '');
|
|
95
97
|
const renamed = lastSlug && lastSlug.toLowerCase() !== String(opts.slug).toLowerCase();
|
|
96
|
-
die(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'A repeat run is allowed, but the orchestrator decides, not the runner: it read the ' +
|
|
103
|
-
`previous response and knows whether work remains. Add ${orderInputName('continue')} if you are finishing ` +
|
|
104
|
-
`the same task; changing ${orderInputName('slug')} with the same task text does not stop it being a repeat. ` +
|
|
105
|
-
`${readyGrantLines(projectRunsRoot, startedChain)} ` +
|
|
106
|
-
'The run folder was not created; quota was not spent.',
|
|
107
|
-
);
|
|
98
|
+
die([orderRuleMessage('repeat-task-grant-required', {
|
|
99
|
+
violation: renamed ? 'renamed' : 'existing',
|
|
100
|
+
continueLabel: orderInputName('continue'), slugLabel: orderInputName('slug'),
|
|
101
|
+
slug: renamed ? lastSlug : opts.slug, count: startedChain.length, latest: path.join(projectRunsRoot, last),
|
|
102
|
+
hints: readyGrantLines(projectRunsRoot, startedChain),
|
|
103
|
+
}), orderDiagnosticMessage('run-not-created')].join(' '));
|
|
108
104
|
}
|
|
109
105
|
|
|
110
106
|
return { taskHash, chain, startedChain, continuationGrant, advisorTask, retryOf };
|
|
@@ -6,36 +6,34 @@
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import { advisorTaskRefusal } from '../meta/advisor-task.mjs';
|
|
9
|
-
import { unavailableRows } from '../meta/
|
|
9
|
+
import { unavailableRows } from '../meta/unavailable-rows.mjs';
|
|
10
10
|
import { activeRunDetails, readJson } from '../write-meta.mjs';
|
|
11
11
|
import { probeCodexAvailability } from './codex-availability.mjs';
|
|
12
12
|
import { agentRole } from '../agents.mjs';
|
|
13
13
|
import { die } from './args.mjs';
|
|
14
14
|
import { orderInputName } from '../order-schema.mjs';
|
|
15
15
|
import { retiredPathRefusal, runsRootResolution } from './runs-root.mjs';
|
|
16
|
+
import { orderRuleMessage } from '../order-rules.mjs';
|
|
17
|
+
import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
|
|
16
18
|
|
|
17
19
|
/** Plan_60 D2: only conclusive evidence says UNAVAILABLE; a PATH miss is inconclusive (Plan_78 D5). */
|
|
18
20
|
export async function codexAvailabilityRefusal({ probe = probeCodexAvailability } = {}) {
|
|
19
21
|
const result = await probe();
|
|
20
22
|
if (result.state === 'available') return null;
|
|
21
23
|
const unavailable = result.state === 'logged-out';
|
|
22
|
-
const rows = unavailable ? unavailableRows(result.detail) :
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
] : [
|
|
26
|
-
`Codex CLI unavailable: ${result.detail}`,
|
|
27
|
-
'Operator check: codex --version (and codex login if authorization is rejected)',
|
|
28
|
-
];
|
|
24
|
+
const rows = unavailable ? unavailableRows(result.detail) : [orderDiagnosticMessage(
|
|
25
|
+
result.pathMiss === true ? 'codex-readiness-unconfirmed' : 'codex-unavailable', { detail: result.detail },
|
|
26
|
+
)];
|
|
29
27
|
return {
|
|
30
28
|
unavailable,
|
|
31
|
-
text: [...rows, '
|
|
29
|
+
text: [...rows, orderDiagnosticMessage('run-not-created')].join('\n'),
|
|
32
30
|
};
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
/** Plan_59 D5/D6: settle design authority before any probe can spend quota. */
|
|
36
34
|
export function taskPreflight({ agent, taskText, header, resolution = runsRootResolution() }) {
|
|
37
35
|
const freeRefusal = (reason) => ({
|
|
38
|
-
refusal:
|
|
36
|
+
refusal: [reason, orderDiagnosticMessage('run-not-created')].join('\n'),
|
|
39
37
|
});
|
|
40
38
|
if (agent === 'codex-advisor') {
|
|
41
39
|
const reason = advisorTaskRefusal(taskText);
|
|
@@ -64,11 +62,7 @@ export function taskPreflight({ agent, taskText, header, resolution = runsRootRe
|
|
|
64
62
|
}
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
|
-
return freeRefusal('
|
|
68
|
-
'value is mechanical | revert | docs-only | test-only ' +
|
|
69
|
-
'or an absolute path to an existing advisor run directory whose meta.json says agent codex-advisor, ' +
|
|
70
|
-
'phase advise, status OK. ' +
|
|
71
|
-
'An order that invents a construction needs a second opinion first; only work with no design choice may skip it.');
|
|
65
|
+
return freeRefusal(orderRuleMessage('build-advice-required'));
|
|
72
66
|
}
|
|
73
67
|
|
|
74
68
|
/** Plan_59 D14: an advice pass may continue only a completed, successful advisor scope run. */
|
|
@@ -81,44 +75,40 @@ export function scopeRunRefusal({ agent, phase, runsRoot, grantRun }) {
|
|
|
81
75
|
meta = null;
|
|
82
76
|
}
|
|
83
77
|
const failed = [
|
|
84
|
-
meta?.agent === 'codex-advisor' ? null :
|
|
85
|
-
meta?.phase === 'scope' ? null :
|
|
86
|
-
meta?.status === 'OK' ? null :
|
|
78
|
+
meta?.agent === 'codex-advisor' ? null : orderRuleMessage('advise-scope-run-required', { violation: 'agent' }),
|
|
79
|
+
meta?.phase === 'scope' ? null : orderRuleMessage('advise-scope-run-required', { violation: 'phase' }),
|
|
80
|
+
meta?.status === 'OK' ? null : orderRuleMessage('advise-scope-run-required', { violation: 'status' }),
|
|
87
81
|
].filter(Boolean);
|
|
88
82
|
if (!failed.length) return null;
|
|
89
|
-
return
|
|
83
|
+
return [orderRuleMessage('advise-scope-run-required', {
|
|
84
|
+
violation: 'meta', failed: failed.join(', '),
|
|
85
|
+
}), orderDiagnosticMessage('run-not-created')].join(' ');
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
/** D2: phase mistakes must refuse before even the sandbox probe can spend quota. */
|
|
93
89
|
export function resolveRunPhase({ agent, phase, continue: isContinue }, budgets) {
|
|
94
90
|
// Plan_59 D7: phase 2 must retain the scope run's predictions before quota is spent.
|
|
95
91
|
if (agent === 'codex-advisor' && phase === 'advise' && !isContinue) {
|
|
96
|
-
die(
|
|
97
|
-
'
|
|
92
|
+
die([orderRuleMessage('advisor-advise-continuation', {
|
|
93
|
+
phaseLabel: orderInputName('phase'), continueLabel: orderInputName('continue'),
|
|
94
|
+
}), orderDiagnosticMessage('run-not-created')].join(' '));
|
|
98
95
|
}
|
|
99
96
|
const phases = budgets[agentRole(agent)];
|
|
100
97
|
const names = Object.keys(phases);
|
|
101
|
-
if (phase === undefined && names.length === 1 && names[0] === 'default') return
|
|
98
|
+
if (phase === undefined && names.length === 1 && names[0] === 'default') return names[0];
|
|
102
99
|
if (phase !== undefined && Object.hasOwn(phases, phase)) return phase;
|
|
103
|
-
die(
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
die([orderRuleMessage('run-phase-required', {
|
|
101
|
+
violation: phase === undefined ? 'missing' : 'undeclared', phaseLabel: orderInputName('phase'), phase, agent, names,
|
|
102
|
+
}), orderDiagnosticMessage('run-not-created')].join(' '));
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
export function preflightRefusal({ agent, projectRunsRoot, repoRoot }) {
|
|
109
106
|
// Two writing runs share one tree: the second snapshot would include the first one's edits.
|
|
110
107
|
const busy = agent === 'codex-build' ? activeRunDetails(projectRunsRoot, repoRoot) : null;
|
|
111
108
|
if (busy) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return [
|
|
116
|
-
`run ${busy.run} is already active for this repository; two writing runs in one tree are prohibited${identityNote}`,
|
|
117
|
-
`Active run: ${path.join(projectRunsRoot, busy.run)}`,
|
|
118
|
-
// One sentence, the same one every other pre-flight refusal ends with. The older wording
|
|
119
|
-
// said the identical thing twice, which is how two copies of a rule start to drift.
|
|
120
|
-
'The run folder was not created; quota was not spent.',
|
|
121
|
-
].join('\n');
|
|
109
|
+
return [orderRuleMessage('writing-run-exclusive', {
|
|
110
|
+
run: busy.run, activePath: path.join(projectRunsRoot, busy.run), identity: busy.identity,
|
|
111
|
+
}), orderDiagnosticMessage('run-not-created')].join('\n');
|
|
122
112
|
}
|
|
123
113
|
|
|
124
114
|
return null;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* (`…cli/install.mjs:` + U+0000) and 2026-09-28 (`…(docs/plans/Plan_` + `恋`).
|
|
9
9
|
*/
|
|
10
10
|
import { DEFAULTS } from '../run-config.mjs';
|
|
11
|
+
import { SERVICE_FOLDERS } from '../meta/paths.mjs';
|
|
11
12
|
import { RUN_ENV } from './run-env.mjs';
|
|
12
13
|
import { advisorSchema, schemaFor } from './schemas.mjs';
|
|
13
14
|
import { QUESTION_KIND } from './question-kind.mjs';
|
|
@@ -140,8 +141,7 @@ Working rules:
|
|
|
140
141
|
(\`dir/{a,b}.ts\`), and lists separated by \`;\` or \`,\` are prohibited: a collapsed entry will
|
|
141
142
|
match no file when the report is checked against the worktree and will fail the run even if
|
|
142
143
|
the work was done honestly.
|
|
143
|
-
- Do not fix the environment. Service directories (
|
|
144
|
-
\`node_modules/\`) and other sessions are not your task, even if they prevent you from working
|
|
144
|
+
- Do not fix the environment. Service directories (${SERVICE_FOLDERS.map((name) => `\`${name}/\``).join(', ')}) and other sessions are not your task, even if they prevent you from working
|
|
145
145
|
or finishing. Put the obstacle in leftovers instead of changing them.
|
|
146
146
|
- report_markdown is a full markdown report with sections: “What was done”, “Changes”
|
|
147
147
|
(file / what changed / why table), “Verification” (command and its actual output),
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Plan_60 D1: only the orderer knows whether a question concerns handed startup context or code;
|
|
4
4
|
* the scout cannot choose its own kind, and startup context currently fails without a command.
|
|
5
5
|
*/
|
|
6
|
+
import { orderRuleMessage } from '../order-rules.mjs';
|
|
7
|
+
|
|
6
8
|
export const CONTEXT_ONLY_MARKER = '[context-only]';
|
|
7
9
|
export const QUESTION_KIND = Object.freeze({
|
|
8
10
|
STARTUP_CONTEXT: 'startup-context',
|
|
@@ -14,13 +16,13 @@ export function questionKindRefusal(texts = []) {
|
|
|
14
16
|
const tokens = text.matchAll(/\[\s*context[\s_-]*only\s*\]/gi);
|
|
15
17
|
for (const token of tokens) {
|
|
16
18
|
if (token.index !== 0 || token[0] !== CONTEXT_ONLY_MARKER) {
|
|
17
|
-
return
|
|
19
|
+
return orderRuleMessage('question-context-marker', { index, text });
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
if (text.startsWith(CONTEXT_ONLY_MARKER)) {
|
|
21
23
|
const question = text.slice(CONTEXT_ONLY_MARKER.length);
|
|
22
24
|
if (!/^\s/.test(question) || !question.trim()) {
|
|
23
|
-
return
|
|
25
|
+
return orderRuleMessage('question-context-marker', { index, text });
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -11,6 +11,8 @@ import { writeHomeJsonAtomic } from '../atomic-json.mjs';
|
|
|
11
11
|
import { stateDirWriter } from '../home-write.mjs';
|
|
12
12
|
import { parseJsonText } from '../json-file.mjs';
|
|
13
13
|
import { normalizeRepoPath } from './project-dir.mjs';
|
|
14
|
+
import { orderRuleMessage } from '../order-rules.mjs';
|
|
15
|
+
import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
|
|
14
16
|
|
|
15
17
|
export const RUNS_MOVE_RECORD = 'runs-root.json';
|
|
16
18
|
|
|
@@ -21,25 +23,31 @@ export function readRunsMoveRecord(stateDir) {
|
|
|
21
23
|
text = fs.readFileSync(file, 'utf8');
|
|
22
24
|
} catch (error) {
|
|
23
25
|
if (error.code === 'ENOENT') return null;
|
|
24
|
-
throw new Error(
|
|
26
|
+
throw new Error(orderDiagnosticMessage('runs-move-record-readable', {
|
|
27
|
+
file, message: error.message,
|
|
28
|
+
}), { cause: error });
|
|
25
29
|
}
|
|
26
30
|
let record;
|
|
27
31
|
try {
|
|
28
32
|
record = parseJsonText(file, text);
|
|
29
33
|
} catch (error) {
|
|
30
|
-
throw new Error(
|
|
34
|
+
throw new Error(orderDiagnosticMessage('runs-move-record-invalid', {
|
|
35
|
+
file, violation: 'parse', detail: error.message,
|
|
36
|
+
}), { cause: error });
|
|
31
37
|
}
|
|
32
|
-
const invalid = (
|
|
33
|
-
|
|
38
|
+
const invalid = (violation, index) => {
|
|
39
|
+
throw new Error(orderDiagnosticMessage('runs-move-record-invalid', { file, violation, index }));
|
|
40
|
+
};
|
|
41
|
+
if (record?.version !== 1) invalid('version');
|
|
34
42
|
if (!Array.isArray(record.retired) || record.retired.length === 0) {
|
|
35
|
-
invalid('retired
|
|
43
|
+
invalid('retired-array');
|
|
36
44
|
}
|
|
37
45
|
for (const [index, entry] of record.retired.entries()) {
|
|
38
46
|
if (typeof entry?.root !== 'string' || !path.isAbsolute(entry.root)) {
|
|
39
|
-
invalid(
|
|
47
|
+
invalid('retired-root', index);
|
|
40
48
|
}
|
|
41
49
|
if (typeof entry.movedAt !== 'string' || !Number.isFinite(Date.parse(entry.movedAt))) {
|
|
42
|
-
invalid(
|
|
50
|
+
invalid('retired-time', index);
|
|
43
51
|
}
|
|
44
52
|
}
|
|
45
53
|
return record;
|
|
@@ -63,8 +71,9 @@ export function retiredRootOf(candidate, retiredRoots) {
|
|
|
63
71
|
|
|
64
72
|
export function retiredRootRefusal({ candidate, retired, destination }) {
|
|
65
73
|
const equivalent = retired.suffix ? path.join(destination, retired.suffix) : destination;
|
|
66
|
-
return
|
|
67
|
-
|
|
74
|
+
return orderRuleMessage('advice-retired-root', {
|
|
75
|
+
candidate, retiredRoot: retired.root, destination, equivalent,
|
|
76
|
+
});
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
/**
|
|
@@ -13,6 +13,7 @@ import path from 'node:path';
|
|
|
13
13
|
import { resolveBrandHome } from '../brand-home.mjs';
|
|
14
14
|
import { RUN_RECORDS_DIR } from '../home-registry.mjs';
|
|
15
15
|
import { readRunsMoveRecord, retiredRootOf, retiredRootRefusal } from './retired-roots.mjs';
|
|
16
|
+
import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
|
|
16
17
|
|
|
17
18
|
export function runsRootResolution({ env = process.env, homedir = os.homedir() } = {}) {
|
|
18
19
|
const brandHome = resolveBrandHome({ env, homedir });
|
|
@@ -50,7 +51,6 @@ export function retiredPathRefusal(candidate, resolution = runsRootResolution())
|
|
|
50
51
|
|
|
51
52
|
export function staleOverrideRefusal(resolution = runsRootResolution()) {
|
|
52
53
|
if (!resolution.staleOverride) return null;
|
|
53
|
-
return retiredRootRefusal({ candidate: resolution.root, retired: resolution.staleOverride,
|
|
54
|
-
destination: resolution.homeRoot })
|
|
55
|
-
'CODEX_RUNS_ROOT points under a retired runs root; remove it or set it to the new location.';
|
|
54
|
+
return [retiredRootRefusal({ candidate: resolution.root, retired: resolution.staleOverride,
|
|
55
|
+
destination: resolution.homeRoot }), orderDiagnosticMessage('stale-runs-root-override')].join('\n');
|
|
56
56
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/** Owns read-only sandbox log evidence and repair formatting after 2026-10-10 TradeForge orders exposed only "setup refresh had errors". */
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
|
|
4
5
|
|
|
5
6
|
const LOG_LIMIT = 64 * 1024;
|
|
6
7
|
const SETUP_LIMIT = 8 * 1024;
|
|
7
8
|
const ERROR_LINE = /setup error|setup refresh completed with errors|failed|error/i;
|
|
8
9
|
// A successful refresh logs `errors=[]`; on the 2026-10-10 log five of them pushed the real error out.
|
|
9
10
|
const CLEAN_REFRESH = /errors=\[\]/;
|
|
10
|
-
const NO_REPAIR = 'No verified repair signature; inspect this log.';
|
|
11
11
|
|
|
12
12
|
// Plan_78 A1: a repair needs all incident fragments on one fresh line, not a generic error word.
|
|
13
13
|
const SIGNATURES = Object.freeze([
|
|
@@ -16,16 +16,18 @@ const SIGNATURES = Object.freeze([
|
|
|
16
16
|
observed: Object.freeze({ date: '2026-10-10', codexCli: '0.162.1' }),
|
|
17
17
|
predicate: (line) => ['runtime read/execute validation failed',
|
|
18
18
|
'open ACL target for root-only update', 'os error 32'].every((fragment) => line.includes(fragment)),
|
|
19
|
-
summary: '
|
|
20
|
-
repair: () => '
|
|
19
|
+
summary: orderDiagnosticMessage('sandbox-known-failure-runtime-file-locked', { part: 'summary' }),
|
|
20
|
+
repair: () => orderDiagnosticMessage('sandbox-known-failure-runtime-file-locked', { part: 'repair' }),
|
|
21
21
|
}),
|
|
22
22
|
Object.freeze({
|
|
23
23
|
id: 'deny-read-state-corrupt',
|
|
24
24
|
observed: Object.freeze({ date: '2026-09-16' }),
|
|
25
25
|
predicate: (line) => ['deny_read_acl_state.json', 'expected value at line 1 column 1']
|
|
26
26
|
.every((fragment) => line.includes(fragment)),
|
|
27
|
-
summary: '
|
|
28
|
-
repair: (codexHome) =>
|
|
27
|
+
summary: orderDiagnosticMessage('sandbox-known-failure-deny-read-state-corrupt', { part: 'summary' }),
|
|
28
|
+
repair: (codexHome) => orderDiagnosticMessage('sandbox-known-failure-deny-read-state-corrupt', {
|
|
29
|
+
part: 'repair', file: path.join(codexHome, '.sandbox', 'deny_read_acl_state.json'),
|
|
30
|
+
}),
|
|
29
31
|
}),
|
|
30
32
|
]);
|
|
31
33
|
|
|
@@ -94,7 +96,7 @@ function readSetupError(file, folderState) {
|
|
|
94
96
|
return {
|
|
95
97
|
state: result.state,
|
|
96
98
|
text: result.state === 'read' ? result.text : result.state === 'missing' ? null
|
|
97
|
-
:
|
|
99
|
+
: orderDiagnosticMessage('sandbox-evidence-unknown', { variant: 'unreadable', file }),
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -127,7 +129,7 @@ export function diagnoseSandbox({ codexHome, before, now = new Date() }) {
|
|
|
127
129
|
if (result.state === 'missing') continue;
|
|
128
130
|
sources.push(file);
|
|
129
131
|
if (result.state === 'unknown') {
|
|
130
|
-
unknown.push(
|
|
132
|
+
unknown.push(orderDiagnosticMessage('sandbox-evidence-unknown', { variant: 'unreadable', file }));
|
|
131
133
|
continue;
|
|
132
134
|
}
|
|
133
135
|
if (baseline !== null && result.bytes > 0) {
|
|
@@ -135,7 +137,7 @@ export function diagnoseSandbox({ codexHome, before, now = new Date() }) {
|
|
|
135
137
|
appended.push(result.text);
|
|
136
138
|
}
|
|
137
139
|
recent.push({ file, mtime: result.mtime });
|
|
138
|
-
if (baseline === null) unknown.push(
|
|
140
|
+
if (baseline === null) unknown.push(orderDiagnosticMessage('sandbox-evidence-unknown', { variant: 'baseline', file }));
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
let text = appended.join('\n');
|
|
@@ -143,7 +145,7 @@ export function diagnoseSandbox({ codexHome, before, now = new Date() }) {
|
|
|
143
145
|
const newest = recent.sort((a, b) => b.mtime - a.mtime || b.file.localeCompare(a.file))[0];
|
|
144
146
|
const fallback = readRange(newest.file, 0, LOG_LIMIT, true, folderState);
|
|
145
147
|
if (fallback.state === 'read') text = fallback.text;
|
|
146
|
-
else unknown.push(
|
|
148
|
+
else unknown.push(orderDiagnosticMessage('sandbox-evidence-unknown', { variant: 'unreadable', file: newest.file }));
|
|
147
149
|
}
|
|
148
150
|
const errors = text.split(/\r?\n/)
|
|
149
151
|
.filter((line) => ERROR_LINE.test(line) && !CLEAN_REFRESH.test(line)).slice(-5);
|
|
@@ -166,15 +168,13 @@ export function diagnoseSandbox({ codexHome, before, now = new Date() }) {
|
|
|
166
168
|
|
|
167
169
|
export function formatSandboxDiagnosis(diagnosis) {
|
|
168
170
|
return [
|
|
169
|
-
...diagnosis.sources.map((source) =>
|
|
170
|
-
diagnosis
|
|
171
|
-
|
|
172
|
-
...diagnosis.lines.map((line) => `> ${line}`),
|
|
171
|
+
...diagnosis.sources.map((source) => orderDiagnosticMessage('sandbox-diagnosis-source', { source })),
|
|
172
|
+
orderDiagnosticMessage('sandbox-diagnosis-attribution', { attributed: diagnosis.attributed }),
|
|
173
|
+
...diagnosis.lines.map((line) => orderDiagnosticMessage('sandbox-evidence-line', { line })),
|
|
173
174
|
...(diagnosis.setupError.text === null ? [] : [
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
...diagnosis.setupError.text.split(/\r?\n/).map((line) => `> ${line}`),
|
|
175
|
+
orderDiagnosticMessage('sandbox-setup-change', { changed: diagnosis.setupError.changed }),
|
|
176
|
+
...diagnosis.setupError.text.split(/\r?\n/).map((line) => orderDiagnosticMessage('sandbox-evidence-line', { line })),
|
|
177
177
|
]),
|
|
178
|
-
...(diagnosis.signature ? [diagnosis.signature.summary, diagnosis.signature.repair] : [
|
|
178
|
+
...(diagnosis.signature ? [diagnosis.signature.summary, diagnosis.signature.repair] : [orderDiagnosticMessage('sandbox-no-repair')]),
|
|
179
179
|
];
|
|
180
180
|
}
|