sprag-cli 3.68.0 → 3.69.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 +4 -4
- package/README.md +15 -13
- package/bin/cli.js +28 -1
- package/package.json +6 -2
- package/src/delegation-agents.js +13 -0
- package/src/first-run-note.js +16 -0
- package/src/formatters/statusline.js +13 -1
- package/src/formatters/table.js +33 -2
- package/src/model-rules.js +25 -4
- package/src/star-note.js +62 -0
- package/src/changelog.js +0 -70
package/README.ko.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<img alt="Sprag" src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup-light.svg" width="220">
|
|
6
6
|
</picture>
|
|
7
7
|
|
|
8
|
-
**Claude Code와 Codex
|
|
8
|
+
**Claude Code와 Codex에 붙이는 모델 위임 라우터. 조회·확인·테스트 실행은 더 싼 모델에 보내고, 아낀 금액은 상태줄에 보여 줍니다.**
|
|
9
9
|
|
|
10
10
|
[](https://www.npmjs.com/package/sprag-cli)
|
|
11
11
|
[](https://www.npmjs.com/package/sprag-cli)
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
**Sprag는 일마다 그 일을 한 번도 틀린 적 없는 가장 싼 모델에게 보냅니다.** Claude Code와 Codex는 프롬프트마다 모델을 바꿀 수 없으므로, 통하는 방법 하나를 씁니다. 메인 세션의 규칙이 조회, 상태 확인, 빌드·테스트 실행, 일괄 수정을 더 싼 모델로 고정한 서브에이전트에 넘깁니다. 규칙은 사용자의 세션 기록에서 나오고 하나씩 승인을 받으며, 위임한 실행마다 가격 차액을 원장에 적습니다. `npm i -g sprag-cli` 하나로 설정이 끝나며, 프로젝트마다 따로 설정할 것은 없습니다.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
두 가지가 함께 따라옵니다. 문서는 가벼운 Markdown으로 바뀌어 컨텍스트에 들어가고(54만 토큰짜리 발표 자료가 2.3만 토큰이 됩니다), 상태줄은 세션마다 쓴 금액과 아낀 금액을 보여 주다가 캐시·컨텍스트·사용량 한도에 문제가 생기면 경고 칩을 맨 앞에 띄웁니다.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
이름은 스프래그 클러치에서 왔습니다. 앞으로 도는 힘은 통과시키고 역회전하는 순간 잠기는 부품입니다. 기록에서 반복되는 실패는 세션 시작 때 불러오는 규칙이 되어, 에이전트는 그대로 일하고 퇴행만 걸립니다.
|
|
34
34
|
|
|
35
35
|
세션 분석은 로컬에서 실행하므로 추가 모델 호출이나 API 키가 필요하지 않습니다. Codex의 선택 기능인 예산·단가 조회는 기존 인증 정보로 사용자가 설정한 LiteLLM에 접속합니다. CLI는 Node.js 20 이상과 TOML 파서 하나를 사용합니다.
|
|
36
36
|
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<img alt="Sprag" src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup-light.svg" width="220">
|
|
6
6
|
</picture>
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**A model-delegation router for Claude Code and Codex: lookups, checks and test runs go to cheaper models, and the savings show up in your statusline.**
|
|
9
9
|
|
|
10
10
|
[](https://www.npmjs.com/package/sprag-cli)
|
|
11
11
|
[](https://www.npmjs.com/package/sprag-cli)
|
|
@@ -26,20 +26,22 @@ Same 12 requests, two terminals: **$6.68 → $1.83 (−72.6%)** with Sprag.<br>
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
**Sprag
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
**Sprag routes each piece of work to the cheapest model that has never got it
|
|
30
|
+
wrong.** Claude Code and Codex cannot switch models per prompt, so Sprag does it
|
|
31
|
+
the one way that works: rules in the main session hand lookups, status checks,
|
|
32
|
+
build and test runs and batch edits to sub-agents pinned to a cheaper model. The
|
|
33
|
+
rules come from your own session logs, you approve each one, and every delegated
|
|
34
|
+
run writes its price difference to a ledger. `npm i -g sprag-cli` sets it all
|
|
35
|
+
up; nothing to configure per project.
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
Two more things ride along. Documents reach the context as compact Markdown (a
|
|
38
|
+
540k-token deck becomes 23k tokens), and the statusline shows what each session
|
|
39
|
+
spent and saved, with a warning chip in front when the cache, the context or a
|
|
40
|
+
rate limit needs attention.
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
delegated run writes its own price difference to a ledger. Cache and rate-limit
|
|
42
|
-
trouble reaches your statusline while you can still act on it.
|
|
42
|
+
The name is the sprag clutch: forward motion passes, backspin locks. Repeated
|
|
43
|
+
failures in your logs become rules loaded at session start, so the agent keeps
|
|
44
|
+
working and only the regressions get caught.
|
|
43
45
|
|
|
44
46
|
Session analysis runs locally on the logs Claude Code and Codex already write.
|
|
45
47
|
No extra model calls or API key are needed for analysis. Optional Codex gateway
|
package/bin/cli.js
CHANGED
|
@@ -59,6 +59,7 @@ import { chipForIssues } from '../src/advice.js';
|
|
|
59
59
|
import { debug } from '../src/debug.js';
|
|
60
60
|
import { createArgs } from '../src/cli-args.js';
|
|
61
61
|
import { readUpdateChip } from '../src/update-notice.js';
|
|
62
|
+
import { readStarChip } from '../src/star-note.js';
|
|
62
63
|
import { selectAgent } from '../src/agent.js';
|
|
63
64
|
|
|
64
65
|
const selection = (() => {
|
|
@@ -940,6 +941,11 @@ async function main() {
|
|
|
940
941
|
options: { days, windowHours, windowLabel, version: PKG_VERSION },
|
|
941
942
|
// Cached-only; the background refresh it may trigger lands on a later render.
|
|
942
943
|
update: format === 'statusline' ? readUpdateChip(PKG_VERSION) : null,
|
|
944
|
+
// The totals are already in hand above; passing them spares a second read of
|
|
945
|
+
// both ledgers on every render.
|
|
946
|
+
star: format === 'statusline'
|
|
947
|
+
? readStarChip({ savedUsd: delegationSaved + (Number(doc2mdTotals?.total) || 0) })
|
|
948
|
+
: null,
|
|
943
949
|
lastActivity,
|
|
944
950
|
monthSpend: monthSpendInfo,
|
|
945
951
|
modelShare,
|
|
@@ -998,8 +1004,29 @@ async function main() {
|
|
|
998
1004
|
singleLine: hasFlag('--single-line'),
|
|
999
1005
|
});
|
|
1000
1006
|
} else {
|
|
1007
|
+
// The first screen is the pitch: what share of recent work a cheaper model
|
|
1008
|
+
// could take. The scan is local and writes only sprag's own cache file;
|
|
1009
|
+
// --project skips it because the scan covers all projects, not one.
|
|
1010
|
+
let routing = null;
|
|
1011
|
+
if (!projectFilter && process.env.CTS_NO_ROUTE_SCAN !== '1') {
|
|
1012
|
+
try {
|
|
1013
|
+
const rs = await import('../src/route-scan.js');
|
|
1014
|
+
let cache = rs.readRouteScan();
|
|
1015
|
+
if (!cache) {
|
|
1016
|
+
process.stderr.write('Scanning for delegation candidates (first run)...\n');
|
|
1017
|
+
cache = await rs.runRouteScan({ days: 14 });
|
|
1018
|
+
}
|
|
1019
|
+
routing = {
|
|
1020
|
+
days: cache.days,
|
|
1021
|
+
total: cache.totalEpisodes,
|
|
1022
|
+
easy: cache.easyEpisodes,
|
|
1023
|
+
scannedAt: cache.scannedAt,
|
|
1024
|
+
candidates: rs.openCandidates(cache).slice(0, 3),
|
|
1025
|
+
};
|
|
1026
|
+
} catch (e) { debug('report:routing', e); }
|
|
1027
|
+
}
|
|
1001
1028
|
const { formatReport } = await import('../src/formatters/table.js');
|
|
1002
|
-
output = formatReport(data, { color: tableColor() });
|
|
1029
|
+
output = formatReport({ ...data, routing }, { color: tableColor() });
|
|
1003
1030
|
}
|
|
1004
1031
|
|
|
1005
1032
|
if (format === 'statusline') {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.69.0",
|
|
4
|
+
"description": "Model-delegation router for Claude Code and Codex: lookups, checks and test runs go to cheaper models, documents go in as compact Markdown, and a live statusline shows spend and savings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sprag": "bin/cli.js",
|
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
"claude-code-statusline",
|
|
50
50
|
"statusline",
|
|
51
51
|
"model-routing",
|
|
52
|
+
"model-router",
|
|
53
|
+
"model-delegation",
|
|
54
|
+
"llm-router",
|
|
55
|
+
"claude-code-router",
|
|
52
56
|
"subagent",
|
|
53
57
|
"prompt-caching",
|
|
54
58
|
"haiku",
|
package/src/delegation-agents.js
CHANGED
|
@@ -22,10 +22,19 @@ const MARK_LINE = `# managed by sprag (${OWNER_MARK}); edits are overwritten`;
|
|
|
22
22
|
|
|
23
23
|
// Models are aliases on purpose: behind a gateway they resolve through
|
|
24
24
|
// ANTHROPIC_DEFAULT_*_MODEL, and a full model id would bypass that mapping.
|
|
25
|
+
//
|
|
26
|
+
// Effort is a separate axis from the tier and is set by how the work gets
|
|
27
|
+
// verified, not by how hard it is. Where the check is command output or a file
|
|
28
|
+
// comparison (lookups, test runs, mechanical conversion) extra thinking buys
|
|
29
|
+
// nothing, so those agents run at low. The worker implements a design the
|
|
30
|
+
// caller already fixed, against tests, so it takes the default for well-specified
|
|
31
|
+
// work, medium. The main session's effort is never touched here, and
|
|
32
|
+
// CLAUDE_CODE_EFFORT_LEVEL must stay unset: it overrides every agent's own value.
|
|
25
33
|
export const DEFINITIONS = [
|
|
26
34
|
{
|
|
27
35
|
name: 'haiku-explore',
|
|
28
36
|
model: 'haiku',
|
|
37
|
+
effort: 'low',
|
|
29
38
|
tools: 'Read, Grep, Glob, Bash',
|
|
30
39
|
role: 'Your role: read-only exploration, lookups, status checks, and questions about pasted logs or text.',
|
|
31
40
|
description: 'Cheap read-only exploration, lookup, status checks and summaries for delegated tasks. It has no MCP tools.',
|
|
@@ -33,6 +42,7 @@ export const DEFINITIONS = [
|
|
|
33
42
|
{
|
|
34
43
|
name: 'haiku-runner',
|
|
35
44
|
model: 'haiku',
|
|
45
|
+
effort: 'low',
|
|
36
46
|
tools: 'Bash, Read, Grep, Glob',
|
|
37
47
|
role: 'Your role: run the build, test, or git commands you are given and report the result.',
|
|
38
48
|
description: 'Cheap runner for build, test and git commands in delegated tasks. It has no MCP tools.',
|
|
@@ -40,6 +50,7 @@ export const DEFINITIONS = [
|
|
|
40
50
|
{
|
|
41
51
|
name: 'haiku-translate',
|
|
42
52
|
model: 'haiku',
|
|
53
|
+
effort: 'low',
|
|
43
54
|
tools: 'Read, Grep, Glob, Edit, Write',
|
|
44
55
|
role: 'Your role: batch translation and mechanical text conversion of the files or text you are given.',
|
|
45
56
|
description: 'Cheap batch translation and structured text conversion for delegated tasks. It has no MCP tools.',
|
|
@@ -47,6 +58,7 @@ export const DEFINITIONS = [
|
|
|
47
58
|
{
|
|
48
59
|
name: 'sonnet-worker',
|
|
49
60
|
model: 'sonnet',
|
|
61
|
+
effort: 'medium',
|
|
50
62
|
tools: 'Read, Grep, Glob, Bash, Edit, Write, NotebookEdit',
|
|
51
63
|
role: 'Your role: multi-step work and the implementation half of a design the caller already fixed.',
|
|
52
64
|
description: 'Mid-tier worker for multi-step tasks and implementation handed over with a fixed design. It has no MCP tools.',
|
|
@@ -60,6 +72,7 @@ function render(def) {
|
|
|
60
72
|
`description: ${def.description}`,
|
|
61
73
|
`tools: ${def.tools}`,
|
|
62
74
|
`model: ${def.model}`,
|
|
75
|
+
`effort: ${def.effort}`,
|
|
63
76
|
MARK_LINE,
|
|
64
77
|
'---',
|
|
65
78
|
def.role,
|
package/src/first-run-note.js
CHANGED
|
@@ -34,6 +34,22 @@ function markShown(key, now) {
|
|
|
34
34
|
writeFileSync(firstRunStatePath(), JSON.stringify(state) + '\n');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** When `key` was marked shown (epoch ms), or null. For notes that stay
|
|
38
|
+
* visible for a window after the first time instead of printing once. */
|
|
39
|
+
export function noteShownAt(key) {
|
|
40
|
+
const at = Number(load()[key]);
|
|
41
|
+
return Number.isFinite(at) && at > 0 ? at : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Persist `key` as shown at `now`. Best-effort, like `shouldShowOnce`. */
|
|
45
|
+
export function markNoteShown(key, now = Date.now()) {
|
|
46
|
+
try {
|
|
47
|
+
markShown(key, now);
|
|
48
|
+
} catch {
|
|
49
|
+
/* state dir unwritable — the note may show again, never a failure */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
/** True only the first time this key is asked about. Best-effort — a
|
|
38
54
|
* read-only state dir just means the note repeats, never that we crash. */
|
|
39
55
|
export function shouldShowOnce(key, { now = Date.now() } = {}) {
|
|
@@ -28,6 +28,7 @@ import { MIN_VOTES } from '../model-alias.js';
|
|
|
28
28
|
import { bedrockDisplayFromId } from '../stdin-payload.js';
|
|
29
29
|
import { modelFamily } from '../savings-ledger.js';
|
|
30
30
|
import { shareLegend } from '../model-share.js';
|
|
31
|
+
import { STAR_URL } from '../star-note.js';
|
|
31
32
|
|
|
32
33
|
// The 8-color ANSI defaults (RED=31, GREEN=32, YELLOW=33…) read as garish
|
|
33
34
|
// next to each other — terminal palettes set them with unbalanced perceptual
|
|
@@ -1032,6 +1033,13 @@ function buildDelegatedSeg(data, singleLine, c, isIcon, verbose, g, color) {
|
|
|
1032
1033
|
};
|
|
1033
1034
|
}
|
|
1034
1035
|
|
|
1036
|
+
/** The one-time star ask (see star-note.js); null unless the CLI passed a chip. */
|
|
1037
|
+
function buildStarSeg(star, c, isIcon) {
|
|
1038
|
+
if (!star || typeof star !== 'object' || !Number.isFinite(star.usd)) return null;
|
|
1039
|
+
const text = `${isIcon ? '⭐ ' : ''}${formatMoney(star.usd)} saved · star ${STAR_URL}`;
|
|
1040
|
+
return `${c(YELLOW)}${text}${c(RESET)}`;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1035
1043
|
/**
|
|
1036
1044
|
* Doc2md chip and headline line. As with routing, they are alternatives: money
|
|
1037
1045
|
* earns the headline line, and the chip is for what a line would be all frame
|
|
@@ -1142,6 +1150,8 @@ export const DEFAULT_SEGMENT_ORDER = [
|
|
|
1142
1150
|
// Lifetime, from the savings ledgers. Delegation stays ahead of the cache
|
|
1143
1151
|
// figure: routing is what the tool is for, the cache number is a brag stat.
|
|
1144
1152
|
'delegated', 'doc2md',
|
|
1153
|
+
// One-time star ask; renders only while readStarChip returns an object.
|
|
1154
|
+
'star',
|
|
1145
1155
|
// Analysis window. These two are the only chips measured over it, so the label
|
|
1146
1156
|
// sits with them and the three render as one group.
|
|
1147
1157
|
'hit', 'saved', 'period',
|
|
@@ -1182,6 +1192,7 @@ function segmentsNamed(name, s, usageByKey) {
|
|
|
1182
1192
|
// the session's own subagent runs.
|
|
1183
1193
|
case 'delegated': return [...s.delegated, s.subagents];
|
|
1184
1194
|
case 'doc2md': return s.doc2md;
|
|
1195
|
+
case 'star': return [s.star];
|
|
1185
1196
|
case 'hit': return [s.hit];
|
|
1186
1197
|
case 'ttl': return [s.ttl];
|
|
1187
1198
|
case 'month': return [s.month];
|
|
@@ -1287,7 +1298,7 @@ function buildHeadlines(delegated, doc2md, want, eol) {
|
|
|
1287
1298
|
* @param {boolean} [opts.verbose=false] - longer layout with labels
|
|
1288
1299
|
* @param {boolean} [opts.timer=true] - show TTL countdown segment
|
|
1289
1300
|
* @param {'text'|'icon'} [opts.mode='text'] - label style. 'icon' uses 🧠 ⏳ 💰 instead of word labels.
|
|
1290
|
-
* @param {string[]|null} [opts.segments] - segments to render, in the order given. Names: cap-warn, spike, version, harness (a full score with no warning draws only when named here), korean (draws only when named here), model, effort, hit, ttl, month, saved, delegated, doc2md, ctx, period, `subagents` (this session's subagent runs; also rides with `delegated`), `rules` (only when named here), `usage` (every rate-limit window), plus per-window keys (`five_hour`, `seven_day`, …). `5h`/`7d` are kept as aliases for back-compat. cap-warn and spike keep the lead regardless of where they appear. Null/undefined = all, in the default order.
|
|
1301
|
+
* @param {string[]|null} [opts.segments] - segments to render, in the order given. Names: cap-warn, spike, version, harness (a full score with no warning draws only when named here), korean (draws only when named here), model, effort, hit, ttl, month, saved, delegated, doc2md, star, ctx, period, `subagents` (this session's subagent runs; also rides with `delegated`), `rules` (only when named here), `usage` (every rate-limit window), plus per-window keys (`five_hour`, `seven_day`, …). `5h`/`7d` are kept as aliases for back-compat. cap-warn and spike keep the lead regardless of where they appear. Null/undefined = all, in the default order.
|
|
1291
1302
|
* @param {boolean} [opts.singleLine=false] - force the legacy one-line layout. By default, when the delegation ledger has lifetime savings, the routing totals lead on their own first line and everything else moves to line 2 (Claude Code renders multi-line statuslines; `--single-line` is the escape hatch for terminals that only show the first line).
|
|
1292
1303
|
*/
|
|
1293
1304
|
export function formatReport(data, { color = true, verbose = false, timer = true, mode = 'text', segments = null, singleLine = false } = {}) {
|
|
@@ -1344,6 +1355,7 @@ export function formatReport(data, { color = true, verbose = false, timer = true
|
|
|
1344
1355
|
delegated: delegated.chips,
|
|
1345
1356
|
// The inline chip steps aside when a headline line owns the story.
|
|
1346
1357
|
doc2md: doc2md.headline ? [] : [doc2md.chip],
|
|
1358
|
+
star: buildStarSeg(data.star, c, isIcon),
|
|
1347
1359
|
// This session's subagent runs by model family. It is a session figure, so
|
|
1348
1360
|
// it sits with the routing chips rather than with the lifetime totals.
|
|
1349
1361
|
subagents: buildSessionDelegationSeg(data.sessionDelegation, c, isIcon, verbose, g),
|
package/src/formatters/table.js
CHANGED
|
@@ -181,17 +181,48 @@ function renderCapWarnSection(caps) {
|
|
|
181
181
|
return lines;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
// Opens the report with what the tool is for, before any warning. A first run
|
|
185
|
+
// (`npx sprag-cli`) has nothing installed yet, so the pitch has to be on screen.
|
|
186
|
+
function renderRoutingSection(routing) {
|
|
187
|
+
if (!routing || typeof routing !== 'object') return [];
|
|
188
|
+
const lines = [];
|
|
189
|
+
lines.push(b(` 🔀 Routing — last ${routing.days}d`));
|
|
190
|
+
lines.push(` ${'─'.repeat(50)}`);
|
|
191
|
+
const total = Number(routing.total) || 0;
|
|
192
|
+
if (total === 0) {
|
|
193
|
+
lines.push(` No Claude Code sessions in the last ${routing.days} days to scan.`);
|
|
194
|
+
lines.push('');
|
|
195
|
+
return lines;
|
|
196
|
+
}
|
|
197
|
+
const easy = Number(routing.easy) || 0;
|
|
198
|
+
const pct = Math.round(easy / total * 100);
|
|
199
|
+
lines.push(` ${easy} of ${total} requests (${pct}%) were lookups, checks or test runs a cheaper model handles.`);
|
|
200
|
+
const cands = Array.isArray(routing.candidates) ? routing.candidates : [];
|
|
201
|
+
if (cands.length === 0) {
|
|
202
|
+
lines.push(' No recurring delegable work yet (a pattern needs 3 repeats). Approved rules apply from the next session.');
|
|
203
|
+
} else {
|
|
204
|
+
for (const c of cands) {
|
|
205
|
+
lines.push(` R${c.id} "${c.labelEn || c.label}" ×${c.count} → delegate to ${c.agent}`);
|
|
206
|
+
}
|
|
207
|
+
lines.push(` Approve one: ${b(`${CLI_NAME} harness promote R${cands[0].id} --global`)} All candidates: ${b(`${CLI_NAME} route-scan`)}`);
|
|
208
|
+
}
|
|
209
|
+
lines.push('');
|
|
210
|
+
return lines;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function formatReport({ summary: sum, trend, ttl, anomalies, cost, options, spikeReport, contextWindow, caps, routing }, { color } = {}) {
|
|
185
214
|
colorOk = color ?? !process.env.NO_COLOR;
|
|
186
215
|
const lines = [];
|
|
187
216
|
|
|
188
217
|
// Header
|
|
189
218
|
lines.push('');
|
|
190
|
-
lines.push(`
|
|
219
|
+
lines.push(` Sprag — Last ${options.days} day${options.days === 1 ? '' : 's'}`);
|
|
191
220
|
lines.push(` (${CLI_NAME} v${options.version || ''})`.trimEnd());
|
|
192
221
|
lines.push(` ${'═'.repeat(50)}`);
|
|
193
222
|
lines.push('');
|
|
194
223
|
|
|
224
|
+
lines.push(...renderRoutingSection(routing));
|
|
225
|
+
|
|
195
226
|
// Cap warning leads — it's the most time-sensitive signal we can show.
|
|
196
227
|
if (caps) {
|
|
197
228
|
lines.push(...renderCapWarnSection(caps));
|
package/src/model-rules.js
CHANGED
|
@@ -272,13 +272,23 @@ export function renderModelRatchet(rules, lang = userLanguage(), { root } = {})
|
|
|
272
272
|
' 알려 주는 작업은 하위 티어가 안전하다. 검증 게이트가 품질 하한이 된다.',
|
|
273
273
|
' 틀려도 조용한 작업(진단, 근본 원인 판정, 보안, 설계)은 티어를 내리지',
|
|
274
274
|
' 않는다. 싼 모델은 결정을 옮겨 적을 뿐, 결정을 내리지 못한다.',
|
|
275
|
+
'- **effort**: 모델 티어와 다른 축이다. 설치된 서브에이전트는 검증 형태에 따라',
|
|
276
|
+
' effort가 고정되어 있다. 명령 출력이나 파일 대조로 검증되는 위임(조회·명령',
|
|
277
|
+
' 실행·정형 변환을 맡는 haiku 에이전트)은 low, 확정된 설계를 테스트에 맞춰',
|
|
278
|
+
' 구현하는 위임(sonnet 워커)은 medium이다. 메인 세션의 effort는 바꾸지 않고,',
|
|
279
|
+
' CLAUDE_CODE_EFFORT_LEVEL은 서브에이전트의 effort를 덮어쓰므로 설정하지 않는다.',
|
|
275
280
|
'- **위임 배치**: 서브에이전트 비용의 지배 항은 생성이 아니라 컨텍스트',
|
|
276
281
|
' 적재다. 같은 파일들을 읽어야 하는 작업 여러 개는 에이전트 하나에 묶어',
|
|
277
282
|
' 보내고, 읽는 영역이 다른 작업만 나눈다.',
|
|
278
283
|
implementationDelegationRule('claude', lang),
|
|
279
284
|
'- **에스컬레이션**: 같은 에러 2회 반복, 답이 복수 후보로 갈림, 서브에이전트가',
|
|
280
|
-
' 질문을 멈추고 헛도는 징후가 보이면 즉시 인계한다.
|
|
281
|
-
'
|
|
285
|
+
' 질문을 멈추고 헛도는 징후가 보이면 즉시 인계한다. 어느 축을 올릴지는 실패',
|
|
286
|
+
' 종류로 정한다. 파일을 읽지 않았거나 테스트를 돌리지 않았거나 상한에 걸려',
|
|
287
|
+
' 멈춘 누락형 실패는 같은 모델에서 effort를 한 단계 올려 한 번만 재시도하고,',
|
|
288
|
+
' 컨텍스트를 다 보고도 틀린 오판형 실패는 모델을 한 티어 올린다. 순서는',
|
|
289
|
+
' ① 재지시 후 재시도(누락형이면 effort 한 단계 위) → ② 한 티어 상향(오판형)',
|
|
290
|
+
' → ③ 메인 모델이 직접 수습. effort를 올릴 때는 에이전트 파일을 고치지 않고',
|
|
291
|
+
' Agent 도구 호출의 effort 인자에 한 단계 위 값(low → medium → high)을 넘긴다.',
|
|
282
292
|
'',
|
|
283
293
|
'## Rules',
|
|
284
294
|
'',
|
|
@@ -323,14 +333,25 @@ export function renderModelRatchet(rules, lang = userLanguage(), { root } = {})
|
|
|
323
333
|
' the verification gate is the quality floor. Never drop the tier on work',
|
|
324
334
|
' that fails silently (diagnosis, root-cause calls, security posture,',
|
|
325
335
|
' design). Cheap models transcribe decisions; they do not make them.',
|
|
336
|
+
'- **Effort**: a separate axis from the tier. The installed subagents pin it',
|
|
337
|
+
' by how their work is verified: low where the check is command output or a',
|
|
338
|
+
' file comparison (the haiku agents for lookups, command runs and mechanical',
|
|
339
|
+
' conversion), medium where a fixed design is implemented against tests (the',
|
|
340
|
+
' sonnet worker). Leave the main session\'s effort alone, and never set',
|
|
341
|
+
' CLAUDE_CODE_EFFORT_LEVEL: it overrides every subagent\'s own value.',
|
|
326
342
|
'- **Batching**: a subagent\'s dominant cost is context ingestion, not',
|
|
327
343
|
' generation. Bundle tasks that read the same files into one agent; split',
|
|
328
344
|
' only when the areas they read differ.',
|
|
329
345
|
implementationDelegationRule('claude', lang),
|
|
330
346
|
'- **Escalation**: hand off immediately on the same error twice, an answer',
|
|
331
347
|
' splitting into multiple candidates, or a subagent spinning without asking',
|
|
332
|
-
' questions.
|
|
333
|
-
'
|
|
348
|
+
' questions. Which axis moves depends on the failure: an omission (a file',
|
|
349
|
+
' not read, tests not run, a cap hit mid-task) retries once on the same',
|
|
350
|
+
' model one effort step up; a wrong answer with the full context in hand',
|
|
351
|
+
' moves up one tier. Order: ① re-instruct and retry (effort +1 on an',
|
|
352
|
+
' omission) → ② move up one tier (on a wrong answer) → ③ the main model',
|
|
353
|
+
' takes over directly. To raise effort, pass the next level (low → medium →',
|
|
354
|
+
' high) in the Agent call\'s effort argument instead of editing the agent file.',
|
|
334
355
|
'',
|
|
335
356
|
'## Rules',
|
|
336
357
|
'',
|
package/src/star-note.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* star-note — a one-time "star the repo" ask in the statusline.
|
|
3
|
+
*
|
|
4
|
+
* The package is downloaded thousands of times a month and starred a handful
|
|
5
|
+
* of times: all of the value lands in the statusline, so users never see the
|
|
6
|
+
* repository. This asks once, at the moment the value is visible — when the
|
|
7
|
+
* savings sprag itself produced (routing + doc2md) first reach $10.
|
|
8
|
+
*
|
|
9
|
+
* Once, because a chip that keeps asking is an advertisement. For 24 hours,
|
|
10
|
+
* which is long enough to be seen across a day of sessions and short enough
|
|
11
|
+
* not to linger. CTS_NO_NOTE=1 suppresses it, the same opt-out as every
|
|
12
|
+
* other one-time note.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { delegationSavedTotals } from './savings-ledger.js';
|
|
17
|
+
import { noteShownAt, markNoteShown } from './first-run-note.js';
|
|
18
|
+
import { userDataDir } from './paths.js';
|
|
19
|
+
import { debug } from './debug.js';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
|
|
23
|
+
export const STAR_THRESHOLD_USD = 10;
|
|
24
|
+
export const STAR_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
export const STAR_URL = 'github.com/rootstudioyaml/sprag';
|
|
26
|
+
|
|
27
|
+
/** Lifetime dollars saved by routing and doc2md; an unreadable ledger counts as 0. */
|
|
28
|
+
export function toolSavedUsd(now = Date.now(), dir = userDataDir()) {
|
|
29
|
+
let total = 0;
|
|
30
|
+
try {
|
|
31
|
+
total += Number(delegationSavedTotals(now, dir).total) || 0;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
debug('star-note:routing', e);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
total += Number(require('./doc2md-ledger.cjs').doc2mdSavedTotals(dir).total) || 0;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
debug('star-note:doc2md', e);
|
|
39
|
+
}
|
|
40
|
+
return total;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `{ usd, shownAt }` while the chip should show, else null. Never throws. */
|
|
44
|
+
export function readStarChip({ now = Date.now(), dir = userDataDir(), savedUsd } = {}) {
|
|
45
|
+
try {
|
|
46
|
+
if (process.env.CTS_NO_NOTE === '1') return null;
|
|
47
|
+
const shownAt = noteShownAt('star');
|
|
48
|
+
if (shownAt === null) {
|
|
49
|
+
const usd = savedUsd ?? toolSavedUsd(now, dir);
|
|
50
|
+
if (!(usd >= STAR_THRESHOLD_USD)) return null;
|
|
51
|
+
markNoteShown('star', now);
|
|
52
|
+
return { usd, shownAt: now };
|
|
53
|
+
}
|
|
54
|
+
if (now - shownAt < STAR_WINDOW_MS) {
|
|
55
|
+
return { usd: savedUsd ?? toolSavedUsd(now, dir), shownAt };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
debug('star-note', e);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/changelog.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* changelog — read one version's section out of CHANGELOG.md, and say whether a
|
|
3
|
-
* notes draft is still waiting on a person.
|
|
4
|
-
*
|
|
5
|
-
* Separate from scripts/release-notes.mjs, which is a CLI: importing that file
|
|
6
|
-
* runs its argument parsing and exits, so the extraction it depends on cannot be
|
|
7
|
-
* tested through it. The boundary logic is the part worth pinning — a section
|
|
8
|
-
* pulled one line short drops the release's last item, and one line long carries
|
|
9
|
-
* the previous release's first, and the upgrade offer shows either without
|
|
10
|
-
* complaint.
|
|
11
|
-
*
|
|
12
|
-
* The draft marker lives here for a reason. Two scripts gate on it — the one that
|
|
13
|
-
* writes drafts and the one that deploys — and they used to carry the string
|
|
14
|
-
* separately. Renaming it in one left the other looking for text that no longer
|
|
15
|
-
* existed, and a gate that greps for an absent string does not fail: it passes,
|
|
16
|
-
* and an unreviewed draft goes out as the release. One definition, imported.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The body of one version's section, heading excluded.
|
|
21
|
-
*
|
|
22
|
-
* @param {string} changelog - CHANGELOG.md contents
|
|
23
|
-
* @param {string} version - e.g. "3.45.0"
|
|
24
|
-
* @returns {string|null} the section text, or null when that version has none
|
|
25
|
-
*/
|
|
26
|
-
export function sectionFor(changelog, version) {
|
|
27
|
-
const lines = String(changelog).split(/\r?\n/);
|
|
28
|
-
// The version has to match to its end: without the boundary, "3.42.0" would
|
|
29
|
-
// also match the "### v3.42.7" heading and publish the wrong notes.
|
|
30
|
-
const head = new RegExp(`^### v${String(version).replace(/\./g, '\\.')}(\\s|$|\\()`);
|
|
31
|
-
const start = lines.findIndex((l) => head.test(l));
|
|
32
|
-
if (start < 0) return null;
|
|
33
|
-
const out = [];
|
|
34
|
-
for (let i = start + 1; i < lines.length; i += 1) {
|
|
35
|
-
// The next version heading ends this section, and so does a shallower one:
|
|
36
|
-
// "## All releases" is a container, not part of any release.
|
|
37
|
-
if (/^### v/.test(lines[i]) || /^## /.test(lines[i])) break;
|
|
38
|
-
out.push(lines[i]);
|
|
39
|
-
}
|
|
40
|
-
return out.join('\n').trim();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The line `release-notes.mjs` writes at the top of a fresh draft, and the line
|
|
45
|
-
* every gate refuses on. A draft still carrying it has not been through a person.
|
|
46
|
-
*/
|
|
47
|
-
export const DRAFT_MARKER =
|
|
48
|
-
'<!-- REVIEW: edit this draft, add the English half, then delete this line -->';
|
|
49
|
-
|
|
50
|
-
/** Heading that opens the Korean half of a bilingual release body. */
|
|
51
|
-
export const KO_HEADING = '## 한국어';
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* The opening of the marker, which is what the gate actually matches. Spelled out
|
|
55
|
-
* rather than sliced off `DRAFT_MARKER`, so that editing the marker's front does
|
|
56
|
-
* not silently change what is being tested for.
|
|
57
|
-
*/
|
|
58
|
-
const REVIEW_OPENING = '<!-- REVIEW:';
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Whether a draft still needs review. Matches on the `<!-- REVIEW:` opening
|
|
62
|
-
* rather than the whole line, so editing the wording of the instruction does not
|
|
63
|
-
* quietly disarm the gate.
|
|
64
|
-
*
|
|
65
|
-
* @param {string} draft - the draft file's contents
|
|
66
|
-
* @returns {boolean} true while the draft is unreviewed
|
|
67
|
-
*/
|
|
68
|
-
export function needsReview(draft) {
|
|
69
|
-
return String(draft || '').includes(REVIEW_OPENING);
|
|
70
|
-
}
|