@lyupro/codex-bridge 0.6.8 → 0.6.10
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 +160 -0
- package/README.md +6 -4
- package/bin/codex-bridge.mjs +35 -177
- package/cli/command-help.mjs +49 -0
- package/cli/command-registry.mjs +284 -0
- package/cli/copy.mjs +42 -5
- package/cli/dispatcher-contract-record.mjs +3 -2
- package/cli/doctor-host-side.mjs +110 -0
- package/cli/doctor.mjs +17 -1
- package/cli/home-inspection.mjs +122 -0
- package/cli/home-plan-execute.mjs +140 -0
- package/cli/home-removal-plan.mjs +61 -0
- package/cli/hook-recognizer.mjs +78 -0
- package/cli/host-contract.mjs +11 -5
- package/cli/host-inspection.mjs +156 -0
- package/cli/host-removal.mjs +91 -0
- package/cli/image-evidence.mjs +44 -0
- package/cli/install-owners.mjs +244 -0
- package/cli/install-record.mjs +71 -9
- package/cli/install.mjs +88 -75
- package/cli/inventory-removal.mjs +84 -0
- package/cli/inventory-transition.mjs +66 -0
- package/cli/lifecycle-lock-clear.mjs +104 -0
- package/cli/lifecycle-lock-inspect.mjs +92 -0
- package/cli/lifecycle-lock.mjs +222 -0
- package/cli/lifecycle-transaction.mjs +57 -0
- package/cli/link-segments.mjs +45 -0
- package/cli/model-set.mjs +4 -3
- package/cli/model-speed.mjs +9 -2
- package/cli/model.mjs +5 -3
- package/cli/owner-record.mjs +40 -0
- package/cli/owner-sync-apply.mjs +108 -0
- package/cli/owner-sync.mjs +115 -0
- package/cli/projects.mjs +5 -2
- package/cli/prune-args.mjs +9 -4
- package/cli/prune.mjs +23 -72
- package/cli/purge-live-runs.mjs +98 -0
- package/cli/purge-preflight.mjs +103 -0
- package/cli/read.mjs +22 -1
- package/cli/record-removal.mjs +43 -0
- package/cli/removal-outcomes.mjs +115 -0
- package/cli/removal-plan.mjs +187 -0
- package/cli/remove-layout.mjs +40 -1
- package/cli/rules-owners.mjs +4 -2
- package/cli/settings-merge.mjs +3 -29
- package/cli/terminal-question.mjs +41 -0
- package/cli/uninstall-purge.mjs +139 -0
- package/cli/uninstall-rules.mjs +72 -0
- package/cli/uninstall.mjs +208 -127
- package/cli/unlock-lifecycle.mjs +102 -0
- package/cli/unlock.mjs +7 -4
- package/cli/update.mjs +102 -27
- package/package.json +4 -3
- package/scripts/home-write-audit.mjs +286 -0
- package/scripts/home-write-inventory.mjs +59 -0
- package/scripts/home-write-lexer.mjs +182 -0
- package/scripts/isolated-roots.mjs +6 -1
- package/scripts/open-work/check.mjs +180 -0
- package/scripts/open-work/live-thresholds.mjs +155 -0
- package/scripts/open-work/register.mjs +56 -0
- package/scripts/root-guard/root-entries.mjs +15 -0
- package/scripts/run-tests.mjs +26 -1
- package/src/agents/codex-advisor.md +23 -10
- package/src/agents/codex-build.md +24 -14
- package/src/agents/codex-review.md +17 -10
- package/src/agents/codex-scout.md +22 -12
- package/src/home/conventions.md +9 -0
- package/src/home/hooks/guard-tries.mjs +6 -4
- package/src/home/hooks/order-gate.mjs +27 -19
- package/src/home/hooks/reply-guard.mjs +2 -7
- package/src/home/hooks/reply-parser.mjs +1 -1
- package/src/home/hooks/worktree-witness.mjs +6 -6
- package/src/home/lib/agents.mjs +1 -1
- package/src/home/lib/atomic-json.mjs +24 -6
- package/src/home/lib/command-path.mjs +32 -0
- package/src/home/lib/config-edit.mjs +25 -10
- package/src/home/lib/dispatcher-command.mjs +4 -2
- package/src/home/lib/dispatcher-state.mjs +25 -10
- package/src/home/lib/file-lock.mjs +41 -10
- package/src/home/lib/handback-witness.mjs +7 -4
- package/src/home/lib/home-registry.mjs +211 -0
- package/src/home/lib/home-write.mjs +189 -0
- package/src/home/lib/hook-diagnostics.mjs +20 -0
- package/src/home/lib/host-observations.mjs +7 -4
- package/src/home/lib/kernel-lock.mjs +171 -0
- package/src/home/lib/meta/advice-status.mjs +2 -2
- package/src/home/lib/meta/advice-verdict.mjs +30 -1
- package/src/home/lib/meta/chain.mjs +19 -8
- package/src/home/lib/meta/context-only.mjs +43 -0
- package/src/home/lib/meta/events.mjs +16 -3
- package/src/home/lib/meta/launch-rows.mjs +23 -0
- package/src/home/lib/meta/paths.mjs +24 -24
- package/src/home/lib/meta/reply.mjs +37 -3
- package/src/home/lib/meta/run-snapshots.mjs +29 -0
- package/src/home/lib/meta/run-state.mjs +3 -0
- package/src/home/lib/meta/snapshot-format.mjs +96 -0
- package/src/home/lib/meta/verdict.mjs +30 -24
- package/src/home/lib/required-inputs.mjs +48 -8
- package/src/home/lib/run-codex.mjs +6 -3
- package/src/home/lib/run-config.mjs +10 -1
- package/src/home/lib/runner/advise-carry.mjs +2 -1
- package/src/home/lib/runner/args.mjs +16 -5
- package/src/home/lib/runner/attach.mjs +21 -14
- package/src/home/lib/runner/build-evidence.mjs +29 -0
- package/src/home/lib/runner/codex-availability.mjs +50 -0
- package/src/home/lib/runner/codex-cmd.mjs +2 -19
- package/src/home/lib/runner/continuation.mjs +60 -8
- package/src/home/lib/runner/exit-codes.mjs +13 -0
- package/src/home/lib/runner/flag-baseline.mjs +105 -0
- package/src/home/lib/runner/flag-scan.mjs +165 -0
- package/src/home/lib/runner/git-paths.mjs +74 -0
- package/src/home/lib/runner/git-state.mjs +37 -66
- package/src/home/lib/runner/launcher.mjs +233 -273
- package/src/home/lib/runner/order-claim.mjs +84 -0
- package/src/home/lib/runner/order-owner.mjs +18 -0
- package/src/home/lib/runner/pass-gate.mjs +109 -0
- package/src/home/lib/runner/preflight.mjs +22 -18
- package/src/home/lib/runner/prompts.mjs +42 -4
- package/src/home/lib/runner/question-kind.mjs +41 -0
- package/src/home/lib/runner/run-env.mjs +2 -1
- package/src/home/lib/runner/sandbox-probe.mjs +1 -1
- package/src/home/lib/runner/schemas.mjs +4 -1
- package/src/home/lib/runner/scope-check.mjs +55 -28
- package/src/home/lib/runner/task-input.mjs +11 -2
- package/src/home/lib/runner/worker-admission.mjs +43 -0
- package/src/home/lib/runner/worker.mjs +19 -18
- package/src/home/lib/task-header.mjs +127 -0
- package/src/home/lib/write-meta.mjs +17 -5
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds one kernel claim per project run store and exact order id during preparation.
|
|
3
|
+
* Plan_60 D4/D4c: on 2026-09-24 two launchers started 2.8 seconds apart with one order id
|
|
4
|
+
* and both ran and billed; a kernel-held claim queues preparation and dies with its holder.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import {
|
|
9
|
+
acquireKernelLock, directoryDigest, kernelLockAddress, kernelLockStrategy,
|
|
10
|
+
} from '../kernel-lock.mjs';
|
|
11
|
+
import { parseJsonText } from '../json-file.mjs';
|
|
12
|
+
import { chainRuns, readJson } from '../write-meta.mjs';
|
|
13
|
+
|
|
14
|
+
// Plan_60 D4/D4c: preparation takes seconds; expiry must refuse rather than permit double billing.
|
|
15
|
+
export const ORDER_CLAIM_TIMING = Object.freeze({ waitMs: 60_000, retryMs: 100, answerTimeoutMs: 1_000 });
|
|
16
|
+
|
|
17
|
+
export function orderClaimAddress(projectRunsRoot, orderId, platform = process.platform) {
|
|
18
|
+
if (typeof orderId !== 'string' || orderId.length === 0) {
|
|
19
|
+
throw new TypeError('orderId must be a non-empty string');
|
|
20
|
+
}
|
|
21
|
+
// Plan_60 A4 r5: owner matching is case-sensitive; retries must retain the exact order id.
|
|
22
|
+
const digest = directoryDigest(projectRunsRoot, { suffix: orderId });
|
|
23
|
+
if (digest === null) throw new Error(`project runs folder does not exist: ${projectRunsRoot}`);
|
|
24
|
+
if (kernelLockStrategy(platform) === null) return null;
|
|
25
|
+
return kernelLockAddress('order', digest, platform);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function acquireOrderClaim({
|
|
29
|
+
projectRunsRoot, orderId, role, platform = process.platform,
|
|
30
|
+
timing = ORDER_CLAIM_TIMING, createServer,
|
|
31
|
+
}) {
|
|
32
|
+
if (role !== 'launcher' && role !== 'worker') {
|
|
33
|
+
throw new TypeError('role must be launcher or worker');
|
|
34
|
+
}
|
|
35
|
+
const address = orderClaimAddress(projectRunsRoot, orderId, platform);
|
|
36
|
+
if (address === null) return { claimed: false, unsupported: true, release: async () => {} };
|
|
37
|
+
const holder = { pid: process.pid, role, orderId, token: randomUUID() };
|
|
38
|
+
const result = await acquireKernelLock({
|
|
39
|
+
address, holder, ...timing, parseAnswer: parseClaimHolder, label: 'order claim', createServer,
|
|
40
|
+
});
|
|
41
|
+
if (result.held) return { claimed: true, release: result.release };
|
|
42
|
+
return { claimed: false, busy: true, holder: result.holder };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseClaimHolder(line) {
|
|
46
|
+
try {
|
|
47
|
+
const holder = parseJsonText('<order claim holder>', line);
|
|
48
|
+
if (holder === null || typeof holder !== 'object') return null;
|
|
49
|
+
if (!Number.isInteger(holder.pid) || holder.pid <= 0) return null;
|
|
50
|
+
if (holder.role !== 'launcher' && holder.role !== 'worker') return null;
|
|
51
|
+
for (const key of ['orderId', 'token', 'acquiredAt']) {
|
|
52
|
+
if (typeof holder[key] !== 'string' || holder[key].length === 0) return null;
|
|
53
|
+
}
|
|
54
|
+
return holder;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Plan_60 D4/D4c: a holder answer diagnoses contention but cannot authorize anything.
|
|
61
|
+
export function orderClaimHolderText(holder) {
|
|
62
|
+
return holder === null
|
|
63
|
+
? 'a process that did not answer (unverified)'
|
|
64
|
+
: `${holder.role} pid ${holder.pid} since ${holder.acquiredAt}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function orderClaimBusyText(orderId, holder) {
|
|
68
|
+
return `order id "${orderId}" is being launched by ${orderClaimHolderText(holder)}; repeat the same command later — `
|
|
69
|
+
+ 'it attaches to that run instead of starting another. The run folder was not created; quota was not spent.';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Plan_60 D4, 2026-09-24: macOS has no kernel claim, so re-read just before registration.
|
|
74
|
+
* This shrinks the double-billing window to milliseconds and never waits on another launch.
|
|
75
|
+
*/
|
|
76
|
+
export function unclaimedRaceRefusal({ projectRunsRoot, repoRoot, slug, taskHash, orderId, grantRun, chainBefore }) {
|
|
77
|
+
const chain = chainRuns(projectRunsRoot, repoRoot, slug, taskHash, orderId, grantRun);
|
|
78
|
+
const before = new Set(chainBefore);
|
|
79
|
+
const appeared = chain.find((run) => !before.has(run)
|
|
80
|
+
&& readJson(path.join(projectRunsRoot, run, 'status.json'))?.order_id === orderId);
|
|
81
|
+
if (!appeared) return null;
|
|
82
|
+
return `another launch of order id "${orderId}" registered ${appeared} while this one was preparing; `
|
|
83
|
+
+ 'repeat the same command — it attaches to that run. The run folder was not created; quota was not spent.';
|
|
84
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Keeps the runner and producer hook on one definition of which task owns an order id. */
|
|
2
|
+
import { TASK_HASH_SCHEME } from '../meta/chain.mjs';
|
|
2
3
|
|
|
3
4
|
const byStartThenName = (left, right) => {
|
|
4
5
|
const leftAt = String(left.status.started_at || '');
|
|
@@ -26,3 +27,20 @@ export function conflictingOrderOwner(runs, orderId, taskHash) {
|
|
|
26
27
|
const incomingHash = String(taskHash ?? '').trim().toLowerCase();
|
|
27
28
|
return ownerHash && incomingHash && ownerHash !== incomingHash ? owner : null;
|
|
28
29
|
}
|
|
30
|
+
|
|
31
|
+
export function ownerPredatesHashScheme(owner) {
|
|
32
|
+
return owner.status.task_hash_scheme !== TASK_HASH_SCHEME;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function orderOwnerConflictText(owner, ownerDir, orderId) {
|
|
36
|
+
const identity = `Order id collision: ${JSON.stringify(String(orderId ?? ''))} already belongs to run folder ${ownerDir} ` +
|
|
37
|
+
`(slug ${owner.status.slug}, started_at ${owner.status.started_at})`;
|
|
38
|
+
// Plan_75 D5, 2026-10-03: an old hash cannot prove that a repeated order is a different task.
|
|
39
|
+
if (ownerPredatesHashScheme(owner)) {
|
|
40
|
+
return `${identity}, recorded before the task header (hash scheme ${TASK_HASH_SCHEME}), so its task cannot be compared. ` +
|
|
41
|
+
`Its answer is already on disk in that folder (codex-bridge read "${ownerDir}"). ` +
|
|
42
|
+
'Another pass of that order needs a continue:/retry: header line; a new task needs a new order id.';
|
|
43
|
+
}
|
|
44
|
+
return `${identity} with a different task. Use a new order id (--order-id), or a continue:/retry: header line ` +
|
|
45
|
+
'if this is another pass of that order.';
|
|
46
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides, before anything is registered, whether a call attaches to an existing run of its order,
|
|
3
|
+
* is a granted continuation, or is a refused repeat.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of launcher.mjs unchanged (Plan_75 P0): the launcher sat at the 400-line gate, and the
|
|
6
|
+
* 2026-10-03 TradeForge advise that died on model capacity showed this gate needs a retry path.
|
|
7
|
+
* It is handed no run directory, so like preflight.mjs it cannot leave a folder behind a refusal.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { chainRuns, startedRuns, taskFingerprint, readJson } from '../write-meta.mjs';
|
|
11
|
+
import { die } from './args.mjs';
|
|
12
|
+
import { EXIT } from './exit-codes.mjs';
|
|
13
|
+
import { attach } from './attach.mjs';
|
|
14
|
+
import { continuationRefusal, retryRefusal, readyGrantLines } from './continuation.mjs';
|
|
15
|
+
import { advisorTaskOrRefuse } from './advise-carry.mjs';
|
|
16
|
+
import { scopeRunRefusal } from './preflight.mjs';
|
|
17
|
+
|
|
18
|
+
export async function passGate({ opts, taskText, header, projectRunsRoot, repoRoot, beforeWait }) {
|
|
19
|
+
// A second pass at the same task is a real need — after a timeout or a LIMIT the work has
|
|
20
|
+
// to be finished — so it is allowed and made visible rather than forbidden. What is
|
|
21
|
+
// forbidden is repeating by accident: the orchestrator saw the previous reply, the runner
|
|
22
|
+
// did not, so only the orchestrator can decide there is something left to finish.
|
|
23
|
+
//
|
|
24
|
+
// The task is identified by its text as well as by its slug, because the slug is chosen by
|
|
25
|
+
// whoever repeats the run: on 2026-08-02 a dispatcher whose launcher was killed restarted
|
|
26
|
+
// the identical order as `<slug>-v2` and spent 46k on it. Refused before the folder exists,
|
|
27
|
+
// like --scope.
|
|
28
|
+
const taskHash = taskFingerprint(taskText);
|
|
29
|
+
const grant = header.grant;
|
|
30
|
+
// OW-040, 2026-09-30: only a retry repeats failed scope without spending advise's continuation.
|
|
31
|
+
if (opts.agent === 'codex-advisor' && opts.phase === 'scope' && opts.continue && grant?.kind !== 'retry') {
|
|
32
|
+
die('codex-advisor --phase scope refuses --continue: a scope pass is never continued, and continuing one ' +
|
|
33
|
+
"spends the order's single continuation that its advise phase needs. Action: repeat the scope under a new order id " +
|
|
34
|
+
"without --continue and without a continue: grant, then run advise as that order's continuation; a " +
|
|
35
|
+
'scope run that failed is repeated with a `retry:` grant instead. The run folder was not created; quota was not spent.');
|
|
36
|
+
}
|
|
37
|
+
let continuationGrant = grant?.kind === 'continue' ? grant : null;
|
|
38
|
+
const retryOf = grant?.kind === 'retry' ? grant.run : null;
|
|
39
|
+
const chain = chainRuns(projectRunsRoot, repoRoot, opts.slug, taskHash, opts.orderId, grant?.run);
|
|
40
|
+
const startedChain = startedRuns(projectRunsRoot, chain);
|
|
41
|
+
const attachExistingRun = () => attach({
|
|
42
|
+
runsRoot: projectRunsRoot, repo: repoRoot, slug: opts.slug, taskHash, orderId: opts.orderId, chain,
|
|
43
|
+
grantRun: continuationGrant?.run, isContinue: opts.continue, noWait: opts.noWait,
|
|
44
|
+
retryRun: retryOf ?? undefined, beforeWait,
|
|
45
|
+
});
|
|
46
|
+
let attachedExitCode = opts.continue ? await attachExistingRun() : null;
|
|
47
|
+
if (attachedExitCode !== null) return { exitCode: attachedExitCode };
|
|
48
|
+
|
|
49
|
+
// The 2026-08-10_220535_plan25-2-install-table-two-roots incident exposed this ordering:
|
|
50
|
+
// a grant without its flag must refuse before attach can print an older run's verdict.
|
|
51
|
+
// markAbandoned() ran before this gate, so a dead runner already has meta.json with FAIL.
|
|
52
|
+
// A run without a verdict can therefore only still be in flight; ordinary repeats go through
|
|
53
|
+
// attach() after this gate. Repeated --continue calls attach first because their grant is spent.
|
|
54
|
+
if (grant?.kind === 'retry') {
|
|
55
|
+
const retryError = retryRefusal(projectRunsRoot, startedChain, opts.continue, opts.orderId, grant);
|
|
56
|
+
if (retryError) die(retryError);
|
|
57
|
+
const namedStatus = readJson(path.join(projectRunsRoot, grant.run, 'status.json'));
|
|
58
|
+
// A run written before phases existed has none; for every agent but the advisor that is 'default'.
|
|
59
|
+
const phaseOf = (value) => String(value ?? '').trim() || 'default';
|
|
60
|
+
if (namedStatus.agent !== opts.agent || phaseOf(namedStatus.phase) !== phaseOf(opts.phase)) {
|
|
61
|
+
die(`a retry repeats the named run's own agent and phase: ${namedStatus.agent}/${namedStatus.phase}. ` +
|
|
62
|
+
'The run folder was not created; quota was not spent.');
|
|
63
|
+
}
|
|
64
|
+
// Plan_75 D4, TradeForge capacity incident: retry advise against its original successful scope.
|
|
65
|
+
continuationGrant = String(namedStatus.continued_from ?? '').trim()
|
|
66
|
+
? { run: namedStatus.continued_from, reason: grant.reason } : null;
|
|
67
|
+
} else {
|
|
68
|
+
const continuationError = continuationRefusal(
|
|
69
|
+
projectRunsRoot,
|
|
70
|
+
startedChain,
|
|
71
|
+
opts.continue,
|
|
72
|
+
opts.orderId,
|
|
73
|
+
continuationGrant,
|
|
74
|
+
);
|
|
75
|
+
if (continuationError) die(continuationError);
|
|
76
|
+
}
|
|
77
|
+
// Plan_59 D14: the grant is valid by now; an advise pass may continue only an OK scope run.
|
|
78
|
+
const scopeRun = continuationGrant &&
|
|
79
|
+
scopeRunRefusal({ agent: opts.agent, phase: opts.phase, runsRoot: projectRunsRoot, grantRun: continuationGrant.run });
|
|
80
|
+
if (scopeRun) die(scopeRun, EXIT.FAIL);
|
|
81
|
+
// Plan_59 D22: the advise snapshot is read before the folder exists, so a broken scope result refuses for free.
|
|
82
|
+
const advisorTask = opts.agent === 'codex-advisor' ? advisorTaskOrRefuse({ taskText, phase: opts.phase, runsRoot: projectRunsRoot, grantRun: continuationGrant?.run }) : null;
|
|
83
|
+
|
|
84
|
+
// One order produced six Codex runs on 2026-08-03 because the caller's time ceiling made it
|
|
85
|
+
// restart the synchronous launcher. A live same-order run is now the repeat target: attach
|
|
86
|
+
// before creating a folder, probing Codex or spending another token.
|
|
87
|
+
if (!opts.continue) attachedExitCode = await attachExistingRun();
|
|
88
|
+
if (attachedExitCode !== null) return { exitCode: attachedExitCode };
|
|
89
|
+
|
|
90
|
+
if (startedChain.length && !opts.continue) {
|
|
91
|
+
const last = startedChain[startedChain.length - 1];
|
|
92
|
+
const lastSlug = String(readJson(path.join(projectRunsRoot, last, 'status.json'))?.slug || '');
|
|
93
|
+
const renamed = lastSlug && lastSlug.toLowerCase() !== String(opts.slug).toLowerCase();
|
|
94
|
+
die(
|
|
95
|
+
`--continue is required: ${
|
|
96
|
+
renamed
|
|
97
|
+
? `this task already ran in this repository under the name “${lastSlug}”`
|
|
98
|
+
: `runs for task “${opts.slug}” already exist in this repository`
|
|
99
|
+
} (${startedChain.length}), latest: ${path.join(projectRunsRoot, last)}. ` +
|
|
100
|
+
'A repeat run is allowed, but the orchestrator decides, not the runner: it read the ' +
|
|
101
|
+
'previous response and knows whether work remains. Add --continue if you are finishing ' +
|
|
102
|
+
'the same task; changing --slug with the same task text does not stop it being a repeat. ' +
|
|
103
|
+
`${readyGrantLines(projectRunsRoot, startedChain)} ` +
|
|
104
|
+
'The run folder was not created; quota was not spent.',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { taskHash, chain, startedChain, continuationGrant, advisorTask, retryOf };
|
|
109
|
+
}
|
|
@@ -6,13 +6,29 @@
|
|
|
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/reply.mjs';
|
|
9
10
|
import { activeRunDetails, readJson } from '../write-meta.mjs';
|
|
10
|
-
import {
|
|
11
|
+
import { probeCodexAvailability } from './codex-availability.mjs';
|
|
11
12
|
import { agentRole } from '../agents.mjs';
|
|
12
13
|
import { die } from './args.mjs';
|
|
13
14
|
|
|
15
|
+
/** Plan_60 D2: only conclusive evidence says UNAVAILABLE; an unclear probe stays an ordinary refusal. */
|
|
16
|
+
export async function codexAvailabilityRefusal({ probe = probeCodexAvailability } = {}) {
|
|
17
|
+
const result = await probe();
|
|
18
|
+
if (result.state === 'available') return null;
|
|
19
|
+
const unavailable = result.state === 'missing' || result.state === 'logged-out';
|
|
20
|
+
const rows = unavailable ? unavailableRows(result.detail) : [
|
|
21
|
+
`Codex CLI unavailable: ${result.detail}`,
|
|
22
|
+
'Operator check: codex --version (and codex login if authorization is rejected)',
|
|
23
|
+
];
|
|
24
|
+
return {
|
|
25
|
+
unavailable,
|
|
26
|
+
text: [...rows, 'The run folder was not created; quota was not spent.'].join('\n'),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
/** Plan_59 D5/D6: settle design authority before any probe can spend quota. */
|
|
15
|
-
export function taskPreflight({ agent, taskText }) {
|
|
31
|
+
export function taskPreflight({ agent, taskText, header }) {
|
|
16
32
|
const freeRefusal = (reason) => ({
|
|
17
33
|
refusal: `${reason}\nThe run folder was not created; quota was not spent.`,
|
|
18
34
|
});
|
|
@@ -21,13 +37,8 @@ export function taskPreflight({ agent, taskText }) {
|
|
|
21
37
|
return reason ? freeRefusal(reason) : { refusal: null };
|
|
22
38
|
}
|
|
23
39
|
if (agent !== 'codex-build') return { refusal: null };
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const match = line.match(/^\s*(?:-\s+)?advice\s*:\s*(.*?)\s*$/i);
|
|
27
|
-
return match ? [match[1]] : [];
|
|
28
|
-
});
|
|
29
|
-
const advice = lines[0];
|
|
30
|
-
if (lines.length === 1) {
|
|
40
|
+
const advice = header.advice;
|
|
41
|
+
if (advice !== null) {
|
|
31
42
|
if (['mechanical', 'revert', 'docs-only', 'test-only'].includes(advice)) {
|
|
32
43
|
return { refusal: null, advice };
|
|
33
44
|
}
|
|
@@ -44,7 +55,8 @@ export function taskPreflight({ agent, taskText }) {
|
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
|
-
return freeRefusal('codex-build requires
|
|
58
|
+
return freeRefusal('codex-build requires the task file to start with the header line `advice: <value>`, where the ' +
|
|
59
|
+
'value is mechanical | revert | docs-only | test-only ' +
|
|
48
60
|
'or an absolute path to an existing advisor run directory whose meta.json says agent codex-advisor, ' +
|
|
49
61
|
'phase advise, status OK. ' +
|
|
50
62
|
'An order that invents a construction needs a second opinion first; only work with no design choice may skip it.');
|
|
@@ -100,13 +112,5 @@ export function preflightRefusal({ agent, projectRunsRoot, repoRoot }) {
|
|
|
100
112
|
].join('\n');
|
|
101
113
|
}
|
|
102
114
|
|
|
103
|
-
const why = codexUnavailableReason();
|
|
104
|
-
if (why !== null) {
|
|
105
|
-
return [
|
|
106
|
-
`Codex CLI unavailable: ${why}`,
|
|
107
|
-
'Operator check: codex --version (and codex login if authorization is rejected)',
|
|
108
|
-
'The run folder was not created; quota was not spent.',
|
|
109
|
-
].join('\n');
|
|
110
|
-
}
|
|
111
115
|
return null;
|
|
112
116
|
}
|
|
@@ -4,9 +4,36 @@
|
|
|
4
4
|
* These are data, not comments: the wording IS the contract a run is graded against, and
|
|
5
5
|
* every rule in it was added by an incident. Keyed by agent name, next to the matching
|
|
6
6
|
* schema in schemas.mjs — one lookup picks both halves of an agent's strategy.
|
|
7
|
+
* Plan_68 D6 renders array bounds from schemas after recommendation truncations on 2026-09-27
|
|
8
|
+
* (`…cli/install.mjs:` + U+0000) and 2026-09-28 (`…(docs/plans/Plan_` + `恋`).
|
|
7
9
|
*/
|
|
8
10
|
import { DEFAULTS } from '../run-config.mjs';
|
|
9
11
|
import { RUN_ENV } from './run-env.mjs';
|
|
12
|
+
import { advisorSchema, schemaFor } from './schemas.mjs';
|
|
13
|
+
import { QUESTION_KIND } from './question-kind.mjs';
|
|
14
|
+
|
|
15
|
+
function boundedArrayRules(schema, path = '') {
|
|
16
|
+
if (schema.type === 'object') {
|
|
17
|
+
return Object.entries(schema.properties ?? {}).flatMap(([name, child]) =>
|
|
18
|
+
boundedArrayRules(child, path ? `${path}.${name}` : name));
|
|
19
|
+
}
|
|
20
|
+
if (schema.type !== 'array') return [];
|
|
21
|
+
|
|
22
|
+
const { minItems, maxItems } = schema;
|
|
23
|
+
const quantity = minItems !== undefined && maxItems !== undefined
|
|
24
|
+
? `${minItems}-${maxItems}`
|
|
25
|
+
: minItems !== undefined ? `at least ${minItems}` : `at most ${maxItems}`;
|
|
26
|
+
const noun = (maxItems ?? minItems) === 1 ? 'item' : 'items';
|
|
27
|
+
const ownRule = minItems === undefined && maxItems === undefined
|
|
28
|
+
? [] : [`- ${path}: ${quantity} ${noun}`];
|
|
29
|
+
return [...ownRule, ...boundedArrayRules(schema.items, `${path}[]`)];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderArrayBounds(schema) {
|
|
33
|
+
const rules = boundedArrayRules(schema);
|
|
34
|
+
return rules.length ? `Output-schema array bounds:\n${rules.join('\n')}` : '';
|
|
35
|
+
}
|
|
36
|
+
|
|
10
37
|
const BODIES = {
|
|
11
38
|
// Plan_59 D3-D5/D7/D10: independent design advice needs explicit lenses and a risk ledger.
|
|
12
39
|
'codex-advisor': (opts) => {
|
|
@@ -27,12 +54,13 @@ Rules for both phases:
|
|
|
27
54
|
an opinion, not advice. Agreeing is fine when your own checks support it.
|
|
28
55
|
${opts.phase === 'scope' ? `
|
|
29
56
|
Scope phase:
|
|
30
|
-
- BEFORE reading in depth, name
|
|
57
|
+
- BEFORE reading in depth, name predicted_risks with ids r1..r5: where this design most likely
|
|
31
58
|
breaks. Record those predictions first, then read; do not replace them with hindsight.
|
|
32
59
|
- Answer sufficient, missing_paths (concrete repository paths you still need), and taken_on_trust
|
|
33
60
|
(facts accepted from the task without checking, never empty), together with predicted_risks.
|
|
34
61
|
` : `
|
|
35
62
|
Advise phase:
|
|
63
|
+
- Give a concise, complete recommendation that can stand on its own.
|
|
36
64
|
- Settle every predicted risk listed under \`## Scope phase results\` in risk_outcomes with its
|
|
37
65
|
risk_id, an outcome of confirmed or refuted, a \`note\` explaining it, and evidence.
|
|
38
66
|
- Give ONE recommendation by task option_id, or none-of-these with a described unlisted_option.
|
|
@@ -66,8 +94,12 @@ ${
|
|
|
66
94
|
: `- Put one object in answers with question_id "Q1": analysis in answer, references in evidence.
|
|
67
95
|
- The top-level answer is the same response summarized in 2-4 sentences.`
|
|
68
96
|
}
|
|
69
|
-
- Base the response on what you read; support every fact with a \`path:line\` reference in where.
|
|
70
|
-
|
|
97
|
+
- Base the response on what you read; support every fact about the code with a \`path:line\` reference in where.
|
|
98
|
+
${questions.some((question) => question?.kind === QUESTION_KIND.STARTUP_CONTEXT)
|
|
99
|
+
? `- A sub-question marked [context-only] asks about what this run was handed at startup
|
|
100
|
+
(task, instructions, schema, environment), not about the repository. Answer it from that context,
|
|
101
|
+
cite evidence as \`startup:<source>\`, and never cite a repository \`path:line\` you did not read
|
|
102
|
+
with a command — that fails the run.\n` : ''}- If facts are missing, list them in unknowns; do not make them up.
|
|
71
103
|
- report_markdown is a full markdown report with sections: “Response” (2-4 sentences),
|
|
72
104
|
“How it works” (required: prose analysis ${questions.length ? 'for each sub-question' : 'of the mechanism'},
|
|
73
105
|
grounded in the code), “Findings” (fact / location / confidence table), “Missing information”.`,
|
|
@@ -142,5 +174,11 @@ const languageRule = () =>
|
|
|
142
174
|
`${RUN_ENV?.answerLanguage || DEFAULTS.answerLanguage}, whatever language the task is written in.`;
|
|
143
175
|
|
|
144
176
|
export const INSTRUCTIONS = Object.fromEntries(
|
|
145
|
-
Object.entries(BODIES).map(([agent, body]) => [agent, (...args) =>
|
|
177
|
+
Object.entries(BODIES).map(([agent, body]) => [agent, (...args) => {
|
|
178
|
+
const opts = args[0] ?? {};
|
|
179
|
+
const instructions = body(...args);
|
|
180
|
+
const schema = agent === 'codex-advisor' ? advisorSchema(opts.phase) : schemaFor(agent);
|
|
181
|
+
const bounds = renderArrayBounds(schema);
|
|
182
|
+
return `${instructions}\n${languageRule()}${bounds ? `\n\n${bounds}` : ''}`;
|
|
183
|
+
}]),
|
|
146
184
|
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifies orderer-marked scout questions and refuses malformed markers before quota is spent.
|
|
3
|
+
* Plan_60 D1: only the orderer knows whether a question concerns handed startup context or code;
|
|
4
|
+
* the scout cannot choose its own kind, and startup context currently fails without a command.
|
|
5
|
+
*/
|
|
6
|
+
export const CONTEXT_ONLY_MARKER = '[context-only]';
|
|
7
|
+
export const QUESTION_KIND = Object.freeze({
|
|
8
|
+
STARTUP_CONTEXT: 'startup-context',
|
|
9
|
+
CODE_REQUIRED: 'code-required',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export function questionKindRefusal(texts = []) {
|
|
13
|
+
for (const [index, text] of texts.entries()) {
|
|
14
|
+
const tokens = text.matchAll(/\[\s*context[\s_-]*only\s*\]/gi);
|
|
15
|
+
for (const token of tokens) {
|
|
16
|
+
if (token.index !== 0 || token[0] !== CONTEXT_ONLY_MARKER) {
|
|
17
|
+
return `Q${index + 1} has an invalid context-only marker in ${JSON.stringify(text)}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (text.startsWith(CONTEXT_ONLY_MARKER)) {
|
|
21
|
+
const question = text.slice(CONTEXT_ONLY_MARKER.length);
|
|
22
|
+
if (!/^\s/.test(question) || !question.trim()) {
|
|
23
|
+
return `Q${index + 1} has an invalid context-only marker in ${JSON.stringify(text)}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function questionsFromTexts(texts = []) {
|
|
31
|
+
const refusal = questionKindRefusal(texts);
|
|
32
|
+
if (refusal) throw new Error(refusal);
|
|
33
|
+
return texts.map((text, index) => {
|
|
34
|
+
const marked = text.startsWith(CONTEXT_ONLY_MARKER);
|
|
35
|
+
return {
|
|
36
|
+
id: `Q${index + 1}`,
|
|
37
|
+
text: marked ? text.slice(CONTEXT_ONLY_MARKER.length).replace(/^\s+/, '') : text,
|
|
38
|
+
kind: marked ? QUESTION_KIND.STARTUP_CONTEXT : QUESTION_KIND.CODE_REQUIRED,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* falling back — a typo must not decide what environment a run gets.
|
|
14
14
|
*/
|
|
15
15
|
import { readRunConfig, disableFlags } from '../run-config.mjs';
|
|
16
|
+
import { EXIT } from './exit-codes.mjs';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* The switches as read (they go to env.json) and the flags they turn into (they go to
|
|
@@ -33,6 +34,6 @@ export function loadRunEnv() {
|
|
|
33
34
|
CLEAN_ENV = disableFlags(RUN_ENV);
|
|
34
35
|
} catch (err) {
|
|
35
36
|
console.log(`FAIL — ${err.message}\nRun was not started; quota was not spent`);
|
|
36
|
-
process.exit(
|
|
37
|
+
process.exit(EXIT.FAIL);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -85,7 +85,7 @@ export async function probeSandbox({
|
|
|
85
85
|
|
|
86
86
|
const version = await attempt('version', ['--version']);
|
|
87
87
|
if (version.error || version.signal || version.status !== 0) {
|
|
88
|
-
//
|
|
88
|
+
// codexAvailabilityRefusal() owns the refusal for an unavailable CLI, earlier in the same pre-flight pass.
|
|
89
89
|
return finish('inconclusive', 'Codex CLI is unavailable.');
|
|
90
90
|
}
|
|
91
91
|
return finish('dead', 'The Codex sandbox on this host cannot start a process.');
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The shape each agent's answer must have, one JSON schema per agent.
|
|
3
3
|
* Plan_59 D10 adds alternatives, rated assumptions, risk outcomes, pre-mortems and open questions.
|
|
4
|
+
* Plan_68 D6 removes recommendation.text's hidden character cap after the 2026-09-27
|
|
5
|
+
* `…cli/install.mjs:` + U+0000 and 2026-09-28 `…(docs/plans/Plan_` + `恋` truncation incidents.
|
|
4
6
|
*
|
|
5
7
|
* Handed to `codex exec --output-schema`, so a run that answers in the wrong shape is
|
|
6
8
|
* rejected by Codex itself rather than discovered later by write-meta.mjs. Keyed by agent
|
|
@@ -222,7 +224,8 @@ export const PHASE_SCHEMAS = {
|
|
|
222
224
|
required: ['option_id', 'text'],
|
|
223
225
|
properties: {
|
|
224
226
|
option_id: { type: 'string' },
|
|
225
|
-
|
|
227
|
+
// Plan_68 D8: the 2026-10-01 runs hit control characters when prose line breaks were banned.
|
|
228
|
+
text: { type: 'string' },
|
|
226
229
|
},
|
|
227
230
|
},
|
|
228
231
|
unlisted_option: { type: 'string' },
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import {
|
|
9
|
+
import { listRepositoryPaths } from './git-paths.mjs';
|
|
10
10
|
import { globToRegExp, normalizePath, SERVICE_RE } from '../meta/paths.mjs';
|
|
11
11
|
|
|
12
12
|
// Directories a self-walk must never descend into. Only reached when the repository has no git:
|
|
@@ -25,6 +25,13 @@ function absolutePattern(pattern) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function structuralRefusal(repoRoot, pattern) {
|
|
28
|
+
// Plan_73 D7: scope lists store one pattern per line; literal line breaks cannot round-trip.
|
|
29
|
+
if (/[\n\r]/.test(pattern)) {
|
|
30
|
+
return {
|
|
31
|
+
reason: 'contains a newline or carriage return',
|
|
32
|
+
action: 'use a glob instead of a literal path containing a line break',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
28
35
|
if (absolutePattern(pattern)) {
|
|
29
36
|
return {
|
|
30
37
|
reason: 'is an absolute or drive-qualified path',
|
|
@@ -74,7 +81,18 @@ function structuralRefusal(repoRoot, pattern) {
|
|
|
74
81
|
return null;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
function noMatchRefusal(pattern) {
|
|
84
|
+
function noMatchRefusal(pattern, repoRoot, gitListed) {
|
|
85
|
+
// OW-042: order plan72-r3-b1-live-thresholds-20261001 ran from a folder outside git, the walk missed a tracked
|
|
86
|
+
// file, and the refusal claimed the file did not exist. A walked folder is not the repository git would list.
|
|
87
|
+
if (gitListed === false) {
|
|
88
|
+
return {
|
|
89
|
+
pattern,
|
|
90
|
+
reason: `does not match any existing path under ${repoRoot}; ` +
|
|
91
|
+
'git could not list that folder as a repository, so it was walked instead',
|
|
92
|
+
action: 'start the run from the repository root (cd into it, or pass --repo <repository root>); ' +
|
|
93
|
+
'if this folder is the intended one, correct the pattern',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
78
96
|
return {
|
|
79
97
|
pattern,
|
|
80
98
|
reason: 'does not match any existing path in the repository',
|
|
@@ -87,21 +105,6 @@ function repositoryPath(repoRoot, absolutePath) {
|
|
|
87
105
|
return normalizePath(relative);
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
/**
|
|
91
|
-
* The repository's own answer to "which files are there": tracked plus new-but-uncommitted, minus
|
|
92
|
-
* everything ignored. It is the same boundary the run is judged by afterwards, it costs one process
|
|
93
|
-
* instead of a full walk, and it cannot wander into `.git` or a dependency tree.
|
|
94
|
-
*/
|
|
95
|
-
function gitPaths(repoRoot) {
|
|
96
|
-
const result = spawnSync(
|
|
97
|
-
'git',
|
|
98
|
-
['-C', repoRoot, 'ls-files', '--cached', '--others', '--exclude-standard'],
|
|
99
|
-
{ encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024 },
|
|
100
|
-
);
|
|
101
|
-
if (result.error || result.status !== 0) return null;
|
|
102
|
-
return result.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
108
|
/** Walks a directory that has no git, skipping dependency trees and anything unreadable. */
|
|
106
109
|
function walkPaths(repoRoot) {
|
|
107
110
|
const pending = [repoRoot];
|
|
@@ -129,9 +132,14 @@ function walkPaths(repoRoot) {
|
|
|
129
132
|
return found;
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Names come from git-paths.mjs so Plan_73's 2026-09-30 Cyrillic incident cannot recur through
|
|
137
|
+
* quoted git output or trimmed names. Its tracked-plus-untracked, non-ignored list costs one
|
|
138
|
+
* process instead of a walk and cannot wander into `.git` or a dependency tree.
|
|
139
|
+
*/
|
|
132
140
|
function matchingPaths(repoRoot, matchers) {
|
|
133
|
-
const
|
|
134
|
-
|
|
141
|
+
const listed = listRepositoryPaths(repoRoot);
|
|
142
|
+
const candidates = listed ?? walkPaths(repoRoot);
|
|
135
143
|
const matched = new Set();
|
|
136
144
|
for (const candidate of candidates) {
|
|
137
145
|
for (const matcher of matchers) {
|
|
@@ -141,12 +149,15 @@ function matchingPaths(repoRoot, matchers) {
|
|
|
141
149
|
}
|
|
142
150
|
if (matched.size === matchers.length) break;
|
|
143
151
|
}
|
|
144
|
-
return matched;
|
|
152
|
+
return { matched, gitListed: listed !== null };
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
function patternList(value, label) {
|
|
148
156
|
if (!Array.isArray(value)) throw new TypeError(`${label} must be an array`);
|
|
149
|
-
return value.map((pattern) =>
|
|
157
|
+
return value.map((pattern) => {
|
|
158
|
+
const text = String(pattern ?? '');
|
|
159
|
+
return /[\n\r]/.test(text) ? text : text.trim();
|
|
160
|
+
});
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
/**
|
|
@@ -159,14 +170,18 @@ export function validateScope(repoRoot, patterns, scopeNewPatterns = []) {
|
|
|
159
170
|
const declared = patternList(patterns, 'patterns');
|
|
160
171
|
const newPaths = patternList(scopeNewPatterns, 'scopeNewPatterns');
|
|
161
172
|
const allPatterns = [...declared, ...newPaths];
|
|
173
|
+
const newPathKeys = new Set(newPaths.map((pattern) => normalizePath(pattern)));
|
|
162
174
|
|
|
175
|
+
// The flag is decided by --scope-new membership, not list position: args.mjs merges every --scope-new
|
|
176
|
+
// pattern into the declared list, so position named --scope for a directory given only to --scope-new
|
|
177
|
+
// (OW-042 B1, caught on the live runner 2026-10-04 after the unit test passed separate lists).
|
|
163
178
|
for (const pattern of allPatterns) {
|
|
179
|
+
const flag = newPathKeys.has(normalizePath(pattern)) ? '--scope-new' : '--scope';
|
|
164
180
|
const refusal = structuralRefusal(repoRoot, pattern);
|
|
165
|
-
if (refusal) return { pattern, ...refusal };
|
|
166
|
-
if (!pattern) return noMatchRefusal(pattern);
|
|
181
|
+
if (refusal) return { pattern, flag, ...refusal };
|
|
182
|
+
if (!pattern) return { ...noMatchRefusal(pattern), flag };
|
|
167
183
|
}
|
|
168
184
|
|
|
169
|
-
const newPathKeys = new Set(newPaths.map((pattern) => normalizePath(pattern)));
|
|
170
185
|
const required = allPatterns.filter((pattern) => !newPathKeys.has(normalizePath(pattern)));
|
|
171
186
|
if (!required.length) return null;
|
|
172
187
|
|
|
@@ -175,13 +190,25 @@ export function validateScope(repoRoot, patterns, scopeNewPatterns = []) {
|
|
|
175
190
|
matchers = required.map((pattern) => ({ pattern, regexp: globToRegExp(pattern) }));
|
|
176
191
|
} catch (error) {
|
|
177
192
|
return {
|
|
178
|
-
pattern: required[0]
|
|
193
|
+
pattern: required[0],
|
|
194
|
+
flag: '--scope',
|
|
179
195
|
reason: `could not parse the pattern: ${error.message}`,
|
|
180
196
|
action: 'correct the pattern and retry',
|
|
181
197
|
};
|
|
182
198
|
}
|
|
183
199
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
200
|
+
let matches;
|
|
201
|
+
try {
|
|
202
|
+
matches = matchingPaths(repoRoot, matchers);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error.code !== 'ERR_GIT_PATH_NOT_UTF8') throw error;
|
|
205
|
+
return {
|
|
206
|
+
pattern: required[0],
|
|
207
|
+
flag: '--scope',
|
|
208
|
+
reason: error.message,
|
|
209
|
+
action: 'rename the non-UTF-8 file using a UTF-8 file name and retry',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const missing = matchers.find((matcher) => !matches.matched.has(matcher.pattern));
|
|
213
|
+
return missing ? { ...noMatchRefusal(missing.pattern, repoRoot, matches.gitListed), flag: '--scope' } : null;
|
|
187
214
|
}
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* questions in the file got refused for not passing --question.
|
|
9
9
|
*/
|
|
10
10
|
import { die, readTaskDocument } from './args.mjs';
|
|
11
|
+
import { questionKindRefusal } from './question-kind.mjs';
|
|
11
12
|
|
|
12
13
|
export function settleTaskInput(opts) {
|
|
13
|
-
const { task, questions: fileQuestions, verify: fileVerify } = readTaskDocument(opts);
|
|
14
|
+
const { task, header, questions: fileQuestions, verify: fileVerify } = readTaskDocument(opts);
|
|
14
15
|
opts.questions ??= fileQuestions.length ? fileQuestions : undefined;
|
|
15
16
|
opts.verify ??= fileVerify;
|
|
16
17
|
// Still before the run folder exists and before a token of someone else's quota is touched,
|
|
@@ -22,5 +23,13 @@ export function settleTaskInput(opts) {
|
|
|
22
23
|
'The runner will not infer questions from the task text; no quota was spent.',
|
|
23
24
|
);
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
if (opts.agent === 'codex-scout') {
|
|
27
|
+
const refusal = questionKindRefusal(opts.questions);
|
|
28
|
+
if (refusal) {
|
|
29
|
+
die(
|
|
30
|
+
`${refusal}; the marker is exactly [context-only] followed by a space and the question; no quota was spent.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { task, header };
|
|
26
35
|
}
|