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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprag-cli",
3
- "version": "3.73.0",
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": {
@@ -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 opus-worker, which names the full id: a bare alias resolves to an older model.",
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": "haiku-explore",
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 general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\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 general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
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": "haiku-runner",
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 general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\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 general-purpose: <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."
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": "haiku-translate",
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 general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\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 general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
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": "sonnet-worker",
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 general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\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 general-purpose: <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."
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": "opus-worker",
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 general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\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 general-purpose: <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."
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. Fix the interface before lowering the model — doing it the other way round explodes the token cost"
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 티어에 위임해 상위 모델 토큰을 아낀다. 티어는 도구가 해석한다. Claude는 그 카테고리의 T2 에이전트 파일을, 없으면 `model: haiku`를 쓰고 Codex는 T2에 매핑된 모델을 쓴다",
43
- "en": "Delegate simple requests — lookup, status checks, running commands — to the T2 tier to keep the expensive model's tokens for real work. Each tool resolves the tier: Claude uses that category's T2 agent file or `model: haiku`, Codex the model mapped to T2"
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 티어에 위임하며, T2에는 구현을 맡기지 않는다. 티어는 도구가 해석한다. Claude는 그 카테고리의 T1 에이전트 파일을, 없으면 `model: sonnet`을 쓰고 Codex는 T1에 매핑된 모델을 쓴다. 위임 프롬프트에는 설계, 수정할 파일, 제약, 완료 기준, 테스트 명령, 예산, 권장 모델과 그 이유를 적어 다른 문맥 없이 실행할 수 있게 한다. 이미 읽은 파일의 약 50줄 이하 변경이나 의존 단계가 하나뿐인 짧은 구현, 그리고 위임 도구나 하위 모델을 쓸 수 없는 경우는 메인 모델이 직접 처리한다. 위임한 구현은 끝날 때까지 기다리고 같은 구현을 중복 실행하지 않으며, 돌아온 diff와 테스트 결과를 메인 모델이 직접 검증한 뒤에 완료로 보고한다",
53
- "en": "When an implementation spans several files → the main model settles the design and specification and delegates the code writing to the T1 tier, never T2. Each tool resolves the tier: Claude uses that category's T1 agent file or `model: sonnet`, Codex the model mapped to T1. Write the delegation prompt so it runs without other context: design, files to change, constraints, completion criteria, test commands, budget, and the recommended model with a one-line reason. Keep changes of roughly 50 lines or less in files already read, a single short dependency chain, or cases where the subagent tools or the lower model are unavailable on the main model. Wait for the delegated work instead of duplicating it, and verify the returned diff and the actual test results before reporting completion"
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 (`haiku-explore`, `haiku-runner`, …).
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 (`sonnet-worker`, `haiku-explore`, installed by
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
- 'haiku-explore': 'haiku',
33
- 'haiku-runner': 'haiku',
34
- 'haiku-translate': 'haiku',
35
- 'sonnet-worker': 'sonnet',
36
- 'opus-worker': 'opus',
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
- * `haiku-explore(model: haiku)` when the scan ran in a project that defines
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()) return rule.agent;
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: 'sonnet-worker', opus: 'opus-worker', haiku: 'haiku-explore' };
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
- if (agentExists(name, root)) return name;
93
- const alt = TIER_AGENT[name];
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 → `haiku-explore(model 인자 생략. …general-purpose에 model: haiku)`
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
- * `haiku-explore(model: haiku)` read as "pass model: haiku", and Claude Code
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 || AGENT_MODEL[name] || 'haiku'}`;
112
- const tier = AGENT_MODEL[resolved] || model || 'haiku';
113
- return `${resolved}(model 인자 생략. 이 에이전트가 없거나 MCP 도구가 필요하면 general-purpose에 model: ${tier})`;
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 haiku-explore subagent (omit the model argument; …)`
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 || AGENT_MODEL[name] || 'haiku'} subagent`;
124
- const tier = AGENT_MODEL[resolved] || model || 'haiku';
125
- return `the ${resolved} subagent (omit the model argument; if it is unavailable or MCP tools are needed, use general-purpose with model: ${tier})`;
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
  }
@@ -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
- return target ? [{ ...rule, ...target, source: 'shared', status: 'active' }] : [];
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
- return `${rule.tier}: model ${rule.model}${rule.effort ? `, reasoning_effort ${rule.effort}` : ''}. ${sharedRuleBounds(rule)}`.trim();
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) {
@@ -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 opus-worker, which names the full id: Claude Code resolves a bare
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
+ }
@@ -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
  '',
@@ -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>\` first. `
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
- const text = lang === 'ko'
152
- ? `"${p.label}" 유형의 ${p.tier === 'T2' ? '단순' : '중간 난도'} 작업은 ${p.tier}에 위임합니다. 실행 모델은 Codex가 자동으로 정하거나 매핑으로 지정합니다.`
153
- : `${p.tier === 'T2' ? 'Simple' : 'Moderate'} "${p.labelEn}" tasks use ${p.tier}. Codex resolves the execution model automatically or from a mapping.`;
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;