sprag-cli 3.73.0 → 3.75.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 +16 -16
- package/presets/ratchet-rules.json +6 -6
- package/src/agents.js +95 -23
- package/src/codex/delegation.js +26 -3
- package/src/delegation-agents.js +1 -1
- package/src/first-sentence.js +22 -0
- package/src/model-rules.js +11 -2
- package/src/route-inject.js +4 -2
- package/src/seed-rules.js +16 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.75.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,7 +5,7 @@
|
|
|
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",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"agents": [
|
|
18
18
|
{
|
|
19
|
-
"name": "
|
|
19
|
+
"name": "t2-explore",
|
|
20
20
|
"model": "haiku",
|
|
21
21
|
"effort": "low",
|
|
22
22
|
"tier": "T2",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"en": "Cheap read-only exploration, lookup, status checks and summaries for delegated tasks. It has no MCP tools."
|
|
38
38
|
},
|
|
39
39
|
"body": {
|
|
40
|
-
"ko": "맡은 역할: 읽기 전용 탐색, 조회, 상태 확인, 붙여넣은 로그나 텍스트를 두고 묻는 질문에 답합니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs
|
|
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
|
|
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."
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
|
-
"name": "
|
|
45
|
+
"name": "t2-runner",
|
|
46
46
|
"model": "haiku",
|
|
47
47
|
"effort": "low",
|
|
48
48
|
"tier": "T2",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"en": "Cheap runner for build, test and git commands in delegated tasks. It has no MCP tools."
|
|
61
61
|
},
|
|
62
62
|
"body": {
|
|
63
|
-
"ko": "맡은 역할: 전달받은 빌드·테스트·git 명령을 실행하고 결과를 보고합니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs
|
|
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
|
|
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."
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
|
-
"name": "
|
|
68
|
+
"name": "t2-translate",
|
|
69
69
|
"model": "haiku",
|
|
70
70
|
"effort": "low",
|
|
71
71
|
"tier": "T2",
|
|
@@ -84,12 +84,12 @@
|
|
|
84
84
|
"en": "Cheap batch translation and structured text conversion for delegated tasks. It has no MCP tools."
|
|
85
85
|
},
|
|
86
86
|
"body": {
|
|
87
|
-
"ko": "맡은 역할: 전달받은 파일이나 텍스트의 일괄 번역과 정형 텍스트 변환입니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs
|
|
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
|
|
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."
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
{
|
|
92
|
-
"name": "
|
|
92
|
+
"name": "t1-worker",
|
|
93
93
|
"model": "sonnet",
|
|
94
94
|
"effort": "medium",
|
|
95
95
|
"tier": "T1",
|
|
@@ -110,12 +110,12 @@
|
|
|
110
110
|
"en": "Mid-tier worker for multi-step tasks and implementation handed over with a fixed design. It has no MCP tools."
|
|
111
111
|
},
|
|
112
112
|
"body": {
|
|
113
|
-
"ko": "맡은 역할: 여러 단계로 이어지는 작업과, 호출자가 설계를 확정한 구현을 맡습니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs
|
|
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
|
|
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."
|
|
115
115
|
}
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
|
-
"name": "
|
|
118
|
+
"name": "t1h-worker",
|
|
119
119
|
"model": "claude-opus-5-5",
|
|
120
120
|
"effort": "high",
|
|
121
121
|
"tier": "T1H",
|
|
@@ -136,8 +136,8 @@
|
|
|
136
136
|
"en": "Heavy worker for Fable sessions: large implementation, debugging and refactoring handed over with a fixed design. It has no MCP tools."
|
|
137
137
|
},
|
|
138
138
|
"body": {
|
|
139
|
-
"ko": "맡은 역할: 호출자가 설계를 확정한 대규모 구현과 디버깅입니다. 설계 판단과 되돌릴 수 없는 작업은 호출자에게 넘기십시오.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs
|
|
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
|
|
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."
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
]
|
|
@@ -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,8 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"id": "fix-f558f9",
|
|
42
|
-
"ko": "탐색·조회·상태 확인·명령 실행 같은 단순 요청은 T2
|
|
43
|
-
"en": "Delegate simple requests
|
|
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"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"id": "fix-aa18bc",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"id": "fix-777a15",
|
|
52
|
-
"ko": "여러 파일에 걸친 구현을 맡으면 → 설계와 스펙 확정은 메인 모델이 하고 코드 작성은 T1
|
|
53
|
-
"en": "When an implementation spans several files → the main model settles the design and specification and delegates the code writing to
|
|
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"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
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,17 +10,31 @@
|
|
|
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.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { existsSync } from 'node:fs';
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { claudeUserDir } from './paths.js';
|
|
22
22
|
import { agentDefinitions } from './presets-source.js';
|
|
23
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
|
+
|
|
24
38
|
/**
|
|
25
39
|
* Bare model alias each routing tier stands for. A rule says only its tier;
|
|
26
40
|
* where no agent serves that tier, this alias is what the text names.
|
|
@@ -29,11 +43,11 @@ export const TIER_MODEL = { T2: 'haiku', T1: 'sonnet', T1H: 'opus' };
|
|
|
29
43
|
|
|
30
44
|
/** Tier each preset agent is pinned to, for the `model:` half of the phrase. */
|
|
31
45
|
const AGENT_MODEL = {
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
46
|
+
't2-explore': 'haiku',
|
|
47
|
+
't2-runner': 'haiku',
|
|
48
|
+
't2-translate': 'haiku',
|
|
49
|
+
't1-worker': 'sonnet',
|
|
50
|
+
't1h-worker': 'opus',
|
|
37
51
|
// A T1 rule names its target by tier alone (`agent: 'sonnet'`). Without these
|
|
38
52
|
// the phrase fell through to the haiku default, and a sonnet rule told the
|
|
39
53
|
// session to delegate to haiku. resolveAgentName maps them to an agent file.
|
|
@@ -48,7 +62,7 @@ const AGENT_MODEL = {
|
|
|
48
62
|
* `root: null` asks about the user directory alone. Text that every project
|
|
49
63
|
* reads (the global ratchet-model.md) has to be worded from what every project
|
|
50
64
|
* can see: checked against the current directory, the same rule read
|
|
51
|
-
* `
|
|
65
|
+
* `t2-explore(model: haiku)` when the scan ran in a project that defines
|
|
52
66
|
* the agent and `model: haiku` when it ran anywhere else, and each flip
|
|
53
67
|
* rewrote a file sitting in the prompt-cache prefix of every session.
|
|
54
68
|
*/
|
|
@@ -71,7 +85,9 @@ export function agentExists(name, root = process.cwd()) {
|
|
|
71
85
|
*/
|
|
72
86
|
export function agentForRule(rule, { agents = agentDefinitions() } = {}) {
|
|
73
87
|
if (!rule) return null;
|
|
74
|
-
if (typeof rule.agent === 'string' && rule.agent.trim())
|
|
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
|
+
}
|
|
75
91
|
const list = (Array.isArray(agents) ? agents : []).filter((a) => a && a.tier === rule.tier);
|
|
76
92
|
const cats = (a) => (Array.isArray(a.categories) ? a.categories : []);
|
|
77
93
|
const exact = list.find((a) => cats(a).includes(rule.category));
|
|
@@ -80,47 +96,103 @@ export function agentForRule(rule, { agents = agentDefinitions() } = {}) {
|
|
|
80
96
|
return any ? any.name : null;
|
|
81
97
|
}
|
|
82
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
|
+
|
|
83
102
|
/** Agent a bare tier name stands for when that agent's file exists. */
|
|
84
|
-
const TIER_AGENT = { sonnet: '
|
|
103
|
+
const TIER_AGENT = { sonnet: 't1-worker', opus: 't1h-worker', haiku: 't2-explore' };
|
|
85
104
|
|
|
86
105
|
/**
|
|
87
106
|
* The agent a rule target really names on this machine, or null when none is
|
|
88
107
|
* installed. A name wins when its own file exists; a bare tier name falls back
|
|
89
|
-
* 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.
|
|
90
112
|
*/
|
|
91
113
|
export function resolveAgentName(name, root = process.cwd()) {
|
|
92
|
-
|
|
93
|
-
|
|
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];
|
|
94
118
|
return alt && agentExists(alt, root) ? alt : null;
|
|
95
119
|
}
|
|
96
120
|
|
|
97
121
|
/**
|
|
98
122
|
* Korean phrase for a delegation target, e.g.
|
|
99
|
-
* agent present → `
|
|
123
|
+
* agent present → `t2-explore(model 인자 생략. …general-purpose에 model: haiku)`
|
|
100
124
|
* agent absent → `model: haiku`
|
|
101
125
|
*
|
|
102
126
|
* A named agent carries its model in its own definition, and the Agent tool's
|
|
103
127
|
* `model` argument overrides that definition. The old phrase
|
|
104
|
-
* `
|
|
128
|
+
* `t2-explore(model: haiku)` read as "pass model: haiku", and Claude Code
|
|
105
129
|
* resolves the bare `haiku` alias to an older Haiku than a definition pinned to
|
|
106
130
|
* a full id (claude-haiku-5-5), so following the rule undid the pin. The phrase
|
|
107
131
|
* now says to leave the argument out.
|
|
108
132
|
*/
|
|
109
133
|
export function agentPhrase(name, { model, root } = {}) {
|
|
110
134
|
const resolved = resolveAgentName(name, root);
|
|
111
|
-
if (!resolved) return `model: ${model ||
|
|
112
|
-
const tier =
|
|
113
|
-
|
|
135
|
+
if (!resolved) return `model: ${model || modelOf(name) || 'haiku'}`;
|
|
136
|
+
const tier = modelOf(resolved) || model || 'haiku';
|
|
137
|
+
const spec = agentSpecLabel(resolved, root);
|
|
138
|
+
return `${resolved}(${spec ? `${spec}. ` : ''}model 인자 생략. 이 에이전트가 없거나 MCP 도구가 필요하면 general-purpose에 model: ${tier})`;
|
|
114
139
|
}
|
|
115
140
|
|
|
116
141
|
/**
|
|
117
142
|
* English phrase, including the article, e.g.
|
|
118
|
-
* agent present → `the
|
|
143
|
+
* agent present → `the t2-explore subagent (omit the model argument; …)`
|
|
119
144
|
* agent absent → `a model: haiku subagent`
|
|
120
145
|
*/
|
|
121
146
|
export function agentPhraseEn(name, { model, root } = {}) {
|
|
122
147
|
const resolved = resolveAgentName(name, root);
|
|
123
|
-
if (!resolved) return `a model: ${model ||
|
|
124
|
-
const tier =
|
|
125
|
-
|
|
148
|
+
if (!resolved) return `a model: ${model || modelOf(name) || 'haiku'} subagent`;
|
|
149
|
+
const tier = modelOf(resolved) || model || 'haiku';
|
|
150
|
+
const spec = agentSpecLabel(resolved, root, ', ');
|
|
151
|
+
return `the ${resolved} subagent (${spec ? `${spec}; ` : ''}omit the model argument; if it is unavailable or MCP tools are needed, use general-purpose with model: ${tier})`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The model and effort an installed agent file really pins, read from its
|
|
156
|
+
* frontmatter, or null when the file has none.
|
|
157
|
+
*
|
|
158
|
+
* The agent name says only the tier (`t1-worker`), and Claude Code draws a
|
|
159
|
+
* delegated run as `t1-worker(…)` with no model beside it. The phrase carries
|
|
160
|
+
* what the file says so the session can show it. The file is read rather
|
|
161
|
+
* than the preset because a user's own copy is never overwritten, and it may
|
|
162
|
+
* pin something else. A file without `model:` inherits the session model,
|
|
163
|
+
* which Claude Code writes as `inherit`.
|
|
164
|
+
*/
|
|
165
|
+
export function agentSpec(name, root = process.cwd()) {
|
|
166
|
+
const paths = [];
|
|
167
|
+
if (root !== null) paths.push(join(root, '.claude', 'agents', `${name}.md`));
|
|
168
|
+
paths.push(join(claudeUserDir(), 'agents', `${name}.md`));
|
|
169
|
+
for (const p of paths) {
|
|
170
|
+
let text;
|
|
171
|
+
try { text = readFileSync(p, 'utf8'); } catch { continue; }
|
|
172
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
173
|
+
if (!m) return null;
|
|
174
|
+
const field = (key) => {
|
|
175
|
+
const hit = new RegExp(`^${key}:[ \\t]*(.*?)[ \\t]*$`, 'm').exec(m[1]);
|
|
176
|
+
return hit && hit[1] ? hit[1] : null;
|
|
177
|
+
};
|
|
178
|
+
return { model: field('model') || 'inherit', effort: field('effort') };
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Display name for a model value: `claude-sonnet-5-5` → `Sonnet 5.5`, a dated
|
|
185
|
+
* id loses its date. An alias or a gateway id stays as written, since an alias
|
|
186
|
+
* can resolve to an older model and that is worth seeing.
|
|
187
|
+
*/
|
|
188
|
+
export function modelLabel(model) {
|
|
189
|
+
const m = /^claude-([a-z]+)-(\d+)-(\d+)(?:-\d{8})?$/.exec(String(model || ''));
|
|
190
|
+
return m ? `${m[1][0].toUpperCase()}${m[1].slice(1)} ${m[2]}.${m[3]}` : String(model || '');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** `Sonnet 5.5 · effort medium` for an installed agent, or '' when unknown. */
|
|
194
|
+
function agentSpecLabel(name, root, sep = ' · ') {
|
|
195
|
+
const spec = agentSpec(name, root);
|
|
196
|
+
if (!spec) return '';
|
|
197
|
+
return spec.effort ? `${modelLabel(spec.model)}${sep}effort ${spec.effort}` : modelLabel(spec.model);
|
|
126
198
|
}
|
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/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/model-rules.js
CHANGED
|
@@ -263,7 +263,11 @@ export function renderModelRatchet(rules, lang = userLanguage(), { root } = {})
|
|
|
263
263
|
'',
|
|
264
264
|
'위임을 실행할 때는 사용자에게 다음 한 줄을 먼저 표시할 것',
|
|
265
265
|
'(어떤 도구가 토큰을 아끼고 있는지 가시화):',
|
|
266
|
-
'`🔀 [sprag] 모델 피팅: "<유형>" → <agent> 위임`',
|
|
266
|
+
'`🔀 [sprag] 모델 피팅: "<유형>" → <agent> (<모델> · effort <effort>) 위임`',
|
|
267
|
+
'',
|
|
268
|
+
'<모델>과 <effort>에는 룰의 에이전트 표기 괄호 안에 적힌 값(예: Sonnet 5.5 · effort medium)을',
|
|
269
|
+
'그대로 옮기고, 값이 적혀 있지 않으면 그 부분을 뺍니다. Claude Code 호출 행에는 에이전트',
|
|
270
|
+
'이름만 보이므로, Agent 도구의 description 맨 앞에도 `[<모델> · <effort>]` 를 붙이십시오.',
|
|
267
271
|
'',
|
|
268
272
|
'## 공통 조항 (룰마다 반복하지 않으므로 여기서 한 번만 읽으십시오)',
|
|
269
273
|
'',
|
|
@@ -326,7 +330,12 @@ export function renderModelRatchet(rules, lang = userLanguage(), { root } = {})
|
|
|
326
330
|
'',
|
|
327
331
|
'When delegating under a rule below, show the user this line first so it is',
|
|
328
332
|
'visible which tool is saving tokens:',
|
|
329
|
-
'`🔀 [sprag] model fitting: "<category>" → delegated to <agent
|
|
333
|
+
'`🔀 [sprag] model fitting: "<category>" → delegated to <agent> (<model> · effort <effort>)`',
|
|
334
|
+
'',
|
|
335
|
+
'Fill <model> and <effort> from the parentheses after the agent in the rule',
|
|
336
|
+
'(e.g. Sonnet 5.5, effort medium), and leave out whichever the rule does not',
|
|
337
|
+
'give. Claude Code shows only the agent name on the call row, so also start',
|
|
338
|
+
'the Agent tool\'s description with `[<model> · <effort>]`.',
|
|
330
339
|
'',
|
|
331
340
|
'## Shared clauses (stated once here, not repeated per rule)',
|
|
332
341
|
'',
|
package/src/route-inject.js
CHANGED
|
@@ -296,9 +296,11 @@ export function routeHint(text, { rules, lang = userLanguage(), root, sessionRan
|
|
|
296
296
|
// string whichever path decided to delegate.
|
|
297
297
|
return ko
|
|
298
298
|
? `[sprag] 이 요청은 사용자가 이미 승인한 위임 룰 "${label}" 에 해당합니다: ${target} 로 위임하십시오 (${capText}). `
|
|
299
|
-
+ `되묻지 말고 위임하고, 위임할 때 \`🔀 [sprag] 모델 피팅: "${label}" → <agent> 위임\` 을 먼저 표시하십시오. `
|
|
299
|
+
+ `되묻지 말고 위임하고, 위임할 때 \`🔀 [sprag] 모델 피팅: "${label}" → <agent> (<모델> · effort <effort>) 위임\` 을 먼저 표시하십시오. `
|
|
300
|
+
+ `모델과 effort 값은 위 에이전트 표기에 적힌 그대로 쓰고, Agent 도구의 description 맨 앞에도 \`[<모델> · <effort>]\` 를 붙이십시오. `
|
|
300
301
|
+ `상한을 넘길 것 같거나 에러가 나면 그 자리에서 멈추고 메인 모델이 이어받습니다.`
|
|
301
302
|
: `[sprag] This request matches a delegation rule the user already approved — "${label}": delegate to ${target} (${capText}). `
|
|
302
|
-
+ `Do not ask again; print \`🔀 [sprag] model fit: "${label}" → <agent
|
|
303
|
+
+ `Do not ask again; print \`🔀 [sprag] model fit: "${label}" → <agent> (<model> · effort <effort>)\` first, `
|
|
304
|
+
+ `with the model and effort the agent's phrase above gives, and start the Agent tool's description with \`[<model> · <effort>]\`. `
|
|
303
305
|
+ `If the run is likely to exceed its cap or hits an error, stop there and the main model takes over.`;
|
|
304
306
|
}
|
package/src/seed-rules.js
CHANGED
|
@@ -32,6 +32,11 @@ 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
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 };
|
|
35
40
|
import { userLanguage } from './config.js';
|
|
36
41
|
import { writeStateFile } from './state-file.js';
|
|
37
42
|
import {
|
|
@@ -148,9 +153,17 @@ export function pendingSeeds({ lang = userLanguage(), root = findProjectRoot(),
|
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
function codexSeedText(p, lang) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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.`);
|
|
154
167
|
return composeRuleText(text, p, lang);
|
|
155
168
|
}
|
|
156
169
|
|
|
@@ -279,26 +292,6 @@ export async function resetSeeds({ agent = 'claude' } = {}) {
|
|
|
279
292
|
* asked for, and because a single "register all of these?" question invites a
|
|
280
293
|
* yes to rules the user never read.
|
|
281
294
|
*/
|
|
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
|
-
|
|
302
295
|
export function seedOfferBlock({ lang = userLanguage(), root = findProjectRoot(), agent = 'claude' } = {}) {
|
|
303
296
|
const pending = pendingSeeds({ lang, root, agent });
|
|
304
297
|
if (pending.length === 0) return null;
|