sprag-cli 3.62.0 → 3.64.0
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/bin/cli.js +3 -25
- package/docs/CODEX.ko.md +65 -6
- package/docs/CODEX.md +105 -13
- package/mod/sprag-delegation-meter/.claude-plugin/plugin.json +6 -0
- package/mod/sprag-delegation-meter/hooks/hooks.json +3 -0
- package/mod/sprag-delegation-meter/hooks/register.ts +197 -0
- package/mod/sprag-doc2md/.claude-plugin/plugin.json +6 -0
- package/mod/sprag-doc2md/hooks/hooks.json +3 -0
- package/mod/sprag-doc2md/hooks/register.ts +97 -0
- package/package.json +4 -1
- package/src/codex-brief.js +10 -1
- package/src/codex-delegation-diagnostics.js +42 -0
- package/src/codex-delegation.js +55 -9
- package/src/codex-doc2md-ledger.js +53 -8
- package/src/codex-doc2md.js +21 -4
- package/src/codex-doctor.js +9 -1
- package/src/codex-harness.js +87 -11
- package/src/codex-hooks.js +23 -9
- package/src/codex-installer.js +1 -0
- package/src/codex-ledger.js +25 -1
- package/src/codex-panel.js +35 -14
- package/src/codex-reply-language.js +43 -0
- package/src/codex-rule-health.js +23 -0
- package/src/commands/codex-delegate.js +19 -5
- package/src/commands/codex.js +48 -11
- package/src/commands/doc2md.js +12 -0
- package/src/commands/install.js +8 -0
- package/src/commands/korean.js +3 -0
- package/src/commands/route-scan.js +2 -36
- package/src/commands/saved.js +10 -4
- package/src/commands/update-check.js +7 -4
- package/src/implementation-delegation.js +34 -0
- package/src/installer.js +135 -4
- package/src/model-rules.js +3 -20
- package/src/reply-language.cjs +3 -2
- package/src/update-notice.js +37 -0
package/src/codex-harness.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { codexUserDir } from './agent.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { koreanStyleEnabled, koreanStyleInjection } from './korean-style.js';
|
|
5
7
|
import { codexToolEvents, codexLegacyToolEvent, readCodexTailLines, forkedCopyFilter } from './codex-parser.js';
|
|
8
|
+
import { implementationDelegationRule } from './implementation-delegation.js';
|
|
6
9
|
|
|
7
10
|
export const CODEX_BEGIN = '<!-- sprag:codex:harness:begin -->';
|
|
8
11
|
export const CODEX_END = '<!-- sprag:codex:harness:end -->';
|
|
12
|
+
export const CODEX_KOREAN_BEGIN = '<!-- sprag:codex:korean-style:begin -->';
|
|
13
|
+
export const CODEX_KOREAN_END = '<!-- sprag:codex:korean-style:end -->';
|
|
9
14
|
const sections = [
|
|
10
15
|
['Ratchet', 'When a mistake recurs, propose a concise condition/action rule. Ask the user for global or project scope before registering it with `sprag harness promote --agent codex --global|--project "rule"`.'],
|
|
11
16
|
['Evidence', 'Support completion claims with actual test results, command output, a file diff, or a screenshot. State any verification you could not run.'],
|
|
@@ -16,11 +21,15 @@ const sections = [
|
|
|
16
21
|
|
|
17
22
|
const scopeDir = (root, scope) => (scope === 'global' ? codexUserDir() : root);
|
|
18
23
|
|
|
24
|
+
/** Codex reads the override instead of AGENTS.md when it is non-empty. */
|
|
25
|
+
function activeInstructionFile(dir) {
|
|
26
|
+
const override = join(dir, 'AGENTS.override.md');
|
|
27
|
+
return existsSync(override) && readFileSync(override, 'utf8').trim() ? override : join(dir, 'AGENTS.md');
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
export function codexHarnessPaths(root, scope = 'project') {
|
|
20
31
|
const dir = scopeDir(root, scope);
|
|
21
|
-
|
|
22
|
-
const override = join(dir, 'AGENTS.override.md');
|
|
23
|
-
const file = existsSync(override) && readFileSync(override, 'utf8').trim() ? override : join(dir, 'AGENTS.md');
|
|
32
|
+
const file = activeInstructionFile(dir);
|
|
24
33
|
return { file, ratchet: scope === 'global' ? join(dir, 'ratchet.md') : join(dir, '.codex', 'ratchet.md') };
|
|
25
34
|
}
|
|
26
35
|
|
|
@@ -31,18 +40,21 @@ function harnessFiles(root, scope) {
|
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
export function codexHarnessBlock() {
|
|
34
|
-
return `${CODEX_BEGIN}\n## Sprag Harness\n\n${sections.map(([name, text], i) => `### ${i + 1}. ${name}\n${text}`).join('\n\n')}\n\nRead the global and project Codex ratchet files before working when present. Run \`sprag harness list --agent codex --global\` and \`sprag harness list --agent codex --project\` to retrieve them. These are instructions, not Claude-style @ imports.\nUse \`sprag doc2md <file> --agent codex\` before reading PDF, Office, or Figma documents. Delegate only through available Codex tools and configured roles; do not assume Claude model names or Task/Agent arguments. A [Sprag model routing] note on a request means the user has asked for that request to be delegated: spawn the sub-agent it describes (its model, reasoning_effort, fork_turns "none", and a self-contained message ending with the route line it gives), wait for it to finish instead of doing the same task yourself, then verify the result before replying.\n${CODEX_END}\n`;
|
|
43
|
+
return `${CODEX_BEGIN}\n## Sprag Harness\n\n${sections.map(([name, text], i) => `### ${i + 1}. ${name}\n${text}`).join('\n\n')}\n\nRead the global and project Codex ratchet files before working when present. Run \`sprag harness list --agent codex --global\` and \`sprag harness list --agent codex --project\` to retrieve them. These are instructions, not Claude-style @ imports.\nUse \`sprag doc2md <file> --agent codex\` before reading PDF, Office, or Figma documents. Delegate only through available Codex tools and configured roles; do not assume Claude model names or Task/Agent arguments. A [Sprag model routing] note on a request means the user has asked for that request to be delegated: spawn the sub-agent it describes (its model, reasoning_effort, fork_turns "none", and a self-contained message ending with the route line it gives), wait for it to finish instead of doing the same task yourself, then verify the result before replying.\n\n${implementationDelegationRule('codex')}\nAfter fixing the design, resolve T1 with \`sprag delegate implementation --agent codex --from <active-parent-model> --provider <active-provider> --session <parent-session-id>\`. This implementation-split policy is the user's delegation request, including selection of the returned model. Do not infer the active model from the default config. Send the returned route line only at the end of the child message, use fork_turns "none", and wait with wait_agent (timeout_ms at least 120000, repeating if needed). If tools are unavailable, do not run the resolver or launch another CLI; work on the main agent and disclose it.\n${CODEX_END}\n`;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
/** The [start, end) span of a marker block, null when absent; malformed markers throw. */
|
|
47
|
+
function range(content, beginMarker = CODEX_BEGIN, endMarker = CODEX_END, label = 'harness') {
|
|
48
|
+
const begin = content.indexOf(beginMarker);
|
|
49
|
+
const end = content.indexOf(endMarker);
|
|
50
|
+
if ((begin < 0) !== (end < 0) || (begin >= 0 && end < begin) || content.indexOf(beginMarker, begin + beginMarker.length) >= 0) {
|
|
51
|
+
throw new Error(`Malformed Sprag Codex ${label} markers; repair them before updating this file.`);
|
|
42
52
|
}
|
|
43
|
-
return begin < 0 ? null : [begin, end +
|
|
53
|
+
return begin < 0 ? null : [begin, end + endMarker.length];
|
|
44
54
|
}
|
|
45
55
|
|
|
56
|
+
const koreanRange = (content) => range(content, CODEX_KOREAN_BEGIN, CODEX_KOREAN_END, 'korean-style');
|
|
57
|
+
|
|
46
58
|
export function initCodexHarness({ root, scope = 'project' }) {
|
|
47
59
|
const paths = codexHarnessPaths(root, scope);
|
|
48
60
|
const existing = existsSync(paths.file) ? readFileSync(paths.file, 'utf8') : '';
|
|
@@ -95,10 +107,74 @@ export function uninitCodexHarness({ root, scope = 'project' }) {
|
|
|
95
107
|
writeFileSync(candidate, withoutBlock(existing, span));
|
|
96
108
|
cleaned.push(candidate);
|
|
97
109
|
}
|
|
110
|
+
// The Korean style block lives only in the global file; project scope never has one.
|
|
111
|
+
if (scope === 'global') {
|
|
112
|
+
for (const candidate of removeKoreanBlocks(scopeDir(root, scope))) if (!cleaned.includes(candidate)) cleaned.push(candidate);
|
|
113
|
+
}
|
|
98
114
|
if (!cleaned.length) return { file, removed: false };
|
|
99
115
|
return { file: cleaned.includes(file) ? file : cleaned[0], removed: true, ...(cleaned.length > 1 ? { files: cleaned } : {}) };
|
|
100
116
|
}
|
|
101
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Backup next to the file. A fresh install writes two backups in one
|
|
120
|
+
* millisecond (harness block, then Korean block); `wx` and a counter keep the
|
|
121
|
+
* second from overwriting the first, which holds the user's original text.
|
|
122
|
+
*/
|
|
123
|
+
function backupFile(file, text) {
|
|
124
|
+
const base = `${file}.bak-${Date.now()}`;
|
|
125
|
+
for (let i = 0; ; i++) {
|
|
126
|
+
try { writeFileSync(i ? `${base}-${i}` : base, text, { flag: 'wx' }); return; }
|
|
127
|
+
catch (e) { if (e?.code !== 'EEXIST') throw e; }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Removes the Korean block from every instruction file of `dir` that holds one; returns those files. */
|
|
132
|
+
function removeKoreanBlocks(dir) {
|
|
133
|
+
const cleaned = [];
|
|
134
|
+
for (const candidate of [join(dir, 'AGENTS.override.md'), join(dir, 'AGENTS.md')]) {
|
|
135
|
+
if (!existsSync(candidate)) continue;
|
|
136
|
+
const existing = readFileSync(candidate, 'utf8');
|
|
137
|
+
const span = koreanRange(existing);
|
|
138
|
+
if (!span) continue;
|
|
139
|
+
const next = withoutBlock(existing, span);
|
|
140
|
+
if (next.trim()) backupFile(candidate, existing);
|
|
141
|
+
writeFileSync(candidate, next);
|
|
142
|
+
cleaned.push(candidate);
|
|
143
|
+
}
|
|
144
|
+
return cleaned;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Keeps the Korean style guide in the global Codex instruction file. Codex
|
|
149
|
+
* keeps only about 2,000 tokens of one hook's context, which the guide never
|
|
150
|
+
* fits, but it loads the global AGENTS.md in full every session. The block is
|
|
151
|
+
* written while the style is enabled and removed when it is not.
|
|
152
|
+
* Returns { action: 'written' | 'unchanged' | 'removed' | 'absent' | 'skipped', file }.
|
|
153
|
+
* `skipped` means the Codex directory does not exist, so nothing was touched.
|
|
154
|
+
*/
|
|
155
|
+
export function syncCodexKoreanBlock({ cfg = loadConfig(), home } = {}) {
|
|
156
|
+
const dir = home ? resolve(home) : codexUserDir();
|
|
157
|
+
const file = activeInstructionFile(dir);
|
|
158
|
+
if (!existsSync(dir)) return { action: 'skipped', file };
|
|
159
|
+
const text = koreanStyleEnabled(cfg) ? koreanStyleInjection({ cfg }) : null;
|
|
160
|
+
if (!text) {
|
|
161
|
+
const cleaned = removeKoreanBlocks(dir);
|
|
162
|
+
return cleaned.length ? { action: 'removed', file: cleaned.includes(file) ? file : cleaned[0] } : { action: 'absent', file };
|
|
163
|
+
}
|
|
164
|
+
const block = `${CODEX_KOREAN_BEGIN}\n${text}\n${CODEX_KOREAN_END}`;
|
|
165
|
+
const existing = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
166
|
+
const span = koreanRange(existing);
|
|
167
|
+
if (span && existing.slice(...span) === block) return { action: 'unchanged', file };
|
|
168
|
+
const body = existing.replace(/\n+$/, '');
|
|
169
|
+
const next = span ? existing.slice(0, span[0]) + block + existing.slice(span[1])
|
|
170
|
+
: body + (body ? '\n\n' : '') + block + '\n';
|
|
171
|
+
// Replacing our own block loses nothing; the first addition to a user file gets a copy.
|
|
172
|
+
if (!span && existing.trim()) backupFile(file, existing);
|
|
173
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
174
|
+
writeFileSync(file, next);
|
|
175
|
+
return { action: 'written', file };
|
|
176
|
+
}
|
|
177
|
+
|
|
102
178
|
export function codexHarnessStatus({ root, scope = 'project' }) {
|
|
103
179
|
const { file } = codexHarnessPaths(root, scope);
|
|
104
180
|
const text = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
package/src/codex-hooks.js
CHANGED
|
@@ -6,17 +6,18 @@ import { join, resolve } from 'node:path';
|
|
|
6
6
|
import { loadConfig, userLanguage } from './config.js';
|
|
7
7
|
import { koreanStyleInjection, koreanStyleEnabled } from './korean-style.js';
|
|
8
8
|
import { cohesionInjection } from './cohesion.js';
|
|
9
|
-
import { codexHarnessPaths } from './codex-harness.js';
|
|
9
|
+
import { codexHarnessPaths, syncCodexKoreanBlock } from './codex-harness.js';
|
|
10
10
|
import { writeViaTmp } from './state-file.js';
|
|
11
11
|
import { findProjectRoot } from './harness.js';
|
|
12
12
|
import { runCodexBrief } from './codex-brief.js';
|
|
13
|
-
import { codexDocumentTool, codexDocumentNote, convertCodexDocument } from './codex-doc2md.js';
|
|
14
|
-
import { codexDelegationTool, codexRouteHint } from './codex-delegation.js';
|
|
13
|
+
import { codexDocumentTool, codexDocumentNote, convertCodexDocument, codexDocumentContext } from './codex-doc2md.js';
|
|
14
|
+
import { codexDelegationTool, codexRouteHint, codexDelegateEnabled } from './codex-delegation.js';
|
|
15
15
|
import { bindCodexSubagent, closeStaleCodexRoutes, unsettledCodexRoutes } from './codex-ledger.js';
|
|
16
16
|
import { seedOfferBlock } from './seed-rules.js';
|
|
17
17
|
import { readCodexRouteScan, openCodexCandidates, shouldRescanCodex } from './codex-route-scan.js';
|
|
18
18
|
import { userDataDir } from './paths.js';
|
|
19
19
|
import { debug } from './debug.js';
|
|
20
|
+
import { updateOffer } from './update-notice.js';
|
|
20
21
|
|
|
21
22
|
const require = createRequire(import.meta.url);
|
|
22
23
|
const lint = require('./korean-lint.cjs');
|
|
@@ -162,10 +163,16 @@ export function lintCodexTool(payload, { scope = 'all' } = {}) {
|
|
|
162
163
|
return messages.length ? messages.join('\n\n') : null;
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
export async function codexHookOutput(event, payload, { cfg = loadConfig(), refreshLedger = defaultLedgerRefresh } = {}) {
|
|
166
|
+
export async function codexHookOutput(event, payload, { cfg = loadConfig(), refreshLedger = defaultLedgerRefresh, version } = {}) {
|
|
166
167
|
if (!payload || typeof payload !== 'object') return null;
|
|
168
|
+
if (event === 'stop') {
|
|
169
|
+
if (payload.stop_hook_active || !koreanStyleEnabled(cfg) || cfg?.koreanReplyCheck === false) return null;
|
|
170
|
+
const { checkCodexReplyLanguage } = await import('./codex-reply-language.js');
|
|
171
|
+
if (!checkCodexReplyLanguage(payload)) return null;
|
|
172
|
+
return { decision: 'block', reason: '사용자는 한국어로 질문했는데 마지막 답변이 영어로 작성됐습니다. 같은 내용을 한국어로 다시 답하십시오. 코드, 명령어, 오류 원문, 커밋 메시지는 그대로 유지하십시오.' };
|
|
173
|
+
}
|
|
167
174
|
if (event === 'session-start' || event === 'subagent-start') {
|
|
168
|
-
if (event === 'subagent-start' && cfg
|
|
175
|
+
if (event === 'subagent-start' && !codexDelegateEnabled(cfg)) return null;
|
|
169
176
|
if (event === 'subagent-start') {
|
|
170
177
|
try { bindCodexSubagent(payload); } catch (e) { debug('codex:bind', e); }
|
|
171
178
|
}
|
|
@@ -193,11 +200,17 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
193
200
|
}
|
|
194
201
|
if (event === 'subagent-start') parts.push({ priority: 0, text:
|
|
195
202
|
'[Sprag Codex delegation]\nStay within the assigned task and any caller-provided budget. Return findings with file references and actual verification results; state unfinished work. Use only available Codex tools and configured roles. Do not infer a model tier or invent a tool-call budget.' });
|
|
196
|
-
|
|
197
|
-
|
|
203
|
+
// Codex reads the Korean guide from the global AGENTS.md, so the hook adds it
|
|
204
|
+
// only when that block was just written (this session may have loaded the old
|
|
205
|
+
// file) or could not be synced. Children read the same file.
|
|
206
|
+
let korean = null;
|
|
207
|
+
try { korean = syncCodexKoreanBlock({ cfg }); } catch (e) { debug('codex:korean-sync', e); }
|
|
208
|
+
if (korean?.action !== 'unchanged') parts.push({ priority: 2, spill: true, text: koreanStyleInjection({ cfg }) });
|
|
209
|
+
parts.push({ priority: 3, spill: true, text: await cohesionInjection({ cfg }) });
|
|
198
210
|
if (process.env.CTS_NO_DOC2MD !== '1' && cfg?.codex?.doc2md !== false) parts.push({ priority: 4, spill: true, text: codexDocumentNote() });
|
|
199
211
|
// Offers ask the user a question; a `codex exec` run has no one to answer it.
|
|
200
212
|
if (event === 'session-start' && payload.source !== 'compact' && !isCodexExecSession(payload.transcript_path)) {
|
|
213
|
+
if (version) parts.push({ priority: 5, text: updateOffer(version, { agent: 'codex', lang: userLanguage() }) });
|
|
201
214
|
try { parts.push({ priority: 5, text: await routeNotice(root) }); } catch (e) { debug('codex:route-notice', e); }
|
|
202
215
|
try { parts.push({ priority: 6, text: seedOfferBlock({ root, agent: 'codex' }) }); } catch (e) { debug('codex:seed-offer', e); }
|
|
203
216
|
}
|
|
@@ -206,7 +219,7 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
206
219
|
}
|
|
207
220
|
if (event === 'prompt') {
|
|
208
221
|
const parts = [];
|
|
209
|
-
if (cfg
|
|
222
|
+
if (codexDelegateEnabled(cfg)) {
|
|
210
223
|
// Before the hint for this prompt is recorded: a hint from an earlier turn
|
|
211
224
|
// that no child claimed is closed, and children that did run get priced.
|
|
212
225
|
try { closeStaleCodexRoutes(payload); } catch (e) { debug('codex:close-routes', e); }
|
|
@@ -223,7 +236,8 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
223
236
|
catch (e) { debug('codex:brief', e); }
|
|
224
237
|
}
|
|
225
238
|
if (process.env.CTS_NO_DOC2MD !== '1' && cfg?.codex?.doc2md !== false) {
|
|
226
|
-
|
|
239
|
+
const identity = codexDocumentContext(payload);
|
|
240
|
+
parts.push(doc2md.contextForPrompt(payload, { lang: userLanguage(), agent: 'codex', convert: (source) => convertCodexDocument(source, identity) }));
|
|
227
241
|
}
|
|
228
242
|
return context('UserPromptSubmit', parts.filter(Boolean).join('\n\n'));
|
|
229
243
|
}
|
package/src/codex-installer.js
CHANGED
|
@@ -9,6 +9,7 @@ export const CODEX_HANDLERS = {
|
|
|
9
9
|
PreToolUse: { matcher: '^(Bash|Read|read_file|Edit|Write|Agent|spawn_agent|mcp__filesystem__read_(?:text_)?file|(?:functions\\.|tools\\.)?(?:exec_command|shell_command|shell|apply_patch|spawn_agent))$', command: 'sprag codex-hook --agent codex --event pre-tool', timeout: 600 },
|
|
10
10
|
PostToolUse: { matcher: '^(Bash|apply_patch|Edit|Write|(?:functions\\.|tools\\.)?(?:exec_command|shell_command|shell|apply_patch))$', command: 'sprag codex-hook --agent codex --event post-tool', timeout: 15 },
|
|
11
11
|
SubagentStart: { command: 'sprag codex-hook --agent codex --event subagent-start', timeout: 15 },
|
|
12
|
+
Stop: { command: 'sprag codex-hook --agent codex --event stop', timeout: 15 },
|
|
12
13
|
};
|
|
13
14
|
const ownCommands = new Set(Object.values(CODEX_HANDLERS).map((h) => h.command));
|
|
14
15
|
const PANEL_COMMAND = 'sprag codex-hook --agent codex --event panel-start';
|
package/src/codex-ledger.js
CHANGED
|
@@ -177,6 +177,29 @@ export function loadCodexLedger({ dir = userDataDir() } = {}) {
|
|
|
177
177
|
return { version: 1, events: {} };
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/** Offers are not executions. Count tier alternatives as one offer per prompt. */
|
|
181
|
+
export function codexDelegationStatus({ dir = userDataDir(), sessionId, now = Date.now() } = {}) {
|
|
182
|
+
const routes = readCodexDelegations({ dir }).filter((r) => now - r.at < PENDING_TTL_MS && r.at <= now &&
|
|
183
|
+
(!sessionId || r.parentSessionId === sessionId));
|
|
184
|
+
const bound = new Set(readCodexBindings({ dir }).map((b) => b.id));
|
|
185
|
+
const events = loadCodexLedger({ dir }).events;
|
|
186
|
+
const closed = closedCodexRouteIds({ dir });
|
|
187
|
+
const offers = new Map();
|
|
188
|
+
for (const r of routes) {
|
|
189
|
+
const key = r.offerId || r.id;
|
|
190
|
+
if (!offers.has(key)) offers.set(key, []);
|
|
191
|
+
offers.get(key).push(r);
|
|
192
|
+
}
|
|
193
|
+
const result = { offers: offers.size, bound: 0, settled: 0, pending: 0, unclaimed: 0 };
|
|
194
|
+
for (const group of offers.values()) {
|
|
195
|
+
if (group.some((r) => events[r.id]?.complete || events[r.id]?.aborted)) result.settled++;
|
|
196
|
+
else if (group.some((r) => bound.has(r.id) || events[r.id])) result.bound++;
|
|
197
|
+
else if (group.every((r) => closed.has(r.id) || now - r.at >= BIND_TTL_MS)) result.unclaimed++;
|
|
198
|
+
else result.pending++;
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
180
203
|
function saveLedger(data, dir) {
|
|
181
204
|
mkdirSync(dir, { recursive: true });
|
|
182
205
|
const file = ledgerFile(dir), tmp = `${file}.${randomUUID()}.tmp`;
|
|
@@ -242,7 +265,7 @@ export async function refreshCodexLedger({ dir = userDataDir(), home, now = Date
|
|
|
242
265
|
if (route.provider && route.provider !== run.provider) continue;
|
|
243
266
|
// A shared policy names its model only through the caller's choice, so
|
|
244
267
|
// credit it only for a child of that parent that ran the mapped model.
|
|
245
|
-
if (route.source
|
|
268
|
+
if (['shared', 'implementation'].includes(route.source) && (run.model !== route.to || !route.parentSessionId ||
|
|
246
269
|
run.parentThreadId !== route.parentSessionId)) continue;
|
|
247
270
|
if (Number.isFinite(run.startedAt) && run.startedAt < route.at - 5000) continue;
|
|
248
271
|
// A closed route ended with its hinted turn. Only a child that started
|
|
@@ -265,6 +288,7 @@ export async function refreshCodexLedger({ dir = userDataDir(), home, now = Date
|
|
|
265
288
|
const event = { ts: run.endedAt ?? route.at, parentSessionId: route.parentSessionId || run.parentThreadId || null,
|
|
266
289
|
childSessionId: run.sessionId, source: route.source, category: route.category ?? null, scope: route.scope ?? null,
|
|
267
290
|
...(route.source === 'shared' ? { tier: route.tier, sharedSignature: route.sharedSignature } : {}),
|
|
291
|
+
...(route.source === 'implementation' ? { tier: route.tier } : {}),
|
|
268
292
|
from: route.from, to, provider,
|
|
269
293
|
...(route.targetRoot !== undefined ? { targetRoot: route.targetRoot } : {}),
|
|
270
294
|
...(route.ruleCreatedAt !== undefined ? { ruleCreatedAt: route.ruleCreatedAt } : {}), tokens: run.tokens, calls: run.calls, toolErrors: run.toolErrors, usd, rates: usd === null ? null : rates,
|
package/src/codex-panel.js
CHANGED
|
@@ -17,15 +17,16 @@ import { formatResetIn } from './format-time.js';
|
|
|
17
17
|
import { codexDocumentTotals } from './codex-doc2md-ledger.js';
|
|
18
18
|
import { codexHookStatus } from './codex-installer.js';
|
|
19
19
|
import { koreanStyleEnabled } from './korean-style.js';
|
|
20
|
-
import { codexRoutingSavedTotals, loadCodexLedger } from './codex-ledger.js';
|
|
21
|
-
import { loadCodexModelRules } from './codex-delegation.js';
|
|
22
|
-
import { codexRulesInReview } from './codex-rule-health.js';
|
|
20
|
+
import { codexRoutingSavedTotals, loadCodexLedger, codexDelegationStatus } from './codex-ledger.js';
|
|
21
|
+
import { loadCodexModelRules, codexDelegateEnabled } from './codex-delegation.js';
|
|
22
|
+
import { codexRulesInReview, codexSharedRuleHealth } from './codex-rule-health.js';
|
|
23
23
|
import { readCodexRouteScan, openCodexCandidates } from './codex-route-scan.js';
|
|
24
24
|
import { codexBudgetProvider } from './codex-budget.js';
|
|
25
|
+
import { readUpdateChip } from './update-notice.js';
|
|
25
26
|
|
|
26
27
|
export const CODEX_SEGMENT_ORDER = [
|
|
27
|
-
'cap-warn', 'usage', 'ctx', 'ttl', 'model', 'effort',
|
|
28
|
-
'hit', 'saved', 'doc2md', 'harness', 'korean', 'version',
|
|
28
|
+
'cap-warn', 'update', 'usage', 'ctx', 'ttl', 'model', 'effort',
|
|
29
|
+
'hit', 'saved', 'doc2md', 'routing', 'harness', 'korean', 'version',
|
|
29
30
|
];
|
|
30
31
|
|
|
31
32
|
// Only cache full-session parses. A moving time cutoff would make an unchanged
|
|
@@ -34,7 +35,8 @@ export const CODEX_SEGMENT_ORDER = [
|
|
|
34
35
|
const RATCHET_SIBLING_MS = 30 * 60000;
|
|
35
36
|
|
|
36
37
|
export function createPanelReader({ root = process.cwd(), home, sessionId: pinnedId, sessionFile, days = 7, readBudget = () => null,
|
|
37
|
-
readCachePolicy = (session) => readCodexCachePolicy(session, { home }), hasFlag = () => false
|
|
38
|
+
readCachePolicy = (session) => readCodexCachePolicy(session, { home }), hasFlag = () => false,
|
|
39
|
+
version, refreshUpdate = false } = {}) {
|
|
38
40
|
if (pinnedId && sessionFile) throw new Error('Choose --session or --session-file, not both.');
|
|
39
41
|
const cache = new Map();
|
|
40
42
|
const ratchetCache = new Map();
|
|
@@ -121,11 +123,15 @@ export function createPanelReader({ root = process.cwd(), home, sessionId: pinne
|
|
|
121
123
|
const modelRules = loadCodexModelRules();
|
|
122
124
|
if (modelRules.length) ruleReview = codexRulesInReview({ root: project, rules: modelRules, events: Object.values(loadCodexLedger().events) }).map((r) => r.index);
|
|
123
125
|
} catch { /* Unreadable rules or ledger show no chip. */ }
|
|
126
|
+
let sharedRuleReview = [];
|
|
127
|
+
try { sharedRuleReview = codexSharedRuleHealth({ root: project }).filter((r) => r.health.status === 'review'); }
|
|
128
|
+
catch { /* Shared policy health must not hide the rest of the panel. */ }
|
|
124
129
|
let routingSaved = null;
|
|
125
130
|
try { routingSaved = { ...codexRoutingSavedTotals(), pricesAvailable: isDirectOpenAI(selected?.provider, home ? { home } : {}) || !!readCodexPrices({ provider: codexBudgetProvider(home ? { home } : {}), ...(home ? { home } : {}) }) }; }
|
|
126
131
|
catch { /* Unreadable ledger reads as n/a. */ }
|
|
127
132
|
return { session: selected, matches, root, sessionId, selection, bindingPending: !!sessionFile && !sessionId, days, harness, rules,
|
|
128
|
-
ratchet: ratchetFor(selected, recent.filter((file) => file !== selected?.filePath)), route, ruleReview, routingSaved,
|
|
133
|
+
ratchet: ratchetFor(selected, recent.filter((file) => file !== selected?.filePath)), route, ruleReview, sharedRuleReview, routingSaved,
|
|
134
|
+
delegation: codexDelegationStatus({ sessionId: sessionId || selected?.sessionId }),
|
|
129
135
|
labelMode: resolveLabelMode({ cfg: statuslineDefaults(), hasFlag }).mode,
|
|
130
136
|
color: statuslineDefaults().color,
|
|
131
137
|
timer: !hasFlag('--no-timer') && (hasFlag('--timer') || statuslineDefaults().timer),
|
|
@@ -138,8 +144,9 @@ export function createPanelReader({ root = process.cwd(), home, sessionId: pinne
|
|
|
138
144
|
lint: cfg?.koreanStyle?.lint || 'block',
|
|
139
145
|
cohesion: cfg?.cohesion?.enabled === true,
|
|
140
146
|
brief: cfg?.codex?.brief !== false,
|
|
141
|
-
delegate: cfg
|
|
147
|
+
delegate: codexDelegateEnabled(cfg),
|
|
142
148
|
doc2md: cfg?.codex?.doc2md !== false && process.env.CTS_NO_DOC2MD !== '1',
|
|
149
|
+
update: version ? readUpdateChip(version, { refresh: refreshUpdate }) : null,
|
|
143
150
|
updatedAt: new Date() };
|
|
144
151
|
};
|
|
145
152
|
}
|
|
@@ -276,16 +283,27 @@ function panelGroups(data, { now, version = '', full = false, mode = full ? 'tex
|
|
|
276
283
|
chip(`Reasoning ${short(s.reasoningOutputTokens)} (in output)`),
|
|
277
284
|
] : []);
|
|
278
285
|
const docs = data.documents;
|
|
279
|
-
|
|
280
|
-
|
|
286
|
+
const docSavings = docs?.scope === 'codex-total' ? docs.savings : null;
|
|
287
|
+
register('doc2md', 'savings', [
|
|
288
|
+
chip(`Doc2md saved ${docSavings?.priced ? `${signedMoney(docSavings.total)} est. (XML vs MD)`
|
|
289
|
+
: `n/a (${docs?.docs ? `${docs.docs} unpriced docs` : 'no documents'})`}`, docSavings?.priced ? (docSavings.total < 0 ? 33 : 32) : 90),
|
|
290
|
+
...(docSavings?.priced && docSavings.unpriced ? [chip(`${docSavings.unpriced} docs unpriced`)] : []),
|
|
291
|
+
...(docs?.scope === 'codex-total' && docs.docs > 0 ? [chip(`${icon('doc')}Doc2md ${number(docs.docs)} docs (Codex total)`, 32),
|
|
281
292
|
...(docs.byExt || []).map((row) => chip(`${terminalText(row.ext)} ${number(row.docs)}x`)),
|
|
282
|
-
|
|
293
|
+
] : []),
|
|
294
|
+
]);
|
|
295
|
+
const delegation = data.delegation;
|
|
296
|
+
register('routing', 'savings', [routingChip(data.routingSaved, chip),
|
|
297
|
+
delegation?.offers ? chip(`Delegation ${delegation.settled} completed / ${delegation.bound} bound / ${delegation.pending} pending / ${delegation.unclaimed} unclaimed (session)`,
|
|
298
|
+
delegation.unclaimed && !delegation.settled && !delegation.bound ? 33 : 90) : null,
|
|
299
|
+
]);
|
|
283
300
|
register('harness', 'preferences', [
|
|
284
301
|
chip(`${icon('harness')}Harness ${h?.error ? 'ERROR' : h ? `${h.configured}/${h.total}` : 'n/a'}`, h?.configured === 5 ? 32 : 33),
|
|
285
302
|
chip(`Ratchet ${data.rules?.global ?? '?'}/${data.rules?.project ?? '?'}`),
|
|
286
303
|
// A repeated failure outranks a delegation candidate, as in the Claude statusline.
|
|
287
304
|
data.ratchet?.length ? chip(`ratchet? #${data.ratchet[0].id} x${data.ratchet[0].count}`, 33) : null,
|
|
288
305
|
data.ruleReview?.length ? chip(`rule-health #${data.ruleReview[0]}${data.ruleReview.length > 1 ? ` +${data.ruleReview.length - 1}` : ''}`, 33) : null,
|
|
306
|
+
data.sharedRuleReview?.length ? chip(`rule-health shared ${terminalText(data.sharedRuleReview[0].rule.tier)} ${terminalText(data.sharedRuleReview[0].rule.category)}${data.sharedRuleReview.length > 1 ? ` +${data.sharedRuleReview.length - 1}` : ''}`, 33) : null,
|
|
289
307
|
data.route?.length ? chip(`route? R${data.route[0].id} ${terminalText(data.route[0].category)}${data.route.length > 1 ? ` +${data.route.length - 1}` : ''}`, 33) : null,
|
|
290
308
|
]);
|
|
291
309
|
register('korean', 'preferences', [
|
|
@@ -294,8 +312,11 @@ function panelGroups(data, { now, version = '', full = false, mode = full ? 'tex
|
|
|
294
312
|
chip(`${icon('doc')}Doc2md ${data.doc2md ? 'on' : 'off'} (prefs)`),
|
|
295
313
|
...(data.hooks ? [chip(`Hooks ${data.hooks.filter((hook) => hook.registered).length}/${data.hooks.length} registered`)] : []),
|
|
296
314
|
...(data.brief !== undefined ? [chip(`Brief ${data.brief ? 'on' : 'off'}`)] : []),
|
|
297
|
-
...(data.delegate !== undefined ? [chip(`Delegate ${data.delegate ? 'on' : 'off'} (
|
|
298
|
-
|
|
315
|
+
...(data.delegate !== undefined ? [chip(`Delegate ${data.delegate ? 'on' : 'off'} (preference; tools unverified)`)] : []),
|
|
316
|
+
]);
|
|
317
|
+
register('update', 'priority', [
|
|
318
|
+
data.update?.available && !data.update.dismissed
|
|
319
|
+
? chip(`${icon('upgrade')}v${terminalText(version)} -> ${terminalText(data.update.latest)}`, 33) : null,
|
|
299
320
|
]);
|
|
300
321
|
register('version', 'preferences', [
|
|
301
322
|
chip(`SPRAG${version ? ` v${terminalText(version)}` : ''} / ${s ? age(s.lastActivity, now) : 'waiting'}`,
|
|
@@ -453,7 +474,7 @@ export async function runCodexPanel({ read, interval = 2000, once = false, color
|
|
|
453
474
|
try { lastData = await read(); }
|
|
454
475
|
catch (e) { lastData = { ...lastData, error: e.message }; }
|
|
455
476
|
if (stopped) break;
|
|
456
|
-
if (compact && !once) fitRows(formatCodexPanel(lastData, { columns: (output.columns || 100) - 1, rows: 10000, color: false, compact: true }).split('\n').length);
|
|
477
|
+
if (compact && !once) fitRows(formatCodexPanel(lastData, { columns: (output.columns || 100) - 1, rows: 10000, color: false, compact: true, version }).split('\n').length);
|
|
457
478
|
draw();
|
|
458
479
|
if (once) break;
|
|
459
480
|
await wait();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { forkedCopyFilter, codexToolEvents } from './codex-parser.js';
|
|
3
|
+
|
|
4
|
+
const { checkReplyLanguage } = createRequire(import.meta.url)('./reply-language.cjs');
|
|
5
|
+
|
|
6
|
+
/** Ignore copied history and commentary; only the current turn's final reply counts. */
|
|
7
|
+
export function lastCodexTurn(lines, { turnId, sessionId } = {}) {
|
|
8
|
+
const copied = forkedCopyFilter();
|
|
9
|
+
let prompt = null, reply = '', currentTurn = null;
|
|
10
|
+
for (const line of lines) {
|
|
11
|
+
let e;
|
|
12
|
+
try { e = JSON.parse(line); } catch { continue; }
|
|
13
|
+
if (copied(e)) continue;
|
|
14
|
+
const p = e?.payload;
|
|
15
|
+
if (!p) continue;
|
|
16
|
+
if (e.type === 'session_meta' && sessionId && (p.id || p.session_id) !== sessionId) return null;
|
|
17
|
+
if (e.type === 'event_msg' && p.type === 'task_started') {
|
|
18
|
+
currentTurn = p.turn_id;
|
|
19
|
+
prompt = null;
|
|
20
|
+
reply = '';
|
|
21
|
+
}
|
|
22
|
+
for (const ev of codexToolEvents(e)) {
|
|
23
|
+
if (ev.kind !== 'user' || (sessionId && ev.threadId && ev.threadId !== sessionId)) continue;
|
|
24
|
+
currentTurn = ev.turnId || currentTurn;
|
|
25
|
+
prompt = ev.text;
|
|
26
|
+
reply = '';
|
|
27
|
+
}
|
|
28
|
+
if (e.type === 'response_item' && p.type === 'message' && p.role === 'assistant' && p.phase === 'final_answer') {
|
|
29
|
+
reply = (Array.isArray(p.content) ? p.content : []).filter((b) => b?.type === 'output_text' && typeof b.text === 'string').map((b) => b.text).join('\n');
|
|
30
|
+
}
|
|
31
|
+
if (e.type === 'event_msg' && p.type === 'task_complete' && (!p.turn_id || p.turn_id === currentTurn) &&
|
|
32
|
+
typeof p.last_agent_message === 'string') reply = p.last_agent_message;
|
|
33
|
+
}
|
|
34
|
+
if (prompt === null || (turnId && currentTurn !== turnId)) return null;
|
|
35
|
+
return { prompt, reply };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function checkCodexReplyLanguage(payload) {
|
|
39
|
+
return checkReplyLanguage(payload.transcript_path, {
|
|
40
|
+
parseTurn: (lines) => lastCodexTurn(lines, { turnId: payload.turn_id, sessionId: payload.session_id }),
|
|
41
|
+
reply: payload.last_assistant_message,
|
|
42
|
+
});
|
|
43
|
+
}
|
package/src/codex-rule-health.js
CHANGED
|
@@ -2,6 +2,7 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { loadCodexLedger } from './codex-ledger.js';
|
|
3
3
|
import { isFailedRun } from './route-scan.js';
|
|
4
4
|
import { wilsonLowerBound, HEALTH_ERR_RATE, HEALTH_MIN_SAMPLE_DELEGATED } from './model-rules.js';
|
|
5
|
+
import { loadSharedModelRules, sharedModelRulesForProject, realProjectRoot } from './shared-model-rules.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Measured outcome of one Codex rule, judged as Claude's rule health judges
|
|
@@ -18,12 +19,34 @@ export function codexRuleHealth(rule, { events = Object.values(loadCodexLedger()
|
|
|
18
19
|
(rule.scope !== 'project' || (typeof e.targetRoot === 'string' && e.targetRoot === rule.targetRoot)) &&
|
|
19
20
|
// A rule deleted and added again starts a fresh record.
|
|
20
21
|
(!Number.isFinite(since) || (Number.isFinite(e.ts) && e.ts >= since)));
|
|
22
|
+
return healthOf(runs.filter((e) => e.source !== 'shared'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function healthOf(runs) {
|
|
21
26
|
const errs = runs.filter((e) => e.aborted || isFailedRun({ toolErrors: e.toolErrors, calls: e.calls })).length;
|
|
22
27
|
const saved = runs.filter((e) => Number.isFinite(e.usd)).reduce((sum, e) => sum + e.usd, 0);
|
|
23
28
|
const review = runs.length >= HEALTH_MIN_SAMPLE_DELEGATED && wilsonLowerBound(errs, runs.length) > HEALTH_ERR_RATE;
|
|
24
29
|
return { runs: runs.length, errs, rate: runs.length ? errs / runs.length : 0, saved, status: review ? 'review' : 'active' };
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
/** Shared policies keep Codex outcomes separate, including each provider/model pair. */
|
|
33
|
+
export function codexSharedRuleHealth({ root = process.cwd(), dir, events = Object.values(loadCodexLedger({ dir }).events) } = {}) {
|
|
34
|
+
const policies = sharedModelRulesForProject(loadSharedModelRules({ dir }), root);
|
|
35
|
+
return policies.flatMap((rule) => {
|
|
36
|
+
const groups = new Map();
|
|
37
|
+
for (const e of events) {
|
|
38
|
+
if (e?.source !== 'shared' || !(e.complete || e.aborted) || e.sharedSignature !== rule.sharedSignature ||
|
|
39
|
+
e.category !== rule.category || e.tier !== rule.tier || e.scope !== rule.scope ||
|
|
40
|
+
(rule.scope === 'project' && (typeof e.targetRoot !== 'string' || realProjectRoot(e.targetRoot) !== rule.targetRoot))) continue;
|
|
41
|
+
const key = JSON.stringify([e.provider, e.from, e.to]);
|
|
42
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
43
|
+
groups.get(key).push(e);
|
|
44
|
+
}
|
|
45
|
+
return [...groups.values()].map((runs) => ({ rule, provider: runs[0].provider, from: runs[0].from,
|
|
46
|
+
model: runs[0].to, health: healthOf(runs) }));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
27
50
|
/**
|
|
28
51
|
* Rules in review that apply to the project at `root`: global rules, and
|
|
29
52
|
* project rules whose targetRoot is `root`; a null `root` lists every rule. `index` is the 1-based number the
|
|
@@ -2,13 +2,14 @@ import { loadConfig, saveConfig } from '../config.js';
|
|
|
2
2
|
import { configureCodexHooks } from '../codex-installer.js';
|
|
3
3
|
import { validateCodexTarget, loadCodexModelRules, addCodexModelRule, removeCodexModelRule,
|
|
4
4
|
validateCodexSharedTarget, codexSharedTargets, codexDelegateMinContext, DEFAULT_DELEGATE_MIN_CONTEXT,
|
|
5
|
-
autoCodexSharedTargets } from '../codex-delegation.js';
|
|
5
|
+
autoCodexSharedTargets, codexDelegateEnabled, codexImplementationHint } from '../codex-delegation.js';
|
|
6
6
|
import { findCodexCandidate, resolveCodexCandidate } from '../codex-route-scan.js';
|
|
7
7
|
import { codexRoutingSavedTotals, loadCodexLedger } from '../codex-ledger.js';
|
|
8
|
-
import { codexRuleHealth, codexRulesInReview } from '../codex-rule-health.js';
|
|
8
|
+
import { codexRuleHealth, codexRulesInReview, codexSharedRuleHealth } from '../codex-rule-health.js';
|
|
9
9
|
import { signedUsd } from '../money.js';
|
|
10
10
|
import { codexProviderName, codexModelName } from '../agent.js';
|
|
11
11
|
import { loadSharedModelRules, sharedModelRulesForProject, SHARED_RULE_TIERS } from '../shared-model-rules.js';
|
|
12
|
+
import { codexDelegationDiagnostics } from '../codex-delegation-diagnostics.js';
|
|
12
13
|
|
|
13
14
|
// Starts from the working directory, as the hooks do, so a project policy
|
|
14
15
|
// stored under either agent's project root is counted.
|
|
@@ -22,6 +23,10 @@ function printShared(cfg, { details = false } = {}) {
|
|
|
22
23
|
if (details) {
|
|
23
24
|
for (const r of policies) console.log(`shared ${r.tier} ${r.category} | ${r.scope}${r.targetRoot ? ` ${r.targetRoot}` : ''}`);
|
|
24
25
|
for (const t of targets) console.log(`${t.provider} | ${t.from} | ${t.tier} -> ${t.model}${t.effort ? ` (${t.effort})` : ''}`);
|
|
26
|
+
for (const { rule, health, provider: source, from, model } of codexSharedRuleHealth({ root: process.cwd() })) {
|
|
27
|
+
console.log(`shared ${rule.tier} ${rule.category} | ${source} | ${from} -> ${model} | measured x${health.runs}, err ${Math.round(health.rate * 100)}%, saved ${signedUsd(health.saved, 4)}`
|
|
28
|
+
+ (health.status === 'review' ? ` | rule-health: ${health.errs}/${health.runs} runs failed; review the mapping or pause with sprag delegate shared off --agent codex` : ''));
|
|
29
|
+
}
|
|
25
30
|
}
|
|
26
31
|
// A mapping routes only sessions on the parent model it names, so a tier
|
|
27
32
|
// mapped for another model still leaves the configured model unrouted. A tier
|
|
@@ -32,7 +37,7 @@ function printShared(cfg, { details = false } = {}) {
|
|
|
32
37
|
const tiers = [...new Set(policies.map((r) => r.tier))];
|
|
33
38
|
for (const tier of tiers) {
|
|
34
39
|
const t = own(tier) || auto.targets.find((a) => a.tier === tier);
|
|
35
|
-
if (t) console.log(`${tier} -> ${t.model}${t.effort ? ` (${t.effort})` : ''}${t.auto ? ' (auto: cheaper priced model
|
|
40
|
+
if (t) console.log(`${tier} -> ${t.model}${t.effort ? ` (${t.effort})` : ''}${t.auto ? ' (auto: cheaper priced model Codex runs or lists)' : ''}`);
|
|
36
41
|
}
|
|
37
42
|
const missing = tiers.filter((tier) => !own(tier) && !auto.targets.some((a) => a.tier === tier));
|
|
38
43
|
if (missing.length) {
|
|
@@ -57,6 +62,15 @@ export function run({ args, getArg, hasFlag, root }) {
|
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
const cfg = loadConfig();
|
|
65
|
+
if (sub === 'doctor') {
|
|
66
|
+
const report = codexDelegationDiagnostics({ sessionId: getArg('--session'), model: getArg('--from') || undefined });
|
|
67
|
+
console.log(JSON.stringify(report, null, 2));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (sub === 'implementation') {
|
|
71
|
+
console.log(codexImplementationHint({ model: getArg('--from'), provider: getArg('--provider'), sessionId: getArg('--session'), cfg }));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
60
74
|
if (sub === 'shared') {
|
|
61
75
|
const action = args[2] || 'status';
|
|
62
76
|
if (!['status', 'on', 'off', 'map', 'unmap', 'min-context', 'auto'].includes(action)) {
|
|
@@ -100,7 +114,7 @@ export function run({ args, getArg, hasFlag, root }) {
|
|
|
100
114
|
}
|
|
101
115
|
if (action !== 'status') saveConfig(cfg);
|
|
102
116
|
printShared(cfg, { details: true });
|
|
103
|
-
console.log(`Codex delegation is ${cfg
|
|
117
|
+
console.log(`Codex delegation is ${codexDelegateEnabled(cfg) ? 'on' : 'off; enable with sprag delegate on --agent codex'}. Target availability and lower cost must be verified with your provider.`);
|
|
104
118
|
return;
|
|
105
119
|
}
|
|
106
120
|
if (sub === 'rules') {
|
|
@@ -145,7 +159,7 @@ export function run({ args, getArg, hasFlag, root }) {
|
|
|
145
159
|
if (target !== undefined) cfg.codex.delegateTarget = target;
|
|
146
160
|
saveConfig(cfg);
|
|
147
161
|
}
|
|
148
|
-
console.log(`Codex delegate: ${cfg
|
|
162
|
+
console.log(`Codex delegate: ${codexDelegateEnabled(cfg) ? 'on' : 'off'} (model routing and guidance)`);
|
|
149
163
|
console.log(`Default target: ${cfg.codex?.delegateTarget?.model || 'inherit (no override)'}`);
|
|
150
164
|
// Turning delegation off must work even when the rule file is damaged.
|
|
151
165
|
let ruleCount;
|