sprag-cli 3.72.0 → 3.73.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/package.json +1 -1
- package/presets/agents.json +27 -1
- package/presets/model-rules.json +2 -11
- package/presets/ratchet-rules.json +4 -10
- package/src/agents.js +29 -0
- package/src/commands/presets.js +2 -1
- package/src/commands/route-scan.js +5 -4
- package/src/formatters/table.js +2 -1
- package/src/model-rules.js +20 -16
- package/src/preset-sign.js +4 -1
- package/src/presets-source.js +21 -5
- package/src/route-inject.js +10 -6
- package/src/route-scan.js +7 -17
- package/src/seed-rules.js +26 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.73.0",
|
|
4
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": {
|
package/presets/agents.json
CHANGED
|
@@ -9,13 +9,23 @@
|
|
|
9
9
|
"Effort follows how the work is verified: low for lookups and command runs, medium for",
|
|
10
10
|
"implementation against tests, high for the larger opus share.",
|
|
11
11
|
"The files are rendered in English (description.en, body.en): they are model-facing",
|
|
12
|
-
"prompts, and the Korean copy is what the /ops console shows next to it."
|
|
12
|
+
"prompts, and the Korean copy is what the /ops console shows next to it.",
|
|
13
|
+
"tier and categories say which model-routing rules an agent serves (agentForRule in",
|
|
14
|
+
"src/agents.js): a rule names only its category and tier, and \"*\" covers every category.",
|
|
15
|
+
"They are not written into the rendered files; Claude Code does not know those keys."
|
|
13
16
|
],
|
|
14
17
|
"agents": [
|
|
15
18
|
{
|
|
16
19
|
"name": "haiku-explore",
|
|
17
20
|
"model": "haiku",
|
|
18
21
|
"effort": "low",
|
|
22
|
+
"tier": "T2",
|
|
23
|
+
"categories": [
|
|
24
|
+
"explore",
|
|
25
|
+
"read",
|
|
26
|
+
"check",
|
|
27
|
+
"paste"
|
|
28
|
+
],
|
|
19
29
|
"tools": [
|
|
20
30
|
"Read",
|
|
21
31
|
"Grep",
|
|
@@ -35,6 +45,10 @@
|
|
|
35
45
|
"name": "haiku-runner",
|
|
36
46
|
"model": "haiku",
|
|
37
47
|
"effort": "low",
|
|
48
|
+
"tier": "T2",
|
|
49
|
+
"categories": [
|
|
50
|
+
"run"
|
|
51
|
+
],
|
|
38
52
|
"tools": [
|
|
39
53
|
"Bash",
|
|
40
54
|
"Read",
|
|
@@ -54,6 +68,10 @@
|
|
|
54
68
|
"name": "haiku-translate",
|
|
55
69
|
"model": "haiku",
|
|
56
70
|
"effort": "low",
|
|
71
|
+
"tier": "T2",
|
|
72
|
+
"categories": [
|
|
73
|
+
"translate"
|
|
74
|
+
],
|
|
57
75
|
"tools": [
|
|
58
76
|
"Read",
|
|
59
77
|
"Grep",
|
|
@@ -74,6 +92,10 @@
|
|
|
74
92
|
"name": "sonnet-worker",
|
|
75
93
|
"model": "sonnet",
|
|
76
94
|
"effort": "medium",
|
|
95
|
+
"tier": "T1",
|
|
96
|
+
"categories": [
|
|
97
|
+
"*"
|
|
98
|
+
],
|
|
77
99
|
"tools": [
|
|
78
100
|
"Read",
|
|
79
101
|
"Grep",
|
|
@@ -96,6 +118,10 @@
|
|
|
96
118
|
"name": "opus-worker",
|
|
97
119
|
"model": "claude-opus-5-5",
|
|
98
120
|
"effort": "high",
|
|
121
|
+
"tier": "T1H",
|
|
122
|
+
"categories": [
|
|
123
|
+
"*"
|
|
124
|
+
],
|
|
99
125
|
"tools": [
|
|
100
126
|
"Read",
|
|
101
127
|
"Grep",
|
package/presets/model-rules.json
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
"counterpart to presets/ratchet-rules.md. These were promoted from the author's",
|
|
5
5
|
"own route-scan candidates over months of real sessions, then stripped of the",
|
|
6
6
|
"originating examples, project paths and recurrence counts: what carries over is",
|
|
7
|
-
"the shape of the work (category, tier,
|
|
7
|
+
"the shape of the work (category, tier, cap), not the author's transcripts. Which",
|
|
8
|
+
"agent serves a tier is said by presets/agents.json (tier, categories).",
|
|
8
9
|
"Each entry is offered one at a time at session start; nothing is registered",
|
|
9
10
|
"without the user saying yes. Caps are the defaults a scan would calibrate to;",
|
|
10
11
|
"later scans refresh the stats on whatever the user accepts.",
|
|
@@ -18,7 +19,6 @@
|
|
|
18
19
|
"category": "run",
|
|
19
20
|
"label": "명령 실행 (빌드·테스트·git)",
|
|
20
21
|
"labelEn": "running commands (build/test/git)",
|
|
21
|
-
"agent": "haiku-runner",
|
|
22
22
|
"example": "npm test 돌려봐",
|
|
23
23
|
"exampleEn": "run npm test",
|
|
24
24
|
"budget": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"category": "run",
|
|
33
33
|
"label": "명령 실행 (빌드·테스트·git)",
|
|
34
34
|
"labelEn": "running commands (build/test/git)",
|
|
35
|
-
"agent": "sonnet",
|
|
36
35
|
"example": "빌드하고 실패한 테스트만 보여줘",
|
|
37
36
|
"exampleEn": "build it and show only the failing tests",
|
|
38
37
|
"budget": {
|
|
@@ -46,7 +45,6 @@
|
|
|
46
45
|
"category": "explore",
|
|
47
46
|
"label": "탐색·조회 (파일/값 찾기)",
|
|
48
47
|
"labelEn": "lookup (finding files/values)",
|
|
49
|
-
"agent": "haiku-explore",
|
|
50
48
|
"example": "이 설정값 어느 파일에 있어?",
|
|
51
49
|
"exampleEn": "which file holds this config value?",
|
|
52
50
|
"budget": {
|
|
@@ -60,7 +58,6 @@
|
|
|
60
58
|
"category": "explore",
|
|
61
59
|
"label": "탐색·조회 (파일/값 찾기)",
|
|
62
60
|
"labelEn": "lookup (finding files/values)",
|
|
63
|
-
"agent": "sonnet",
|
|
64
61
|
"example": "이 함수를 호출하는 곳 전부 찾아줘",
|
|
65
62
|
"exampleEn": "find every caller of this function",
|
|
66
63
|
"budget": {
|
|
@@ -74,7 +71,6 @@
|
|
|
74
71
|
"category": "check",
|
|
75
72
|
"label": "상태 확인·검증",
|
|
76
73
|
"labelEn": "status checks / verification",
|
|
77
|
-
"agent": "haiku-explore",
|
|
78
74
|
"example": "지금 배포된 버전이 뭔지 확인해줘",
|
|
79
75
|
"exampleEn": "check which version is deployed right now",
|
|
80
76
|
"budget": {
|
|
@@ -88,7 +84,6 @@
|
|
|
88
84
|
"category": "check",
|
|
89
85
|
"label": "상태 확인·검증",
|
|
90
86
|
"labelEn": "status checks / verification",
|
|
91
|
-
"agent": "sonnet",
|
|
92
87
|
"example": "마이그레이션이 실제로 적용됐는지 확인해줘",
|
|
93
88
|
"exampleEn": "verify the migration actually applied",
|
|
94
89
|
"budget": {
|
|
@@ -102,7 +97,6 @@
|
|
|
102
97
|
"category": "paste",
|
|
103
98
|
"label": "붙여넣은 화면·로그 질문",
|
|
104
99
|
"labelEn": "questions about pasted screens/logs",
|
|
105
|
-
"agent": "haiku-explore",
|
|
106
100
|
"example": "이 에러 로그는 무슨 뜻이야?",
|
|
107
101
|
"exampleEn": "what does this error log mean?",
|
|
108
102
|
"budget": {
|
|
@@ -116,7 +110,6 @@
|
|
|
116
110
|
"category": "paste",
|
|
117
111
|
"label": "붙여넣은 화면·로그 질문",
|
|
118
112
|
"labelEn": "questions about pasted screens/logs",
|
|
119
|
-
"agent": "sonnet",
|
|
120
113
|
"example": "이 스택트레이스에서 어느 모듈이 죽은 건지 짚어줘",
|
|
121
114
|
"exampleEn": "point out which module failed in this stack trace",
|
|
122
115
|
"budget": {
|
|
@@ -130,7 +123,6 @@
|
|
|
130
123
|
"category": "read",
|
|
131
124
|
"label": "읽기·요약·설명",
|
|
132
125
|
"labelEn": "reading / summarizing / explaining",
|
|
133
|
-
"agent": "haiku-explore",
|
|
134
126
|
"example": "이 README 요약해줘",
|
|
135
127
|
"exampleEn": "summarize this README",
|
|
136
128
|
"budget": {
|
|
@@ -144,7 +136,6 @@
|
|
|
144
136
|
"category": "read",
|
|
145
137
|
"label": "읽기·요약·설명",
|
|
146
138
|
"labelEn": "reading / summarizing / explaining",
|
|
147
|
-
"agent": "sonnet",
|
|
148
139
|
"example": "이 함수가 하는 일을 단계별로 정리해줘",
|
|
149
140
|
"exampleEn": "walk through what this function does, step by step",
|
|
150
141
|
"budget": {
|
|
@@ -39,11 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"id": "fix-f558f9",
|
|
42
|
-
"ko": "탐색·조회·상태 확인·명령 실행 같은 단순 요청은
|
|
43
|
-
"en": "Delegate simple requests — lookup, status checks, running commands — to
|
|
44
|
-
"agents": [
|
|
45
|
-
"claude"
|
|
46
|
-
]
|
|
42
|
+
"ko": "탐색·조회·상태 확인·명령 실행 같은 단순 요청은 T2 티어에 위임해 상위 모델 토큰을 아낀다. 티어는 도구가 해석한다. Claude는 그 카테고리의 T2 에이전트 파일을, 없으면 `model: haiku`를 쓰고 Codex는 T2에 매핑된 모델을 쓴다",
|
|
43
|
+
"en": "Delegate simple requests — lookup, status checks, running commands — to the T2 tier to keep the expensive model's tokens for real work. Each tool resolves the tier: Claude uses that category's T2 agent file or `model: haiku`, Codex the model mapped to T2"
|
|
47
44
|
},
|
|
48
45
|
{
|
|
49
46
|
"id": "fix-aa18bc",
|
|
@@ -52,11 +49,8 @@
|
|
|
52
49
|
},
|
|
53
50
|
{
|
|
54
51
|
"id": "fix-777a15",
|
|
55
|
-
"ko": "여러 파일에 걸친 구현을 맡으면 → 설계와 스펙 확정은 메인 모델이 하고 코드 작성은
|
|
56
|
-
"en": "When an implementation spans several files → the main model settles the design and specification
|
|
57
|
-
"agents": [
|
|
58
|
-
"claude"
|
|
59
|
-
]
|
|
52
|
+
"ko": "여러 파일에 걸친 구현을 맡으면 → 설계와 스펙 확정은 메인 모델이 하고 코드 작성은 T1 티어에 위임하며, T2에는 구현을 맡기지 않는다. 티어는 도구가 해석한다. Claude는 그 카테고리의 T1 에이전트 파일을, 없으면 `model: sonnet`을 쓰고 Codex는 T1에 매핑된 모델을 쓴다. 위임 프롬프트에는 설계, 수정할 파일, 제약, 완료 기준, 테스트 명령, 예산, 권장 모델과 그 이유를 적어 다른 문맥 없이 실행할 수 있게 한다. 이미 읽은 파일의 약 50줄 이하 변경이나 의존 단계가 하나뿐인 짧은 구현, 그리고 위임 도구나 하위 모델을 쓸 수 없는 경우는 메인 모델이 직접 처리한다. 위임한 구현은 끝날 때까지 기다리고 같은 구현을 중복 실행하지 않으며, 돌아온 diff와 테스트 결과를 메인 모델이 직접 검증한 뒤에 완료로 보고한다",
|
|
53
|
+
"en": "When an implementation spans several files → the main model settles the design and specification and delegates the code writing to the T1 tier, never T2. Each tool resolves the tier: Claude uses that category's T1 agent file or `model: sonnet`, Codex the model mapped to T1. Write the delegation prompt so it runs without other context: design, files to change, constraints, completion criteria, test commands, budget, and the recommended model with a one-line reason. Keep changes of roughly 50 lines or less in files already read, a single short dependency chain, or cases where the subagent tools or the lower model are unavailable on the main model. Wait for the delegated work instead of duplicating it, and verify the returned diff and the actual test results before reporting completion"
|
|
60
54
|
},
|
|
61
55
|
{
|
|
62
56
|
"id": "fix-bec8e9",
|
package/src/agents.js
CHANGED
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
import { existsSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { claudeUserDir } from './paths.js';
|
|
22
|
+
import { agentDefinitions } from './presets-source.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Bare model alias each routing tier stands for. A rule says only its tier;
|
|
26
|
+
* where no agent serves that tier, this alias is what the text names.
|
|
27
|
+
*/
|
|
28
|
+
export const TIER_MODEL = { T2: 'haiku', T1: 'sonnet', T1H: 'opus' };
|
|
22
29
|
|
|
23
30
|
/** Tier each preset agent is pinned to, for the `model:` half of the phrase. */
|
|
24
31
|
const AGENT_MODEL = {
|
|
@@ -51,6 +58,28 @@ export function agentExists(name, root = process.cwd()) {
|
|
|
51
58
|
existsSync(join(claudeUserDir(), 'agents', `${name}.md`));
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The agent a model-routing rule delegates to, or null when no agent serves its
|
|
63
|
+
* tier. A rule carries only `category` and `tier`; the agents preset says which
|
|
64
|
+
* tier and categories each agent serves, so Claude and Codex read one key.
|
|
65
|
+
*
|
|
66
|
+
* (a) `rule.agent` when it is a non-empty string: legacy registries and the
|
|
67
|
+
* published v1 bundle still carry it, and it stays an explicit override,
|
|
68
|
+
* (b) else the first agent of that tier whose categories name the category,
|
|
69
|
+
* (c) else the first agent of that tier whose categories hold "*",
|
|
70
|
+
* (d) else null.
|
|
71
|
+
*/
|
|
72
|
+
export function agentForRule(rule, { agents = agentDefinitions() } = {}) {
|
|
73
|
+
if (!rule) return null;
|
|
74
|
+
if (typeof rule.agent === 'string' && rule.agent.trim()) return rule.agent;
|
|
75
|
+
const list = (Array.isArray(agents) ? agents : []).filter((a) => a && a.tier === rule.tier);
|
|
76
|
+
const cats = (a) => (Array.isArray(a.categories) ? a.categories : []);
|
|
77
|
+
const exact = list.find((a) => cats(a).includes(rule.category));
|
|
78
|
+
if (exact) return exact.name;
|
|
79
|
+
const any = list.find((a) => cats(a).includes('*'));
|
|
80
|
+
return any ? any.name : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
54
83
|
/** Agent a bare tier name stands for when that agent's file exists. */
|
|
55
84
|
const TIER_AGENT = { sonnet: 'sonnet-worker', opus: 'opus-worker', haiku: 'haiku-explore' };
|
|
56
85
|
|
package/src/commands/presets.js
CHANGED
|
@@ -92,8 +92,9 @@ export async function run({ args, hasFlag }) {
|
|
|
92
92
|
}
|
|
93
93
|
console.log('');
|
|
94
94
|
console.log(ko ? `모델 라우팅 프리셋 ${out.model_rules.length}건` : `Model-routing presets (${out.model_rules.length})`);
|
|
95
|
+
const { agentForRule, TIER_MODEL } = await import('../agents.js');
|
|
95
96
|
for (const m of out.model_rules) {
|
|
96
|
-
console.log(` [${m.id}] ${m.tier} · ${ko ? m.label : m.labelEn} → ${m.
|
|
97
|
+
console.log(` [${m.id}] ${m.tier} · ${ko ? m.label : m.labelEn} → ${agentForRule(m, { agents: out.agents }) ?? TIER_MODEL[m.tier]}`);
|
|
97
98
|
}
|
|
98
99
|
console.log('');
|
|
99
100
|
console.log(ko ? `위임 에이전트 ${out.agents.length}개` : `Delegation agents (${out.agents.length})`);
|
|
@@ -11,6 +11,7 @@ const require = createRequire(import.meta.url);
|
|
|
11
11
|
import { readStdinJson } from '../stdin-payload.js';
|
|
12
12
|
import { debug } from '../debug.js';
|
|
13
13
|
import { signedUsd } from '../money.js';
|
|
14
|
+
import { agentForRule, TIER_MODEL } from '../agents.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Whether the user turned doc2md on.
|
|
@@ -418,12 +419,12 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
418
419
|
const rule = composed(lang === 'ko' ? c.rule : (c.ruleEn || c.rule), c);
|
|
419
420
|
if (lang === 'ko') {
|
|
420
421
|
lines.push(` 후보 R${c.id} — "${label}" 유형, ${c.count}회 반복 (프로젝트: ${c.project})`);
|
|
421
|
-
lines.push(` 판정: ${tier} (${rs.tierLabel(tier)}) → ${c
|
|
422
|
+
lines.push(` 판정: ${tier} (${rs.tierLabel(tier)}) → ${agentForRule(c) ?? TIER_MODEL[tier]} 서브에이전트 위임 권장 · 적용 범위 제안: ${rs.scopeLabel(c.suggestedScope)}`);
|
|
422
423
|
lines.push(` 예시 요청: "${c.example}"`);
|
|
423
424
|
lines.push(` 등록 시 ratchet-model.md에 기록될 룰: "${rule}"`);
|
|
424
425
|
} else {
|
|
425
426
|
lines.push(` Candidate R${c.id} — "${label}", seen ×${c.count} (project: ${c.project})`);
|
|
426
|
-
lines.push(` verdict: ${tier} (${rs.tierLabel(tier, 'en')}) → delegate to the ${c
|
|
427
|
+
lines.push(` verdict: ${tier} (${rs.tierLabel(tier, 'en')}) → delegate to the ${agentForRule(c) ?? TIER_MODEL[tier]} subagent · suggested scope: ${rs.scopeLabel(c.suggestedScope, 'en')}`);
|
|
427
428
|
lines.push(` example request: "${c.example}"`);
|
|
428
429
|
lines.push(` rule that would be written to ratchet-model.md: "${rule}"`);
|
|
429
430
|
}
|
|
@@ -510,10 +511,10 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
510
511
|
const tier = c.tier || 'T2';
|
|
511
512
|
if (lang === 'ko') {
|
|
512
513
|
console.log(` R${c.id} "${c.label}" ×${c.count}회 [${c.project}]`);
|
|
513
|
-
console.log(` 판정: ${tier} (${rs.tierLabel(tier)}) → ${c
|
|
514
|
+
console.log(` 판정: ${tier} (${rs.tierLabel(tier)}) → ${agentForRule(c) ?? TIER_MODEL[tier]} 위임 권장 · 적용 범위 제안: ${rs.scopeLabel(c.suggestedScope)}`);
|
|
514
515
|
} else {
|
|
515
516
|
console.log(` R${c.id} "${c.labelEn || c.label}" ×${c.count} [${c.project}]`);
|
|
516
|
-
console.log(` verdict: ${tier} (${rs.tierLabel(tier, 'en')}) → delegate to ${c
|
|
517
|
+
console.log(` verdict: ${tier} (${rs.tierLabel(tier, 'en')}) → delegate to ${agentForRule(c) ?? TIER_MODEL[tier]} · suggested scope: ${rs.scopeLabel(c.suggestedScope, 'en')}`);
|
|
517
518
|
}
|
|
518
519
|
console.log(` ${lang === 'ko' ? '예시' : 'example'}: "${c.example}"`);
|
|
519
520
|
const mrList = await import('../model-rules.js');
|
package/src/formatters/table.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ISSUE_MESSAGES } from '../advice.js';
|
|
|
5
5
|
import { CLI_NAME } from '../cli-name.js';
|
|
6
6
|
import { formatResetIn, formatResetClock } from '../format-time.js';
|
|
7
7
|
import { labelForKey } from '../window-labels.js';
|
|
8
|
+
import { agentForRule, TIER_MODEL } from '../agents.js';
|
|
8
9
|
|
|
9
10
|
const C = {
|
|
10
11
|
reset: '\x1b[0m',
|
|
@@ -202,7 +203,7 @@ function renderRoutingSection(routing) {
|
|
|
202
203
|
lines.push(' No recurring delegable work yet (a pattern needs 3 repeats). Approved rules apply from the next session.');
|
|
203
204
|
} else {
|
|
204
205
|
for (const c of cands) {
|
|
205
|
-
lines.push(` R${c.id} "${c.labelEn || c.label}" ×${c.count} → delegate to ${c.
|
|
206
|
+
lines.push(` R${c.id} "${c.labelEn || c.label}" ×${c.count} → delegate to ${agentForRule(c) ?? TIER_MODEL[c.tier || 'T2']}`);
|
|
206
207
|
}
|
|
207
208
|
lines.push(` Approve one: ${b(`${CLI_NAME} harness promote R${cands[0].id} --global`)} All candidates: ${b(`${CLI_NAME} route-scan`)}`);
|
|
208
209
|
}
|
package/src/model-rules.js
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* (rule-health, per docs/TIER_CRITERIA.md).
|
|
17
17
|
*
|
|
18
18
|
* Registry file (source of truth): <stateDir>/model-rules.json
|
|
19
|
-
* { rules: [ { signature, tier, category, label, labelEn,
|
|
19
|
+
* { rules: [ { signature, tier, category, label, labelEn, scope, // 'project'|'global'
|
|
20
|
+
* agent, // legacy only: honoured when present, never written
|
|
20
21
|
* targetRoot, // project root path (project scope)
|
|
21
22
|
* rule, example, count, errRate, promotedAt, lastSeen,
|
|
22
23
|
* status } ] } // 'active' | 'review'
|
|
@@ -33,7 +34,7 @@ import { join, dirname } from 'node:path';
|
|
|
33
34
|
import { homedir } from 'node:os';
|
|
34
35
|
import { userDataDir } from './paths.js';
|
|
35
36
|
import { userLanguage } from './config.js';
|
|
36
|
-
import { agentPhrase, agentPhraseEn, resolveAgentName } from './agents.js';
|
|
37
|
+
import { agentPhrase, agentPhraseEn, resolveAgentName, agentForRule, TIER_MODEL } from './agents.js';
|
|
37
38
|
import { writeStateFile } from './state-file.js';
|
|
38
39
|
import { signedUsd } from './money.js';
|
|
39
40
|
import { implementationDelegationRule } from './implementation-delegation.js';
|
|
@@ -120,7 +121,8 @@ export function composeRuleText(baseText, rule, lang = userLanguage()) {
|
|
|
120
121
|
* wording — two copies of this template would drift the moment one is reworded,
|
|
121
122
|
* and the drift only shows up in a file the model reads as instructions.
|
|
122
123
|
*
|
|
123
|
-
* `g` carries { tier, label, labelEn, example, exampleEn,
|
|
124
|
+
* `g` carries { tier, category, label, labelEn, example, exampleEn } and, on a
|
|
125
|
+
* legacy rule, `agent`; agentForRule turns tier and category into the agent.
|
|
124
126
|
*/
|
|
125
127
|
/**
|
|
126
128
|
* Trim an example to something a reader takes in at a glance. Scan-derived
|
|
@@ -133,30 +135,34 @@ function shortExample(text) {
|
|
|
133
135
|
return one.length <= EXAMPLE_MAX ? one : `${one.slice(0, EXAMPLE_MAX - 1)}…`;
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/** The agent (or bare alias) a rule's text names, before the file check. */
|
|
139
|
+
const targetName = (g) => agentForRule(g) ?? TIER_MODEL[g.tier];
|
|
140
|
+
|
|
136
141
|
// T1 (and T1H) is named through its delegation agent only when that file
|
|
137
142
|
// exists; without it the literal `model: sonnet` / `model: opus` stays so
|
|
138
143
|
// output on such machines is unchanged.
|
|
139
|
-
function t1Target(root, ko,
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
function t1Target(root, ko, g) {
|
|
145
|
+
const name = targetName(g);
|
|
146
|
+
if (resolveAgentName(name, root) === null) return null;
|
|
147
|
+
return (ko ? agentPhrase : agentPhraseEn)(name, { root });
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
export function modelRuleBaseText(g, lang = userLanguage(), { root } = {}) {
|
|
145
151
|
if (lang === 'ko') {
|
|
146
152
|
if (g.tier === 'T1H') {
|
|
147
|
-
return `"${g.label}" 고난도 구현 요청(예: "${shortExample(g.example)}")은 Fable·Mythos 세션에 한해 ${t1Target(root, true,
|
|
153
|
+
return `"${g.label}" 고난도 구현 요청(예: "${shortExample(g.example)}")은 Fable·Mythos 세션에 한해 ${t1Target(root, true, g) ?? `model: ${TIER_MODEL.T1H}`}에 위임`;
|
|
148
154
|
}
|
|
149
155
|
return g.tier === 'T2'
|
|
150
|
-
? `"${g.label}" 단순 요청(예: "${shortExample(g.example)}")은 ${agentPhrase(g
|
|
151
|
-
: `"${g.label}" 중간 난도 요청(예: "${shortExample(g.example)}")은 ${t1Target(root, true) ??
|
|
156
|
+
? `"${g.label}" 단순 요청(예: "${shortExample(g.example)}")은 ${agentPhrase(targetName(g), { root })}에 위임`
|
|
157
|
+
: `"${g.label}" 중간 난도 요청(예: "${shortExample(g.example)}")은 ${t1Target(root, true, g) ?? `model: ${TIER_MODEL.T1}`}에 위임`;
|
|
152
158
|
}
|
|
153
159
|
const example = g.exampleEn || g.example;
|
|
154
160
|
if (g.tier === 'T1H') {
|
|
155
|
-
return `Heavy "${g.labelEn}" implementation requests (e.g. "${shortExample(example)}") go to ${t1Target(root, false,
|
|
161
|
+
return `Heavy "${g.labelEn}" implementation requests (e.g. "${shortExample(example)}") go to ${t1Target(root, false, g) ?? `a model: ${TIER_MODEL.T1H} subagent`}, in Fable/Mythos sessions only`;
|
|
156
162
|
}
|
|
157
163
|
return g.tier === 'T2'
|
|
158
|
-
? `Simple "${g.labelEn}" requests (e.g. "${shortExample(example)}") go to ${agentPhraseEn(g
|
|
159
|
-
: `Moderate "${g.labelEn}" requests (e.g. "${shortExample(example)}") go to ${t1Target(root, false) ??
|
|
164
|
+
? `Simple "${g.labelEn}" requests (e.g. "${shortExample(example)}") go to ${agentPhraseEn(targetName(g), { root })}`
|
|
165
|
+
: `Moderate "${g.labelEn}" requests (e.g. "${shortExample(example)}") go to ${t1Target(root, false, g) ?? `a model: ${TIER_MODEL.T1} subagent`}`;
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
// See the note in route-scan.js — paths.js is the only place that resolves
|
|
@@ -403,7 +409,6 @@ export function renderModelRatchet(rules, lang = userLanguage(), { root } = {})
|
|
|
403
409
|
};
|
|
404
410
|
// Merged rules carry one cap per tier, so they state them once rather than
|
|
405
411
|
// repeating the whole stop-condition per tier.
|
|
406
|
-
const TIER_MODEL = { T2: 'haiku', T1: 'sonnet', T1H: 'opus' };
|
|
407
412
|
const mergedBudget = (tiers) => (ko ? '상한 ' : 'cap ') + tiers
|
|
408
413
|
.map((r) => `${TIER_MODEL[r.tier]} ${budgetCapPhrase(r, ko ? 'ko' : 'en')}`).join(' / ');
|
|
409
414
|
|
|
@@ -440,9 +445,8 @@ export function renderModelRatchet(rules, lang = userLanguage(), { root } = {})
|
|
|
440
445
|
// user's own prompt, so exampleEn is a preference, not a requirement.
|
|
441
446
|
const ex = (r) => shortExample(ko ? r.example : (r.exampleEn || r.example));
|
|
442
447
|
const target = (r) => {
|
|
443
|
-
if (r.tier === 'T2') return (ko ? agentPhrase : agentPhraseEn)(r
|
|
444
|
-
|
|
445
|
-
return t1Target(root, ko, name) ?? `model: ${name}`;
|
|
448
|
+
if (r.tier === 'T2') return (ko ? agentPhrase : agentPhraseEn)(targetName(r), { root });
|
|
449
|
+
return t1Target(root, ko, r) ?? `model: ${TIER_MODEL[r.tier]}`;
|
|
446
450
|
};
|
|
447
451
|
// One line per category, stating only what differs between the tiers.
|
|
448
452
|
// The exceptions this used to spell out (design judgement, irreversible
|
package/src/preset-sign.js
CHANGED
|
@@ -37,7 +37,10 @@ import { createHash, createPublicKey, verify as cryptoVerify } from 'node:crypto
|
|
|
37
37
|
* adding the new entry, releasing, then publishing with the new key; remove the
|
|
38
38
|
* old entry one release later.
|
|
39
39
|
*/
|
|
40
|
-
export const PRESET_SIGNING_KEYS = [
|
|
40
|
+
export const PRESET_SIGNING_KEYS = [
|
|
41
|
+
// Created 2026-10-20 for sprag.io/ops; the private half lives in the superuser's browser and its backup file.
|
|
42
|
+
{ kid: '8bc624f874f5620a', jwk: { kty: 'EC', crv: 'P-256', x: 'Lx3x--sWgDZ2_7jWxO3_ks86MtihDNRRbckITpR5bLc', y: 'L2D7puHKmZhQRh7mWjGYsmIX_UrPBYjKt3nH8B51Bhg' } },
|
|
43
|
+
];
|
|
41
44
|
|
|
42
45
|
export const PRESET_BUNDLE_FORMAT = 1;
|
|
43
46
|
export const PRESET_MAX_BYTES = 200 * 1024;
|
package/src/presets-source.js
CHANGED
|
@@ -90,7 +90,10 @@ export function bundleShapeError(b) {
|
|
|
90
90
|
}
|
|
91
91
|
for (const m of mr) {
|
|
92
92
|
if (!m || !ID_RE.test(m.id) || !TIERS.includes(m.tier) || !ID_RE.test(m.category)) return 'shape';
|
|
93
|
-
if (!str(m.label) || !str(m.labelEn)
|
|
93
|
+
if (!str(m.label) || !str(m.labelEn)) return 'shape';
|
|
94
|
+
// `agent` is optional: a rule names its tier, and the agents say which tier
|
|
95
|
+
// and categories they serve. A legacy rule that still names one is checked.
|
|
96
|
+
if (m.agent != null && (!str(m.agent) || !LINE_RE.test(m.agent))) return 'shape';
|
|
94
97
|
if (m.budget != null && (typeof m.budget !== 'object' || Array.isArray(m.budget))) return 'shape';
|
|
95
98
|
}
|
|
96
99
|
for (const a of ag) {
|
|
@@ -98,6 +101,10 @@ export function bundleShapeError(b) {
|
|
|
98
101
|
if (!Array.isArray(a.tools) || a.tools.length === 0 || a.tools.some((t) => typeof t !== 'string' || !TOOL_RE.test(t))) return 'shape';
|
|
99
102
|
if (!a.description || !LINE_RE.test(a.description.en || '') || (a.description.ko != null && !LINE_RE.test(a.description.ko))) return 'shape';
|
|
100
103
|
if (!a.body || !str(a.body.en)) return 'shape';
|
|
104
|
+
// tier and categories are optional (a legacy bundle lacks them), checked when present.
|
|
105
|
+
if (a.tier != null && !TIERS.includes(a.tier)) return 'shape';
|
|
106
|
+
if (a.categories != null && (!Array.isArray(a.categories) || a.categories.length === 0 ||
|
|
107
|
+
a.categories.some((c) => c !== '*' && (typeof c !== 'string' || !ID_RE.test(c))))) return 'shape';
|
|
101
108
|
}
|
|
102
109
|
if (!uniq(rr, 'id') || !uniq(mr, 'id') || !uniq(ag, 'name')) return 'shape';
|
|
103
110
|
return null;
|
|
@@ -188,12 +195,21 @@ export function modelPresets(opts = {}) {
|
|
|
188
195
|
return Array.isArray(data?.presets) ? data.presets : [];
|
|
189
196
|
}
|
|
190
197
|
|
|
191
|
-
|
|
198
|
+
// A legacy bundle has no tier or categories on its agents. Readers get one
|
|
199
|
+
// shape either way: categories always an array, tier a string or null.
|
|
200
|
+
function normaliseAgents(list) {
|
|
201
|
+
return (Array.isArray(list) ? list : []).filter(Boolean).map((a) => ({
|
|
202
|
+
...a,
|
|
203
|
+
tier: typeof a.tier === 'string' && a.tier ? a.tier : null,
|
|
204
|
+
categories: Array.isArray(a.categories) ? a.categories.filter((c) => typeof c === 'string') : [],
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Delegation agents: [{ name, model, effort, tier, categories[], tools[], description{ko,en}, body{ko,en} }] */
|
|
192
209
|
export function agentDefinitions(opts = {}) {
|
|
193
210
|
const r = activeRemote(opts);
|
|
194
|
-
if (r.bundle) return r.bundle.agents
|
|
195
|
-
|
|
196
|
-
return Array.isArray(data?.agents) ? data.agents : [];
|
|
211
|
+
if (r.bundle) return normaliseAgents(r.bundle.agents);
|
|
212
|
+
return normaliseAgents(readBundled('agents.json')?.agents);
|
|
197
213
|
}
|
|
198
214
|
|
|
199
215
|
/** The bundle the bundled files make: the shape the server seeds as version 1. */
|
package/src/route-inject.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import { categorizeScored, ESCALATE_RE, EDIT_RE, worthDelegating } from './route-scan.js';
|
|
35
35
|
import { loadModelRules, budgetCapPhrase, ruleBudget } from './model-rules.js';
|
|
36
36
|
import { sharedProjectRoots, realProjectRoot, scopeRank } from './shared-model-rules.js';
|
|
37
|
-
import { agentPhrase, agentPhraseEn, resolveAgentName } from './agents.js';
|
|
37
|
+
import { agentPhrase, agentPhraseEn, resolveAgentName, agentForRule, TIER_MODEL } from './agents.js';
|
|
38
38
|
import { userLanguage } from './config.js';
|
|
39
39
|
import { aliasForRole, resolveModelAlias } from './model-alias.js';
|
|
40
40
|
import { isRecognizedModelId, modelRank } from './cost.js';
|
|
@@ -263,12 +263,17 @@ export function routeHint(text, { rules, lang = userLanguage(), root, sessionRan
|
|
|
263
263
|
|
|
264
264
|
// T1 is named through its delegation agent only when that file exists here;
|
|
265
265
|
// otherwise the literal tier string stays, so output without agents is unchanged.
|
|
266
|
+
// A rule names its tier; agentForRule picks the agent that serves it.
|
|
266
267
|
const t1Root = root === undefined ? undefined : root;
|
|
267
|
-
const
|
|
268
|
+
const nameOf = (r) => agentForRule(r) ?? TIER_MODEL[r.tier];
|
|
269
|
+
const tierPhrase = (r) => {
|
|
270
|
+
const name = nameOf(r);
|
|
271
|
+
return resolveAgentName(name, t1Root) !== null ? phrase(name) : `model: ${TIER_MODEL[r.tier]}`;
|
|
272
|
+
};
|
|
268
273
|
const picked = [t2, t1, t1h].filter(Boolean);
|
|
269
274
|
let target;
|
|
270
275
|
if (picked.length >= 2) {
|
|
271
|
-
const named = (r) => (r.tier === 'T2' ? phrase(r
|
|
276
|
+
const named = (r) => (r.tier === 'T2' ? phrase(nameOf(r)) : tierPhrase(r));
|
|
272
277
|
const when = (r, i) => {
|
|
273
278
|
if (i === 0) return null;
|
|
274
279
|
if (r.tier === 'T1H') return ko ? '범위가 크면(Fable·Mythos 세션 한정)' : 'for large scope (Fable/Mythos sessions only)';
|
|
@@ -279,13 +284,12 @@ export function routeHint(text, { rules, lang = userLanguage(), root, sessionRan
|
|
|
279
284
|
: picked.map((r, i) => (i === 0 ? `${named(r)} by default` : `${named(r)} ${when(r, i)}`)).join(', ');
|
|
280
285
|
} else {
|
|
281
286
|
const only = picked[0];
|
|
282
|
-
target = only.tier === 'T1H' ? tierPhrase(
|
|
287
|
+
target = only.tier === 'T1H' ? tierPhrase(only) : phrase(nameOf(only));
|
|
283
288
|
}
|
|
284
289
|
|
|
285
|
-
const MODEL_OF = { T2: 'haiku', T1: 'sonnet', T1H: 'opus' };
|
|
286
290
|
const caps = picked.map((r) => budgetCapPhrase(r, lang));
|
|
287
291
|
const capText = picked.length >= 2
|
|
288
|
-
? (ko ? '상한 ' : 'cap ') + picked.map((r, i) => `${
|
|
292
|
+
? (ko ? '상한 ' : 'cap ') + picked.map((r, i) => `${TIER_MODEL[r.tier]} ${caps[i]}`).join(' / ')
|
|
289
293
|
: (ko ? `상한 ${caps[0]}` : `cap ${caps[0]}`);
|
|
290
294
|
|
|
291
295
|
// The marker line is the one in ratchet-model.md, so the user sees the same
|
package/src/route-scan.js
CHANGED
|
@@ -30,7 +30,7 @@ import { sessionCost } from './claude-price.js';
|
|
|
30
30
|
import { learnProfileMapping, resetModelAliasCache } from './model-alias.js';
|
|
31
31
|
import { modelRuleBaseText } from './model-rules.js';
|
|
32
32
|
import { readResetMarks, isBeforeReset } from './saved-reset.cjs';
|
|
33
|
-
import {
|
|
33
|
+
import { agentForRule } from './agents.js';
|
|
34
34
|
|
|
35
35
|
// ── Tier bands (docs/TIER_CRITERIA.md §3) ────────────────────────────────
|
|
36
36
|
// T2 (haiku): finished in few calls, tiny output, near-zero mutation, no
|
|
@@ -115,7 +115,7 @@ export const RESCAN_MAX_AGE_MS = 24 * 60 * 60 * 1000; // any new data + a d
|
|
|
115
115
|
// together triggers one scan rather than one per agent.
|
|
116
116
|
export const RESCAN_AFTER_DELEGATION_MS = 30 * 1000;
|
|
117
117
|
|
|
118
|
-
// Categories
|
|
118
|
+
// Categories a request is filed under. Classification is behavior-first with
|
|
119
119
|
// weighted keyword scoring as fallback — see categorize().
|
|
120
120
|
const PASTE_MIN_LEN = 400;
|
|
121
121
|
const CATEGORIES = [
|
|
@@ -123,21 +123,18 @@ const CATEGORIES = [
|
|
|
123
123
|
id: 'paste',
|
|
124
124
|
label: '붙여넣은 화면·로그 질문',
|
|
125
125
|
labelEn: 'questions about pasted screens/logs',
|
|
126
|
-
agent: 'haiku-explore',
|
|
127
126
|
kw: null, // matched by length, see categorize()
|
|
128
127
|
},
|
|
129
128
|
{
|
|
130
129
|
id: 'translate',
|
|
131
130
|
label: '배치 번역·정형 텍스트 변환',
|
|
132
131
|
labelEn: 'batch translation / mechanical text transforms',
|
|
133
|
-
agent: 'haiku-translate',
|
|
134
132
|
kw: [[/번역|translate/i, 2], [/변환해|표로 정리|포맷팅/i, 1]],
|
|
135
133
|
},
|
|
136
134
|
{
|
|
137
135
|
id: 'explore',
|
|
138
136
|
label: '탐색·조회 (파일/값 찾기)',
|
|
139
137
|
labelEn: 'lookup (finding files/values)',
|
|
140
|
-
agent: 'haiku-explore',
|
|
141
138
|
kw: [[/\bgrep\b|검색|\bsearch|\bfind\b/i, 2], [/찾아|어디|위치|목록|살펴/i, 1],
|
|
142
139
|
[/정의(?:돼|되어)\s*있|어느\s*파일|어떤\s*파일에|\bwhich\s+file\b|\bwhere\s+(?:is|are)\b[^?\n]*\bdefined\b/i, 1]],
|
|
143
140
|
},
|
|
@@ -145,34 +142,28 @@ const CATEGORIES = [
|
|
|
145
142
|
id: 'read',
|
|
146
143
|
label: '읽기·요약·설명',
|
|
147
144
|
labelEn: 'reading / summarizing / explaining',
|
|
148
|
-
agent: 'haiku-explore',
|
|
149
145
|
kw: [[/요약|summar|explain/i, 2], [/읽어|설명|정리해|보여줘|알려줘|뭐야|\bwhat\b/i, 1]],
|
|
150
146
|
},
|
|
151
147
|
{
|
|
152
148
|
id: 'check',
|
|
153
149
|
label: '상태 확인·검증',
|
|
154
150
|
labelEn: 'status checks / verification',
|
|
155
|
-
agent: 'haiku-explore',
|
|
156
151
|
kw: [[/확인|검증|\bverif|점검/i, 2], [/맞아\?|되나|됐나|됐어|되는지|괜찮|체크|\bcheck\b|\bstatus\b/i, 1]],
|
|
157
152
|
},
|
|
158
153
|
{
|
|
159
154
|
id: 'run',
|
|
160
155
|
label: '명령 실행 (빌드·테스트·git)',
|
|
161
156
|
labelEn: 'running commands (build/test/git)',
|
|
162
|
-
agent: 'haiku-runner',
|
|
163
157
|
kw: [[/\bgit |\bcommit(?:s|ted)?\b|\bpush(?:ed|es)?\b|\bnpm |\bpip3? |빌드해|빌드 돌/i, 2], [/실행|돌려|\brun\b|\bbuild\b|빌드|테스트|설치/i, 1]],
|
|
164
158
|
},
|
|
165
159
|
];
|
|
166
|
-
// The T2 agent a category's candidates name
|
|
167
|
-
//
|
|
168
|
-
// /ops console
|
|
169
|
-
// (translate) keep the name above.
|
|
160
|
+
// The T2 agent a category's candidates name comes from the agents preset (the
|
|
161
|
+
// agent whose tier and categories cover it, see agentForRule), so renaming or
|
|
162
|
+
// re-scoping an agent in the /ops console carries over to new candidates.
|
|
170
163
|
function t2AgentFor(cat) {
|
|
171
164
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
} catch { /* the built-in name below */ }
|
|
175
|
-
return cat.agent;
|
|
165
|
+
return agentForRule({ tier: 'T2', category: cat.id }) ?? 'haiku';
|
|
166
|
+
} catch { return 'haiku'; }
|
|
176
167
|
}
|
|
177
168
|
|
|
178
169
|
// English keywords are matched as whole words. As bare substrings, `pip`
|
|
@@ -815,7 +806,6 @@ export async function runRouteScan({ days = 14 } = {}) {
|
|
|
815
806
|
category: g.category,
|
|
816
807
|
label: g.label,
|
|
817
808
|
labelEn: g.labelEn,
|
|
818
|
-
agent: g.agent,
|
|
819
809
|
project: g.project,
|
|
820
810
|
// Real session cwd for the project (munged `project` is lossy) — lets
|
|
821
811
|
// `harness promote R<N> --project` write the rule into the project the
|
package/src/seed-rules.js
CHANGED
|
@@ -31,6 +31,7 @@ import { join, resolve } from 'node:path';
|
|
|
31
31
|
import { modelPresets as sourceModelPresets } from './presets-source.js';
|
|
32
32
|
import { sharedModelRulesForProject, loadSharedModelRules, sharedProjectRoots, realProjectRoot } from './shared-model-rules.js';
|
|
33
33
|
import { userDataDir } from './paths.js';
|
|
34
|
+
import { agentForRule, TIER_MODEL } from './agents.js';
|
|
34
35
|
import { userLanguage } from './config.js';
|
|
35
36
|
import { writeStateFile } from './state-file.js';
|
|
36
37
|
import {
|
|
@@ -130,7 +131,7 @@ export function pendingSeeds({ lang = userLanguage(), root = findProjectRoot(),
|
|
|
130
131
|
id: p.id,
|
|
131
132
|
tier: p.tier,
|
|
132
133
|
label: lang === 'ko' ? p.label : p.labelEn,
|
|
133
|
-
agent: agent === 'codex' ? `Codex ${p.tier} mapping` : p.
|
|
134
|
+
agent: agent === 'codex' ? `Codex ${p.tier} mapping` : (agentForRule(p) ?? TIER_MODEL[p.tier]),
|
|
134
135
|
ruleText: agent === 'codex' ? codexSeedText(p, lang)
|
|
135
136
|
: composeRuleText(modelRuleBaseText(p, lang), { budget: p.budget }, lang),
|
|
136
137
|
preset: p,
|
|
@@ -212,7 +213,9 @@ export async function acceptSeed(id, { scope, root = findProjectRoot(), lang = u
|
|
|
212
213
|
category: p.category,
|
|
213
214
|
label: p.label,
|
|
214
215
|
labelEn: p.labelEn,
|
|
215
|
-
|
|
216
|
+
// A preset names its tier; only a legacy (v1) bundle preset still names an
|
|
217
|
+
// agent, and that override is kept rather than lost on accept.
|
|
218
|
+
...(typeof p.agent === 'string' && p.agent ? { agent: p.agent } : {}),
|
|
216
219
|
scope,
|
|
217
220
|
targetRoot: scope === 'project' ? root : null,
|
|
218
221
|
project,
|
|
@@ -276,6 +279,26 @@ export async function resetSeeds({ agent = 'claude' } = {}) {
|
|
|
276
279
|
* asked for, and because a single "register all of these?" question invites a
|
|
277
280
|
* yes to rules the user never read.
|
|
278
281
|
*/
|
|
282
|
+
/**
|
|
283
|
+
* The first sentence of a rule, for the Codex seed offer. Codex keeps about
|
|
284
|
+
* 2,000 tokens of a hook's context and drops a part that does not fit whole,
|
|
285
|
+
* so listing every ratchet rule in full pushed the whole offer out. The offer
|
|
286
|
+
* only has to say what each rule is about; accepting writes the full text.
|
|
287
|
+
* A sentence ends at "다." or a full stop followed by whitespace (not an
|
|
288
|
+
* ellipsis), or at "다" before a newline. Without one, a text over 140
|
|
289
|
+
* characters is cut at the last space before 140 and marked with "…".
|
|
290
|
+
*/
|
|
291
|
+
export const OFFER_CUT = 140;
|
|
292
|
+
export function firstSentence(text) {
|
|
293
|
+
const t = String(text ?? '').trim();
|
|
294
|
+
const m = /(?<!\.)\.(?=\s)|다(?=\n)/.exec(t);
|
|
295
|
+
if (m && m.index + 1 < t.length) return t.slice(0, m.index + m[0].length).trim();
|
|
296
|
+
if (t.length <= OFFER_CUT) return t;
|
|
297
|
+
const head = t.slice(0, OFFER_CUT);
|
|
298
|
+
const sp = head.lastIndexOf(' ');
|
|
299
|
+
return `${(sp > 0 ? head.slice(0, sp) : head).trimEnd()}…`;
|
|
300
|
+
}
|
|
301
|
+
|
|
279
302
|
export function seedOfferBlock({ lang = userLanguage(), root = findProjectRoot(), agent = 'claude' } = {}) {
|
|
280
303
|
const pending = pendingSeeds({ lang, root, agent });
|
|
281
304
|
if (pending.length === 0) return null;
|
|
@@ -291,7 +314,7 @@ export function seedOfferBlock({ lang = userLanguage(), root = findProjectRoot()
|
|
|
291
314
|
` 4. Register none: ${cmd('skip all')} (never offered again)`,
|
|
292
315
|
'Always confirm the scope with the user before running an accept command.',
|
|
293
316
|
'Delegation policies are shared across Claude and Codex; approval retains the chosen scope. Codex execution also requires delegate on; tier models resolve from cheaper priced models already run in Codex, or from mappings: sprag delegate shared status --agent codex. Never assume Claude model names are Codex models.',
|
|
294
|
-
...pending.map((s) => ` [${s.id}] ${s.ruleText}`),
|
|
317
|
+
...pending.map((s) => ` [${s.id}] ${s.kind === 'ratchet' ? firstSentence(s.ruleText) : s.ruleText}`),
|
|
295
318
|
].join('\n');
|
|
296
319
|
}
|
|
297
320
|
const models = pending.filter((s) => s.kind === 'model');
|