sprag-cli 3.59.0 → 3.61.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/README.ko.md +17 -0
- package/README.md +23 -0
- package/bin/cli.js +51 -3
- package/docs/CODEX.ko.md +69 -6
- package/docs/CODEX.md +84 -12
- package/package.json +1 -1
- package/src/agent.js +9 -0
- package/src/caps-cache.js +3 -3
- package/src/codex-brief.js +4 -5
- package/src/codex-cache-policy.js +3 -3
- package/src/codex-delegation.js +114 -15
- package/src/codex-doc2md-ledger.js +3 -3
- package/src/codex-doctor.js +2 -2
- package/src/codex-hooks.js +4 -5
- package/src/codex-installer.js +3 -3
- package/src/codex-ledger.js +25 -10
- package/src/codex-panel-session.js +3 -3
- package/src/codex-panel-state.js +51 -5
- package/src/codex-route-scan.js +12 -7
- package/src/commands/brief.js +1 -1
- package/src/commands/codex-delegate.js +81 -6
- package/src/commands/codex.js +8 -2
- package/src/commands/compact-window.js +6 -2
- package/src/commands/install.js +4 -1
- package/src/commands/korean.js +69 -3
- package/src/commands/route-scan.js +6 -4
- package/src/commands/saved.js +40 -1
- package/src/ctx-window-cache.js +14 -0
- package/src/formatters/statusline.js +167 -18
- package/src/harness-analyzer.cjs +8 -2
- package/src/harness.js +32 -23
- package/src/installer.js +52 -0
- package/src/model-alias.js +3 -3
- package/src/model-rules.js +14 -9
- package/src/preset-ratchet.js +3 -3
- package/src/reply-language.cjs +167 -0
- package/src/route-inject.js +57 -8
- package/src/route-scan.js +51 -11
- package/src/savings-ledger.js +3 -2
- package/src/seed-rules.js +32 -23
- package/src/session-cache.js +12 -4
- package/src/shared-model-rules.js +73 -0
- package/src/state-file.js +99 -2
- package/src/statusline-data.js +97 -0
- package/src/update-check.js +46 -6
|
@@ -1,20 +1,94 @@
|
|
|
1
1
|
import { loadConfig, saveConfig } from '../config.js';
|
|
2
2
|
import { configureCodexHooks } from '../codex-installer.js';
|
|
3
|
-
import { validateCodexTarget, loadCodexModelRules, addCodexModelRule, removeCodexModelRule
|
|
3
|
+
import { validateCodexTarget, loadCodexModelRules, addCodexModelRule, removeCodexModelRule,
|
|
4
|
+
validateCodexSharedTarget, codexSharedTargets, codexDelegateMinContext, DEFAULT_DELEGATE_MIN_CONTEXT } from '../codex-delegation.js';
|
|
4
5
|
import { findCodexCandidate, resolveCodexCandidate } from '../codex-route-scan.js';
|
|
5
6
|
import { codexRoutingSavedTotals, loadCodexLedger } from '../codex-ledger.js';
|
|
6
7
|
import { codexRuleHealth, codexRulesInReview } from '../codex-rule-health.js';
|
|
7
8
|
import { signedUsd } from '../money.js';
|
|
8
|
-
import { codexProviderName } from '../agent.js';
|
|
9
|
+
import { codexProviderName, codexModelName } from '../agent.js';
|
|
10
|
+
import { loadSharedModelRules, sharedModelRulesForProject, SHARED_RULE_TIERS } from '../shared-model-rules.js';
|
|
11
|
+
|
|
12
|
+
// Starts from the working directory, as the hooks do, so a project policy
|
|
13
|
+
// stored under either agent's project root is counted.
|
|
14
|
+
function printShared(cfg, { details = false } = {}) {
|
|
15
|
+
const policies = sharedModelRulesForProject(loadSharedModelRules(), process.cwd());
|
|
16
|
+
const targets = codexSharedTargets(cfg);
|
|
17
|
+
const provider = codexProviderName();
|
|
18
|
+
const parent = codexModelName();
|
|
19
|
+
console.log(`Shared delegation policies: ${policies.length} in this project (${cfg.codex?.sharedRules?.enabled === false ? 'off' : 'on'}); Codex tier mappings: ${targets.length}`);
|
|
20
|
+
if (!policies.length) console.log('First use: sprag seed --agent codex. Approve policies with seed accept <id>|all --global|--project; no Claude installation or history is required.');
|
|
21
|
+
if (details) {
|
|
22
|
+
for (const r of policies) console.log(`shared ${r.tier} ${r.category} | ${r.scope}${r.targetRoot ? ` ${r.targetRoot}` : ''}`);
|
|
23
|
+
for (const t of targets) console.log(`${t.provider} | ${t.from} | ${t.tier} -> ${t.model}${t.effort ? ` (${t.effort})` : ''}`);
|
|
24
|
+
}
|
|
25
|
+
// A mapping routes only sessions on the parent model it names, so a tier
|
|
26
|
+
// mapped for another model still leaves the configured model unrouted.
|
|
27
|
+
const mapped = (tier) => targets.some((t) => t.provider === provider && t.tier === tier && (!parent || t.from === parent));
|
|
28
|
+
const missing = [...new Set(policies.map((r) => r.tier))].filter((tier) => !mapped(tier));
|
|
29
|
+
if (missing.length) {
|
|
30
|
+
console.log(`Unmapped tiers for ${parent || 'any parent model'} on ${provider || 'unknown provider'}: ${missing.join(', ')}. `
|
|
31
|
+
+ `Set: sprag delegate shared map <T1|T2> --from ${parent || '<parent-model>'} --model <target-model> --agent codex`);
|
|
32
|
+
}
|
|
33
|
+
if (!parent && targets.length) console.log('Codex config names no default model; each mapping routes only sessions on its --from model.');
|
|
34
|
+
const minimum = codexDelegateMinContext(cfg);
|
|
35
|
+
console.log(`Shared prompt guidance starts at ${minimum} parent input tokens${cfg.codex?.delegateMinContext === minimum ? '' : ' (default)'}. `
|
|
36
|
+
+ 'Change: sprag delegate shared min-context <tokens|default> --agent codex');
|
|
37
|
+
console.log('Shared policies keep their original scope and bounds. Models require an exact parent/provider mapping; agent-specific roles and usage totals are not imported.');
|
|
38
|
+
}
|
|
9
39
|
|
|
10
40
|
export function run({ args, getArg, hasFlag, root }) {
|
|
11
41
|
const sub = args[1] || 'status';
|
|
12
42
|
if (args.some((arg) => arg.startsWith('--hook'))) throw new Error('Use codex-hook for Codex hook payloads.');
|
|
13
|
-
for (const flag of ['--model', '--effort', '--from']) {
|
|
43
|
+
for (const flag of ['--model', '--effort', '--from', '--provider']) {
|
|
14
44
|
if (args.some((arg) => arg === flag || arg.startsWith(`${flag}=`)) && (!getArg(flag) || getArg(flag).startsWith('-'))) {
|
|
15
45
|
throw new Error(`${flag} requires a value.`);
|
|
16
46
|
}
|
|
17
47
|
}
|
|
48
|
+
const cfg = loadConfig();
|
|
49
|
+
if (sub === 'shared') {
|
|
50
|
+
const action = args[2] || 'status';
|
|
51
|
+
if (!['status', 'on', 'off', 'map', 'unmap', 'min-context'].includes(action)) {
|
|
52
|
+
throw new Error('Usage: delegate shared status|on|off|map <T1|T2>|unmap <T1|T2>|min-context <tokens|default> --agent codex');
|
|
53
|
+
}
|
|
54
|
+
if (hasFlag('--global') || hasFlag('--project')) throw new Error('Shared policies retain their approved scope; mappings do not change it.');
|
|
55
|
+
const provided = (flag) => args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
56
|
+
if (!['map', 'unmap'].includes(action) && ['--model', '--effort', '--from', '--provider'].some(provided)) {
|
|
57
|
+
throw new Error('Model options require delegate shared map or unmap.');
|
|
58
|
+
}
|
|
59
|
+
const targets = codexSharedTargets(cfg);
|
|
60
|
+
if (action === 'map' || action === 'unmap') {
|
|
61
|
+
const tier = args[3];
|
|
62
|
+
const from = getArg('--from');
|
|
63
|
+
const provider = getArg('--provider') ?? codexProviderName();
|
|
64
|
+
if (!SHARED_RULE_TIERS.includes(tier)) throw new Error('Shared rule tier must be T1 or T2.');
|
|
65
|
+
const same = (t) => t.tier === tier && t.from === from && t.provider === provider;
|
|
66
|
+
if (action === 'map') {
|
|
67
|
+
const target = validateCodexSharedTarget({ tier, from, provider, model: getArg('--model'), effort: getArg('--effort') });
|
|
68
|
+
cfg.codex = { ...cfg.codex, sharedRules: { ...cfg.codex?.sharedRules, targets: [...targets.filter((t) => !same(t)), target] } };
|
|
69
|
+
} else {
|
|
70
|
+
if (provided('--model') || provided('--effort')) throw new Error('unmap accepts only tier, --from and --provider.');
|
|
71
|
+
if (!targets.some(same)) throw new Error('No matching shared tier mapping; specify --from and the correct provider.');
|
|
72
|
+
cfg.codex = { ...cfg.codex, sharedRules: { ...cfg.codex?.sharedRules, targets: targets.filter((t) => !same(t)) } };
|
|
73
|
+
}
|
|
74
|
+
} else if (action === 'min-context') {
|
|
75
|
+
const value = args[3];
|
|
76
|
+
if (value === 'default') {
|
|
77
|
+
cfg.codex = { ...cfg.codex };
|
|
78
|
+
delete cfg.codex.delegateMinContext;
|
|
79
|
+
} else if (/^\d+$/.test(value ?? '') && Number.isSafeInteger(Number(value))) {
|
|
80
|
+
cfg.codex = { ...cfg.codex, delegateMinContext: Number(value) };
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error(`min-context takes a whole number of parent input tokens, or default (${DEFAULT_DELEGATE_MIN_CONTEXT}).`);
|
|
83
|
+
}
|
|
84
|
+
} else if (action !== 'status') {
|
|
85
|
+
cfg.codex = { ...cfg.codex, sharedRules: { ...cfg.codex?.sharedRules, enabled: action === 'on' } };
|
|
86
|
+
}
|
|
87
|
+
if (action !== 'status') saveConfig(cfg);
|
|
88
|
+
printShared(cfg, { details: true });
|
|
89
|
+
console.log(`Codex delegation is ${cfg.codex?.delegate === true ? 'on' : 'off; enable with sprag delegate on --agent codex'}. Target availability and lower cost must be verified with your provider.`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
18
92
|
if (sub === 'rules') {
|
|
19
93
|
if (args[2] === 'add') {
|
|
20
94
|
if (hasFlag('--global') === hasFlag('--project')) throw new Error('Choose --global or --project for the rule.');
|
|
@@ -31,7 +105,7 @@ export function run({ args, getArg, hasFlag, root }) {
|
|
|
31
105
|
} else if (args[2] === 'rm') removeCodexModelRule(Number(args[3]));
|
|
32
106
|
else if (args[2]) throw new Error('Usage: delegate rules [add <category|R<N>> --from <model> --model <model> --global|--project | rm <N>]');
|
|
33
107
|
const rules = loadCodexModelRules();
|
|
34
|
-
if (!rules.length) console.log('No Codex model rules.
|
|
108
|
+
if (!rules.length) console.log('No Codex-only model rules.');
|
|
35
109
|
let events = [];
|
|
36
110
|
try { events = Object.values(loadCodexLedger().events); } catch { /* No ledger yet: rules list without outcomes. */ }
|
|
37
111
|
const inReview = new Set(codexRulesInReview({ rules, events, root: null }).map((x) => x.index));
|
|
@@ -41,10 +115,10 @@ export function run({ args, getArg, hasFlag, root }) {
|
|
|
41
115
|
const warn = inReview.has(i + 1) ? ` | rule-health: ${h.errs}/${h.runs} runs failed, narrow the condition or remove (delegate rules rm ${i + 1} --agent codex)` : '';
|
|
42
116
|
console.log(`#${i + 1} ${r.category} | ${r.from} -> ${r.model} | ${r.scope}${r.targetRoot ? ` ${r.targetRoot}` : ''}${outcome}${warn}`);
|
|
43
117
|
});
|
|
118
|
+
printShared(cfg, { details: true });
|
|
44
119
|
return;
|
|
45
120
|
}
|
|
46
|
-
if (!['on', 'off', 'status', 'model'].includes(sub)) throw new Error('Usage: delegate on|off|status|model <model>|rules --agent codex');
|
|
47
|
-
const cfg = loadConfig();
|
|
121
|
+
if (!['on', 'off', 'status', 'model'].includes(sub)) throw new Error('Usage: delegate on|off|status|model <model>|rules|shared --agent codex');
|
|
48
122
|
let target;
|
|
49
123
|
if (sub === 'model') target = args[2] === 'off' ? null : validateCodexTarget(args[2], getArg('--effort'));
|
|
50
124
|
else if (getArg('--model') !== undefined) target = validateCodexTarget(getArg('--model'), getArg('--effort'));
|
|
@@ -63,6 +137,7 @@ export function run({ args, getArg, hasFlag, root }) {
|
|
|
63
137
|
let ruleCount;
|
|
64
138
|
try { ruleCount = String(loadCodexModelRules().length); } catch (e) { ruleCount = `unreadable (${e.message})`; }
|
|
65
139
|
console.log(`Codex rules: ${ruleCount}`);
|
|
140
|
+
printShared(cfg);
|
|
66
141
|
console.log('Explicit spawn models and custom roles are preserved. Targets must be available from your Codex provider.');
|
|
67
142
|
const saved = codexRoutingSavedTotals();
|
|
68
143
|
console.log(saved.priced
|
package/src/commands/codex.js
CHANGED
|
@@ -26,11 +26,16 @@ Usage: sprag <command> --agent codex [options]
|
|
|
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
28
|
delegate model <model> Default target for spawns without an explicit model
|
|
29
|
-
delegate rules Codex
|
|
29
|
+
delegate rules Codex rules and shared delegation policies
|
|
30
|
+
delegate shared on|off|status Reuse approved policies; keep original scopes
|
|
31
|
+
delegate shared map T1|T2 Map a tier: --from <parent> --model <target>
|
|
32
|
+
Optional --provider <id> and --effort <level>
|
|
33
|
+
delegate shared unmap T1|T2 Remove a mapping: --from <parent> [--provider <id>]
|
|
34
|
+
delegate shared min-context N Parent input tokens before shared guidance; default resets
|
|
30
35
|
route-scan [--refresh] Discover recurring simple turns in local logs
|
|
31
36
|
route-scan dismiss R<N> Dismiss a candidate without registering a rule
|
|
32
37
|
route-scan savings Attributed routing estimates; --refresh or --format json
|
|
33
|
-
seed [accept|skip|reset]
|
|
38
|
+
seed [accept|skip|reset] Shared delegation and Codex ratchet presets; explicit scope
|
|
34
39
|
cache [auto|off|<duration>] Display timer policy (e.g. 30m); never changes retention
|
|
35
40
|
cache status --refresh Resolve the session's gateway deployment and cache policy
|
|
36
41
|
harness init|uninit|check|promote|list|rm|prune
|
|
@@ -413,6 +418,7 @@ export async function run({ args, version }) {
|
|
|
413
418
|
console.log('After approval, start or resume Codex and run: sprag panel doctor --agent codex');
|
|
414
419
|
if (panel.mode === 'window') console.log('Some Codex versions defer SessionStart until the first prompt. For immediate opening on typing codex: sprag panel shell install --agent codex (optional zsh integration).');
|
|
415
420
|
console.log('Existing config.toml, Claude Code settings, and ratchet rules are preserved.');
|
|
421
|
+
console.log('First-use delegation: sprag seed --agent codex, then sprag delegate shared status --agent codex. Approve a scope and map the tiers before enabling delegation. No Claude installation is required.');
|
|
416
422
|
console.log('Korean guidance is opt-in: sprag korean on --agent codex');
|
|
417
423
|
return;
|
|
418
424
|
}
|
|
@@ -58,9 +58,13 @@ export async function run({ args, hasFlag }) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
if (!sub || sub === 'status' || sub === 'check') {
|
|
61
|
-
|
|
61
|
+
// The statusline knows the real window; the settings model id often does
|
|
62
|
+
// not (a bare `opus` carries no `[1m]`), so the last reported size decides.
|
|
63
|
+
let liveWindow = null;
|
|
64
|
+
try { liveWindow = (await import('../ctx-window-cache.js')).latestLiveWindow(); } catch { /* id-only fallback */ }
|
|
65
|
+
const s = cw.compactWindowStatus({ root, liveWindow });
|
|
62
66
|
console.log(`model: ${s.model || '(not set — Claude Code default)'}${s.modelSource ? ` [${s.modelSource}]` : ''}`);
|
|
63
|
-
console.log(`window: ${s.is1m ? '1M context' : '200k context'}`);
|
|
67
|
+
console.log(`window: ${s.is1m ? '1M context' : '200k context'}${liveWindow ? ` [statusline: ${liveWindow >= 1e6 ? `${liveWindow / 1e6}M` : fmt(liveWindow)}]` : ''}`);
|
|
64
68
|
console.log(`autoCompactWindow: ${s.window === null ? '(unset)' : `${s.window} (${fmt(s.window)})`}${s.windowSource ? ` [${s.windowSource}${s.windowPath ? ` → ${s.windowPath}` : ''}]` : ''}`);
|
|
65
69
|
if (s.ok && s.reason === 'not-1m') {
|
|
66
70
|
console.log(ko
|
package/src/commands/install.js
CHANGED
|
@@ -251,8 +251,11 @@ export async function run({ hasFlag }) {
|
|
|
251
251
|
// good, and upgrading would silently leave shell-written files
|
|
252
252
|
// unchecked. installKoreanLintHook widens in place and is idempotent.
|
|
253
253
|
if (ks.koreanStyleEnabled()) {
|
|
254
|
-
const { installKoreanLintHook } = await import('../installer.js');
|
|
254
|
+
const { installKoreanLintHook, installReplyLanguageHook } = await import('../installer.js');
|
|
255
255
|
const migrated = installKoreanLintHook();
|
|
256
|
+
// Older installs have the lint hook but no Stop hook; add it here so
|
|
257
|
+
// an upgrade closes the English-reply gap without a manual `korean on`.
|
|
258
|
+
installReplyLanguageHook();
|
|
256
259
|
if (migrated.action === 'updated') {
|
|
257
260
|
console.log(lang === 'ko'
|
|
258
261
|
? ' 검사 범위 갱신: Bash 로 쓴 파일도 이제 확인합니다'
|
package/src/commands/korean.js
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
* wrote:
|
|
21
21
|
* sprag korean lint block|warn|off # how findings are handled
|
|
22
22
|
* sprag korean lint <file...> # check files on disk
|
|
23
|
+
*
|
|
24
|
+
* The third half is a Stop hook for the one slip a write-time check cannot
|
|
25
|
+
* see: an English reply to a Korean prompt, which writes no file at all.
|
|
26
|
+
* sprag korean reply on|off # toggle it (default on)
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
29
|
// Enforcement is only useful if it is on by default — a check the user has to
|
|
@@ -39,6 +43,12 @@ function koreanLintScope(cfg) {
|
|
|
39
43
|
return cfg?.koreanStyle?.lintScope === 'prose' ? 'prose' : 'all';
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
// Default on, like the lint: a check the user has to discover is the same
|
|
47
|
+
// hole in a different shape. Only an explicit false turns it off.
|
|
48
|
+
function koreanReplyCheckOn(cfg) {
|
|
49
|
+
return cfg?.koreanReplyCheck !== false;
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
export async function run({ args, hasFlag }) {
|
|
43
53
|
const sub = args[1] || 'status';
|
|
44
54
|
const ks = await import('../korean-style.js');
|
|
@@ -72,6 +82,55 @@ export async function run({ args, hasFlag }) {
|
|
|
72
82
|
return;
|
|
73
83
|
}
|
|
74
84
|
|
|
85
|
+
// Stop hook. Fires when the model finishes a turn; if the reply is English
|
|
86
|
+
// to a Korean prompt, block the stop so the model answers again in Korean.
|
|
87
|
+
// Imports stay minimal on purpose: this runs after every turn.
|
|
88
|
+
if (hasFlag?.('--reply-hook') || sub === '--reply-hook') {
|
|
89
|
+
const { readStdinJson } = await import('../stdin-payload.js');
|
|
90
|
+
const payload = readStdinJson();
|
|
91
|
+
if (!payload) return;
|
|
92
|
+
// Claude Code sets stop_hook_active while the model is already continuing
|
|
93
|
+
// because of a Stop hook. Blocking again would loop forever.
|
|
94
|
+
if (payload.stop_hook_active) return;
|
|
95
|
+
if (!ks.koreanStyleEnabled()) return;
|
|
96
|
+
if (!koreanReplyCheckOn(loadConfig())) return;
|
|
97
|
+
const { createRequire } = await import('node:module');
|
|
98
|
+
const found = createRequire(import.meta.url)('../reply-language.cjs').checkReplyLanguage(payload.transcript_path);
|
|
99
|
+
if (!found) return;
|
|
100
|
+
console.log(JSON.stringify({
|
|
101
|
+
decision: 'block',
|
|
102
|
+
reason: '사용자는 한국어로 질문했는데 마지막 답변이 영어로 작성됐습니다. 같은 내용을 한국어로 다시 답하십시오. 코드, 명령어, 오류 원문, 커밋 메시지처럼 원문을 유지해야 하는 부분은 그대로 둡니다.',
|
|
103
|
+
}));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Toggle: `korean reply on|off`. Off keeps the hook installed and makes it
|
|
108
|
+
// a no-op, the same way lint mode `off` works; `korean off` removes it.
|
|
109
|
+
if (sub === 'reply') {
|
|
110
|
+
const cfg = loadConfig();
|
|
111
|
+
const next = args[2];
|
|
112
|
+
if (!next) {
|
|
113
|
+
console.log(lang === 'ko'
|
|
114
|
+
? `답변 언어 검사: ${koreanReplyCheckOn(cfg) ? '켜짐' : '꺼짐'}`
|
|
115
|
+
: `Reply-language check: ${koreanReplyCheckOn(cfg) ? 'on' : 'off'}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!['on', 'off'].includes(next)) {
|
|
119
|
+
console.error('Usage: sprag korean reply [on|off]');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
cfg.koreanReplyCheck = next === 'on';
|
|
123
|
+
saveConfig(cfg);
|
|
124
|
+
console.log(next === 'on'
|
|
125
|
+
? (lang === 'ko'
|
|
126
|
+
? '답변 언어 검사를 켰습니다. 한국어 질문에 영어로 답하면 모델에게 되돌려 보내 다시 답하게 합니다.'
|
|
127
|
+
: 'Reply-language check is on. An English reply to a Korean prompt is sent back for a Korean answer.')
|
|
128
|
+
: (lang === 'ko'
|
|
129
|
+
? '답변 언어 검사를 껐습니다. 훅은 남아 있지만 아무 일도 하지 않습니다.'
|
|
130
|
+
: 'Reply-language check is off. The hook stays installed and does nothing.'));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
75
134
|
// Manual run over files already on disk: `korean lint docs/*.md`.
|
|
76
135
|
if (sub === 'lint' && args.length > 2 && !['on', 'off', 'warn', 'block', 'scope'].includes(args[2])) {
|
|
77
136
|
const { readFileSync } = await import('node:fs');
|
|
@@ -116,8 +175,8 @@ export async function run({ args, hasFlag }) {
|
|
|
116
175
|
saveConfig(cfg);
|
|
117
176
|
console.log(lang === 'ko'
|
|
118
177
|
? (next === 'all'
|
|
119
|
-
? '검사
|
|
120
|
-
: '검사
|
|
178
|
+
? '검사 범위를 all 로 바꿨습니다. 문서와 코드 주석, UI 문자열까지 세션이 쓴 모든 텍스트 파일을 검사합니다.'
|
|
179
|
+
: '검사 범위를 prose 로 좁혔습니다. 마크다운과 텍스트 문서만 검사합니다.')
|
|
121
180
|
: (next === 'all'
|
|
122
181
|
? 'Check scope: all — every text file the session writes, comments and UI strings included.'
|
|
123
182
|
: 'Check scope: prose — documents only.'));
|
|
@@ -163,8 +222,13 @@ export async function run({ args, hasFlag }) {
|
|
|
163
222
|
if (sub === 'on' || sub === 'off') {
|
|
164
223
|
const enabled = sub === 'on';
|
|
165
224
|
ks.setKoreanStyleEnabled(enabled);
|
|
166
|
-
const {
|
|
225
|
+
const {
|
|
226
|
+
installKoreanLintHook, removeKoreanLintHook,
|
|
227
|
+
installReplyLanguageHook, removeReplyLanguageHook,
|
|
228
|
+
} = await import('../installer.js');
|
|
167
229
|
const hook = enabled ? installKoreanLintHook() : removeKoreanLintHook();
|
|
230
|
+
if (enabled) installReplyLanguageHook();
|
|
231
|
+
else removeReplyLanguageHook();
|
|
168
232
|
if (enabled) {
|
|
169
233
|
console.log(lang === 'ko'
|
|
170
234
|
? '한국어 문체 지침을 켰습니다. 다음 세션부터 모든 프로젝트에 적용됩니다.'
|
|
@@ -202,6 +266,7 @@ export async function run({ args, hasFlag }) {
|
|
|
202
266
|
console.log(`한국어 문체 지침: ${on ? '켜짐' : '꺼짐'}`);
|
|
203
267
|
console.log(` 비용: 세션당 약 ${tokens} 토큰 (세션 시작 1회 주입)`);
|
|
204
268
|
console.log(` 쓰기 시점 검사: ${koreanLintMode(loadConfig())}, 범위 ${koreanLintScope(loadConfig())} (바꾸려면 sprag korean lint block|warn|off, korean lint scope all|prose)`);
|
|
269
|
+
console.log(` 답변 언어 검사: ${koreanReplyCheckOn(loadConfig()) ? '켜짐' : '꺼짐'} (바꾸려면 sprag korean reply on|off)`);
|
|
205
270
|
console.log(` 출처: ${ks.KOREAN_STYLE_SOURCE}`);
|
|
206
271
|
console.log(` 라이선스 전문: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
|
|
207
272
|
console.log(on
|
|
@@ -211,6 +276,7 @@ export async function run({ args, hasFlag }) {
|
|
|
211
276
|
console.log(`Korean writing guidance: ${on ? 'on' : 'off'}`);
|
|
212
277
|
console.log(` Cost: ~${tokens} tokens per session (injected once at session start)`);
|
|
213
278
|
console.log(` Write-time check: ${koreanLintMode(loadConfig())}, scope ${koreanLintScope(loadConfig())} (change with: sprag korean lint block|warn|off, korean lint scope all|prose)`);
|
|
279
|
+
console.log(` Reply-language check: ${koreanReplyCheckOn(loadConfig()) ? 'on' : 'off'} (change with: sprag korean reply on|off)`);
|
|
214
280
|
console.log(` Source: ${ks.KOREAN_STYLE_SOURCE}`);
|
|
215
281
|
console.log(` License text: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
|
|
216
282
|
console.log(on
|
|
@@ -360,12 +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 { updateStatus, maybeSpawnUpdateCheck } = await import('../update-check.js');
|
|
363
|
+
const { updateStatus, maybeSpawnUpdateCheck, SESSION_START_CHECK_INTERVAL_MS } = await import('../update-check.js');
|
|
364
364
|
const pkgVersion = readPackageVersion();
|
|
365
365
|
// Session start is the one moment where a stale cache matters most, so
|
|
366
|
-
// kick the detached refresh here too
|
|
367
|
-
// statusline
|
|
368
|
-
|
|
366
|
+
// kick the detached refresh here too, on a shorter interval than the
|
|
367
|
+
// statusline's 24h: releases can land several times a day, and a
|
|
368
|
+
// session opened that afternoon should hear about them. It lands in
|
|
369
|
+
// time for the statusline and for the next session, not for this line.
|
|
370
|
+
if (pkgVersion) maybeSpawnUpdateCheck(pkgVersion, { intervalMs: SESSION_START_CHECK_INTERVAL_MS });
|
|
369
371
|
const u = pkgVersion ? updateStatus(pkgVersion) : null;
|
|
370
372
|
// `dismissed` is the whole point of asking once: a user who said no to
|
|
371
373
|
// this version must not be asked again every `/clear`.
|
package/src/commands/saved.js
CHANGED
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Nothing is deleted. A reset only records a timestamp, and the totals count
|
|
10
10
|
* events from that moment on; see src/saved-reset.cjs.
|
|
11
|
+
*
|
|
12
|
+
* The routing mark also bounds the per-rule `saved ~$` figures in
|
|
13
|
+
* model-rules.json and ratchet-model.md, which are rebuilt by a scan rather than
|
|
14
|
+
* summed from the ledger. So a reset or undo that touches `routing` finishes by
|
|
15
|
+
* running that scan; see refreshRuleSavings.
|
|
11
16
|
*/
|
|
12
17
|
|
|
18
|
+
import { debug } from '../debug.js';
|
|
19
|
+
|
|
13
20
|
const SCOPE_SETS = {
|
|
14
21
|
claude: ['routing', 'doc2md'],
|
|
15
22
|
codex: ['codex-routing', 'codex-docs'],
|
|
@@ -33,7 +40,32 @@ async function currentTotals(scope, dir) {
|
|
|
33
40
|
return { docs: codexDocumentTotals({ dir }).docs };
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Rebuild each rule's savedUsd and re-render ratchet-model.md after the routing
|
|
45
|
+
* mark moved. The 14-day scan does both (it reads the mark, refreshes the
|
|
46
|
+
* registry, writes the managed files) in about half a second, so it is reused
|
|
47
|
+
* rather than duplicated here.
|
|
48
|
+
*
|
|
49
|
+
* Best-effort by design: the mark is already saved, and a failed rescan must not
|
|
50
|
+
* turn a successful reset into an error. It says so in one line instead, since
|
|
51
|
+
* the figures then stay stale until the next scan.
|
|
52
|
+
*
|
|
53
|
+
* @param {boolean} ko
|
|
54
|
+
* @param {Function} [rescan] injected by tests; defaults to runRouteScan
|
|
55
|
+
*/
|
|
56
|
+
async function refreshRuleSavings(ko, rescan) {
|
|
57
|
+
try {
|
|
58
|
+
const scan = rescan || (await import('../route-scan.js')).runRouteScan;
|
|
59
|
+
await scan({ days: 14 });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
debug('saved:rule-savings', e);
|
|
62
|
+
console.log(ko
|
|
63
|
+
? '룰별 절감액은 다음 route-scan 때 다시 계산됩니다.'
|
|
64
|
+
: 'Per-rule savings will be recalculated at the next route-scan.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function run({ args, hasFlag, agent = 'claude', dir: dirArg, rescan }) {
|
|
37
69
|
const { userLanguage } = await import('../config.js');
|
|
38
70
|
const { userDataDir } = await import('../paths.js');
|
|
39
71
|
const { readResetMarks, resetSaved, undoReset } = await import('../saved-reset.cjs');
|
|
@@ -90,6 +122,9 @@ export async function run({ args, hasFlag, agent = 'claude', dir: dirArg }) {
|
|
|
90
122
|
console.log(ko
|
|
91
123
|
? `원장 기록은 지우지 않았습니다. \`sprag saved undo${suffix}\`로 되돌릴 수 있습니다.`
|
|
92
124
|
: `No ledger data was deleted. Run \`sprag saved undo${suffix}\` to restore the previous counters.`);
|
|
125
|
+
// Codex keeps no per-rule figure (its rules are a separate store with no
|
|
126
|
+
// savedUsd), so only the Claude `routing` scope has anything to recompute.
|
|
127
|
+
if (targets.includes('routing')) await refreshRuleSavings(ko, rescan);
|
|
93
128
|
return;
|
|
94
129
|
}
|
|
95
130
|
|
|
@@ -105,6 +140,10 @@ export async function run({ args, hasFlag, agent = 'claude', dir: dirArg }) {
|
|
|
105
140
|
? `${label(s)} 카운터를 되돌렸습니다. 현재 합계는 ${fmtTotal(t)}입니다.`
|
|
106
141
|
: `Restored the ${label(s)} counter. The total is now ${fmtTotal(t)}.`);
|
|
107
142
|
}
|
|
143
|
+
// Keyed on what was undone, not on --agent: undo pops the latest reset
|
|
144
|
+
// whichever agent made it, so a bare `undo --agent codex` can restore
|
|
145
|
+
// the Claude routing mark and the rule figures must follow it.
|
|
146
|
+
if (undone.scopes.includes('routing')) await refreshRuleSavings(ko, rescan);
|
|
108
147
|
return;
|
|
109
148
|
}
|
|
110
149
|
|
package/src/ctx-window-cache.js
CHANGED
|
@@ -59,3 +59,17 @@ export function liveWindowFor(sessionId) {
|
|
|
59
59
|
const s = load().sessions[sessionId];
|
|
60
60
|
return s && validSize(s.size) ? s.size : null;
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The window most recently reported by any session's statusline, or null.
|
|
65
|
+
* `sprag compact-window` runs outside a session and has no payload of its own;
|
|
66
|
+
* judged by the settings model id alone, a bare `opus` (1M by default on Opus
|
|
67
|
+
* 5.5, with no `[1m]` suffix) read as 200k while the statusline said 1M.
|
|
68
|
+
*/
|
|
69
|
+
export function latestLiveWindow() {
|
|
70
|
+
let best = null;
|
|
71
|
+
for (const s of Object.values(load().sessions)) {
|
|
72
|
+
if (s && validSize(s.size) && Number.isFinite(s.ts) && (!best || s.ts > best.ts)) best = s;
|
|
73
|
+
}
|
|
74
|
+
return best ? best.size : null;
|
|
75
|
+
}
|