sprag-cli 3.63.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 +55 -1
- package/docs/CODEX.md +85 -3
- package/mod/sprag-delegation-meter/hooks/hooks.json +3 -0
- package/mod/sprag-delegation-meter/hooks/register.ts +197 -0
- package/mod/sprag-doc2md/hooks/hooks.json +3 -0
- package/mod/sprag-doc2md/hooks/register.ts +97 -0
- package/package.json +2 -2
- package/src/codex-brief.js +10 -1
- package/src/codex-delegation-diagnostics.js +42 -0
- package/src/codex-delegation.js +17 -0
- 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 +2 -1
- package/src/codex-hooks.js +12 -3
- package/src/codex-installer.js +1 -0
- package/src/codex-ledger.js +25 -1
- package/src/codex-panel.js +33 -12
- package/src/codex-reply-language.js +43 -0
- package/src/codex-rule-health.js +23 -0
- package/src/commands/codex-delegate.js +16 -2
- package/src/commands/codex.js +40 -8
- 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/model-rules.js +3 -20
- package/src/reply-language.cjs +3 -2
- package/src/update-notice.js +37 -0
|
@@ -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, codexDelegateEnabled } 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
|
|
@@ -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)) {
|
package/src/commands/codex.js
CHANGED
|
@@ -25,7 +25,9 @@ Usage: sprag <command> --agent codex [options]
|
|
|
25
25
|
handoff [--cwd path] Write Git state and Codex usage to HANDOFF-*.md
|
|
26
26
|
saved [reset|undo] Show, reset or restore the saved-money counters
|
|
27
27
|
delegate on|off|status Native model routing and SubagentStart guidance
|
|
28
|
+
delegate doctor Local CLI features, model catalog and matched child activity
|
|
28
29
|
delegate model <model> Default target for spawns without an explicit model
|
|
30
|
+
delegate implementation After design: resolve T1 with --from, --provider, --session
|
|
29
31
|
delegate rules Codex rules and shared delegation policies
|
|
30
32
|
delegate shared on|off|status Reuse approved policies; keep original scopes
|
|
31
33
|
delegate shared map T1|T2 Map a tier: --from <parent> --model <target>
|
|
@@ -42,6 +44,7 @@ Usage: sprag <command> --agent codex [options]
|
|
|
42
44
|
harness init|uninit|check|promote|list|rm|prune
|
|
43
45
|
Rules in Codex AGENTS.md and ratchet files
|
|
44
46
|
korean / cohesion / doc2md Writing guidance and document conversion
|
|
47
|
+
doc2md savings Codex document estimates; --format json for reasons and totals
|
|
45
48
|
mode Shared label/color preferences
|
|
46
49
|
install [--no-panel] Register hooks and global harness; trust via /hooks
|
|
47
50
|
uninstall Remove global integration; keep rules and shared state
|
|
@@ -141,6 +144,10 @@ export async function run({ args, version }) {
|
|
|
141
144
|
console.log(`Hooks: ${report.hooks.error || `${report.hooks.events.filter((hook) => hook.registered).length}/${report.hooks.events.length} registered; trust unknown`}`);
|
|
142
145
|
for (const hook of report.hooks.events) console.log(` ${hook.event}: ${hook.registered ? 'registered' : 'missing/outdated; run sprag install --agent codex'}`);
|
|
143
146
|
if (report.config.hooksDisabled) console.log('WARNING: user config disables hooks. Review features.hooks in Codex config.');
|
|
147
|
+
if (report.config.agentsDisabled) console.log('WARNING: user config disables agent tools. Review agents.enabled and features.multi_agent in Codex config.');
|
|
148
|
+
console.log(`Delegation: ${report.delegation.settled} completed, ${report.delegation.bound} bound, ${report.delegation.pending} pending, ${report.delegation.unclaimed} unclaimed offers (session)`);
|
|
149
|
+
console.log(`Agent tools: ${report.delegation.tools}`);
|
|
150
|
+
if (report.delegation.offers && !report.delegation.settled && !report.delegation.bound) console.log('Routing hints have no matched child. Check native subagent tool availability in the active session; preference and hook registration do not prove execution.');
|
|
144
151
|
for (const [scope, harness] of Object.entries(report.harness)) console.log(`Harness ${scope}: ${harness.error ? terminalText(harness.error) : `${harness.configured}/${harness.total}`}`);
|
|
145
152
|
console.log(`Session: ${report.session ? `${terminalText(report.session.id)} (${terminalText(report.session.model)})` : 'no matching rollout log'}`);
|
|
146
153
|
console.log(`Panel: ${report.panel.live ? 'recent rendered frame observed' : 'no recent rendered frame'}`);
|
|
@@ -263,7 +270,7 @@ export async function run({ args, version }) {
|
|
|
263
270
|
const statusline = hasFlag('--statusline') || format === 'statusline';
|
|
264
271
|
if (statusline && (!cmd || cmd.startsWith('-'))) {
|
|
265
272
|
const { createPanelReader, formatCodexPanel } = await import('../codex-panel.js');
|
|
266
|
-
const data = await createPanelReader({ root: selectedRoot, sessionId: getArg('--session'), days: numArg('--days', { dflt: 7, min: 0 }), hasFlag })();
|
|
273
|
+
const data = await createPanelReader({ root: selectedRoot, sessionId: getArg('--session'), days: numArg('--days', { dflt: 7, min: 0 }), hasFlag, version })();
|
|
267
274
|
const columns = numArg('--columns', { dflt: process.stdout.columns || 100, min: 1, max: 10000 });
|
|
268
275
|
const text = formatCodexPanel(data, { compact: true, columns: hasFlag('--single-line') ? 100000 : columns,
|
|
269
276
|
rows: 10000, version, color: !hasFlag('--no-color') && !process.env.NO_COLOR && !!process.stdout.isTTY && data.color });
|
|
@@ -354,7 +361,7 @@ export async function run({ args, version }) {
|
|
|
354
361
|
return runCodexPanel({
|
|
355
362
|
read: createPanelReader({ root: getArg('--project') ? resolve(getArg('--project')) : root,
|
|
356
363
|
sessionId: getArg('--session'), sessionFile: getArg('--session-file'), days: numArg('--days', { dflt: 7, min: 0 }), readBudget: readBudget.read,
|
|
357
|
-
readCachePolicy: hasFlag('--once') ? async (session) => {
|
|
364
|
+
version, refreshUpdate: !hasFlag('--once'), readCachePolicy: hasFlag('--once') ? async (session) => {
|
|
358
365
|
const { readCodexCachePolicy } = await import('../codex-cache-policy.js');
|
|
359
366
|
try { return readCodexCachePolicy(session) || await refreshCodexCachePolicy(session); }
|
|
360
367
|
catch (e) { debug('codex:cache-policy', e); return null; }
|
|
@@ -364,7 +371,7 @@ export async function run({ args, version }) {
|
|
|
364
371
|
fitRows: createInlinePanelResizer(),
|
|
365
372
|
onFrame: (data) => {
|
|
366
373
|
renderedSessionId = data.sessionId || data.session?.sessionId;
|
|
367
|
-
recordPanelState(panelRoot, 'frame', { status: 'rendered', pid: process.pid, sessionId: renderedSessionId });
|
|
374
|
+
recordPanelState(panelRoot, 'frame', { status: 'rendered', pid: process.pid, version, sessionId: renderedSessionId });
|
|
368
375
|
},
|
|
369
376
|
onClose: () => recordPanelState(panelRoot, 'frame', { status: 'closed', pid: process.pid, sessionId: renderedSessionId }),
|
|
370
377
|
});
|
|
@@ -392,7 +399,7 @@ export async function run({ args, version }) {
|
|
|
392
399
|
}
|
|
393
400
|
return;
|
|
394
401
|
}
|
|
395
|
-
const out = await codexHookOutput(getArg('--event'), payload);
|
|
402
|
+
const out = await codexHookOutput(getArg('--event'), payload, { version });
|
|
396
403
|
if (out) console.log(JSON.stringify(out));
|
|
397
404
|
} catch (e) { debug('codex:hook', e); /* Hook failures must not break the agent loop. */ }
|
|
398
405
|
return;
|
|
@@ -440,8 +447,11 @@ export async function run({ args, version }) {
|
|
|
440
447
|
if (hasFlag('--purge')) throw new Error('Codex uninstall keeps shared Sprag state; --purge is not supported.');
|
|
441
448
|
const hook = configureCodexHooks({ remove: true });
|
|
442
449
|
const h = uninitCodexHarness({ root, scope: 'global' });
|
|
450
|
+
const { installPanelShell } = await import('../codex-panel-shell.js');
|
|
451
|
+
const shellFile = installPanelShell({ remove: true });
|
|
443
452
|
console.log(`Codex hooks: ${hook.file} (${hook.action})`);
|
|
444
453
|
console.log(`Codex harness: ${h.file} (${h.removed ? 'removed' : 'absent'})`);
|
|
454
|
+
console.log(`Codex shell wrapper removed: ${shellFile}. Open a new shell to use the standalone codex command.`);
|
|
445
455
|
console.log('Ratchet rules, project harnesses, backups, and shared Sprag state kept.');
|
|
446
456
|
return;
|
|
447
457
|
}
|
|
@@ -547,9 +557,27 @@ export async function run({ args, version }) {
|
|
|
547
557
|
console.log('Prompt conversion, supported reads, and document/cache write guards require trusted hooks. Review in Codex /hooks.');
|
|
548
558
|
return;
|
|
549
559
|
}
|
|
560
|
+
if (cmd === 'doc2md' && args[1] === 'savings') {
|
|
561
|
+
const { codexDocumentTotals } = await import('../codex-doc2md-ledger.js');
|
|
562
|
+
const totals = codexDocumentTotals();
|
|
563
|
+
if (format === 'json') console.log(JSON.stringify({ agent: 'codex', ...totals }, null, 2));
|
|
564
|
+
else {
|
|
565
|
+
console.log(`Doc2md saved ${totals.savings.priced ? `${signedUsd(totals.savings.total, 4)} estimated` : 'n/a'} | ${totals.docs} docs, ${totals.savings.unpriced} unpriced`);
|
|
566
|
+
console.log('Estimate: Office XML versus prepared Markdown at UTF-8 bytes/4, priced at the recorded Codex model input rate. Not billed savings.');
|
|
567
|
+
for (const [reason, count] of Object.entries(totals.savings.reasons)) console.log(`${reason}: ${count}`);
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
550
571
|
if (cmd === 'doc2md' && args[1] && !args[1].startsWith('-') && args[1] !== 'install-converter') {
|
|
551
572
|
const { convertCodexDocument } = await import('../codex-doc2md.js');
|
|
552
|
-
const
|
|
573
|
+
const sessionId = getArg('--session') || process.env.CODEX_THREAD_ID;
|
|
574
|
+
let identity = {};
|
|
575
|
+
if (sessionId) {
|
|
576
|
+
const { createPanelReader } = await import('../codex-panel.js');
|
|
577
|
+
const { session } = await createPanelReader({ root, sessionId })();
|
|
578
|
+
if (session) identity = { model: session.model, provider: session.provider };
|
|
579
|
+
}
|
|
580
|
+
const result = convertCodexDocument(resolve(args[1]), identity);
|
|
553
581
|
if (!result.ok) {
|
|
554
582
|
console.error(`Doc2md failed: ${result.reason}`);
|
|
555
583
|
if (result.reason === 'no-markitdown') console.error('Run sprag doc2md install-converter --agent codex, then retry.');
|
|
@@ -560,21 +588,25 @@ export async function run({ args, version }) {
|
|
|
560
588
|
console.log(`Source ${result.meta.size} bytes; Markdown ${result.meta.markdownBytes} bytes.`);
|
|
561
589
|
if (result.meta.clipped || result.meta.truncated) console.log('Text extraction is incomplete; inspect the source for omitted content.');
|
|
562
590
|
if (result.meta.note) console.log(result.meta.note);
|
|
591
|
+
const saving = result.meta.codexSaving;
|
|
592
|
+
console.log(`Doc2md saved ${Number.isFinite(saving?.usd) ? `${signedUsd(saving.usd, 4)} estimated (Office XML vs Markdown, bytes/4)` : `n/a (${saving?.reason || 'unknown'})`}`);
|
|
563
593
|
return;
|
|
564
594
|
}
|
|
565
595
|
if (cmd === 'upgrade') {
|
|
566
|
-
const
|
|
596
|
+
const { upgradeInstructions } = await import('../update-notice.js');
|
|
597
|
+
const [command, setup] = upgradeInstructions('codex');
|
|
567
598
|
if (!hasFlag('--print')) throw new Error(`For a Codex-only upgrade, run ${command}, then sprag install --agent codex.`);
|
|
568
599
|
console.log(command);
|
|
569
|
-
console.log(
|
|
600
|
+
console.log(setup);
|
|
570
601
|
return;
|
|
571
602
|
}
|
|
572
603
|
const shared = new Set(['korean', 'cohesion', 'doc2md', 'mode', 'feedback', 'update-check']);
|
|
573
604
|
if (shared.has(cmd)) {
|
|
574
605
|
if (args.some((a) => a.startsWith('--hook'))) throw new Error('Use codex-hook for Codex hook payloads.');
|
|
575
606
|
if (cmd === 'cohesion' && args[1] === 'on') configureCodexHooks();
|
|
607
|
+
if (cmd === 'korean' && args[1] === 'reply' && args[2] === 'on') configureCodexHooks();
|
|
576
608
|
const { userDataDir } = await import('../paths.js');
|
|
577
|
-
const result = await (await import(`./${cmd}.js`)).run({ args, getArg, hasFlag, numArg, version, dataDir: userDataDir() });
|
|
609
|
+
const result = await (await import(`./${cmd}.js`)).run({ args, getArg, hasFlag, numArg, version, agent: 'codex', dataDir: userDataDir() });
|
|
578
610
|
if (cmd === 'cohesion' && args[1] === 'on') console.log('Review/trust the Sprag hooks in Codex /hooks.');
|
|
579
611
|
return result;
|
|
580
612
|
}
|
|
@@ -360,48 +360,14 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
360
360
|
// — the refresh runs detached off the statusline path, never here.
|
|
361
361
|
let updateBlock = null;
|
|
362
362
|
try {
|
|
363
|
-
const {
|
|
363
|
+
const { updateOffer } = await import('../update-notice.js');
|
|
364
364
|
const pkgVersion = readPackageVersion();
|
|
365
365
|
// Session start is the one moment where a stale cache matters most, so
|
|
366
366
|
// kick the detached refresh here too, on a shorter interval than the
|
|
367
367
|
// statusline's 24h: releases can land several times a day, and a
|
|
368
368
|
// session opened that afternoon should hear about them. It lands in
|
|
369
369
|
// time for the statusline and for the next session, not for this line.
|
|
370
|
-
if (pkgVersion)
|
|
371
|
-
const u = pkgVersion ? updateStatus(pkgVersion) : null;
|
|
372
|
-
// `dismissed` is the whole point of asking once: a user who said no to
|
|
373
|
-
// this version must not be asked again every `/clear`.
|
|
374
|
-
if (u && u.available && !u.dismissed) {
|
|
375
|
-
// What the release adds, when we managed to read it. "A newer version
|
|
376
|
-
// exists" is not a reason to upgrade; the reason is in this list, and
|
|
377
|
-
// without it the user is being asked to decide on no information.
|
|
378
|
-
const notes = Array.isArray(u.highlights) ? u.highlights : [];
|
|
379
|
-
const koNotes = notes.length
|
|
380
|
-
? [' 이 버전에서 새로 생긴 것:', ...notes.map((n) => ` · ${n}`)]
|
|
381
|
-
: [];
|
|
382
|
-
const enNotes = notes.length
|
|
383
|
-
? [' What this release adds:', ...notes.map((n) => ` · ${n}`)]
|
|
384
|
-
: [];
|
|
385
|
-
updateBlock = lang === 'ko'
|
|
386
|
-
? [
|
|
387
|
-
`[sprag update] 새 버전이 나와 있습니다: v${u.current} → ${u.latest}.`,
|
|
388
|
-
...koNotes,
|
|
389
|
-
notes.length
|
|
390
|
-
? ' 위 내용을 사용자에게 보여 주고 업그레이드할지 물어보십시오. 승낙하면 아래 명령을 실행하고, 묻지 않고 설치하지는 마십시오.'
|
|
391
|
-
: ' 사용자에게 지금 업그레이드할지 물어보고, 승낙하면 아래 명령을 실행하십시오. 묻지 않고 설치하지는 마십시오.',
|
|
392
|
-
' sprag upgrade # 업그레이드 실행',
|
|
393
|
-
' sprag update-check --dismiss # 사용자가 원치 않으면 (다음 버전이 나올 때까지 다시 묻지 않습니다)',
|
|
394
|
-
].join('\n')
|
|
395
|
-
: [
|
|
396
|
-
`[sprag update] A newer version is available: v${u.current} → ${u.latest}.`,
|
|
397
|
-
...enNotes,
|
|
398
|
-
notes.length
|
|
399
|
-
? ' Show that list to the user and ask whether to upgrade. Run the command below only if they agree; do not install without asking.'
|
|
400
|
-
: 'Ask the user whether to upgrade now, and run the command below only if they agree. Do not install without asking.',
|
|
401
|
-
' sprag upgrade # perform the upgrade',
|
|
402
|
-
' sprag update-check --dismiss # if they decline (stays quiet until a newer release)',
|
|
403
|
-
].join('\n');
|
|
404
|
-
}
|
|
370
|
+
if (pkgVersion) updateBlock = updateOffer(pkgVersion, { lang });
|
|
405
371
|
} catch (e) { debug('route-scan:update-check', e); /* the offer is optional */ }
|
|
406
372
|
|
|
407
373
|
if (open.length === 0 && reviewRules.length === 0) {
|
package/src/commands/saved.js
CHANGED
|
@@ -22,7 +22,7 @@ const SCOPE_SETS = {
|
|
|
22
22
|
codex: ['codex-routing', 'codex-docs'],
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
/** Totals since each scope's mark
|
|
25
|
+
/** Totals since each scope's mark; Codex keeps unavailable estimates explicit. */
|
|
26
26
|
async function currentTotals(scope, dir) {
|
|
27
27
|
if (scope === 'routing') {
|
|
28
28
|
const { delegationSavedTotals } = await import('../savings-ledger.js');
|
|
@@ -34,10 +34,12 @@ async function currentTotals(scope, dir) {
|
|
|
34
34
|
}
|
|
35
35
|
if (scope === 'codex-routing') {
|
|
36
36
|
const { codexRoutingSavedTotals } = await import('../codex-ledger.js');
|
|
37
|
-
|
|
37
|
+
const totals = codexRoutingSavedTotals({ dir });
|
|
38
|
+
return { usd: totals.priced ? totals.total : null, unpriced: totals.runs - totals.priced };
|
|
38
39
|
}
|
|
39
40
|
const { codexDocumentTotals } = await import('../codex-doc2md-ledger.js');
|
|
40
|
-
|
|
41
|
+
const totals = codexDocumentTotals({ dir });
|
|
42
|
+
return { docs: totals.docs, usd: totals.savings.priced ? totals.savings.total : null, unpriced: totals.savings.unpriced };
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/**
|
|
@@ -75,7 +77,11 @@ export async function run({ args, hasFlag, agent = 'claude', dir: dirArg, rescan
|
|
|
75
77
|
const suffix = agent === 'codex' ? ' --agent codex' : '';
|
|
76
78
|
|
|
77
79
|
const money = (v) => (v < 0 ? `-$${(-v).toFixed(2)}` : `$${v.toFixed(2)}`);
|
|
78
|
-
const fmtTotal = (t) =>
|
|
80
|
+
const fmtTotal = (t) => {
|
|
81
|
+
const value = Number.isFinite(t.usd) ? money(t.usd) : 'n/a';
|
|
82
|
+
return t.docs !== undefined ? `${ko ? `${t.docs}건` : `${t.docs} doc(s)`}, ${value} (${ko ? 'XML 대비 추정' : 'XML estimate'}; ${t.unpriced} unpriced)`
|
|
83
|
+
: `${value}${t.unpriced ? ` +${t.unpriced} unpriced` : ''}`;
|
|
84
|
+
};
|
|
79
85
|
const fmtTime = (ms) => new Date(ms).toLocaleString(ko ? 'ko-KR' : 'en-US');
|
|
80
86
|
const label = (scope) => ({
|
|
81
87
|
routing: ko ? '라우팅 절감' : 'Routing saved',
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* command detached so the render itself stays offline and instant.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export async function run({ hasFlag, version }) {
|
|
14
|
+
export async function run({ hasFlag, version, agent = 'claude' }) {
|
|
15
15
|
const {
|
|
16
16
|
updateStatus,
|
|
17
17
|
refreshUpdateState,
|
|
@@ -56,7 +56,10 @@ export async function run({ hasFlag, version }) {
|
|
|
56
56
|
console.log(lang === 'ko'
|
|
57
57
|
? `새 버전이 있습니다: v${version} → ${s.latest}`
|
|
58
58
|
: `Update available: v${version} → ${s.latest}`);
|
|
59
|
-
|
|
59
|
+
if (agent === 'codex') {
|
|
60
|
+
const { upgradeInstructions } = await import('../update-notice.js');
|
|
61
|
+
for (const command of upgradeInstructions(agent)) console.log(command);
|
|
62
|
+
} else console.log(lang === 'ko'
|
|
60
63
|
? `업그레이드: ${upgradeCommand()} (또는 sprag upgrade)`
|
|
61
64
|
: `Upgrade with: ${upgradeCommand()} (or: sprag upgrade)`);
|
|
62
65
|
if (s.dismissed) {
|
|
@@ -71,7 +74,7 @@ export async function run({ hasFlag, version }) {
|
|
|
71
74
|
: `Up to date (v${version}).`);
|
|
72
75
|
if (!s.latest) {
|
|
73
76
|
console.log(lang === 'ko'
|
|
74
|
-
? `아직 확인된 기록이 없습니다. 지금 확인하려면: sprag update-check --refresh (기록 파일: ${updateStatePath()})`
|
|
75
|
-
: `No check has completed yet. Run: sprag update-check --refresh (state: ${updateStatePath()})`);
|
|
77
|
+
? `아직 확인된 기록이 없습니다. 지금 확인하려면: sprag update-check --refresh${agent === 'codex' ? ' --agent codex' : ''} (기록 파일: ${updateStatePath()})`
|
|
78
|
+
: `No check has completed yet. Run: sprag update-check --refresh${agent === 'codex' ? ' --agent codex' : ''} (state: ${updateStatePath()})`);
|
|
76
79
|
}
|
|
77
80
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Shared policy, rendered with each client's native model selector. */
|
|
2
|
+
export function implementationDelegationRule(agent = 'claude', lang = 'en') {
|
|
3
|
+
const target = agent === 'codex'
|
|
4
|
+
? 'the resolved T1 model for the current parent model and provider (explicit mapping first, automatic resolution otherwise)'
|
|
5
|
+
: 'a model: sonnet subagent';
|
|
6
|
+
if (lang === 'ko') return [
|
|
7
|
+
'- **구현 분업**: 여러 파일을 고칠 때 설계와 스펙 확정은 메인 모델이 맡습니다.',
|
|
8
|
+
` 구현은 ${agent === 'codex' ? '현재 부모 모델과 프로바이더의 T1 모델' : 'model: sonnet 서브에이전트'}에 위임합니다.`,
|
|
9
|
+
' Claude는 sonnet을 쓰고, Codex는 명시적 T1 매핑을 우선하며 없으면 자동 해석한 T1을 씁니다.',
|
|
10
|
+
' 두 모델의 성능이 같다고 가정하지 않으며 haiku나 T2에 구현을 맡기지 않습니다.',
|
|
11
|
+
' 위임 프롬프트에는 설계, 수정할 파일, 제약, 완료 기준, 테스트 명령, 예산과',
|
|
12
|
+
' 권장 모델 및 선택 이유를 적어 다른 문맥 없이 실행할 수 있게 합니다.',
|
|
13
|
+
' 이미 읽은 파일의 약 50줄 이하 변경이나 의존 단계가 하나인 짧은 구현은',
|
|
14
|
+
' 분업 비용을 고려해 메인 모델이 직접 처리합니다. Codex에서 T1을 해석하지 못하면',
|
|
15
|
+
' 메인이 이어받습니다. 위임이 꺼져 있거나 하위 모델 또는 위임 도구를 사용할 수',
|
|
16
|
+
' 없어도 메인이 처리하고 그 이유를 알립니다.',
|
|
17
|
+
' 메인 모델은 위임 완료를 기다리고 같은 구현을 중복 실행하지 않습니다.',
|
|
18
|
+
' 반환된 diff와 테스트 결과를 직접 검증한 뒤 완료로 보고합니다.',
|
|
19
|
+
].join('\n');
|
|
20
|
+
return [
|
|
21
|
+
'- **Implementation split**: the main model fixes the design and specification,',
|
|
22
|
+
` then delegates multi-file implementation to ${target}.`,
|
|
23
|
+
' Claude uses sonnet; Codex uses its resolved T1, not a presumed Sonnet equivalent.',
|
|
24
|
+
' Never use haiku or T2 for implementation. Give a self-contained specification',
|
|
25
|
+
' with owned files, constraints, completion criteria, test commands, a budget,',
|
|
26
|
+
' and the recommended model with a one-line reason. Changes of roughly',
|
|
27
|
+
' 50 lines or less in files already in context, or one short dependency chain,',
|
|
28
|
+
' stay on the main model. Codex also stays on the main model if T1 is unresolved.',
|
|
29
|
+
' If delegation is off, or the model or subagent tools are unavailable,',
|
|
30
|
+
' stay on the main model and say why.',
|
|
31
|
+
' Wait for the child instead of duplicating its implementation. The main model',
|
|
32
|
+
' verifies the diff and actual test results before reporting completion.',
|
|
33
|
+
].join('\n');
|
|
34
|
+
}
|
package/src/model-rules.js
CHANGED
|
@@ -36,6 +36,7 @@ import { userLanguage } from './config.js';
|
|
|
36
36
|
import { agentPhrase, agentPhraseEn } from './agents.js';
|
|
37
37
|
import { writeStateFile } from './state-file.js';
|
|
38
38
|
import { signedUsd } from './money.js';
|
|
39
|
+
import { implementationDelegationRule } from './implementation-delegation.js';
|
|
39
40
|
|
|
40
41
|
// Post-promotion delegated-category error rate above this flags the rule
|
|
41
42
|
// for review (rule-health). Calibrated against local T0 avg error incidence.
|
|
@@ -267,15 +268,7 @@ export function renderModelRatchet(rules, lang = userLanguage(), { root } = {})
|
|
|
267
268
|
'- **위임 배치**: 서브에이전트 비용의 지배 항은 생성이 아니라 컨텍스트',
|
|
268
269
|
' 적재다. 같은 파일들을 읽어야 하는 작업 여러 개는 에이전트 하나에 묶어',
|
|
269
270
|
' 보내고, 읽는 영역이 다른 작업만 나눈다.',
|
|
270
|
-
'
|
|
271
|
-
' 출력 토큰이 많은 실제 코드 작성은 스펙을 자기완결적으로(참조 없이 실행',
|
|
272
|
-
' 가능하게) 적어 model: sonnet 서브에이전트에 위임한다. haiku에는 맡기지',
|
|
273
|
-
' 않는다. 위임 프롬프트에는 작업별 권장 모델과 그 이유를 한 줄로 적는다.',
|
|
274
|
-
' 스펙을 쓴 쪽이 난도를 제일 잘 알기 때문이다. 단, 이미 컨텍스트에 있는',
|
|
275
|
-
' 파일의 약 50줄 이하 변경이나 의존 단계가 하나뿐인 짧은 구현은 분해',
|
|
276
|
-
' 오버헤드가 더 크므로 메인 모델이 직접 한다. 위임한 구현이 돌아오면 메인',
|
|
277
|
-
' 모델이 diff와 테스트 결과를 검증하고 나서 완료로 보고한다. 이 검증',
|
|
278
|
-
' 비용까지가 위임 비용이다.',
|
|
271
|
+
implementationDelegationRule('claude', lang),
|
|
279
272
|
'- **에스컬레이션**: 같은 에러 2회 반복, 답이 복수 후보로 갈림, 서브에이전트가',
|
|
280
273
|
' 질문을 멈추고 헛도는 징후가 보이면 즉시 인계한다. 대응 순서는',
|
|
281
274
|
' ① 재지시 후 재시도 → ② 한 티어 상향 → ③ 메인 모델이 직접 수습.',
|
|
@@ -326,17 +319,7 @@ export function renderModelRatchet(rules, lang = userLanguage(), { root } = {})
|
|
|
326
319
|
'- **Batching**: a subagent\'s dominant cost is context ingestion, not',
|
|
327
320
|
' generation. Bundle tasks that read the same files into one agent; split',
|
|
328
321
|
' only when the areas they read differ.',
|
|
329
|
-
'
|
|
330
|
-
' fixes the design and writes a self-contained spec (executable without',
|
|
331
|
-
' back-references), then delegates the output-heavy code writing to a',
|
|
332
|
-
' model: sonnet subagent — never haiku. The delegation prompt names the',
|
|
333
|
-
' recommended model per task with a one-line why: whoever wrote the spec',
|
|
334
|
-
' knows the difficulty best. A change of roughly 50 lines or less in files',
|
|
335
|
-
' already in context, or a short implementation with a single dependency',
|
|
336
|
-
' chain, stays on the main model: decomposition overhead outweighs the',
|
|
337
|
-
' saving. When a delegated implementation comes back, the',
|
|
338
|
-
' main model verifies the diff and test results before reporting done —',
|
|
339
|
-
' that verification is part of the delegation cost.',
|
|
322
|
+
implementationDelegationRule('claude', lang),
|
|
340
323
|
'- **Escalation**: hand off immediately on the same error twice, an answer',
|
|
341
324
|
' splitting into multiple candidates, or a subagent spinning without asking',
|
|
342
325
|
' questions. Order: ① re-instruct and retry → ② move up one tier → ③ the',
|
package/src/reply-language.cjs
CHANGED
|
@@ -149,11 +149,12 @@ function readTailLines(path) {
|
|
|
149
149
|
* null when the reply is fine (or cannot be judged), otherwise the measured
|
|
150
150
|
* ratios so a caller can report them. Never throws.
|
|
151
151
|
*/
|
|
152
|
-
function checkReplyLanguage(transcriptPath) {
|
|
152
|
+
function checkReplyLanguage(transcriptPath, { parseTurn = lastTurn, reply } = {}) {
|
|
153
153
|
try {
|
|
154
154
|
if (!transcriptPath) return null;
|
|
155
|
-
const turn =
|
|
155
|
+
const turn = parseTurn(readTailLines(transcriptPath));
|
|
156
156
|
if (!turn) return null;
|
|
157
|
+
if (typeof reply === 'string') turn.reply = reply;
|
|
157
158
|
if (ENGLISH_REQUESTED.test(turn.prompt)) return null;
|
|
158
159
|
if (!isKoreanPrompt(turn.prompt) || !isEnglishReply(turn.reply)) return null;
|
|
159
160
|
const u = letterCounts(turn.prompt);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { updateStatus, maybeSpawnUpdateCheck, readUpdateState, updateCheckDisabled, SESSION_START_CHECK_INTERVAL_MS } from './update-check.js';
|
|
2
|
+
import { debug } from './debug.js';
|
|
3
|
+
|
|
4
|
+
/** Shared by terminal renders; registry I/O stays in the detached checker. */
|
|
5
|
+
export function readUpdateChip(version, { refresh = true, ...opts } = {}) {
|
|
6
|
+
try {
|
|
7
|
+
if (refresh) maybeSpawnUpdateCheck(version, opts);
|
|
8
|
+
const status = updateStatus(version, opts);
|
|
9
|
+
const checkedAt = updateCheckDisabled() ? null : (Number(readUpdateState().checkedAt) || null);
|
|
10
|
+
return { ...status, checkedAt };
|
|
11
|
+
} catch (e) {
|
|
12
|
+
debug('update-check:chip', e);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function upgradeInstructions(agent = 'claude') {
|
|
18
|
+
return agent === 'codex'
|
|
19
|
+
? ['npm i -g sprag-cli --ignore-scripts', 'sprag install --agent codex']
|
|
20
|
+
: ['sprag upgrade'];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The model must obtain consent; a hook never installs a package itself. */
|
|
24
|
+
export function updateOffer(version, { agent = 'claude', lang = 'en', ...opts } = {}) {
|
|
25
|
+
const u = readUpdateChip(version, { intervalMs: SESSION_START_CHECK_INTERVAL_MS, ...opts });
|
|
26
|
+
if (!u?.available || u.dismissed) return null;
|
|
27
|
+
const ko = lang === 'ko';
|
|
28
|
+
return [
|
|
29
|
+
ko ? `[sprag update] 새 버전이 나와 있습니다: v${u.current} → ${u.latest}.`
|
|
30
|
+
: `[sprag update] A newer version is available: v${u.current} → ${u.latest}.`,
|
|
31
|
+
...(u.highlights || []).slice(0, 3).map((note) => ` - ${note}`),
|
|
32
|
+
ko ? '사용자에게 변경 내용을 알리고 업그레이드할지 물어보십시오. 승낙한 경우에만 아래 명령을 실행하십시오.'
|
|
33
|
+
: 'Show the changes and ask the user whether to upgrade. Run these commands only if they agree; do not install without asking.',
|
|
34
|
+
...upgradeInstructions(agent).map((command) => ` ${command}`),
|
|
35
|
+
` sprag update-check --dismiss${agent === 'codex' ? ' --agent codex' : ''}`,
|
|
36
|
+
].join('\n');
|
|
37
|
+
}
|