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/commands/codex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createArgs } from '../cli-args.js';
|
|
2
2
|
import { configureCodexHooks } from '../codex-installer.js';
|
|
3
|
-
import { initCodexHarness, uninitCodexHarness, codexHarnessStatus } from '../codex-harness.js';
|
|
3
|
+
import { initCodexHarness, uninitCodexHarness, codexHarnessStatus, syncCodexKoreanBlock } from '../codex-harness.js';
|
|
4
4
|
import { findProjectRoot, harnessPromote, harnessListRules, harnessRmRule, harnessPrune } from '../harness.js';
|
|
5
5
|
import { loadConfig, saveConfig } from '../config.js';
|
|
6
6
|
import { resolve } from 'node:path';
|
|
@@ -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;
|
|
@@ -408,6 +415,10 @@ export async function run({ args, version }) {
|
|
|
408
415
|
if (process.env.CTS_NO_HARNESS !== '1') {
|
|
409
416
|
const h = initCodexHarness({ root, scope: 'global' });
|
|
410
417
|
console.log(`Codex harness: ${h.file}`);
|
|
418
|
+
try {
|
|
419
|
+
const k = syncCodexKoreanBlock({ cfg });
|
|
420
|
+
console.log(`Codex korean-style: ${k.file} (${k.action})`);
|
|
421
|
+
} catch (e) { debug('codex:korean-sync', e); }
|
|
411
422
|
}
|
|
412
423
|
try {
|
|
413
424
|
const { preparePresetRatchet } = await import('../preset-ratchet.js');
|
|
@@ -426,9 +437,9 @@ export async function run({ args, version }) {
|
|
|
426
437
|
const auto = parent ? autoCodexSharedTargets({ model: parent, provider }) : { targets: [], reason: 'Codex config names no default model' };
|
|
427
438
|
console.log(auto.targets.length
|
|
428
439
|
? `Delegation tiers for ${parent} on ${provider} (auto): ${auto.targets.map((t) => `${t.tier} -> ${t.model}`).join(', ')}. Change: sprag delegate shared status --agent codex`
|
|
429
|
-
: `Delegation tiers: not resolved yet (${auto.reason}). They resolve once
|
|
440
|
+
: `Delegation tiers: not resolved yet (${auto.reason}). They resolve once Codex lists or has run a cheaper priced model, or map one: sprag delegate shared map T2 --from <parent> --model <target> --agent codex`);
|
|
430
441
|
} catch (e) { debug('codex:auto-tiers', e); }
|
|
431
|
-
console.log('First-use delegation: sprag seed --agent codex to approve policies
|
|
442
|
+
console.log('First-use delegation: sprag seed --agent codex to approve policies; delegation is on by default (turn off: sprag delegate off --agent codex). No Claude installation is required.');
|
|
432
443
|
console.log('Korean guidance is opt-in: sprag korean on --agent codex');
|
|
433
444
|
return;
|
|
434
445
|
}
|
|
@@ -436,8 +447,11 @@ export async function run({ args, version }) {
|
|
|
436
447
|
if (hasFlag('--purge')) throw new Error('Codex uninstall keeps shared Sprag state; --purge is not supported.');
|
|
437
448
|
const hook = configureCodexHooks({ remove: true });
|
|
438
449
|
const h = uninitCodexHarness({ root, scope: 'global' });
|
|
450
|
+
const { installPanelShell } = await import('../codex-panel-shell.js');
|
|
451
|
+
const shellFile = installPanelShell({ remove: true });
|
|
439
452
|
console.log(`Codex hooks: ${hook.file} (${hook.action})`);
|
|
440
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.`);
|
|
441
455
|
console.log('Ratchet rules, project harnesses, backups, and shared Sprag state kept.');
|
|
442
456
|
return;
|
|
443
457
|
}
|
|
@@ -513,6 +527,7 @@ export async function run({ args, version }) {
|
|
|
513
527
|
const { setKoreanStyleEnabled } = await import('../korean-style.js');
|
|
514
528
|
if (args[1] === 'on') configureCodexHooks();
|
|
515
529
|
setKoreanStyleEnabled(args[1] === 'on');
|
|
530
|
+
try { syncCodexKoreanBlock({ cfg: loadConfig() }); } catch (e) { debug('codex:korean-sync', e); }
|
|
516
531
|
console.log(`Korean guidance: ${args[1]} (shared Sprag preference). Review/trust the Sprag hooks in Codex /hooks.`);
|
|
517
532
|
return;
|
|
518
533
|
}
|
|
@@ -542,9 +557,27 @@ export async function run({ args, version }) {
|
|
|
542
557
|
console.log('Prompt conversion, supported reads, and document/cache write guards require trusted hooks. Review in Codex /hooks.');
|
|
543
558
|
return;
|
|
544
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
|
+
}
|
|
545
571
|
if (cmd === 'doc2md' && args[1] && !args[1].startsWith('-') && args[1] !== 'install-converter') {
|
|
546
572
|
const { convertCodexDocument } = await import('../codex-doc2md.js');
|
|
547
|
-
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);
|
|
548
581
|
if (!result.ok) {
|
|
549
582
|
console.error(`Doc2md failed: ${result.reason}`);
|
|
550
583
|
if (result.reason === 'no-markitdown') console.error('Run sprag doc2md install-converter --agent codex, then retry.');
|
|
@@ -555,21 +588,25 @@ export async function run({ args, version }) {
|
|
|
555
588
|
console.log(`Source ${result.meta.size} bytes; Markdown ${result.meta.markdownBytes} bytes.`);
|
|
556
589
|
if (result.meta.clipped || result.meta.truncated) console.log('Text extraction is incomplete; inspect the source for omitted content.');
|
|
557
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'})`}`);
|
|
558
593
|
return;
|
|
559
594
|
}
|
|
560
595
|
if (cmd === 'upgrade') {
|
|
561
|
-
const
|
|
596
|
+
const { upgradeInstructions } = await import('../update-notice.js');
|
|
597
|
+
const [command, setup] = upgradeInstructions('codex');
|
|
562
598
|
if (!hasFlag('--print')) throw new Error(`For a Codex-only upgrade, run ${command}, then sprag install --agent codex.`);
|
|
563
599
|
console.log(command);
|
|
564
|
-
console.log(
|
|
600
|
+
console.log(setup);
|
|
565
601
|
return;
|
|
566
602
|
}
|
|
567
603
|
const shared = new Set(['korean', 'cohesion', 'doc2md', 'mode', 'feedback', 'update-check']);
|
|
568
604
|
if (shared.has(cmd)) {
|
|
569
605
|
if (args.some((a) => a.startsWith('--hook'))) throw new Error('Use codex-hook for Codex hook payloads.');
|
|
570
606
|
if (cmd === 'cohesion' && args[1] === 'on') configureCodexHooks();
|
|
607
|
+
if (cmd === 'korean' && args[1] === 'reply' && args[2] === 'on') configureCodexHooks();
|
|
571
608
|
const { userDataDir } = await import('../paths.js');
|
|
572
|
-
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() });
|
|
573
610
|
if (cmd === 'cohesion' && args[1] === 'on') console.log('Review/trust the Sprag hooks in Codex /hooks.');
|
|
574
611
|
return result;
|
|
575
612
|
}
|
package/src/commands/doc2md.js
CHANGED
|
@@ -104,6 +104,7 @@ export async function run({ args, hasFlag }) {
|
|
|
104
104
|
console.log(res.action === 'skipped'
|
|
105
105
|
? `✗ ${res.reason}`
|
|
106
106
|
: `✓ Read hook ${res.action} (${res.path})`);
|
|
107
|
+
if (res.modReason) console.log(` mod: not registered — ${res.modReason}`);
|
|
107
108
|
// A registered hook with no converter behind it does nothing at all, and
|
|
108
109
|
// says nothing about it either, which reads as a broken feature. Offer the
|
|
109
110
|
// one command that closes the gap right where the gap is visible.
|
|
@@ -160,6 +161,17 @@ export async function run({ args, hasFlag }) {
|
|
|
160
161
|
console.log(` converter: ${python ? `markitdown via ${python}` : `not installed — ${doc2md.INSTALL_HINT}`}`);
|
|
161
162
|
console.log(` cache: ${dir} (${cached} file(s))`);
|
|
162
163
|
console.log(` hook: ${hookRegistered() ? 'registered on Read' : 'not registered'}`);
|
|
164
|
+
// The mod has the same two halves as the hook: `doc2md on` can leave it out
|
|
165
|
+
// (a shell value of the variable, a /plugin copy) while the hook goes in.
|
|
166
|
+
// A path the user added to their own shell variable counts as registered.
|
|
167
|
+
{
|
|
168
|
+
const { modDirsState, modDir, DOC2MD_MOD } = await import('../installer.js');
|
|
169
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
170
|
+
const inShell = (process.env.CLAUDE_CODE_PLUGIN_DIRS || '').split(sep).includes(modDir(DOC2MD_MOD));
|
|
171
|
+
console.log(` mod: ${modDirsState()[DOC2MD_MOD] || inShell
|
|
172
|
+
? 'registered (Claude Code 2.1.287+ reads the Markdown in the same Read)'
|
|
173
|
+
: 'not registered — `doc2md on` registers it or says why not'}`);
|
|
174
|
+
}
|
|
163
175
|
console.log('');
|
|
164
176
|
if (!python) console.log(`Install the converter: ${doc2md.INSTALL_HINT}`);
|
|
165
177
|
console.log('Enable with: sprag doc2md on');
|
package/src/commands/install.js
CHANGED
|
@@ -85,6 +85,8 @@ export async function run({ hasFlag }) {
|
|
|
85
85
|
print('skill', r.skill);
|
|
86
86
|
print('SessionStart hook (route-scan)', r.sessionStartHook);
|
|
87
87
|
print('PostToolUse hook (delegation rescan)', r.delegationHook);
|
|
88
|
+
print('mod (delegation meter, Claude Code 2.1.287+)', r.delegationMeterMod);
|
|
89
|
+
if (r.delegationMeterMod.reason) console.log(` ${r.delegationMeterMod.reason}`);
|
|
88
90
|
print('UserPromptSubmit hook (brief)', r.briefHook);
|
|
89
91
|
{
|
|
90
92
|
let s = r.statusline;
|
|
@@ -333,6 +335,7 @@ export async function run({ hasFlag }) {
|
|
|
333
335
|
console.log(res.action === 'skipped'
|
|
334
336
|
? ` doc2md: ${res.reason}`
|
|
335
337
|
: ` doc2md: Read/Write hook ${res.action} (${res.path})`);
|
|
338
|
+
if (res.modReason) console.log(` doc2md: mod not registered — ${res.modReason}`);
|
|
336
339
|
console.log(lang === 'ko'
|
|
337
340
|
? ` 대상 형식: ${doc2md.TARGET_EXTENSIONS.join(' ')} — 모델이 읽기 전에 Markdown으로 변환합니다.`
|
|
338
341
|
: ` formats: ${doc2md.TARGET_EXTENSIONS.join(' ')} — converted to Markdown before the model reads them.`);
|
|
@@ -449,6 +452,11 @@ export async function run({ hasFlag }) {
|
|
|
449
452
|
if (process.env.CTS_NO_HARNESS !== '1') {
|
|
450
453
|
const h = initCodexHarness({ scope: 'global' });
|
|
451
454
|
console.log(` codex: harness ${h.file}`);
|
|
455
|
+
try {
|
|
456
|
+
const { syncCodexKoreanBlock } = await import('../codex-harness.js');
|
|
457
|
+
const k = syncCodexKoreanBlock({ cfg: config });
|
|
458
|
+
console.log(` codex: korean-style ${k.action} (${k.file})`);
|
|
459
|
+
} catch (e) { debug('install:codex-korean', e); }
|
|
452
460
|
}
|
|
453
461
|
try { (await import('../preset-ratchet.js')).preparePresetRatchet({ agent: 'codex' }); } catch (e) { debug('install:codex-preset', e); }
|
|
454
462
|
if (!manual) {
|
package/src/commands/korean.js
CHANGED
|
@@ -53,6 +53,7 @@ export async function run({ args, hasFlag }) {
|
|
|
53
53
|
const sub = args[1] || 'status';
|
|
54
54
|
const ks = await import('../korean-style.js');
|
|
55
55
|
const { userLanguage, loadConfig, saveConfig } = await import('../config.js');
|
|
56
|
+
const { debug } = await import('../debug.js');
|
|
56
57
|
const lang = userLanguage();
|
|
57
58
|
|
|
58
59
|
// PostToolUse hook. Claude Code feeds the tool-call payload on stdin; we
|
|
@@ -222,6 +223,8 @@ export async function run({ args, hasFlag }) {
|
|
|
222
223
|
if (sub === 'on' || sub === 'off') {
|
|
223
224
|
const enabled = sub === 'on';
|
|
224
225
|
ks.setKoreanStyleEnabled(enabled);
|
|
226
|
+
// Codex reads the guide from its global AGENTS.md; keep that block in step.
|
|
227
|
+
try { (await import('../codex-harness.js')).syncCodexKoreanBlock({ cfg: loadConfig() }); } catch (e) { debug('korean:codex-sync', e); }
|
|
225
228
|
const {
|
|
226
229
|
installKoreanLintHook, removeKoreanLintHook,
|
|
227
230
|
installReplyLanguageHook, removeReplyLanguageHook,
|
|
@@ -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/installer.js
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
import { writeFileSync, mkdirSync, existsSync, unlinkSync, readFileSync, rmSync, copyFileSync, renameSync, realpathSync, statSync } from 'node:fs';
|
|
17
17
|
import { loadConfig, saveConfig } from './config.js';
|
|
18
18
|
import { CLI_NAME, LEGACY_CLI_NAME } from './cli-name.js';
|
|
19
|
-
import { join } from 'node:path';
|
|
19
|
+
import { join, dirname } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
20
21
|
import { claudeUserDir, userDataDir } from './paths.js';
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -692,6 +693,122 @@ export function removeReplyLanguageHook() {
|
|
|
692
693
|
return { path: file, action: 'removed' };
|
|
693
694
|
}
|
|
694
695
|
|
|
696
|
+
// Mods: plugin folders shipped in this package's mod/ directory, which Claude
|
|
697
|
+
// Code (2.1.287 and later) runs inside its own process. They are loaded through
|
|
698
|
+
// CLAUDE_CODE_PLUGIN_DIRS in the env block of ~/.claude/settings.json, the one
|
|
699
|
+
// way to load a plugin folder that needs neither a marketplace nor the `claude`
|
|
700
|
+
// CLI at install time, so `npm i -g` stays the whole setup. An older Claude
|
|
701
|
+
// Code ignores the variable, and the settings hooks keep doing the work alone.
|
|
702
|
+
//
|
|
703
|
+
// The variable holds absolute paths, so an entry goes stale when the package
|
|
704
|
+
// moves (a Node version switch under nvm puts it in another global root). An
|
|
705
|
+
// entry counts as ours by its last two segments, `mod/<a name below>`, and
|
|
706
|
+
// adding a mod first drops every older path to it, so a reinstall repairs the
|
|
707
|
+
// list instead of growing it. Other folders in the variable are left alone.
|
|
708
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
709
|
+
const PLUGIN_DIRS_ENV = 'CLAUDE_CODE_PLUGIN_DIRS';
|
|
710
|
+
export const DOC2MD_MOD = 'sprag-doc2md';
|
|
711
|
+
export const DELEGATION_METER_MOD = 'sprag-delegation-meter';
|
|
712
|
+
export const MOD_NAMES = [DOC2MD_MOD, DELEGATION_METER_MOD];
|
|
713
|
+
|
|
714
|
+
export function modDir(name) {
|
|
715
|
+
return join(PACKAGE_ROOT, 'mod', name);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const pluginDirsSeparator = () => (process.platform === 'win32' ? ';' : ':');
|
|
719
|
+
|
|
720
|
+
function isOurModEntry(entry, names) {
|
|
721
|
+
const parts = String(entry).split(/[\\/]/).filter(Boolean);
|
|
722
|
+
return parts.length >= 2 && parts[parts.length - 2] === 'mod' && names.includes(parts[parts.length - 1]);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function pluginDirEntries(settings) {
|
|
726
|
+
const value = isPlainObject(settings.env) ? settings.env[PLUGIN_DIRS_ENV] : undefined;
|
|
727
|
+
return typeof value === 'string' ? value.split(pluginDirsSeparator()).filter(Boolean) : [];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Point CLAUDE_CODE_PLUGIN_DIRS at this package's copy of one mod, in place on
|
|
732
|
+
* a settings object read by the caller. Returns true when the object changed,
|
|
733
|
+
* false when it already held exactly that, or a reason string when the mod is
|
|
734
|
+
* better left out: `env` is not something this can safely extend, or the user
|
|
735
|
+
* already loads mods in a way a write here would break or duplicate.
|
|
736
|
+
*
|
|
737
|
+
* `inherited` is the variable as this process received it. When settings.json
|
|
738
|
+
* has no value of its own, that is the user's shell value. Claude Code's docs
|
|
739
|
+
* say a settings `env` entry replaces the inherited value in most sessions, so
|
|
740
|
+
* a value written here would stop the user's own mods from loading; were this
|
|
741
|
+
* variable one of the exceptions, ours would be ignored instead. Either way
|
|
742
|
+
* the write is wrong, so the case is refused with the path to add by hand.
|
|
743
|
+
*/
|
|
744
|
+
function addModDir(settings, name, inherited = process.env[PLUGIN_DIRS_ENV]) {
|
|
745
|
+
if (settings.env !== undefined && !isPlainObject(settings.env)) return 'env is not an object — fix settings.json manually';
|
|
746
|
+
const current = settings.env?.[PLUGIN_DIRS_ENV];
|
|
747
|
+
if (current !== undefined && typeof current !== 'string') return `env.${PLUGIN_DIRS_ENV} is not a string — fix settings.json manually`;
|
|
748
|
+
// A copy installed through /plugin would run beside this one, every hook twice.
|
|
749
|
+
const installed = isPlainObject(settings.enabledPlugins)
|
|
750
|
+
? Object.keys(settings.enabledPlugins).find((id) => id.split('@')[0] === name && settings.enabledPlugins[id])
|
|
751
|
+
: undefined;
|
|
752
|
+
if (installed) return `${name} is already installed as the plugin ${installed} — that copy stays the one that runs`;
|
|
753
|
+
const target = modDir(name);
|
|
754
|
+
const sep = pluginDirsSeparator();
|
|
755
|
+
if (current === undefined && typeof inherited === 'string') {
|
|
756
|
+
const theirs = inherited.split(sep).filter((e) => e && !isOurModEntry(e, MOD_NAMES));
|
|
757
|
+
if (theirs.length) {
|
|
758
|
+
return `${PLUGIN_DIRS_ENV} is set in your shell (${theirs.join(sep)}), and a value in settings.json would replace it — add ${target} to that variable yourself`;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const before = pluginDirEntries(settings);
|
|
762
|
+
const stale = before.filter((e) => e !== target && isOurModEntry(e, [name]));
|
|
763
|
+
if (before.includes(target) && stale.length === 0) return false;
|
|
764
|
+
const entries = [...before.filter((e) => !isOurModEntry(e, [name])), target];
|
|
765
|
+
settings.env = { ...(settings.env || {}), [PLUGIN_DIRS_ENV]: entries.join(sep) };
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Drop every entry of the named mods, wherever it points. The variable goes
|
|
771
|
+
* when nothing is left in it, and `env` when this emptied it. Returns whether
|
|
772
|
+
* the object changed.
|
|
773
|
+
*/
|
|
774
|
+
function dropModDirs(settings, names) {
|
|
775
|
+
const before = pluginDirEntries(settings);
|
|
776
|
+
const kept = before.filter((e) => !isOurModEntry(e, names));
|
|
777
|
+
if (kept.length === before.length) return false;
|
|
778
|
+
if (kept.length) settings.env[PLUGIN_DIRS_ENV] = kept.join(pluginDirsSeparator());
|
|
779
|
+
else delete settings.env[PLUGIN_DIRS_ENV];
|
|
780
|
+
if (Object.keys(settings.env).length === 0) delete settings.env;
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Which mods settings.json currently loads from this package's own copy. */
|
|
785
|
+
export function modDirsState() {
|
|
786
|
+
const read = readSettings(join(claudeUserDir(), 'settings.json'));
|
|
787
|
+
const entries = read.state === 'ok' ? pluginDirEntries(read.settings) : [];
|
|
788
|
+
return Object.fromEntries(MOD_NAMES.map((name) => [name, entries.includes(modDir(name))]));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// The delegation meter only observes (see mod/sprag-delegation-meter), so it
|
|
792
|
+
// ships on with the rest of installAll(), beside the PostToolUse hook that
|
|
793
|
+
// feeds the transcript-based ledger it is measured against. uninstallAll()
|
|
794
|
+
// takes it out with every other mod.
|
|
795
|
+
export function installDelegationMeterMod() {
|
|
796
|
+
const dir = claudeUserDir();
|
|
797
|
+
const file = join(dir, 'settings.json');
|
|
798
|
+
mkdirSync(dir, { recursive: true });
|
|
799
|
+
const read = readSettings(file);
|
|
800
|
+
if (read.state === 'unusable') return { path: file, action: 'skipped', reason: read.reason };
|
|
801
|
+
const settings = read.settings;
|
|
802
|
+
const changed = addModDir(settings, DELEGATION_METER_MOD);
|
|
803
|
+
if (typeof changed === 'string') return { path: file, action: 'skipped', reason: changed };
|
|
804
|
+
if (!changed) return { path: file, action: 'exists' };
|
|
805
|
+
const writeProblem = writeSettings(file, settings);
|
|
806
|
+
if (writeProblem) return { path: file, action: 'skipped', reason: writeProblem };
|
|
807
|
+
// The action names what happened to the file, as installDelegationGuardHook's
|
|
808
|
+
// does: one entry added to a settings.json already there is an update.
|
|
809
|
+
return { path: file, action: read.state === 'absent' ? 'created' : 'updated' };
|
|
810
|
+
}
|
|
811
|
+
|
|
695
812
|
// Registers the PreToolUse hook that converts attached documents to Markdown
|
|
696
813
|
// before the model reads them. PreToolUse rather than PostToolUse because the
|
|
697
814
|
// point is to intervene before a pptx lands in the context window; afterwards
|
|
@@ -775,14 +892,22 @@ export function installDoc2mdHook() {
|
|
|
775
892
|
}
|
|
776
893
|
}
|
|
777
894
|
|
|
778
|
-
|
|
895
|
+
// The mod answers a document Read with the converted Markdown in the same
|
|
896
|
+
// call, where the hook above can only refuse it (mod/sprag-doc2md). It rides
|
|
897
|
+
// this hook's on and off so the two never disagree about doc2md being on. An
|
|
898
|
+
// `env` this cannot extend costs only the mod: the hooks still go in.
|
|
899
|
+
const modChange = addModDir(settings, DOC2MD_MOD);
|
|
900
|
+
const addedMod = modChange === true;
|
|
901
|
+
const modNote = typeof modChange === 'string' ? { modReason: modChange } : {};
|
|
902
|
+
|
|
903
|
+
if (!hasReadHook || addedPrompt || addedWriteGuard || addedMod) {
|
|
779
904
|
{
|
|
780
905
|
const writeProblem = writeSettings(file, settings);
|
|
781
906
|
if (writeProblem) return { path: file, action: 'skipped', reason: writeProblem };
|
|
782
907
|
}
|
|
783
|
-
return { path: file, action: hasReadHook ? 'updated' : 'created' };
|
|
908
|
+
return { path: file, action: hasReadHook ? 'updated' : 'created', ...modNote };
|
|
784
909
|
}
|
|
785
|
-
return { path: file, action: 'exists' };
|
|
910
|
+
return { path: file, action: 'exists', ...modNote };
|
|
786
911
|
}
|
|
787
912
|
|
|
788
913
|
export function removeDoc2mdHook() {
|
|
@@ -805,6 +930,7 @@ export function removeDoc2mdHook() {
|
|
|
805
930
|
if (kept.length === 0) delete settings.hooks[event];
|
|
806
931
|
else settings.hooks[event] = kept;
|
|
807
932
|
}
|
|
933
|
+
if (dropModDirs(settings, [DOC2MD_MOD])) touched = true;
|
|
808
934
|
if (!touched) return { path: file, action: 'absent' };
|
|
809
935
|
{
|
|
810
936
|
const writeProblem = writeSettings(file, settings);
|
|
@@ -1022,6 +1148,10 @@ export function uninstallAll({ purge = false } = {}) {
|
|
|
1022
1148
|
}
|
|
1023
1149
|
if (isPlainObject(settings.hooks) && Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
1024
1150
|
|
|
1151
|
+
// Mods are found by their folder name, so a path left by an older install
|
|
1152
|
+
// location goes with the current one.
|
|
1153
|
+
if (dropModDirs(settings, MOD_NAMES)) result.removed.push(`env.${PLUGIN_DIRS_ENV} (mods)`);
|
|
1154
|
+
|
|
1025
1155
|
// Nothing of ours in the file means nothing to rewrite (and no backup to churn).
|
|
1026
1156
|
if (result.removed.length > 0) {
|
|
1027
1157
|
const writeProblem = writeSettings(file, settings);
|
|
@@ -1116,6 +1246,7 @@ export function installAll({ force = false } = {}) {
|
|
|
1116
1246
|
statusline: installStatusline({ force }),
|
|
1117
1247
|
sessionStartHook: installSessionStartHook(),
|
|
1118
1248
|
delegationHook: installDelegationHook(),
|
|
1249
|
+
delegationMeterMod: installDelegationMeterMod(),
|
|
1119
1250
|
briefHook: installBriefHook(),
|
|
1120
1251
|
cacheMonitorMigration: migrateLegacyCacheMonitorHook(),
|
|
1121
1252
|
legacy: removeLegacyCommand(),
|
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',
|