sprag-cli 3.72.0 → 3.74.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 +43 -17
- package/presets/model-rules.json +2 -11
- package/presets/ratchet-rules.json +6 -12
- package/src/agents.js +72 -19
- package/src/codex/delegation.js +26 -3
- package/src/commands/presets.js +2 -1
- package/src/commands/route-scan.js +5 -4
- package/src/delegation-agents.js +1 -1
- package/src/first-sentence.js +22 -0
- 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 +22 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.74.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
|
@@ -5,17 +5,27 @@
|
|
|
5
5
|
"and a remote bundle render through one path. A remote bundle replaces this list",
|
|
6
6
|
"when its signature verifies; otherwise this file is used.",
|
|
7
7
|
"Models are aliases on purpose (a gateway resolves them through ANTHROPIC_DEFAULT_*_MODEL),",
|
|
8
|
-
"except
|
|
8
|
+
"except t1h-worker, which names the full id: a bare alias resolves to an older model.",
|
|
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
|
-
"name": "
|
|
19
|
+
"name": "t2-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",
|
|
@@ -27,14 +37,18 @@
|
|
|
27
37
|
"en": "Cheap read-only exploration, lookup, status checks and summaries for delegated tasks. It has no MCP tools."
|
|
28
38
|
},
|
|
29
39
|
"body": {
|
|
30
|
-
"ko": "맡은 역할: 읽기 전용 탐색, 조회, 상태 확인, 붙여넣은 로그나 텍스트를 두고 묻는 질문에 답합니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs
|
|
31
|
-
"en": "Your role: read-only exploration, lookups, status checks, and questions about pasted logs or text.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs
|
|
40
|
+
"ko": "맡은 역할: 읽기 전용 탐색, 조회, 상태 확인, 붙여넣은 로그나 텍스트를 두고 묻는 질문에 답합니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs tools: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
41
|
+
"en": "Your role: read-only exploration, lookups, status checks, and questions about pasted logs or text.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs tools: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
32
42
|
}
|
|
33
43
|
},
|
|
34
44
|
{
|
|
35
|
-
"name": "
|
|
45
|
+
"name": "t2-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",
|
|
@@ -46,14 +60,18 @@
|
|
|
46
60
|
"en": "Cheap runner for build, test and git commands in delegated tasks. It has no MCP tools."
|
|
47
61
|
},
|
|
48
62
|
"body": {
|
|
49
|
-
"ko": "맡은 역할: 전달받은 빌드·테스트·git 명령을 실행하고 결과를 보고합니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs
|
|
50
|
-
"en": "Your role: run the build, test, or git commands you are given and report the result.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs
|
|
63
|
+
"ko": "맡은 역할: 전달받은 빌드·테스트·git 명령을 실행하고 결과를 보고합니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs tools: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.\n전체 테스트, 빌드, 렌더처럼 프로세스를 여럿 띄우는 명령은 프로세스 상한을 걸고 실행하십시오. macOS 와 Linux 에서는 `(ulimit -u $(( $(ps -u $(id -u) -o pid= | wc -l) + 150 )); <명령>)` 으로 감싸고, Windows 에서는 그대로 실행합니다. 출력에 `spawn ... EAGAIN` 이나 `Resource temporarily unavailable` 이 보이면 테스트 실패가 아니라 프로세스 폭주이므로, 명령을 다시 돌리지 말고 그 줄을 인용해 보고하십시오.",
|
|
64
|
+
"en": "Your role: run the build, test, or git commands you are given and report the result.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs tools: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it.\nRun a command that starts many processes (a full test suite, a build, a render) under a process cap: on macOS and Linux wrap it as `(ulimit -u $(( $(ps -u $(id -u) -o pid= | wc -l) + 150 )); <command>)`, on Windows run it as is. If the output shows `spawn ... EAGAIN` or `Resource temporarily unavailable`, that is a process runaway, not a test failure: do not rerun the command; quote that line and report."
|
|
51
65
|
}
|
|
52
66
|
},
|
|
53
67
|
{
|
|
54
|
-
"name": "
|
|
68
|
+
"name": "t2-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",
|
|
@@ -66,14 +84,18 @@
|
|
|
66
84
|
"en": "Cheap batch translation and structured text conversion for delegated tasks. It has no MCP tools."
|
|
67
85
|
},
|
|
68
86
|
"body": {
|
|
69
|
-
"ko": "맡은 역할: 전달받은 파일이나 텍스트의 일괄 번역과 정형 텍스트 변환입니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs
|
|
70
|
-
"en": "Your role: batch translation and mechanical text conversion of the files or text you are given.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs
|
|
87
|
+
"ko": "맡은 역할: 전달받은 파일이나 텍스트의 일괄 번역과 정형 텍스트 변환입니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs tools: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
88
|
+
"en": "Your role: batch translation and mechanical text conversion of the files or text you are given.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs tools: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
71
89
|
}
|
|
72
90
|
},
|
|
73
91
|
{
|
|
74
|
-
"name": "
|
|
92
|
+
"name": "t1-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",
|
|
@@ -88,14 +110,18 @@
|
|
|
88
110
|
"en": "Mid-tier worker for multi-step tasks and implementation handed over with a fixed design. It has no MCP tools."
|
|
89
111
|
},
|
|
90
112
|
"body": {
|
|
91
|
-
"ko": "맡은 역할: 여러 단계로 이어지는 작업과, 호출자가 설계를 확정한 구현을 맡습니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs
|
|
92
|
-
"en": "Your role: multi-step work and the implementation half of a design the caller already fixed.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs
|
|
113
|
+
"ko": "맡은 역할: 여러 단계로 이어지는 작업과, 호출자가 설계를 확정한 구현을 맡습니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs tools: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.\n전체 테스트, 빌드, 렌더처럼 프로세스를 여럿 띄우는 명령은 프로세스 상한을 걸고 실행하십시오. macOS 와 Linux 에서는 `(ulimit -u $(( $(ps -u $(id -u) -o pid= | wc -l) + 150 )); <명령>)` 으로 감싸고, Windows 에서는 그대로 실행합니다. 출력에 `spawn ... EAGAIN` 이나 `Resource temporarily unavailable` 이 보이면 테스트 실패가 아니라 프로세스 폭주이므로, 명령을 다시 돌리지 말고 그 줄을 인용해 보고하십시오.",
|
|
114
|
+
"en": "Your role: multi-step work and the implementation half of a design the caller already fixed.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs tools: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it.\nRun a command that starts many processes (a full test suite, a build, a render) under a process cap: on macOS and Linux wrap it as `(ulimit -u $(( $(ps -u $(id -u) -o pid= | wc -l) + 150 )); <command>)`, on Windows run it as is. If the output shows `spawn ... EAGAIN` or `Resource temporarily unavailable`, that is a process runaway, not a test failure: do not rerun the command; quote that line and report."
|
|
93
115
|
}
|
|
94
116
|
},
|
|
95
117
|
{
|
|
96
|
-
"name": "
|
|
118
|
+
"name": "t1h-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",
|
|
@@ -110,8 +136,8 @@
|
|
|
110
136
|
"en": "Heavy worker for Fable sessions: large implementation, debugging and refactoring handed over with a fixed design. It has no MCP tools."
|
|
111
137
|
},
|
|
112
138
|
"body": {
|
|
113
|
-
"ko": "맡은 역할: 호출자가 설계를 확정한 대규모 구현과 디버깅입니다. 설계 판단과 되돌릴 수 없는 작업은 호출자에게 넘기십시오.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs
|
|
114
|
-
"en": "Your role: large implementation and debugging work whose design the caller already fixed. Hand design judgement and irreversible actions back to the caller.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs
|
|
139
|
+
"ko": "맡은 역할: 호출자가 설계를 확정한 대규모 구현과 디버깅입니다. 설계 판단과 되돌릴 수 없는 작업은 호출자에게 넘기십시오.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs tools: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.\n전체 테스트, 빌드, 렌더처럼 프로세스를 여럿 띄우는 명령은 프로세스 상한을 걸고 실행하십시오. macOS 와 Linux 에서는 `(ulimit -u $(( $(ps -u $(id -u) -o pid= | wc -l) + 150 )); <명령>)` 으로 감싸고, Windows 에서는 그대로 실행합니다. 출력에 `spawn ... EAGAIN` 이나 `Resource temporarily unavailable` 이 보이면 테스트 실패가 아니라 프로세스 폭주이므로, 명령을 다시 돌리지 말고 그 줄을 인용해 보고하십시오.",
|
|
140
|
+
"en": "Your role: large implementation and debugging work whose design the caller already fixed. Hand design judgement and irreversible actions back to the caller.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs tools: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it.\nRun a command that starts many processes (a full test suite, a build, a render) under a process cap: on macOS and Linux wrap it as `(ulimit -u $(( $(ps -u $(id -u) -o pid= | wc -l) + 150 )); <command>)`, on Windows run it as is. If the output shows `spawn ... EAGAIN` or `Resource temporarily unavailable`, that is a process runaway, not a test failure: do not rerun the command; quote that line and report."
|
|
115
141
|
}
|
|
116
142
|
}
|
|
117
143
|
]
|
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": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"id": "fix-b1da7a",
|
|
32
|
-
"ko": "자동화 설계에서 vision/screenshot 루프가 보이면 → 호출 가능한 함수·API·MCP tool부터 찾는다. 통제 가능한 내부 도구는 무조건 API, 통제 불가 외부 SaaS만 vision 차선책. 인터페이스를 바꾼 뒤에
|
|
33
|
-
"en": "When an automation design calls for a vision/screenshot loop → look for a callable function, API or MCP tool first. Anything you control gets an API; vision is the fallback only for external SaaS you cannot change.
|
|
32
|
+
"ko": "자동화 설계에서 vision/screenshot 루프가 보이면 → 호출 가능한 함수·API·MCP tool부터 찾는다. 통제 가능한 내부 도구는 무조건 API, 통제 불가 외부 SaaS만 vision 차선책. 인터페이스를 바꾼 뒤에 티어를 내린다(T1 → T2) — 거꾸로 가면 토큰 폭발",
|
|
33
|
+
"en": "When an automation design calls for a vision/screenshot loop → look for a callable function, API or MCP tool first. Anything you control gets an API; vision is the fallback only for external SaaS you cannot change. Change the interface first, then lower the tier (T1 → T2) — doing it the other way round explodes the token cost"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"id": "fix-1dee5f",
|
|
@@ -39,11 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"id": "fix-f558f9",
|
|
42
|
-
"ko": "탐색·조회·상태 확인·명령 실행 같은 단순 요청은
|
|
43
|
-
"en": "Delegate simple requests
|
|
44
|
-
"agents": [
|
|
45
|
-
"claude"
|
|
46
|
-
]
|
|
42
|
+
"ko": "탐색·조회·상태 확인·명령 실행 같은 단순 요청은 T2 에이전트(t2-explore, t2-runner)에 위임해 상위 모델 토큰을 아낀다. 티어를 실제 모델로 바꾸는 일은 도구가 맡는다",
|
|
43
|
+
"en": "Delegate simple requests (lookup, status checks, running commands) to a T2 agent (t2-explore, t2-runner) to keep the expensive model's tokens for real work. The tool turns the tier into a model"
|
|
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 에이전트(t1-worker)에 위임하며, T2에는 구현을 맡기지 않는다. 위임 프롬프트에는 설계, 수정할 파일, 제약, 완료 기준, 테스트 명령, 예산, 권장 티어와 그 이유를 적어 다른 문맥 없이 실행할 수 있게 한다. 이미 읽은 파일의 약 50줄 이하 변경이나 의존 단계가 하나뿐인 짧은 구현, 그리고 위임 도구나 하위 티어를 쓸 수 없는 경우는 메인 모델이 직접 처리한다. 위임한 구현은 끝날 때까지 기다리고 같은 구현을 중복 실행하지 않으며, 돌아온 diff와 테스트 결과를 메인 모델이 직접 검증한 뒤에 완료로 보고한다",
|
|
53
|
+
"en": "When an implementation spans several files → the main model settles the design and specification and delegates the code writing to a T1 agent (t1-worker), never T2. Write the delegation prompt so it runs without other context: design, files to change, constraints, completion criteria, test commands, budget, and the recommended tier 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 tier 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* How a delegation target is phrased in generated rules.
|
|
3
3
|
*
|
|
4
|
-
* Rules used to name concrete subagents (`
|
|
4
|
+
* Rules used to name concrete subagents (`t2-explore`, `t2-runner`, …).
|
|
5
5
|
* Those live in the user's own `~/.claude/agents/` and are NOT shipped by this
|
|
6
6
|
* package, so on any machine that never created them the generated rule told
|
|
7
7
|
* the model to delegate to an agent that does not exist. The model tier is the
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* there, which keeps the tool-restricted preset in play for users who have it.
|
|
11
11
|
*
|
|
12
12
|
* A rule that names a tier alone (`sonnet`, `haiku`) resolves to that tier's
|
|
13
|
-
* delegation-only agent (`
|
|
13
|
+
* delegation-only agent (`t1-worker`, `t2-explore`, installed by
|
|
14
14
|
* delegation-agents.js) when its file exists. A phrase that names an agent
|
|
15
15
|
* always carries a general-purpose fallback, because Cowork reads no agents
|
|
16
16
|
* directory and a copied rule would otherwise name an agent that is not there.
|
|
@@ -19,14 +19,35 @@
|
|
|
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
|
+
* Agents used to be named after their model. They are named by tier now, since
|
|
26
|
+
* the model behind a tier changes and a stale model name inside an agent name
|
|
27
|
+
* misleads. A registry, a hand-written rule or a user's own file may still carry
|
|
28
|
+
* the old name, so it keeps resolving to the new one.
|
|
29
|
+
*/
|
|
30
|
+
export const LEGACY_AGENT_NAMES = {
|
|
31
|
+
'haiku-explore': 't2-explore',
|
|
32
|
+
'haiku-runner': 't2-runner',
|
|
33
|
+
'haiku-translate': 't2-translate',
|
|
34
|
+
'sonnet-worker': 't1-worker',
|
|
35
|
+
'opus-worker': 't1h-worker',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Bare model alias each routing tier stands for. A rule says only its tier;
|
|
40
|
+
* where no agent serves that tier, this alias is what the text names.
|
|
41
|
+
*/
|
|
42
|
+
export const TIER_MODEL = { T2: 'haiku', T1: 'sonnet', T1H: 'opus' };
|
|
22
43
|
|
|
23
44
|
/** Tier each preset agent is pinned to, for the `model:` half of the phrase. */
|
|
24
45
|
const AGENT_MODEL = {
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
46
|
+
't2-explore': 'haiku',
|
|
47
|
+
't2-runner': 'haiku',
|
|
48
|
+
't2-translate': 'haiku',
|
|
49
|
+
't1-worker': 'sonnet',
|
|
50
|
+
't1h-worker': 'opus',
|
|
30
51
|
// A T1 rule names its target by tier alone (`agent: 'sonnet'`). Without these
|
|
31
52
|
// the phrase fell through to the haiku default, and a sonnet rule told the
|
|
32
53
|
// session to delegate to haiku. resolveAgentName maps them to an agent file.
|
|
@@ -41,7 +62,7 @@ const AGENT_MODEL = {
|
|
|
41
62
|
* `root: null` asks about the user directory alone. Text that every project
|
|
42
63
|
* reads (the global ratchet-model.md) has to be worded from what every project
|
|
43
64
|
* can see: checked against the current directory, the same rule read
|
|
44
|
-
* `
|
|
65
|
+
* `t2-explore(model: haiku)` when the scan ran in a project that defines
|
|
45
66
|
* the agent and `model: haiku` when it ran anywhere else, and each flip
|
|
46
67
|
* rewrote a file sitting in the prompt-cache prefix of every session.
|
|
47
68
|
*/
|
|
@@ -51,47 +72,79 @@ export function agentExists(name, root = process.cwd()) {
|
|
|
51
72
|
existsSync(join(claudeUserDir(), 'agents', `${name}.md`));
|
|
52
73
|
}
|
|
53
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The agent a model-routing rule delegates to, or null when no agent serves its
|
|
77
|
+
* tier. A rule carries only `category` and `tier`; the agents preset says which
|
|
78
|
+
* tier and categories each agent serves, so Claude and Codex read one key.
|
|
79
|
+
*
|
|
80
|
+
* (a) `rule.agent` when it is a non-empty string: legacy registries and the
|
|
81
|
+
* published v1 bundle still carry it, and it stays an explicit override,
|
|
82
|
+
* (b) else the first agent of that tier whose categories name the category,
|
|
83
|
+
* (c) else the first agent of that tier whose categories hold "*",
|
|
84
|
+
* (d) else null.
|
|
85
|
+
*/
|
|
86
|
+
export function agentForRule(rule, { agents = agentDefinitions() } = {}) {
|
|
87
|
+
if (!rule) return null;
|
|
88
|
+
if (typeof rule.agent === 'string' && rule.agent.trim()) {
|
|
89
|
+
return Object.hasOwn(LEGACY_AGENT_NAMES, rule.agent) ? LEGACY_AGENT_NAMES[rule.agent] : rule.agent;
|
|
90
|
+
}
|
|
91
|
+
const list = (Array.isArray(agents) ? agents : []).filter((a) => a && a.tier === rule.tier);
|
|
92
|
+
const cats = (a) => (Array.isArray(a.categories) ? a.categories : []);
|
|
93
|
+
const exact = list.find((a) => cats(a).includes(rule.category));
|
|
94
|
+
if (exact) return exact.name;
|
|
95
|
+
const any = list.find((a) => cats(a).includes('*'));
|
|
96
|
+
return any ? any.name : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Model alias an agent (or a legacy agent name) is pinned to, or undefined. */
|
|
100
|
+
const modelOf = (name) => AGENT_MODEL[Object.hasOwn(LEGACY_AGENT_NAMES, name) ? LEGACY_AGENT_NAMES[name] : name];
|
|
101
|
+
|
|
54
102
|
/** Agent a bare tier name stands for when that agent's file exists. */
|
|
55
|
-
const TIER_AGENT = { sonnet: '
|
|
103
|
+
const TIER_AGENT = { sonnet: 't1-worker', opus: 't1h-worker', haiku: 't2-explore' };
|
|
56
104
|
|
|
57
105
|
/**
|
|
58
106
|
* The agent a rule target really names on this machine, or null when none is
|
|
59
107
|
* installed. A name wins when its own file exists; a bare tier name falls back
|
|
60
|
-
* to its delegation-only agent.
|
|
108
|
+
* to its delegation-only agent. A legacy name maps to its tier-named successor
|
|
109
|
+
* first; when only the legacy file is installed (a user-edited file sprag does
|
|
110
|
+
* not own, so it was never removed) the legacy name is returned, because that is
|
|
111
|
+
* the file the phrase has to point at.
|
|
61
112
|
*/
|
|
62
113
|
export function resolveAgentName(name, root = process.cwd()) {
|
|
63
|
-
|
|
64
|
-
|
|
114
|
+
const current = Object.hasOwn(LEGACY_AGENT_NAMES, name) ? LEGACY_AGENT_NAMES[name] : name;
|
|
115
|
+
if (agentExists(current, root)) return current;
|
|
116
|
+
if (current !== name && agentExists(name, root)) return name;
|
|
117
|
+
const alt = TIER_AGENT[current];
|
|
65
118
|
return alt && agentExists(alt, root) ? alt : null;
|
|
66
119
|
}
|
|
67
120
|
|
|
68
121
|
/**
|
|
69
122
|
* Korean phrase for a delegation target, e.g.
|
|
70
|
-
* agent present → `
|
|
123
|
+
* agent present → `t2-explore(model 인자 생략. …general-purpose에 model: haiku)`
|
|
71
124
|
* agent absent → `model: haiku`
|
|
72
125
|
*
|
|
73
126
|
* A named agent carries its model in its own definition, and the Agent tool's
|
|
74
127
|
* `model` argument overrides that definition. The old phrase
|
|
75
|
-
* `
|
|
128
|
+
* `t2-explore(model: haiku)` read as "pass model: haiku", and Claude Code
|
|
76
129
|
* resolves the bare `haiku` alias to an older Haiku than a definition pinned to
|
|
77
130
|
* a full id (claude-haiku-5-5), so following the rule undid the pin. The phrase
|
|
78
131
|
* now says to leave the argument out.
|
|
79
132
|
*/
|
|
80
133
|
export function agentPhrase(name, { model, root } = {}) {
|
|
81
134
|
const resolved = resolveAgentName(name, root);
|
|
82
|
-
if (!resolved) return `model: ${model ||
|
|
83
|
-
const tier =
|
|
135
|
+
if (!resolved) return `model: ${model || modelOf(name) || 'haiku'}`;
|
|
136
|
+
const tier = modelOf(resolved) || model || 'haiku';
|
|
84
137
|
return `${resolved}(model 인자 생략. 이 에이전트가 없거나 MCP 도구가 필요하면 general-purpose에 model: ${tier})`;
|
|
85
138
|
}
|
|
86
139
|
|
|
87
140
|
/**
|
|
88
141
|
* English phrase, including the article, e.g.
|
|
89
|
-
* agent present → `the
|
|
142
|
+
* agent present → `the t2-explore subagent (omit the model argument; …)`
|
|
90
143
|
* agent absent → `a model: haiku subagent`
|
|
91
144
|
*/
|
|
92
145
|
export function agentPhraseEn(name, { model, root } = {}) {
|
|
93
146
|
const resolved = resolveAgentName(name, root);
|
|
94
|
-
if (!resolved) return `a model: ${model ||
|
|
95
|
-
const tier =
|
|
147
|
+
if (!resolved) return `a model: ${model || modelOf(name) || 'haiku'} subagent`;
|
|
148
|
+
const tier = modelOf(resolved) || model || 'haiku';
|
|
96
149
|
return `the ${resolved} subagent (omit the model argument; if it is unavailable or MCP tools are needed, use general-purpose with model: ${tier})`;
|
|
97
150
|
}
|
package/src/codex/delegation.js
CHANGED
|
@@ -13,6 +13,9 @@ import { loadConfig } from '../config.js';
|
|
|
13
13
|
import { loadSharedModelRules, sharedModelRulesForProject, sharedRuleBounds, SHARED_RULE_TIERS } from '../shared-model-rules.js';
|
|
14
14
|
import { codexPriceBook, readCodexPrices } from './cache-policy.js';
|
|
15
15
|
import { codexRunCost } from '../gateway-prices.js';
|
|
16
|
+
import { agentForRule } from '../agents.js';
|
|
17
|
+
import { agentDefinitions } from '../presets-source.js';
|
|
18
|
+
import { firstSentence } from '../first-sentence.js';
|
|
16
19
|
|
|
17
20
|
export const CODEX_ROUTE_CATEGORIES = ['paste', 'translate', 'explore', 'read', 'check', 'run'];
|
|
18
21
|
const MARKER = '<!-- sprag:codex:delegation -->';
|
|
@@ -124,13 +127,30 @@ export function codexTierTargets({ cfg = loadConfig(), dir = userDataDir(), home
|
|
|
124
127
|
return [...explicit, ...auto];
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
/**
|
|
131
|
+
* The delegation role a rule runs as: the agent agentForRule() picks for its
|
|
132
|
+
* tier and category, the same one Claude delegates to, and the first sentence
|
|
133
|
+
* of that agent's English body. Codex runs the role on the model mapped to the
|
|
134
|
+
* tier, so both tools show one value for one rule. Null when no agent serves it.
|
|
135
|
+
*/
|
|
136
|
+
export function codexRoleFor(rule, { agents = agentDefinitions() } = {}) {
|
|
137
|
+
const role = agentForRule(rule, { agents });
|
|
138
|
+
if (!role) return { role: null, roleLine: null };
|
|
139
|
+
const def = agents.find((a) => a && a.name === role);
|
|
140
|
+
// The body opens with "Your role: …"; the hint already says "Role <name>:", so the prefix is dropped.
|
|
141
|
+
return { role, roleLine: def?.body?.en ? firstSentence(def.body.en.replace(/^\s*Your role:\s*/i, '')) : null };
|
|
142
|
+
}
|
|
143
|
+
|
|
127
144
|
export function sharedCodexModelRules({ cfg = loadConfig(), dir = userDataDir(), home = codexUserDir(), root, start = root,
|
|
128
145
|
model, provider = codexProviderName() } = {}) {
|
|
129
146
|
if (cfg.codex?.sharedRules?.enabled === false) return [];
|
|
130
147
|
const targets = codexTierTargets({ cfg, dir, home, model, provider });
|
|
148
|
+
let agents = null;
|
|
131
149
|
return sharedModelRulesForProject(loadSharedModelRules({ dir }), start).flatMap((rule) => {
|
|
132
150
|
const target = targets.find((t) => t.tier === rule.tier);
|
|
133
|
-
|
|
151
|
+
if (!target) return [];
|
|
152
|
+
agents ??= agentDefinitions();
|
|
153
|
+
return [{ ...rule, ...target, ...codexRoleFor(rule, { agents }), source: 'shared', status: 'active' }];
|
|
134
154
|
});
|
|
135
155
|
}
|
|
136
156
|
|
|
@@ -254,18 +274,21 @@ export function codexImplementationHint({ model, provider, sessionId, cfg = load
|
|
|
254
274
|
if (!codexDelegateEnabled(cfg) || cfg.codex?.sharedRules?.enabled === false) return 'Implementation delegation is off. Keep the implementation on the main agent.';
|
|
255
275
|
const target = codexTierTargets({ cfg, dir, home, model, provider }).find((t) => t.tier === 'T1');
|
|
256
276
|
if (!target) return 'T1 is unresolved for this parent/provider. Keep the implementation on the main agent; do not fall back to T2.';
|
|
277
|
+
const { role, roleLine } = codexRoleFor({ tier: 'T1', category: 'implement' });
|
|
278
|
+
const asRole = role ? ` Run it as role ${role}${roleLine ? `: ${roleLine}` : '.'}` : '';
|
|
257
279
|
const id = newRouteId();
|
|
258
280
|
record({ id, parentSessionId: sessionId, via: 'prompt', source: 'implementation', tier: 'T1',
|
|
259
281
|
from: model, to: target.model, provider, effort: target.effort ?? null }, { dir });
|
|
260
282
|
return `[Sprag implementation delegation] The user-approved implementation-split policy keeps design and specification on the main agent. `
|
|
261
|
-
+ `Delegate only the implementation of that completed design to model ${target.model}${target.effort ? `, reasoning_effort ${target.effort}` : ''}, with fork_turns "none"
|
|
283
|
+
+ `Delegate only the implementation of that completed design to model ${target.model}${target.effort ? `, reasoning_effort ${target.effort}` : ''}, with fork_turns "none".${asRole} `
|
|
262
284
|
+ 'Never delegate design judgement or irreversible actions. Give one child a self-contained specification, owned files, constraints, test commands, completion criteria and the reason for this model choice. '
|
|
263
285
|
+ 'Cap: 8000 output tokens. Stop and report partial work before exceeding it. Do not recursively delegate. '
|
|
264
286
|
+ WAIT_TEXT + `\nEnd the child task message with this route line only:\n<!-- sprag:codex:route id=${id} -->`;
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
function sharedTargetText(rule) {
|
|
268
|
-
|
|
290
|
+
const role = rule.role ? ` Role ${rule.role}:${rule.roleLine ? ` ${rule.roleLine}` : ''}` : '';
|
|
291
|
+
return `${rule.tier}: model ${rule.model}${rule.effort ? `, reasoning_effort ${rule.effort}` : ''}.${role} ${sharedRuleBounds(rule)}`.trim();
|
|
269
292
|
}
|
|
270
293
|
|
|
271
294
|
function sharedDenyReason(routes) {
|
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/delegation-agents.js
CHANGED
|
@@ -27,7 +27,7 @@ const MARK_LINE = `# managed by sprag (${OWNER_MARK}); edits are overwritten`;
|
|
|
27
27
|
//
|
|
28
28
|
// Models are aliases on purpose: behind a gateway they resolve through
|
|
29
29
|
// ANTHROPIC_DEFAULT_*_MODEL, and a full model id would bypass that mapping. The
|
|
30
|
-
// exception is
|
|
30
|
+
// exception is t1h-worker, which names the full id: Claude Code resolves a bare
|
|
31
31
|
// alias to an older model than the one the tier means.
|
|
32
32
|
//
|
|
33
33
|
// Effort is a separate axis from the tier and is set by how the work gets
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first sentence of a text, shared by the Codex seed offer (seed-rules.js)
|
|
3
|
+
* and the Codex delegation hints (codex/delegation.js). It has no imports so
|
|
4
|
+
* the per-prompt Codex hook can use it without loading the seed module.
|
|
5
|
+
*
|
|
6
|
+
* Codex keeps about 2,000 tokens of a hook's context and drops a part that does
|
|
7
|
+
* not fit whole, so listing every ratchet rule in full pushed the whole offer
|
|
8
|
+
* out. The offer only has to say what each rule is about; accepting writes the
|
|
9
|
+
* full text. A sentence ends at "다." or a full stop followed by whitespace
|
|
10
|
+
* (not an ellipsis), or at "다" before a newline. Without one, a text over 140
|
|
11
|
+
* characters is cut at the last space before 140 and marked with "…".
|
|
12
|
+
*/
|
|
13
|
+
export const OFFER_CUT = 140;
|
|
14
|
+
export function firstSentence(text) {
|
|
15
|
+
const t = String(text ?? '').trim();
|
|
16
|
+
const m = /(?<!\.)\.(?=\s)|다(?=\n)/.exec(t);
|
|
17
|
+
if (m && m.index + 1 < t.length) return t.slice(0, m.index + m[0].length).trim();
|
|
18
|
+
if (t.length <= OFFER_CUT) return t;
|
|
19
|
+
const head = t.slice(0, OFFER_CUT);
|
|
20
|
+
const sp = head.lastIndexOf(' ');
|
|
21
|
+
return `${(sp > 0 ? head.slice(0, sp) : head).trimEnd()}…`;
|
|
22
|
+
}
|
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,12 @@ 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';
|
|
35
|
+
import { firstSentence, OFFER_CUT } from './first-sentence.js';
|
|
36
|
+
|
|
37
|
+
// Re-exported: the Codex delegation hints use the same cut through first-sentence.js,
|
|
38
|
+
// which stays dependency-free so the per-prompt hook does not load this module.
|
|
39
|
+
export { firstSentence, OFFER_CUT };
|
|
34
40
|
import { userLanguage } from './config.js';
|
|
35
41
|
import { writeStateFile } from './state-file.js';
|
|
36
42
|
import {
|
|
@@ -130,7 +136,7 @@ export function pendingSeeds({ lang = userLanguage(), root = findProjectRoot(),
|
|
|
130
136
|
id: p.id,
|
|
131
137
|
tier: p.tier,
|
|
132
138
|
label: lang === 'ko' ? p.label : p.labelEn,
|
|
133
|
-
agent: agent === 'codex' ? `Codex ${p.tier} mapping` : p.
|
|
139
|
+
agent: agent === 'codex' ? `Codex ${p.tier} mapping` : (agentForRule(p) ?? TIER_MODEL[p.tier]),
|
|
134
140
|
ruleText: agent === 'codex' ? codexSeedText(p, lang)
|
|
135
141
|
: composeRuleText(modelRuleBaseText(p, lang), { budget: p.budget }, lang),
|
|
136
142
|
preset: p,
|
|
@@ -147,9 +153,17 @@ export function pendingSeeds({ lang = userLanguage(), root = findProjectRoot(),
|
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
function codexSeedText(p, lang) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
// The role is the agent Claude delegates the same rule to; Codex runs that
|
|
157
|
+
// role on the model its tier mapping names, so both tools offer one value.
|
|
158
|
+
const role = agentForRule(p);
|
|
159
|
+
const level = lang === 'ko' ? (p.tier === 'T2' ? '단순' : '중간 난도') : (p.tier === 'T2' ? 'Simple' : 'Moderate');
|
|
160
|
+
const text = role
|
|
161
|
+
? (lang === 'ko'
|
|
162
|
+
? `"${p.label}" 유형의 ${level} 작업은 ${p.tier}에 위임합니다. 역할은 ${role}이고 실행 모델은 Codex가 티어 매핑으로 정합니다.`
|
|
163
|
+
: `${level} "${p.labelEn}" tasks use ${p.tier} as role ${role}; Codex resolves the model from the tier mapping.`)
|
|
164
|
+
: (lang === 'ko'
|
|
165
|
+
? `"${p.label}" 유형의 ${level} 작업은 ${p.tier}에 위임합니다. 실행 모델은 Codex가 자동으로 정하거나 매핑으로 지정합니다.`
|
|
166
|
+
: `${level} "${p.labelEn}" tasks use ${p.tier}. Codex resolves the execution model automatically or from a mapping.`);
|
|
153
167
|
return composeRuleText(text, p, lang);
|
|
154
168
|
}
|
|
155
169
|
|
|
@@ -212,7 +226,9 @@ export async function acceptSeed(id, { scope, root = findProjectRoot(), lang = u
|
|
|
212
226
|
category: p.category,
|
|
213
227
|
label: p.label,
|
|
214
228
|
labelEn: p.labelEn,
|
|
215
|
-
|
|
229
|
+
// A preset names its tier; only a legacy (v1) bundle preset still names an
|
|
230
|
+
// agent, and that override is kept rather than lost on accept.
|
|
231
|
+
...(typeof p.agent === 'string' && p.agent ? { agent: p.agent } : {}),
|
|
216
232
|
scope,
|
|
217
233
|
targetRoot: scope === 'project' ? root : null,
|
|
218
234
|
project,
|
|
@@ -291,7 +307,7 @@ export function seedOfferBlock({ lang = userLanguage(), root = findProjectRoot()
|
|
|
291
307
|
` 4. Register none: ${cmd('skip all')} (never offered again)`,
|
|
292
308
|
'Always confirm the scope with the user before running an accept command.',
|
|
293
309
|
'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}`),
|
|
310
|
+
...pending.map((s) => ` [${s.id}] ${s.kind === 'ratchet' ? firstSentence(s.ruleText) : s.ruleText}`),
|
|
295
311
|
].join('\n');
|
|
296
312
|
}
|
|
297
313
|
const models = pending.filter((s) => s.kind === 'model');
|