sprag-cli 3.69.0 → 3.70.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/NOTICE +5 -2
- package/README.ko.md +10 -0
- package/README.md +10 -0
- package/bin/cli.js +8 -1
- package/docs/CODEX.ko.md +1 -0
- package/docs/CODEX.md +1 -0
- package/docs/TEAM.ko.md +119 -0
- package/docs/TEAM.md +119 -0
- package/package.json +4 -1
- package/src/agents.js +3 -1
- package/src/codex/delegation.js +8 -5
- package/src/codex/hooks.js +14 -2
- package/src/codex/route-scan.js +6 -4
- package/src/commands/codex-delegate.js +3 -3
- package/src/commands/codex.js +3 -3
- package/src/commands/harness.js +1 -1
- package/src/commands/route-scan.js +16 -3
- package/src/commands/team.js +278 -0
- package/src/cost.js +6 -2
- package/src/cowork-sync.js +5 -4
- package/src/delegation-agents.js +13 -1
- package/src/delegation-guard.js +3 -2
- package/src/harness-templates.js +10 -0
- package/src/model-rules.js +58 -29
- package/src/route-caps.js +3 -3
- package/src/route-inject.js +24 -15
- package/src/route-scan.js +26 -15
- package/src/shared-model-rules.js +1 -1
- package/src/team-ratchet.js +176 -0
- package/src/team-sync.js +326 -0
package/NOTICE
CHANGED
|
@@ -31,5 +31,8 @@ presets/korean-style/fluent-korean.md
|
|
|
31
31
|
Trademarks
|
|
32
32
|
|
|
33
33
|
Section 6 of the License grants no rights in the project's names or marks.
|
|
34
|
-
"sprag" and the sprag
|
|
35
|
-
implies endorsement of a fork or a derived distribution
|
|
34
|
+
"sprag", the sprag logo and the domain sprag.io are not licensed for use in a
|
|
35
|
+
way that states or implies endorsement of a fork or a derived distribution, or
|
|
36
|
+
as the name of a fork, a modified build or a hosted service. The permitted uses
|
|
37
|
+
are listed in TRADEMARK.md at the root of the repository
|
|
38
|
+
(https://github.com/rootstudioyaml/sprag/blob/main/TRADEMARK.md).
|
package/README.ko.md
CHANGED
|
@@ -250,6 +250,16 @@ sprag feedback "Bedrock에서 5m TTL 칩이 사라지지 않아요"
|
|
|
250
250
|
|
|
251
251
|
버그를 제보하실 때는 도구 버전(`sprag --version`)과 OS 를 함께 적어 주십시오. statusline 이나 경고 관련 문제라면 statusline 출력이나 `sprag last` 결과를 덧붙여 주시면 원인을 빨리 찾을 수 있습니다.
|
|
252
252
|
|
|
253
|
+
## 팀
|
|
254
|
+
|
|
255
|
+
Sprag 은 켜지 않으면 로컬에서만 동작합니다. `sprag team join <토큰>` 을 실행하거나(조직 전체는 `SPRAG_TEAM_TOKEN`) 하면 컴퓨터마다 하루에 한 행씩 올라갑니다. 내용은 지출, 절감액, 위임 실행 횟수, 모델 계열 비중입니다. 프롬프트, 대화 기록, 파일 경로, 프로젝트 이름은 보내지 않으며, `sprag team status --show-payload` 로 올라갈 내용을 그대로 볼 수 있습니다. 그 대신 팀 소유자는 대시보드에서 구성원별 합계를 보고, 모든 구성원에게 자동으로 적용될 래칫 룰을 승인할 수 있습니다.
|
|
256
|
+
|
|
257
|
+
`CTS_NO_TEAM_SYNC=1` 은 팀 관련 통신을 모두 끄고, `sprag team leave` 는 참여를 해제합니다.
|
|
258
|
+
|
|
259
|
+
- [팀 기능 안내](docs/TEAM.ko.md)
|
|
260
|
+
- 대시보드: https://sprag.io/team/
|
|
261
|
+
- 플랜과 가격: https://sprag.io/pricing/
|
|
262
|
+
|
|
253
263
|
## 라이선스
|
|
254
264
|
|
|
255
265
|
Apache License 2.0 입니다. 원문은 [LICENSE](https://github.com/rootstudioyaml/sprag/blob/main/LICENSE) 에 있고, 그 라이선스가 요구하는 출처 표기는 [NOTICE](https://github.com/rootstudioyaml/sprag/blob/main/NOTICE) 에 정리했습니다.
|
package/README.md
CHANGED
|
@@ -304,6 +304,16 @@ It files a GitHub issue through the `gh` CLI when one is authenticated. Otherwis
|
|
|
304
304
|
|
|
305
305
|
For a bug, please include the tool version (`sprag --version`) and your OS. For a statusline or warning issue, add the statusline output or the `sprag last` result.
|
|
306
306
|
|
|
307
|
+
## Teams
|
|
308
|
+
|
|
309
|
+
Sprag stays local unless you opt in. With `sprag team join <token>` (or `SPRAG_TEAM_TOKEN` for a whole organization), each machine uploads one row per day: spend, savings, delegated-run count and model-family share. Prompts, transcripts, file paths and project names are never sent, and `sprag team status --show-payload` prints exactly what would go up. In return the team owner sees per-member totals in a dashboard and can approve ratchet rules that every member picks up automatically.
|
|
310
|
+
|
|
311
|
+
`CTS_NO_TEAM_SYNC=1` turns all team traffic off, and `sprag team leave` removes the membership.
|
|
312
|
+
|
|
313
|
+
- [Team layer guide](docs/TEAM.md)
|
|
314
|
+
- Dashboard: https://sprag.io/team/
|
|
315
|
+
- Plans and pricing: https://sprag.io/pricing/
|
|
316
|
+
|
|
307
317
|
## License
|
|
308
318
|
|
|
309
319
|
Apache License 2.0. The full text is in [LICENSE](https://github.com/rootstudioyaml/sprag/blob/main/LICENSE), and the attributions
|
package/bin/cli.js
CHANGED
|
@@ -102,7 +102,7 @@ function segmentsFromArgs() {
|
|
|
102
102
|
const KNOWN_SUBCOMMANDS = new Set([
|
|
103
103
|
'last', 'brief', 'history', 'handoff', 'install', 'uninstall', 'mode', 'korean', 'cohesion',
|
|
104
104
|
'doc2md', 'delegate', 'harness', 'route-scan', 'compact-window', 'update-check', 'upgrade',
|
|
105
|
-
'seed', 'litellm-budget', 'profile-map', 'feedback', 'saved', 'cowork',
|
|
105
|
+
'seed', 'litellm-budget', 'profile-map', 'feedback', 'saved', 'cowork', 'team',
|
|
106
106
|
]);
|
|
107
107
|
|
|
108
108
|
// Flags each entry path reads, collected from the hasFlag/getArg/numArg calls in
|
|
@@ -207,6 +207,7 @@ Usage:
|
|
|
207
207
|
sprag update-check is a newer version out?
|
|
208
208
|
sprag brief --hook internal: the UserPromptSubmit hook entry point
|
|
209
209
|
sprag feedback "<msg>" file a bug report / feature request (no browser needed)
|
|
210
|
+
sprag team create|join|status|sync|leave opt-in team layer: daily aggregates only (CTS_NO_TEAM_SYNC=1 turns it off)
|
|
210
211
|
sprag upgrade install the latest release
|
|
211
212
|
sprag --install-hook install cache-monitor PostToolUse hook
|
|
212
213
|
sprag --uninstall-hook remove that hook
|
|
@@ -302,6 +303,12 @@ async function main() {
|
|
|
302
303
|
return (await import('../src/commands/feedback.js')).run({ args, getArg, version: PKG_VERSION, dataDir: userDataDir() });
|
|
303
304
|
}
|
|
304
305
|
|
|
306
|
+
// Subcommand: team — opt-in team layer (join with an invite token, daily
|
|
307
|
+
// aggregates only, team rules). See src/commands/team.js and src/team-sync.js.
|
|
308
|
+
if (args[0] === 'team') {
|
|
309
|
+
return (await import('../src/commands/team.js')).run({ args, hasFlag, getArg, version: PKG_VERSION });
|
|
310
|
+
}
|
|
311
|
+
|
|
305
312
|
if (args[0] === 'handoff') {
|
|
306
313
|
return (await import('../src/commands/handoff.js')).run({ getArg });
|
|
307
314
|
}
|
package/docs/CODEX.ko.md
CHANGED
|
@@ -200,6 +200,7 @@ sprag delegate shared status --agent codex
|
|
|
200
200
|
# 자동으로 정해진 난도별 모델을 바꾸고 싶을 때만:
|
|
201
201
|
sprag delegate shared map T2 --from PARENT_MODEL --model SMALL_MODEL --effort high --agent codex
|
|
202
202
|
sprag delegate shared map T1 --from PARENT_MODEL --model MEDIUM_MODEL --effort medium --agent codex
|
|
203
|
+
sprag delegate shared map T1H --from PARENT_MODEL --model LARGE_MODEL --effort high --agent codex
|
|
203
204
|
```
|
|
204
205
|
|
|
205
206
|
등록 전에 전역과 프로젝트 범위를 선택합니다. 프로젝트에만 적용하려면 `--global` 대신
|
package/docs/CODEX.md
CHANGED
|
@@ -302,6 +302,7 @@ sprag delegate shared status --agent codex
|
|
|
302
302
|
# optional, to override the automatic tier models:
|
|
303
303
|
sprag delegate shared map T2 --from PARENT_MODEL --model SMALL_MODEL --effort high --agent codex
|
|
304
304
|
sprag delegate shared map T1 --from PARENT_MODEL --model MEDIUM_MODEL --effort medium --agent codex
|
|
305
|
+
sprag delegate shared map T1H --from PARENT_MODEL --model LARGE_MODEL --effort high --agent codex
|
|
305
306
|
```
|
|
306
307
|
|
|
307
308
|
The map lines are optional. A tier without a mapping resolves automatically,
|
package/docs/TEAM.ko.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# 팀 기능
|
|
2
|
+
|
|
3
|
+
[← README](../README.ko.md) · [English](./TEAM.md)
|
|
4
|
+
|
|
5
|
+
Sprag 은 기본적으로 로컬에서만 동작합니다. 팀 기능은 켜야만 쓰이는 선택 기능입니다. 팀원마다 하루 지출과 절감액을 한 대시보드에서 보고, 팀 소유자가 승인한 래칫 룰을 함께 쓸 수 있습니다. `sprag team join <토큰>` 을 실행하거나 환경에 `SPRAG_TEAM_TOKEN` 을 지정하기 전에는 아무것도 전송하지 않습니다. `CTS_NO_TEAM_SYNC=1` 을 지정하면 다른 설정과 상관없이 팀 관련 네트워크 통신이 모두 꺼집니다.
|
|
6
|
+
|
|
7
|
+
## 보내는 것과 보내지 않는 것
|
|
8
|
+
|
|
9
|
+
참여하면 Sprag 은 로컬 날짜 기준으로 하루에 한 행씩 올립니다.
|
|
10
|
+
|
|
11
|
+
| 항목 | 내용 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| 지출(USD) | 로컬 세션 로그로 계산한 금액 |
|
|
14
|
+
| 절감(USD) | 라우팅 원장과 doc2md 원장에서 가져온 금액 |
|
|
15
|
+
| 위임 실행 횟수 | 그날 위임한 실행의 수 |
|
|
16
|
+
| 모델 계열 비중 | 위임 실행 중 모델 계열별 비율 |
|
|
17
|
+
| Sprag 버전 | 설치된 버전 |
|
|
18
|
+
| 에이전트 | `claude` 또는 `codex` |
|
|
19
|
+
| 표시 이름 | 참여할 때 정한 이름이며, 기본값은 OS 사용자 이름 |
|
|
20
|
+
|
|
21
|
+
보내지 않는 것은 프롬프트, 대화 기록, 파일 경로, 프로젝트 이름, 룰 내용, API 키입니다. 룰은 직접 `sprag team propose` 를 실행할 때만 밖으로 나갑니다.
|
|
22
|
+
|
|
23
|
+
업로드는 6시간에 한 번 이하로 실행됩니다. SessionStart 훅이 띄운 분리된 자식 프로세스가 보내며, statusline 을 그리는 경로에서는 절대 실행되지 않습니다.
|
|
24
|
+
|
|
25
|
+
목적지는 `https://hiycfjwswtkixyoqkznm.supabase.co/rest/v1/rpc/*` 입니다(Supabase, ap-northeast-2 리전). 호출하는 RPC 는 `team_join`, `ledger_upsert`, `team_rules_get`, `team_rule_propose`, `team_leave` 입니다.
|
|
26
|
+
|
|
27
|
+
## CLI에서 팀 만들기
|
|
28
|
+
|
|
29
|
+
웹 대시보드 없이 팀을 만들 수 있습니다. 로그인은 메일로 받는 6자리 코드(15분간 유효)로 하며, Sprag 은 로그인 세션을 저장하지 않습니다.
|
|
30
|
+
|
|
31
|
+
터미널에서는 명령 하나를 실행하고 묻는 대로 답하면 됩니다 (이메일, 코드, 팀 이름).
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
sprag team create
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
에이전트나 터미널이 없는 셸에서는 두 단계로 나눕니다.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
sprag team create --email you@example.com --name "Acme"
|
|
41
|
+
# 메일로 코드가 오면 이어서 실행합니다
|
|
42
|
+
sprag team create --email you@example.com --name "Acme" --code 123456
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
첫 단계는 코드만 보냅니다. 두 번째 단계에서 코드를 확인하고 팀을 만든 뒤 초대 토큰, 멤버용 명령(`npm i -g sprag-cli@latest && sprag team join <토큰>`), 조직용 환경 변수(`export SPRAG_TEAM_TOKEN=<토큰>`), 대시보드 주소를 출력합니다. 같은 이메일로 https://sprag.io/team/ 에 로그인하면 팀을 관리할 수 있습니다.
|
|
46
|
+
|
|
47
|
+
기본적으로 이 PC가 첫 멤버로 참여하고 동기화를 한 번 실행합니다. 참여하지 않으려면 `--no-join` 을 붙입니다. 이미 팀에 참여한 PC에서는 먼저 `sprag team leave` 를 실행하거나 `--no-join` 을 붙여야 합니다.
|
|
48
|
+
|
|
49
|
+
## 개인으로 참여하기
|
|
50
|
+
|
|
51
|
+
팀 소유자에게 초대 토큰을 받은 뒤 실행합니다.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
sprag team join <토큰> --name "미나"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Sprag 이 올라갈 항목 목록을 보여 주고 확인을 묻습니다. `--name` 은 표시 이름이며 기본값은 OS 사용자 이름입니다. 터미널이 없는 셸에서는 `--yes` 를 붙여 확인을 대신합니다. 토큰을 생략하면 `SPRAG_TEAM_TOKEN` 을 읽습니다.
|
|
58
|
+
|
|
59
|
+
결과는 `sprag team status` 로 확인합니다.
|
|
60
|
+
|
|
61
|
+
## 조직 단위로 등록하기
|
|
62
|
+
|
|
63
|
+
구성원이 각자 명령을 실행하지 않아도 모든 컴퓨터를 등록하려면 환경에 `SPRAG_TEAM_TOKEN=<토큰>` 을 지정합니다. 흔한 위치는 두 곳입니다.
|
|
64
|
+
|
|
65
|
+
- 셸 프로필: `export SPRAG_TEAM_TOKEN=<토큰>`
|
|
66
|
+
- Claude Code 관리 설정: `{"env":{"SPRAG_TEAM_TOKEN":"…"}}`
|
|
67
|
+
|
|
68
|
+
컴퓨터는 다음 세션이 시작될 때 참여합니다. 그다음 첫 세션에는 팀 이름과 두 명령을 알리는 한 줄 안내가 나옵니다. `sprag team status --show-payload` 는 보내는 내용을 보여 주고, `sprag team leave` 는 전송을 멈춥니다.
|
|
69
|
+
|
|
70
|
+
`leave` 는 환경 변수보다 우선합니다. 팀에서 나온 컴퓨터는 변수가 남아 있어도 저절로 다시 참여하지 않습니다. 소유자가 새 토큰을 발급하고 구성원이 `sprag team join` 을 다시 실행해야 다시 참여합니다.
|
|
71
|
+
|
|
72
|
+
## 팀 룰
|
|
73
|
+
|
|
74
|
+
팀 소유자는 대시보드에서 룰을 승인합니다. 구성원도 룰을 제안할 수 있습니다.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sprag team propose "커밋 전에 lint 실행" --scope ratchet
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`--scope` 는 `ratchet`(기본값) 또는 `model` 입니다. 제안은 소유자가 승인할 때까지 대기 상태로 남습니다.
|
|
81
|
+
|
|
82
|
+
승인된 룰은 다음 동기화 때 내려옵니다. Sprag 은 이를 `~/.claude/ratchet-team.md`(Codex 는 `<CODEX_HOME>/ratchet-team.md`)에 쓰고, 그 파일이 있는 동안 `~/.claude/CLAUDE.md` 의 전역 harness 블록에서 import 합니다. Sprag 이 세션을 시작할 때 파일을 통째로 다시 쓰므로 직접 고치지 마십시오. `sprag team rules` 는 받아 둔 룰을 보여 줍니다. `sprag team leave` 는 멤버 키, 캐시, import 를 지웁니다.
|
|
83
|
+
|
|
84
|
+
## 상태 확인, 전송 내용 확인, 나가기
|
|
85
|
+
|
|
86
|
+
| 명령 | 설명 |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `sprag team join [토큰] [--name <이름>] [--yes]` | 팀에 참여 |
|
|
89
|
+
| `sprag team status [--show-payload]` | 팀, 플랜, 프롬프트 수집 설정, 표시 이름, 마지막 동기화를 보여 줍니다. `--show-payload` 를 붙이면 올라갈 하루 단위 행도 그대로 출력합니다 |
|
|
90
|
+
| `sprag team sync [--force]` | 지금 업로드하고 룰을 받습니다. `--force` 는 6시간 간격을 무시합니다 |
|
|
91
|
+
| `sprag team rules` | 받아 둔 팀 룰 보기 |
|
|
92
|
+
| `sprag team propose "<내용>" [--scope ratchet\|model]` | 팀에 룰 제안 |
|
|
93
|
+
| `sprag team leave` | 팀에서 나가고 업로드 중단 |
|
|
94
|
+
|
|
95
|
+
## 대시보드
|
|
96
|
+
|
|
97
|
+
https://sprag.io/team/ 에서 소유자가 이메일 코드로 로그인하고, 팀을 만들고, 초대 토큰을 받습니다. 여기서 최근 30일 동안의 구성원별 지출과 절감액을 보고, 룰을 승인하거나 삭제하고, 구성원을 내보낼 수 있습니다.
|
|
98
|
+
|
|
99
|
+
## 플랜
|
|
100
|
+
|
|
101
|
+
Free, Team(좌석당 월 $5), 비즈니스(좌석당 월 $10, 프롬프트 수집 포함)가 있습니다. 자세한 내용은 https://sprag.io/pricing/ 에 있습니다.
|
|
102
|
+
|
|
103
|
+
## 개인정보 관련 참고
|
|
104
|
+
|
|
105
|
+
- 소유자는 구성원별 하루 합계를 봅니다. 세션이나 프롬프트는 보지 못합니다.
|
|
106
|
+
- 구성원은 `sprag team status --show-payload` 로 같은 내용을 직접 볼 수 있습니다.
|
|
107
|
+
- 프롬프트 수집은 비즈니스 정책이며, 소유자가 대시보드에서 켤 수 있습니다. 구성원은 `sprag team status` 에서 설정 상태를 확인합니다. 현재 CLI 는 프롬프트를 아직 올리지 않습니다. 업로드는 이후 별도 릴리스에서 다룹니다.
|
|
108
|
+
- 룰 내용은 `sprag team propose` 를 통해서만 밖으로 나갑니다.
|
|
109
|
+
|
|
110
|
+
## 파일과 환경 변수
|
|
111
|
+
|
|
112
|
+
| 이름 | 용도 |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `<사용자 데이터 디렉터리>/team.json` | 참여 상태와 멤버 키(권한 0600) |
|
|
115
|
+
| `<사용자 데이터 디렉터리>/team-rules.json` | 마지막 동기화 때 받은 룰 |
|
|
116
|
+
| `~/.claude/ratchet-team.md` | Claude Code 용 승인된 팀 룰이며 `~/.claude/CLAUDE.md` 에서 import 합니다 |
|
|
117
|
+
| `<CODEX_HOME>/ratchet-team.md` | Codex 용 같은 룰 |
|
|
118
|
+
| `SPRAG_TEAM_TOKEN` | 초대 토큰이며, 다음 세션 시작 때 컴퓨터를 등록합니다 |
|
|
119
|
+
| `CTS_NO_TEAM_SYNC=1` | 팀 관련 네트워크 통신을 모두 끕니다 |
|
package/docs/TEAM.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Team layer
|
|
2
|
+
|
|
3
|
+
[← README](../README.md) · [한국어](./TEAM.ko.md)
|
|
4
|
+
|
|
5
|
+
Sprag is local-only by default. The team layer is an opt-in add-on that lets a group see each member's daily spend and savings in one dashboard, and share the ratchet rules the team owner approves. Nothing is sent until you run `sprag team join <token>` or your environment sets `SPRAG_TEAM_TOKEN`. `CTS_NO_TEAM_SYNC=1` turns off all team network traffic, whatever else is configured.
|
|
6
|
+
|
|
7
|
+
## What is sent and what is not
|
|
8
|
+
|
|
9
|
+
After you join, Sprag uploads one row per local calendar day:
|
|
10
|
+
|
|
11
|
+
| Field | Meaning |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Spend (USD) | priced from your local session logs |
|
|
14
|
+
| Savings (USD) | from the routing and doc2md ledgers |
|
|
15
|
+
| Delegated runs | how many runs were delegated that day |
|
|
16
|
+
| Model-family share | the share of those runs that went to each model family |
|
|
17
|
+
| Sprag version | the installed version |
|
|
18
|
+
| Agent | `claude` or `codex` |
|
|
19
|
+
| Display name | the name you chose at join; the default is your OS user name |
|
|
20
|
+
|
|
21
|
+
Not sent: prompts, transcripts, file paths, project names, rule text, API keys. A rule leaves your machine only when you run `sprag team propose` yourself.
|
|
22
|
+
|
|
23
|
+
The upload runs at most once every 6 hours, from a detached child process that the SessionStart hook spawns. It never runs on the statusline path.
|
|
24
|
+
|
|
25
|
+
Destination: `https://hiycfjwswtkixyoqkznm.supabase.co/rest/v1/rpc/*` (Supabase, region ap-northeast-2). The calls are the RPCs `team_join`, `ledger_upsert`, `team_rules_get`, `team_rule_propose` and `team_leave`.
|
|
26
|
+
|
|
27
|
+
## Create a team from the CLI
|
|
28
|
+
|
|
29
|
+
You can set up a team without the web dashboard. Signing in uses a 6-digit code emailed to you (valid 15 minutes). Sprag never stores the sign-in session.
|
|
30
|
+
|
|
31
|
+
In a terminal, run one command and answer the prompts (email, code, team name):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
sprag team create
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
From an agent or any shell without a terminal, it takes two steps:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
sprag team create --email you@example.com --name "Acme"
|
|
41
|
+
# the code arrives by email, then:
|
|
42
|
+
sprag team create --email you@example.com --name "Acme" --code 123456
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The first step only sends the code. The second verifies it and creates the team. It then prints the invite token, the member command (`npm i -g sprag-cli@latest && sprag team join <token>`), the organization line (`export SPRAG_TEAM_TOKEN=<token>`) and the dashboard address. Sign in to https://sprag.io/team/ with the same email to manage the team.
|
|
46
|
+
|
|
47
|
+
By default this machine joins as the first member and runs one sync. Pass `--no-join` to skip that. If this machine is already in a team, run `sprag team leave` first or pass `--no-join`.
|
|
48
|
+
|
|
49
|
+
## Join as an individual
|
|
50
|
+
|
|
51
|
+
The team owner gives you an invite token. Then:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
sprag team join <token> --name "Mina"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Sprag prints the list of what will be uploaded and asks for confirmation. `--name` sets the display name (default: your OS user name). In a shell without a terminal, pass `--yes` to confirm. If you leave the token out, Sprag reads `SPRAG_TEAM_TOKEN`.
|
|
58
|
+
|
|
59
|
+
Check the result with `sprag team status`.
|
|
60
|
+
|
|
61
|
+
## Enroll an organization
|
|
62
|
+
|
|
63
|
+
To enroll every machine without each person running a command, set `SPRAG_TEAM_TOKEN=<token>` in the environment. Two common places:
|
|
64
|
+
|
|
65
|
+
- a shell profile: `export SPRAG_TEAM_TOKEN=<token>`
|
|
66
|
+
- Claude Code managed settings: `{"env":{"SPRAG_TEAM_TOKEN":"…"}}`
|
|
67
|
+
|
|
68
|
+
The machine joins at the next session start. The first session after that prints a one-line notice that names the team and the two commands that matter: `sprag team status --show-payload` shows what is sent, and `sprag team leave` stops it.
|
|
69
|
+
|
|
70
|
+
`leave` wins over the environment. A machine that left does not rejoin on its own, even with the variable still set. It rejoins only after the owner issues a new token and the member runs `sprag team join` again.
|
|
71
|
+
|
|
72
|
+
## Team rules
|
|
73
|
+
|
|
74
|
+
The team owner approves rules in the dashboard. Members can also suggest one:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sprag team propose "Run lint before every commit" --scope ratchet
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`--scope` is `ratchet` (the default) or `model`. A proposal stays pending until the owner approves it.
|
|
81
|
+
|
|
82
|
+
Approved rules come back with the next sync. Sprag writes them to `~/.claude/ratchet-team.md` (Codex: `<CODEX_HOME>/ratchet-team.md`) and imports that file from the global harness block of `~/.claude/CLAUDE.md` for as long as the file exists. Do not edit the file by hand, because Sprag rewrites it at session start. `sprag team rules` lists the cached rules. `sprag team leave` removes the member key, the cache and the import.
|
|
83
|
+
|
|
84
|
+
## Status, payload and leaving
|
|
85
|
+
|
|
86
|
+
| Command | What it does |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `sprag team join [token] [--name <n>] [--yes]` | Join a team |
|
|
89
|
+
| `sprag team status [--show-payload]` | Team, plan, prompt-capture flag, display name, last sync. `--show-payload` also prints the exact daily rows that would be uploaded |
|
|
90
|
+
| `sprag team sync [--force]` | Upload now and pull rules. `--force` ignores the 6-hour interval |
|
|
91
|
+
| `sprag team rules` | Show the cached team rules |
|
|
92
|
+
| `sprag team propose "<text>" [--scope ratchet\|model]` | Propose a rule to the team |
|
|
93
|
+
| `sprag team leave` | Leave the team and stop uploading |
|
|
94
|
+
|
|
95
|
+
## The dashboard
|
|
96
|
+
|
|
97
|
+
https://sprag.io/team/ is where the owner signs in with an email code, creates a team and gets the invite token. From there the owner sees per-member spend and savings for the last 30 days, approves or removes rules, and removes members.
|
|
98
|
+
|
|
99
|
+
## Plans
|
|
100
|
+
|
|
101
|
+
Free, Team ($5 per seat per month) and Business ($10 per seat per month, with prompt capture). Details are on https://sprag.io/pricing/.
|
|
102
|
+
|
|
103
|
+
## Privacy notes
|
|
104
|
+
|
|
105
|
+
- The owner sees per-member daily totals, not sessions or prompts.
|
|
106
|
+
- A member sees the same payload with `sprag team status --show-payload`.
|
|
107
|
+
- Prompt capture is a Business policy that the owner can switch on in the dashboard. Members see its state in `sprag team status`. The current CLI does not upload prompts yet; that is a separate, later release.
|
|
108
|
+
- Rule text leaves your machine only through `sprag team propose`.
|
|
109
|
+
|
|
110
|
+
## Files and environment variables
|
|
111
|
+
|
|
112
|
+
| Name | Purpose |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `<user data dir>/team.json` | Membership state and the member key (mode 0600) |
|
|
115
|
+
| `<user data dir>/team-rules.json` | Rules pulled at the last sync |
|
|
116
|
+
| `~/.claude/ratchet-team.md` | Approved team rules for Claude Code, imported from `~/.claude/CLAUDE.md` |
|
|
117
|
+
| `<CODEX_HOME>/ratchet-team.md` | The same rules for Codex |
|
|
118
|
+
| `SPRAG_TEAM_TOKEN` | Invite token; enrolls the machine at the next session start |
|
|
119
|
+
| `CTS_NO_TEAM_SYNC=1` | Disables all team network traffic |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.70.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": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"publish:legacy": "node scripts/publish-legacy.mjs",
|
|
15
15
|
"release:notes": "node scripts/release-notes.mjs",
|
|
16
16
|
"site:docs": "node scripts/build-site-docs.mjs",
|
|
17
|
+
"site:serve": "node scripts/serve-site.mjs",
|
|
17
18
|
"test": "node --test",
|
|
18
19
|
"test:mods": "claude plugin test mod/sprag-doc2md && claude plugin test mod/sprag-delegation-meter && claude plugin test mod/sprag-statusline",
|
|
19
20
|
"version": "node scripts/sync-site-version.mjs"
|
|
@@ -30,6 +31,8 @@
|
|
|
30
31
|
"README.ko.md",
|
|
31
32
|
"docs/CODEX.md",
|
|
32
33
|
"docs/CODEX.ko.md",
|
|
34
|
+
"docs/TEAM.md",
|
|
35
|
+
"docs/TEAM.ko.md",
|
|
33
36
|
"LICENSE",
|
|
34
37
|
"NOTICE"
|
|
35
38
|
],
|
package/src/agents.js
CHANGED
|
@@ -26,10 +26,12 @@ const AGENT_MODEL = {
|
|
|
26
26
|
'haiku-runner': 'haiku',
|
|
27
27
|
'haiku-translate': 'haiku',
|
|
28
28
|
'sonnet-worker': 'sonnet',
|
|
29
|
+
'opus-worker': 'opus',
|
|
29
30
|
// A T1 rule names its target by tier alone (`agent: 'sonnet'`). Without these
|
|
30
31
|
// the phrase fell through to the haiku default, and a sonnet rule told the
|
|
31
32
|
// session to delegate to haiku. resolveAgentName maps them to an agent file.
|
|
32
33
|
sonnet: 'sonnet',
|
|
34
|
+
opus: 'opus',
|
|
33
35
|
haiku: 'haiku',
|
|
34
36
|
};
|
|
35
37
|
|
|
@@ -50,7 +52,7 @@ export function agentExists(name, root = process.cwd()) {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/** Agent a bare tier name stands for when that agent's file exists. */
|
|
53
|
-
const TIER_AGENT = { sonnet: 'sonnet-worker', haiku: 'haiku-explore' };
|
|
55
|
+
const TIER_AGENT = { sonnet: 'sonnet-worker', opus: 'opus-worker', haiku: 'haiku-explore' };
|
|
54
56
|
|
|
55
57
|
/**
|
|
56
58
|
* The agent a rule target really names on this machine, or null when none is
|
package/src/codex/delegation.js
CHANGED
|
@@ -36,7 +36,7 @@ export function validateCodexTarget(model, effort) {
|
|
|
36
36
|
|
|
37
37
|
export function validateCodexSharedTarget({ tier, from, model, effort, provider }) {
|
|
38
38
|
validateCodexTarget(model, effort);
|
|
39
|
-
if (!SHARED_RULE_TIERS.includes(tier)) throw new Error('Shared rule tier must be T1 or T2.');
|
|
39
|
+
if (!SHARED_RULE_TIERS.includes(tier)) throw new Error('Shared rule tier must be T1, T1H or T2.');
|
|
40
40
|
if (!safeId(from) || from === model) throw new Error('--from must name a different parent model.');
|
|
41
41
|
if (!safeId(provider)) throw new Error('A shared model mapping requires a Codex provider ID.');
|
|
42
42
|
return { tier, from, model, ...(effort === undefined ? {} : { effort }), provider };
|
|
@@ -78,7 +78,7 @@ function catalogCodexModels(home) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* The models Codex resolves T2 and
|
|
81
|
+
* The models Codex resolves T2, T1 and T1H to when the user mapped none, the way
|
|
82
82
|
* Claude Code resolves `haiku` and `sonnet`. Candidates are the models that
|
|
83
83
|
* already ran in this user's Codex sessions plus, for the built-in OpenAI
|
|
84
84
|
* provider only, the models Codex's catalog lists for the account (a gateway
|
|
@@ -86,7 +86,8 @@ function catalogCodexModels(home) {
|
|
|
86
86
|
* qualifies only if it is priced on the parent's provider and costs less than
|
|
87
87
|
* the parent, so the hint never names a model the gateway cannot serve. The
|
|
88
88
|
* cheapest is T2 and the next one up is T1; with a single candidate T1 stays
|
|
89
|
-
* on the main agent.
|
|
89
|
+
* on the main agent. T1H is the most expensive cheaper model, and only when
|
|
90
|
+
* there are at least three candidates, so that it differs from T1. Returns { targets, reason }.
|
|
90
91
|
*/
|
|
91
92
|
export function autoCodexSharedTargets({ dir = userDataDir(), home = codexUserDir(), model, provider, now = Date.now() } = {}) {
|
|
92
93
|
if (!safeId(model) || !safeId(provider)) return { targets: [], reason: 'unknown parent model or provider' };
|
|
@@ -104,7 +105,9 @@ export function autoCodexSharedTargets({ dir = userDataDir(), home = codexUserDi
|
|
|
104
105
|
? `${model} is already the cheapest available model on ${provider}`
|
|
105
106
|
: `no cheaper priced model has run in your Codex sessions on ${provider}` };
|
|
106
107
|
}
|
|
107
|
-
const
|
|
108
|
+
const picks = cheaper.slice(0, 2).map((x, i) => ({ tier: i === 0 ? 'T2' : 'T1', x }));
|
|
109
|
+
if (cheaper.length >= 3) picks.push({ tier: 'T1H', x: cheaper.at(-1) });
|
|
110
|
+
const targets = picks.map(({ tier, x }) => ({ tier, from: model, model: x.model, provider, auto: true }));
|
|
108
111
|
return { targets, reason: null };
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -240,7 +243,7 @@ export function codexDelegateMinContext(cfg) {
|
|
|
240
243
|
// Codex tells the model to set spawn_agent's model only on the user's request,
|
|
241
244
|
// so the shared hint says where this request came from.
|
|
242
245
|
const SHARED_APPROVAL = 'The user approved this policy and its model mapping, so setting model here follows the user\'s request.';
|
|
243
|
-
const SHARED_TIER_GUIDANCE = 'Choose T2 only for a simple bounded lookup or command. Use T1 for multi-step tool chains or work spanning multiple files. If the required tier is not mapped, keep the task on the main agent. Do not delegate design judgement, diagnosis, or irreversible actions.';
|
|
246
|
+
const SHARED_TIER_GUIDANCE = 'Choose T2 only for a simple bounded lookup or command. Use T1 for multi-step tool chains or work spanning multiple files. Use T1H only for large-scope work that T1 would likely get wrong. If the required tier is not mapped, keep the task on the main agent. Do not delegate design judgement, diagnosis, or irreversible actions.';
|
|
244
247
|
const WAIT_TEXT = 'Wait for it to finish (call wait_agent with timeout_ms of at least 120000, and again if it times out) and do not do the delegated task yourself while it runs; then verify its result and report unfinished work. Do not spawn another CLI process. '
|
|
245
248
|
+ 'If sub-agent tools or the target model are unavailable, keep the task on the main agent and state that delegation was unavailable.';
|
|
246
249
|
|
package/src/codex/hooks.js
CHANGED
|
@@ -183,10 +183,21 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
183
183
|
try { (await import('../preset-ratchet.js')).preparePresetRatchet({ agent: 'codex' }); } catch (e) { debug('codex:preset', e); }
|
|
184
184
|
}
|
|
185
185
|
const { presetRatchetPath } = await import('../preset-ratchet.js');
|
|
186
|
+
let teamRatchetPath = () => '';
|
|
187
|
+
let teamNotice = null;
|
|
188
|
+
try {
|
|
189
|
+
const tr = await import('../team-ratchet.js');
|
|
190
|
+
teamRatchetPath = tr.teamRatchetPath;
|
|
191
|
+
if (event === 'session-start') {
|
|
192
|
+
tr.syncTeamRatchet({ agent: 'codex' });
|
|
193
|
+
tr.spawnTeamSyncIfDue();
|
|
194
|
+
teamNotice = tr.autoJoinNoticeBlock(undefined, 'en');
|
|
195
|
+
}
|
|
196
|
+
} catch (e) { debug('codex:team-ratchet', e); }
|
|
186
197
|
// Lower priority survives the budget first. Rules and style move to a file
|
|
187
198
|
// when they do not fit; offers and notices are simply left out.
|
|
188
199
|
for (const [scope, ratchet] of [['global', codexHarnessPaths(root, 'global').ratchet],
|
|
189
|
-
['preset', presetRatchetPath({ agent: 'codex' })], ['project', codexHarnessPaths(root, 'project').ratchet]]) {
|
|
200
|
+
['preset', presetRatchetPath({ agent: 'codex' })], ['team', teamRatchetPath({ agent: 'codex' })], ['project', codexHarnessPaths(root, 'project').ratchet]]) {
|
|
190
201
|
try {
|
|
191
202
|
if (statSync(ratchet).size > 32 * 1024) {
|
|
192
203
|
parts.push({ priority: 1, text: `Read the ${scope} Sprag ratchet rules at ${JSON.stringify(ratchet)} before working; they exceed the automatic injection limit.` });
|
|
@@ -194,10 +205,11 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
194
205
|
const text = readFileSync(ratchet, 'utf8').trim();
|
|
195
206
|
// An empty preset file is only its header; injecting it would spend
|
|
196
207
|
// tokens every session on no rules.
|
|
197
|
-
if (text && (scope !== 'preset' || /^- /m.test(text))) parts.push({ priority: 1, spill: true, text: `[Sprag Codex ratchet: ${scope}]\n${text}` });
|
|
208
|
+
if (text && ((scope !== 'preset' && scope !== 'team') || /^- /m.test(text))) parts.push({ priority: 1, spill: true, text: `[Sprag Codex ratchet: ${scope}]\n${text}` });
|
|
198
209
|
}
|
|
199
210
|
} catch (e) { if (e?.code !== 'ENOENT') debug('codex:ratchet', e); }
|
|
200
211
|
}
|
|
212
|
+
if (teamNotice) parts.push({ priority: 5, text: teamNotice });
|
|
201
213
|
if (event === 'subagent-start') parts.push({ priority: 0, text:
|
|
202
214
|
'[Sprag Codex delegation]\nStay within the assigned task and any caller-provided budget. Return findings with file references and actual verification results; state unfinished work. Use only available Codex tools and configured roles. Do not infer a model tier or invent a tool-call budget.' });
|
|
203
215
|
// Codex reads the Korean guide from the global AGENTS.md, so the hook adds it
|
package/src/codex/route-scan.js
CHANGED
|
@@ -92,7 +92,9 @@ function pickTarget(group, { prices, known, home }) {
|
|
|
92
92
|
.sort((a, b) => a.usd - b.usd);
|
|
93
93
|
const eligible = cheaper.filter((x) => known.has(`${group.provider}|${x.model}`));
|
|
94
94
|
// Simple work goes to the cheapest model; moderate work steps down one notch.
|
|
95
|
-
|
|
95
|
+
// The heaviest band goes to the most expensive cheaper model (the T1H target).
|
|
96
|
+
const { T2, T1, T1H } = group.tiers;
|
|
97
|
+
const pick = T2 >= T1 && T2 >= T1H ? eligible[0] : eligible.at(-1);
|
|
96
98
|
return {
|
|
97
99
|
suggestedModel: pick?.model ?? null,
|
|
98
100
|
alternatives: pick ? [] : cheaper.slice(0, 3).map((x) => x.model),
|
|
@@ -135,10 +137,10 @@ export async function runCodexRouteScan({ days = 14, now = Date.now(), home, dir
|
|
|
135
137
|
const runtime = codexPromptCategory(ep.text);
|
|
136
138
|
if (!runtime || !categorize(ep.text, ep.tools)) continue;
|
|
137
139
|
const tier = tierOf(ep, runtime, thresholds);
|
|
138
|
-
if (tier !== 'T1' && tier !== 'T2') continue;
|
|
140
|
+
if (tier !== 'T1' && tier !== 'T2' && tier !== 'T1H') continue;
|
|
139
141
|
const key = `${runtime.id}|${ep.provider}|${ep.model}|${ep.projectRoot}`;
|
|
140
142
|
const g = groups.get(key) || { signature: key, category: runtime.id, label: runtime.label, labelEn: runtime.labelEn,
|
|
141
|
-
from: ep.model, provider: ep.provider, projectRoot: ep.projectRoot, count: 0, tiers: { T1: 0, T2: 0 }, example: ep.text,
|
|
143
|
+
from: ep.model, provider: ep.provider, projectRoot: ep.projectRoot, count: 0, tiers: { T1: 0, T2: 0, T1H: 0 }, example: ep.text,
|
|
142
144
|
tokens: { input: 0, cached: 0, cacheWrite: 0, output: 0 }, lastSeen: 0 };
|
|
143
145
|
g.count++;
|
|
144
146
|
g.tiers[tier]++;
|
|
@@ -154,7 +156,7 @@ export async function runCodexRouteScan({ days = 14, now = Date.now(), home, dir
|
|
|
154
156
|
(!r.provider || r.provider === g.provider) &&
|
|
155
157
|
(r.scope === 'global' || r.targetRoot === g.projectRoot))) return true;
|
|
156
158
|
const shared = sharedCodexModelRules({ cfg, dir, root: g.projectRoot, model: g.from, provider: g.provider });
|
|
157
|
-
return ['T1', 'T2'].every((tier) => !g.tiers[tier] || shared.some((r) => r.category === g.category && r.tier === tier));
|
|
159
|
+
return ['T1', 'T2', 'T1H'].every((tier) => !g.tiers[tier] || shared.some((r) => r.category === g.category && r.tier === tier));
|
|
158
160
|
};
|
|
159
161
|
const known = new Set([...observed, ...rules.filter((r) => r.provider).map((r) => `${r.provider}|${r.model}`)]);
|
|
160
162
|
const target = cfg?.codex?.delegateTarget?.model;
|
|
@@ -43,7 +43,7 @@ function printShared(cfg, { details = false } = {}) {
|
|
|
43
43
|
if (missing.length) {
|
|
44
44
|
console.log(`Unmapped tiers for ${parent || 'any parent model'} on ${provider || 'unknown provider'}: ${missing.join(', ')}`
|
|
45
45
|
+ `${autoOn && auto.reason ? ` (auto: ${auto.reason})` : ''}. `
|
|
46
|
-
+ `Set: sprag delegate shared map <T1|T2> --from ${parent || '<parent-model>'} --model <target-model> --agent codex`);
|
|
46
|
+
+ `Set: sprag delegate shared map <T1|T1H|T2> --from ${parent || '<parent-model>'} --model <target-model> --agent codex`);
|
|
47
47
|
}
|
|
48
48
|
console.log(`Automatic tier models: ${autoOn ? 'on' : 'off'} (turn ${autoOn ? 'off' : 'on'}: sprag delegate shared auto ${autoOn ? 'off' : 'on'} --agent codex)`);
|
|
49
49
|
if (!parent && targets.length) console.log('Codex config names no default model; each mapping routes only sessions on its --from model.');
|
|
@@ -74,7 +74,7 @@ export function run({ args, getArg, hasFlag, root }) {
|
|
|
74
74
|
if (sub === 'shared') {
|
|
75
75
|
const action = args[2] || 'status';
|
|
76
76
|
if (!['status', 'on', 'off', 'map', 'unmap', 'min-context', 'auto'].includes(action)) {
|
|
77
|
-
throw new Error('Usage: delegate shared status|on|off|map <T1|T2>|unmap <T1|T2>|min-context <tokens|default>|auto on|off --agent codex');
|
|
77
|
+
throw new Error('Usage: delegate shared status|on|off|map <T1|T1H|T2>|unmap <T1|T1H|T2>|min-context <tokens|default>|auto on|off --agent codex');
|
|
78
78
|
}
|
|
79
79
|
if (hasFlag('--global') || hasFlag('--project')) throw new Error('Shared policies retain their approved scope; mappings do not change it.');
|
|
80
80
|
const provided = (flag) => args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
@@ -86,7 +86,7 @@ export function run({ args, getArg, hasFlag, root }) {
|
|
|
86
86
|
const tier = args[3];
|
|
87
87
|
const from = getArg('--from');
|
|
88
88
|
const provider = getArg('--provider') ?? codexProviderName();
|
|
89
|
-
if (!SHARED_RULE_TIERS.includes(tier)) throw new Error('Shared rule tier must be T1 or T2.');
|
|
89
|
+
if (!SHARED_RULE_TIERS.includes(tier)) throw new Error('Shared rule tier must be T1, T1H or T2.');
|
|
90
90
|
const same = (t) => t.tier === tier && t.from === from && t.provider === provider;
|
|
91
91
|
if (action === 'map') {
|
|
92
92
|
const target = validateCodexSharedTarget({ tier, from, provider, model: getArg('--model'), effort: getArg('--effort') });
|
package/src/commands/codex.js
CHANGED
|
@@ -30,9 +30,9 @@ Usage: sprag <command> --agent codex [options]
|
|
|
30
30
|
delegate implementation After design: resolve T1 with --from, --provider, --session
|
|
31
31
|
delegate rules Codex rules and shared delegation policies
|
|
32
32
|
delegate shared on|off|status Reuse approved policies; keep original scopes
|
|
33
|
-
delegate shared map T1|T2
|
|
33
|
+
delegate shared map T1|T1H|T2 Map a tier: --from <parent> --model <target>
|
|
34
34
|
Optional --provider <id> and --effort <level>
|
|
35
|
-
delegate shared unmap T1|T2
|
|
35
|
+
delegate shared unmap T1|T1H|T2 Remove a mapping: --from <parent> [--provider <id>]
|
|
36
36
|
delegate shared min-context N Parent input tokens before shared guidance; default resets
|
|
37
37
|
delegate shared auto on|off Resolve unmapped tiers from priced models already run in Codex
|
|
38
38
|
route-scan [--refresh] Discover recurring simple turns in local logs
|
|
@@ -202,7 +202,7 @@ export async function run({ args, version }) {
|
|
|
202
202
|
for (const c of open) {
|
|
203
203
|
const target = c.suggestedModel ? `-> ${c.suggestedModel}${Number.isFinite(c.estSavedUsd) ? ` (~${signedUsd(c.estSavedUsd, 4)} over ${c.count} turns)` : ''}`
|
|
204
204
|
: `-> choose --model${c.alternatives.length ? ` (priced cheaper: ${c.alternatives.join(', ')})` : ''}`;
|
|
205
|
-
console.log(`R${c.id} ${c.category} x${c.count} [T2 ${c.tiers.T2} / T1 ${c.tiers.T1}] ${c.provider || 'unknown provider'} / ${c.from} ${target}`);
|
|
205
|
+
console.log(`R${c.id} ${c.category} x${c.count} [T2 ${c.tiers.T2} / T1 ${c.tiers.T1} / T1H ${c.tiers.T1H ?? 0}] ${c.provider || 'unknown provider'} / ${c.from} ${target}`);
|
|
206
206
|
console.log(` ${c.projectRoot}`);
|
|
207
207
|
console.log(` e.g. ${JSON.stringify(c.example)}`);
|
|
208
208
|
}
|
package/src/commands/harness.js
CHANGED
|
@@ -255,7 +255,7 @@ export async function run({ args, hasFlag }) {
|
|
|
255
255
|
example: routeCandidate.example,
|
|
256
256
|
count: routeCandidate.count,
|
|
257
257
|
// Calibrated budget snapshot — ratchet-model.md restates it when it
|
|
258
|
-
// merges a category's T2 and
|
|
258
|
+
// merges a category's T2, T1 and T1H rules into one conditional rule.
|
|
259
259
|
budget: routeCandidate.budget || null,
|
|
260
260
|
promotedAt: new Date().toISOString().slice(0, 10),
|
|
261
261
|
lastSeen: new Date().toISOString().slice(0, 10),
|
|
@@ -303,6 +303,17 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
303
303
|
// bundled rule reaches ratchet-preset.md here, without a reinstall. It
|
|
304
304
|
// also moves presets older versions appended to ratchet.md.
|
|
305
305
|
try { (await import('../preset-ratchet.js')).preparePresetRatchet(); } catch (e) { debug('route-scan:sync-preset', e); }
|
|
306
|
+
// Team layer (opt-in): re-render the team ratchet file from the cached rules,
|
|
307
|
+
// keep the CLAUDE.md import in step with the file, and kick the detached sync
|
|
308
|
+
// when it is due. Nothing here touches the network.
|
|
309
|
+
let teamBlock = null;
|
|
310
|
+
try {
|
|
311
|
+
const tr = await import('../team-ratchet.js');
|
|
312
|
+
tr.syncTeamRatchet({ lang });
|
|
313
|
+
tr.ensureTeamImport();
|
|
314
|
+
tr.spawnTeamSyncIfDue();
|
|
315
|
+
teamBlock = tr.autoJoinNoticeBlock(undefined, lang);
|
|
316
|
+
} catch (e) { debug('route-scan:team', e); }
|
|
306
317
|
// Cowork cannot follow the ~/.claude imports, so it gets a pasted copy.
|
|
307
318
|
try { (await import('../cowork-sync.js')).syncCoworkRatchets({ discover: true }); } catch (e) { debug('route-scan:cowork-sync', e); }
|
|
308
319
|
// Registered rules whose delegated-category error rate crossed the
|
|
@@ -375,6 +386,7 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
375
386
|
} catch (e) { debug('route-scan:update-check', e); /* the offer is optional */ }
|
|
376
387
|
|
|
377
388
|
if (open.length === 0 && reviewRules.length === 0) {
|
|
389
|
+
if (teamBlock) console.log(teamBlock);
|
|
378
390
|
if (updateBlock) console.log(updateBlock);
|
|
379
391
|
if (seedBlock) console.log(seedBlock);
|
|
380
392
|
if (doc2mdBlock) console.log(doc2mdBlock);
|
|
@@ -382,6 +394,7 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
382
394
|
if (cohesionBlock) console.log(cohesionBlock);
|
|
383
395
|
return; // nothing else to inject
|
|
384
396
|
}
|
|
397
|
+
if (teamBlock) console.log(teamBlock);
|
|
385
398
|
if (updateBlock) console.log(updateBlock);
|
|
386
399
|
if (seedBlock) console.log(seedBlock);
|
|
387
400
|
if (doc2mdBlock) console.log(doc2mdBlock);
|
|
@@ -392,10 +405,10 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
392
405
|
if (open.length > 0) {
|
|
393
406
|
if (lang === 'ko') {
|
|
394
407
|
lines.push(`[sprag route-scan] 최근 ${cache.days}일 세션에서 비싼 모델(opus/fable)이 반복 처리해 온, 더 싼 모델로 넘겨도 되는 작업이 감지되었습니다.`);
|
|
395
|
-
lines.push('(R<N>은 후보 번호, T2/T1
|
|
408
|
+
lines.push('(R<N>은 후보 번호, T2/T1/T1H는 난이도 등급입니다 — 사용자에게 전달할 때는 코드가 아니라 아래 풀어쓴 설명으로 브리핑하세요)');
|
|
396
409
|
} else {
|
|
397
410
|
lines.push(`[sprag route-scan] Over the last ${cache.days} days, expensive models (opus/fable) repeatedly handled work that a cheaper model could take.`);
|
|
398
|
-
lines.push('(R<N> is the candidate id, T2/T1 the difficulty tier — brief the user with the spelled-out wording below, not the codes.)');
|
|
411
|
+
lines.push('(R<N> is the candidate id, T2/T1/T1H the difficulty tier — brief the user with the spelled-out wording below, not the codes.)');
|
|
399
412
|
}
|
|
400
413
|
for (const c of open) {
|
|
401
414
|
const tier = c.tier || 'T2';
|
|
@@ -490,7 +503,7 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
490
503
|
(await import('../first-run-note.js')).printOnce('route-scan', lang);
|
|
491
504
|
return;
|
|
492
505
|
}
|
|
493
|
-
console.log(lang === 'ko' ? '\n위임 후보 (R<N>=후보 번호, T2/T1=난이도 등급):' : '\nDelegation candidates (R<N> = candidate id, T2/T1 = difficulty tier):');
|
|
506
|
+
console.log(lang === 'ko' ? '\n위임 후보 (R<N>=후보 번호, T2/T1/T1H=난이도 등급):' : '\nDelegation candidates (R<N> = candidate id, T2/T1/T1H = difficulty tier):');
|
|
494
507
|
for (const c of open) {
|
|
495
508
|
const tier = c.tier || 'T2';
|
|
496
509
|
if (lang === 'ko') {
|