sprag-cli 3.54.0 → 3.55.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/README.ko.md +13 -5
- package/README.md +19 -6
- package/bin/cli.js +4 -4
- package/docs/CODEX.ko.md +17 -5
- package/docs/CODEX.md +20 -8
- package/package.json +6 -3
- package/src/codex-hooks.js +10 -3
- package/src/codex-panel-setup.js +98 -0
- package/src/commands/codex.js +24 -2
- package/src/commands/harness.js +13 -2
- package/src/commands/install.js +106 -10
- package/src/commands/route-scan.js +4 -0
- package/src/commands/seed.js +7 -4
- package/src/commands/uninstall.js +23 -0
- package/src/commands/upgrade.js +13 -0
- package/src/harness-templates.js +33 -1
- package/src/harness.js +26 -1
- package/src/preset-ratchet.js +133 -0
- package/src/seed-rules.js +32 -7
package/README.ko.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<img alt="Sprag" src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup-light.svg" width="220">
|
|
6
6
|
</picture>
|
|
7
7
|
|
|
8
|
-
**Claude Code와 Codex
|
|
8
|
+
**Claude Code와 Codex가 쓰는 토큰을 첫 세션부터 줄입니다**
|
|
9
9
|
|
|
10
10
|
[](https://www.npmjs.com/package/sprag-cli)
|
|
11
11
|
[](https://www.npmjs.com/package/sprag-cli)
|
|
@@ -15,10 +15,17 @@
|
|
|
15
15
|
|
|
16
16
|
[sprag.io](https://sprag.io) · [벤치마크](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md)
|
|
17
17
|
|
|
18
|
+
<a href="https://sprag.io/assets/video/sprag-compare-ko.mp4"><img src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/sprag-compare-ko.gif" width="760" alt="두 터미널에 같은 요청 12건을 넣습니다. sprag 를 적용한 쪽은 조회와 테스트 실행을 저렴한 모델에 넘기고 문서를 Markdown 으로 바꿔 넣어, 하루를 $6.68 대신 $1.83 로 마칩니다."></a>
|
|
19
|
+
|
|
20
|
+
같은 요청 12건, 두 터미널: sprag 적용 시 **$6.68 → $1.83 (72.6% 절감)**<br>
|
|
21
|
+
<sub>예시 요청을 공개 단가로 계산했습니다. <a href="https://sprag.io/assets/video/sprag-compare-ko.mp4">전체 영상 (41초)</a></sub>
|
|
22
|
+
|
|
18
23
|
</div>
|
|
19
24
|
|
|
20
25
|
---
|
|
21
26
|
|
|
27
|
+
**일하는 방식은 그대로 두고 토큰만 줄입니다.** 조회와 확인, 테스트 실행은 저렴한 모델에 넘기고, 문서는 가벼운 Markdown으로 바꿔 넣습니다(54만 토큰짜리 발표 자료가 2.3만 토큰이 됩니다). 세션마다 쓴 금액과 아낀 금액은 상태줄에 바로 나타납니다. `npm i -g sprag-cli` 하나로 설정이 끝나며, 프로젝트마다 따로 설정할 것은 없습니다.
|
|
28
|
+
|
|
22
29
|
**에이전트는 그대로 일하고, 퇴행만 막습니다.** 이름은 스프래그 클러치에서 왔습니다. 앞으로 도는 힘은 그대로 통과시키고, 역회전하는 순간 잠기는 부품입니다.
|
|
23
30
|
|
|
24
31
|
Sprag는 Claude Code와 Codex가 남기는 세션 기록을 읽어 반복 실패와 위임 후보를 찾습니다. 사용자가 승인한 규칙은 다음 세션에도 적용하며, 위임 기록과 모델 단가를 확인할 수 있으면 예상 차액을 원장에 적습니다. 캐시와 사용량 한도 문제는 상태줄이나 패널에서 확인할 수 있습니다.
|
|
@@ -33,6 +40,8 @@ npm i -g sprag-cli # 예전 이름 claude-token-saver 와 같은 패키지입
|
|
|
33
40
|
|
|
34
41
|
Codex 기본 훅과 `AGENTS.md` 하네스, 범위별 래칫 규칙,
|
|
35
42
|
한국어 검사, 프롬프트의 문서 경로 자동 변환, 로컬 토큰 보고서를 사용할 수 있습니다.
|
|
43
|
+
Codex가 설치되어 있으면 `npm i -g sprag-cli` 하나로 Codex도 설정하고, 추천 룰을 묻지 않고
|
|
44
|
+
전역으로 등록합니다(단계마다 확인하려면 `sprag install --manual`). Claude Code를 건드리지 않고
|
|
36
45
|
Codex만 쓴다면 다음과 같이 설치하십시오.
|
|
37
46
|
|
|
38
47
|
```bash
|
|
@@ -41,14 +50,13 @@ sprag install --agent codex
|
|
|
41
50
|
sprag --agent codex --days 7
|
|
42
51
|
sprag panel --agent codex # 별도 터미널에서 실시간 패널 실행
|
|
43
52
|
sprag --statusline --agent codex --text
|
|
44
|
-
sprag doctor --agent codex
|
|
53
|
+
sprag doctor --agent codex # 문제가 생겼을 때만
|
|
45
54
|
sprag route-scan --agent codex
|
|
46
55
|
sprag seed --agent codex
|
|
47
56
|
```
|
|
48
57
|
|
|
49
|
-
설치 후 Codex의 `/hooks`에서 새 훅을 검토하고 신뢰해야 실행됩니다.
|
|
50
|
-
여전히 Claude Code
|
|
51
|
-
Codex 연동을 추가할 수 있습니다. Codex에서도 세션 브리핑, 경고 이력, 인수인계
|
|
58
|
+
설치 후 Codex의 `/hooks`에서 새 훅을 검토하고 신뢰해야 실행됩니다. Codex가 요구하는
|
|
59
|
+
절차입니다. 명령의 기본 대상은 여전히 Claude Code이며, `--agent codex`를 붙이면 Codex를 고릅니다. Codex에서도 세션 브리핑, 경고 이력, 인수인계
|
|
52
60
|
파일을 지원합니다. `sprag delegate on --agent codex`로 모델 위임과 하위 에이전트
|
|
53
61
|
지침 전달을 켤 수 있습니다. 상태 표시는 Sprag 터미널 상태줄과 패널을 사용하십시오.
|
|
54
62
|
Codex 기본 상태줄은 셸 명령을 실행하지 않습니다. 반복된 단순 작업은 위임 후보로,
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<img alt="Sprag" src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup-light.svg" width="220">
|
|
6
6
|
</picture>
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**Cut the tokens Claude Code and Codex spend, from the first session.**
|
|
9
9
|
|
|
10
10
|
[](https://www.npmjs.com/package/sprag-cli)
|
|
11
11
|
[](https://www.npmjs.com/package/sprag-cli)
|
|
@@ -15,10 +15,21 @@
|
|
|
15
15
|
|
|
16
16
|
[sprag.io](https://sprag.io) · [Benchmark](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md)
|
|
17
17
|
|
|
18
|
+
<a href="https://sprag.io/assets/video/sprag-compare-en.mp4"><img src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/sprag-compare-en.gif" width="760" alt="Two terminals receive the same twelve requests. With Sprag, lookups and test runs go to cheaper models and documents go in as Markdown; the day ends at $1.83 against $6.68 without Sprag."></a>
|
|
19
|
+
|
|
20
|
+
Same 12 requests, two terminals: **$6.68 → $1.83 (−72.6%)** with Sprag.<br>
|
|
21
|
+
<sub>Example requests priced at public list rates. <a href="https://sprag.io/assets/video/sprag-compare-en.mp4">Full video (41s)</a></sub>
|
|
22
|
+
|
|
18
23
|
</div>
|
|
19
24
|
|
|
20
25
|
---
|
|
21
26
|
|
|
27
|
+
**Sprag saves tokens without changing how you work.** Lookups, checks and test
|
|
28
|
+
runs go to a cheaper model, documents reach the context as compact Markdown (a
|
|
29
|
+
540k-token deck becomes 23k tokens), and the statusline shows what each session
|
|
30
|
+
spent and saved while it runs. `npm i -g sprag-cli` sets it all up; nothing to
|
|
31
|
+
configure per project.
|
|
32
|
+
|
|
22
33
|
Sprag is named after the sprag clutch: forward motion passes, backspin locks.
|
|
23
34
|
Your agent keeps working, it just stops regressing.
|
|
24
35
|
|
|
@@ -34,14 +45,16 @@ budget and price lookups contact your configured LiteLLM endpoint using its
|
|
|
34
45
|
existing credentials. The CLI requires Node.js 18+ and one TOML parser dependency.
|
|
35
46
|
|
|
36
47
|
```bash
|
|
37
|
-
npm i -g sprag-cli #
|
|
48
|
+
npm i -g sprag-cli # Claude Code, plus Codex when it is installed
|
|
38
49
|
```
|
|
39
50
|
|
|
40
51
|
## Codex
|
|
41
52
|
|
|
42
53
|
Native hooks, an `AGENTS.md` harness, scoped ratchet rules, Korean lint,
|
|
43
54
|
document conversion, token reports, and a session-bound companion panel.
|
|
44
|
-
|
|
55
|
+
`npm i -g sprag-cli` sets Codex up too when it is installed, with the rule
|
|
56
|
+
presets registered globally and nothing asked (`sprag install --manual` asks at
|
|
57
|
+
each step). For a Codex-only install that leaves Claude Code alone:
|
|
45
58
|
|
|
46
59
|
```bash
|
|
47
60
|
npm i -g sprag-cli --ignore-scripts
|
|
@@ -49,13 +62,13 @@ sprag install --agent codex
|
|
|
49
62
|
sprag --agent codex --days 7
|
|
50
63
|
sprag panel --agent codex # live companion in a separate terminal
|
|
51
64
|
sprag --statusline --agent codex --text
|
|
52
|
-
sprag doctor --agent codex
|
|
65
|
+
sprag doctor --agent codex # troubleshooting only
|
|
53
66
|
sprag route-scan --agent codex
|
|
54
67
|
sprag seed --agent codex
|
|
55
68
|
```
|
|
56
69
|
|
|
57
|
-
Review and trust the new hooks in Codex's `/hooks` menu
|
|
58
|
-
Claude Code;
|
|
70
|
+
Review and trust the new hooks in Codex's `/hooks` menu; Codex requires that
|
|
71
|
+
step. Commands still default to Claude Code; add `--agent codex` to pick Codex.
|
|
59
72
|
Codex also supports session briefings, warning history, handoffs, and opt-in
|
|
60
73
|
model routing and subagent guidance (`sprag delegate on --agent codex`). The native Codex footer
|
|
61
74
|
does not accept shell commands; Sprag provides a terminal statusline and panel.
|
package/bin/cli.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* npx sprag --format csv # CSV output
|
|
11
11
|
* npx sprag --project myproj # filter by project
|
|
12
12
|
* npx sprag route-scan # detect recurring easy work → haiku-delegation candidates
|
|
13
|
-
* npx sprag install # set up
|
|
14
|
-
* npx sprag install --
|
|
13
|
+
* npx sprag install # set up everything, presets and Codex included, without asking
|
|
14
|
+
* npx sprag install --manual # confirm each optional step
|
|
15
15
|
* npx sprag --install-hook # install PostToolUse hook
|
|
16
16
|
* npx sprag --uninstall-hook # remove hook
|
|
17
17
|
* npx claude-token-saver --hook-run # internal: called by hook
|
|
@@ -123,8 +123,8 @@ Usage:
|
|
|
123
123
|
--agent claude|codex (default: claude; accepted before or after subcommands)
|
|
124
124
|
sprag route-scan detect recurring easy work → delegation candidates
|
|
125
125
|
sprag profile-map show/refresh the gateway model map (LiteLLM /model/info)
|
|
126
|
-
sprag install set up
|
|
127
|
-
sprag install --
|
|
126
|
+
sprag install set up everything (presets, and Codex when present) without asking
|
|
127
|
+
sprag install --manual confirm each optional step
|
|
128
128
|
sprag uninstall remove everything install added
|
|
129
129
|
sprag harness check score the harness setup in CLAUDE.md
|
|
130
130
|
sprag harness analyze run the harness transcript analysis manually
|
package/docs/CODEX.ko.md
CHANGED
|
@@ -7,13 +7,20 @@ Sprag는 Codex의 기본 훅과 `AGENTS.md`를 사용합니다. Claude Code와
|
|
|
7
7
|
|
|
8
8
|
## 설치
|
|
9
9
|
|
|
10
|
+
```sh
|
|
11
|
+
npm i -g sprag-cli
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Codex가 설치되어 있으면 패키지 설치가 Claude Code 연동, 추천 룰과 함께 Codex도 설정합니다.
|
|
15
|
+
설치 중에 묻는 것은 없으며, 단계마다 확인하려면 `sprag install --manual`을 쓰십시오.
|
|
16
|
+
Claude Code 설정을 건드리지 않고 Codex만 쓰려면 postinstall을 건너뛰고 Codex 설치를 직접 실행합니다.
|
|
17
|
+
|
|
10
18
|
```sh
|
|
11
19
|
npm i -g sprag-cli --ignore-scripts
|
|
12
20
|
sprag install --agent codex
|
|
13
|
-
sprag doctor --agent codex
|
|
14
21
|
```
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
`sprag doctor --agent codex`는 문제가 생겼을 때 쓰는 진단 명령입니다. Codex의 `/hooks`에서 등록된 명령을
|
|
17
24
|
검토하고 신뢰해야 훅이 동작합니다. 설치는 훅을 등록할 뿐 신뢰 설정을 바꾸지 않습니다.
|
|
18
25
|
관리자 정책이나 `features.hooks = false` 설정이 훅을 막을 수도 있습니다.
|
|
19
26
|
|
|
@@ -22,7 +29,12 @@ sprag doctor --agent codex
|
|
|
22
29
|
`AGENTS.override.md`가 있으면 그 파일을 사용하며, 변경 전 사본을 남깁니다.
|
|
23
30
|
Claude 설정이나 `config.toml`은 고치지 않습니다.
|
|
24
31
|
|
|
25
|
-
macOS
|
|
32
|
+
macOS zsh 환경에서는 설치가 tmux 설치 여부를 먼저 확인합니다. tmux가 없으면 Homebrew로 설치하고,
|
|
33
|
+
`CTS_NO_TMUX=1`을 지정하면 이 단계를 건너뜁니다. 이어서 zsh 연동을 추가해 `codex`를 입력하면 같은
|
|
34
|
+
터미널 안에 패널이 인라인으로 붙으며, 이때는 별도 창을 여는 훅을 등록하지 않습니다. tmux를 설치할 수
|
|
35
|
+
없거나 로그인 셸이 zsh가 아니거나 이미 사용자의 `codex` 별칭·함수가 있으면 별도 Terminal 창 방식으로
|
|
36
|
+
대신 동작합니다. `sprag panel shell remove --agent codex`로 끈 설정은 재설치와 업그레이드에도 그대로
|
|
37
|
+
유지되며, `--no-panel`은 tmux 설정과 zsh 연동을 모두 건너뜁니다.
|
|
26
38
|
`sprag panel auto off --agent codex`로 끈 설정은 재설치해도 유지합니다.
|
|
27
39
|
패널 실행 여부는 `sprag panel doctor --agent codex`로 확인하십시오.
|
|
28
40
|
|
|
@@ -62,8 +74,8 @@ tmux 실행은 `--no-daemon`과 호출별 상태줄 설정을 사용합니다. C
|
|
|
62
74
|
설정 파일은 바꾸지 않으며, `--keep-native-statusline`으로 기존 상태줄을 유지할 수 있습니다.
|
|
63
75
|
패널은 Codex 기본 상태줄에 셸 명령을 넣는 방식이 아닙니다.
|
|
64
76
|
|
|
65
|
-
macOS zsh에서 `codex`를 입력할 때 tmux 패널을 함께 열려면
|
|
66
|
-
기존 별칭이나 함수는 덮어쓰지 않습니다.
|
|
77
|
+
설치는 이제 이 연동을 기본으로 추가합니다. macOS zsh에서 `codex`를 입력할 때 tmux 패널을 함께 열려면
|
|
78
|
+
다음 명령을 사용하십시오. 기존 별칭이나 함수는 덮어쓰지 않습니다.
|
|
67
79
|
|
|
68
80
|
```sh
|
|
69
81
|
sprag panel shell install --agent codex
|
package/docs/CODEX.md
CHANGED
|
@@ -8,14 +8,21 @@ subcommand to select Codex explicitly; this does not change a global default.
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
```sh
|
|
12
|
+
npm i -g sprag-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package install sets up Codex on its own when Codex is present, together
|
|
16
|
+
with the Claude Code integration and the bundled presets; nothing is asked
|
|
17
|
+
(`sprag install --manual` asks at each step). For a Codex-only installation that
|
|
18
|
+
leaves Claude Code settings alone, skip the postinstall and run the Codex
|
|
19
|
+
install directly:
|
|
12
20
|
|
|
13
21
|
```sh
|
|
14
22
|
npm i -g sprag-cli --ignore-scripts
|
|
15
23
|
sprag install --agent codex
|
|
16
24
|
```
|
|
17
25
|
|
|
18
|
-
If Sprag is already installed for Claude Code, just run the second command.
|
|
19
26
|
Both integrations can coexist. Codex installation uses `$CODEX_HOME` when set,
|
|
20
27
|
otherwise `~/.codex`. It adds the integration hooks to `hooks.json` and a marked harness
|
|
21
28
|
block to the global `AGENTS.md`. A non-empty `AGENTS.override.md` takes precedence,
|
|
@@ -28,10 +35,15 @@ changed hooks do not run until trusted. Use a Codex release that supports the
|
|
|
28
35
|
events documented below. Admin policies or `features.hooks = false` may disable
|
|
29
36
|
hooks. The `sprag` executable and Node.js must be on Codex's PATH.
|
|
30
37
|
|
|
31
|
-
On macOS, installation also
|
|
32
|
-
|
|
33
|
-
`
|
|
34
|
-
|
|
38
|
+
On macOS with zsh, installation also makes sure tmux is present: it installs
|
|
39
|
+
tmux with Homebrew when missing (skip this with `CTS_NO_TMUX=1`) and adds the
|
|
40
|
+
zsh integration so typing `codex` shows the panel inline in the same terminal.
|
|
41
|
+
The separate-window hook is then not registered. When tmux cannot be
|
|
42
|
+
installed, the login shell is not zsh, or an existing `codex` alias or
|
|
43
|
+
function is already present, installation falls back to the separate window
|
|
44
|
+
instead. `sprag panel shell remove --agent codex` is remembered across
|
|
45
|
+
reinstalls and upgrades, and `--no-panel` skips both.
|
|
46
|
+
An explicit `panel auto off` preference survives reinstall. Installation
|
|
35
47
|
reports registration, not successful activation. After approval and restarting
|
|
36
48
|
Codex, run `sprag panel doctor --agent codex` to check the last hook execution,
|
|
37
49
|
any launch failure, and recent rendered frames. These diagnostics are local;
|
|
@@ -308,8 +320,8 @@ counts rather than a Codex dollar saving or billing measurement.
|
|
|
308
320
|
### Live Companion Panel
|
|
309
321
|
|
|
310
322
|
Codex 0.157.1 was observed to defer SessionStart until the first submitted prompt,
|
|
311
|
-
even after hook trust is granted.
|
|
312
|
-
macOS zsh
|
|
323
|
+
even after hook trust is granted. Installation now adds this shell integration by
|
|
324
|
+
default on macOS zsh; to install or remove it manually:
|
|
313
325
|
|
|
314
326
|
```sh
|
|
315
327
|
sprag panel shell install --agent codex
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.55.0",
|
|
4
|
+
"description": "Cut the tokens Claude Code and Codex spend: routes simple work to cheaper models, converts documents to compact Markdown, and shows spend and savings live. Ratchet rules and Korean writing checks included.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sprag": "bin/cli.js",
|
|
@@ -14,7 +14,8 @@
|
|
|
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
|
-
"test": "node --test"
|
|
17
|
+
"test": "node --test",
|
|
18
|
+
"version": "node scripts/sync-site-version.mjs"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"bin/",
|
|
@@ -48,6 +49,8 @@
|
|
|
48
49
|
"subagent",
|
|
49
50
|
"haiku",
|
|
50
51
|
"cost-savings",
|
|
52
|
+
"token-saver",
|
|
53
|
+
"reduce-tokens",
|
|
51
54
|
"token-usage",
|
|
52
55
|
"prompt-caching",
|
|
53
56
|
"cache",
|
package/src/codex-hooks.js
CHANGED
|
@@ -76,14 +76,21 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig() } = {
|
|
|
76
76
|
if (event === 'subagent-start' && cfg?.codex?.delegate !== true) return null;
|
|
77
77
|
const root = findProjectRoot(payload.cwd || process.cwd(), { agent: 'codex' });
|
|
78
78
|
const parts = [];
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// Keep the preset file current with this version's wording before it is read.
|
|
80
|
+
if (event === 'session-start') {
|
|
81
|
+
try { (await import('./preset-ratchet.js')).preparePresetRatchet({ agent: 'codex' }); } catch { /* Presets are optional. */ }
|
|
82
|
+
}
|
|
83
|
+
const { presetRatchetPath } = await import('./preset-ratchet.js');
|
|
84
|
+
for (const [scope, ratchet] of [['global', codexHarnessPaths(root, 'global').ratchet],
|
|
85
|
+
['preset', presetRatchetPath({ agent: 'codex' })], ['project', codexHarnessPaths(root, 'project').ratchet]]) {
|
|
81
86
|
try {
|
|
82
87
|
if (statSync(ratchet).size > 32 * 1024) {
|
|
83
88
|
parts.push(`Read the ${scope} Sprag ratchet rules at ${JSON.stringify(ratchet)} before working; they exceed the automatic injection limit.`);
|
|
84
89
|
} else {
|
|
85
90
|
const text = readFileSync(ratchet, 'utf8').trim();
|
|
86
|
-
|
|
91
|
+
// An empty preset file is only its header; injecting it would spend
|
|
92
|
+
// tokens every session on no rules.
|
|
93
|
+
if (text && (scope !== 'preset' || /^- /m.test(text))) parts.push(`[Sprag Codex ratchet: ${scope}]\n${text}`);
|
|
87
94
|
}
|
|
88
95
|
} catch { /* No rules registered in this scope. */ }
|
|
89
96
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets up the Codex companion panel to run inline, inside the same terminal,
|
|
3
|
+
* instead of opening a separate macOS Terminal window. Codex's native footer
|
|
4
|
+
* cannot run a command, so the panel is the closest thing Codex has to a
|
|
5
|
+
* statusline; showing it below the Codex pane in tmux (via a zsh `codex()`
|
|
6
|
+
* wrapper) means a user does not get a new window per session. Install falls
|
|
7
|
+
* back to the separate window when tmux cannot be set up, the login shell is
|
|
8
|
+
* not zsh, or the user already has their own `codex` alias/function.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { basename, dirname, join } from 'node:path';
|
|
14
|
+
import { installPanelShell } from './codex-panel-shell.js';
|
|
15
|
+
|
|
16
|
+
/** Whether a tmux binary answers `-V`. */
|
|
17
|
+
export function tmuxAvailable({ exec = execFileSync } = {}) {
|
|
18
|
+
try { exec('tmux', ['-V'], { stdio: 'pipe' }); return true; }
|
|
19
|
+
catch { return false; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ensureTmux({ platform = process.platform, env = process.env, exec = execFileSync, exists = existsSync } = {}) {
|
|
23
|
+
// Checked first, before probing anything, so tests and CI never spawn brew.
|
|
24
|
+
if (env.CTS_NO_TMUX === '1' || (env.CI && env.CI !== 'false')) return { status: 'skipped' };
|
|
25
|
+
if (tmuxAvailable({ exec })) return { status: 'present' };
|
|
26
|
+
if (platform !== 'darwin') {
|
|
27
|
+
return { status: 'unavailable', detail: 'install tmux with your package manager (e.g. sudo apt install tmux)' };
|
|
28
|
+
}
|
|
29
|
+
let brew;
|
|
30
|
+
try { exec('brew', ['--prefix'], { stdio: 'pipe' }); brew = 'brew'; }
|
|
31
|
+
catch {
|
|
32
|
+
for (const p of ['/opt/homebrew/bin/brew', '/usr/local/bin/brew']) {
|
|
33
|
+
if (exists(p)) { brew = p; break; }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!brew) {
|
|
37
|
+
return { status: 'unavailable', detail: 'Homebrew not found; install tmux from https://github.com/tmux/tmux/wiki/Installing or MacPorts (sudo port install tmux)' };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
// brew update alone can take minutes; postinstall cannot afford that here.
|
|
41
|
+
exec(brew, ['install', 'tmux'], { stdio: 'inherit', timeout: 600_000,
|
|
42
|
+
env: { ...env, HOMEBREW_NO_AUTO_UPDATE: '1', HOMEBREW_NO_INSTALL_CLEANUP: '1' } });
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { status: 'failed', via: 'brew', detail: String(e.message).split('\n')[0] };
|
|
45
|
+
}
|
|
46
|
+
// No npm-package fallback: no official npm tmux exists, and a third-party
|
|
47
|
+
// prebuilt binary would be an unaudited supply-chain risk.
|
|
48
|
+
const fallback = join(dirname(brew === 'brew' ? '/opt/homebrew/bin/brew' : brew), 'tmux');
|
|
49
|
+
if (tmuxAvailable({ exec }) || exists(fallback)) return { status: 'installed', via: 'brew' };
|
|
50
|
+
return { status: 'failed', via: 'brew', detail: 'tmux not on PATH after brew install' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function setupCodexPanel({ platform = process.platform, env = process.env, config = {}, noPanel = false,
|
|
54
|
+
exec, exists, installShell = installPanelShell, tmux = ensureTmux } = {}) {
|
|
55
|
+
if (noPanel) return { panelAuto: false, mode: 'off' };
|
|
56
|
+
// Panel auto and the zsh integration are macOS-only today.
|
|
57
|
+
if (platform !== 'darwin') return { panelAuto: false, mode: 'off' };
|
|
58
|
+
const windowAuto = config?.codex?.panelAuto !== false;
|
|
59
|
+
if (config?.codex?.panelShell === false) {
|
|
60
|
+
return { panelAuto: windowAuto, mode: windowAuto ? 'window' : 'off', reason: 'shell integration turned off' };
|
|
61
|
+
}
|
|
62
|
+
if (basename(env.SHELL || '') !== 'zsh') {
|
|
63
|
+
return { panelAuto: windowAuto, mode: windowAuto ? 'window' : 'off', reason: 'login shell is not zsh' };
|
|
64
|
+
}
|
|
65
|
+
const t = tmux({ platform, env, ...(exec && { exec }), ...(exists && { exists }) });
|
|
66
|
+
if (!['present', 'installed'].includes(t.status)) {
|
|
67
|
+
return { panelAuto: windowAuto, mode: windowAuto ? 'window' : 'off', reason: t.detail || t.status, tmux: t };
|
|
68
|
+
}
|
|
69
|
+
let shellFile;
|
|
70
|
+
try { shellFile = installShell(); }
|
|
71
|
+
catch (e) {
|
|
72
|
+
return { panelAuto: windowAuto, mode: windowAuto ? 'window' : 'off', reason: e.message, tmux: t };
|
|
73
|
+
}
|
|
74
|
+
// The inline panel replaces the separate window by default; an explicit
|
|
75
|
+
// `panel auto on` keeps both.
|
|
76
|
+
return { panelAuto: config?.codex?.panelAuto === true, mode: 'inline', tmux: t, shellFile };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Formats a `setupCodexPanel` result as lines a caller prefixes and prints. */
|
|
80
|
+
export function describePanelSetup(result, lang = 'en') {
|
|
81
|
+
if (result.mode === 'off') return [];
|
|
82
|
+
if (result.mode === 'inline') {
|
|
83
|
+
const lines = [lang === 'ko'
|
|
84
|
+
? `패널: tmux 인라인 (${result.shellFile}). 새 터미널에서 codex 를 실행하면 같은 창 아래쪽에 패널이 붙습니다.`
|
|
85
|
+
: `panel: inline tmux (${result.shellFile}). Open a new terminal and run codex; the panel attaches below it.`];
|
|
86
|
+
if (result.tmux?.status === 'installed') {
|
|
87
|
+
lines.push(lang === 'ko' ? 'tmux 를 Homebrew 로 설치했습니다.' : 'installed tmux with Homebrew.');
|
|
88
|
+
}
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
const lines = [lang === 'ko'
|
|
92
|
+
? `패널: 별도 Terminal 창${result.reason ? ` (${result.reason})` : ''}`
|
|
93
|
+
: `panel: separate Terminal window${result.reason ? ` (${result.reason})` : ''}`];
|
|
94
|
+
if (result.tmux?.detail && ['unavailable', 'failed'].includes(result.tmux.status)) {
|
|
95
|
+
lines.push(`tmux: ${result.tmux.detail}`);
|
|
96
|
+
}
|
|
97
|
+
return lines;
|
|
98
|
+
}
|
package/src/commands/codex.js
CHANGED
|
@@ -272,6 +272,11 @@ export async function run({ args, version }) {
|
|
|
272
272
|
if (!['install', 'remove'].includes(args[2])) throw new Error('Usage: sprag panel shell install|remove --agent codex');
|
|
273
273
|
const { installPanelShell } = await import('../codex-panel-shell.js');
|
|
274
274
|
const file = installPanelShell({ remove: args[2] === 'remove' });
|
|
275
|
+
// install adds the integration by default, so a removal must be
|
|
276
|
+
// remembered or the next upgrade puts it back.
|
|
277
|
+
const cfg = loadConfig();
|
|
278
|
+
cfg.codex = { ...cfg.codex, panelShell: args[2] === 'install' };
|
|
279
|
+
saveConfig(cfg);
|
|
275
280
|
console.log(`Shell integration ${args[2]}: ${file}. Open a new terminal to apply it.`);
|
|
276
281
|
return;
|
|
277
282
|
}
|
|
@@ -377,17 +382,25 @@ export async function run({ args, version }) {
|
|
|
377
382
|
}
|
|
378
383
|
if (cmd === 'install') {
|
|
379
384
|
const cfg = loadConfig();
|
|
380
|
-
const
|
|
385
|
+
const { setupCodexPanel, describePanelSetup } = await import('../codex-panel-setup.js');
|
|
386
|
+
const panel = setupCodexPanel({ config: cfg || {}, noPanel: hasFlag('--no-panel') });
|
|
387
|
+
const panelAuto = panel.panelAuto;
|
|
381
388
|
const hook = configureCodexHooks({ panelAuto });
|
|
382
389
|
console.log(`Codex hooks: ${hook.file} (${hook.action})`);
|
|
390
|
+
for (const line of describePanelSetup(panel)) console.log(`Codex ${line}`);
|
|
383
391
|
if (process.env.CTS_NO_HARNESS !== '1') {
|
|
384
392
|
const h = initCodexHarness({ root, scope: 'global' });
|
|
385
393
|
console.log(`Codex harness: ${h.file}`);
|
|
386
394
|
}
|
|
395
|
+
try {
|
|
396
|
+
const { preparePresetRatchet } = await import('../preset-ratchet.js');
|
|
397
|
+
const p = preparePresetRatchet({ agent: 'codex' });
|
|
398
|
+
console.log(`Codex preset rules: ${p.file} (${p.rules} registered${p.moved ? `, ${p.moved} moved from ratchet.md` : ''})`);
|
|
399
|
+
} catch { /* Presets are optional; `seed` still registers them. */ }
|
|
387
400
|
console.log('Open /hooks in Codex and review/trust the Sprag hooks before they can run.');
|
|
388
401
|
console.log(`Panel auto-start: ${panelAuto ? 'registered with the other hooks' : 'off'}. Hook registration is complete; runtime activation is not verified.`);
|
|
389
402
|
console.log('After approval, start or resume Codex and run: sprag panel doctor --agent codex');
|
|
390
|
-
if (
|
|
403
|
+
if (panel.mode === 'window') console.log('Some Codex versions defer SessionStart until the first prompt. For immediate opening on typing codex: sprag panel shell install --agent codex (optional zsh integration).');
|
|
391
404
|
console.log('Existing config.toml, Claude Code settings, and ratchet rules are preserved.');
|
|
392
405
|
console.log('Korean guidance is opt-in: sprag korean on --agent codex');
|
|
393
406
|
return;
|
|
@@ -444,6 +457,15 @@ export async function run({ args, version }) {
|
|
|
444
457
|
const r = harnessListRules(opts);
|
|
445
458
|
console.log(`Codex ratchet [${scope}]: ${r.path}`);
|
|
446
459
|
for (const rule of r.rules) console.log(`#${rule.index} ${rule.text}`);
|
|
460
|
+
if (scope === 'global') {
|
|
461
|
+
const { acceptedGlobalPresets, presetRatchetPath } = await import('../preset-ratchet.js');
|
|
462
|
+
const presets = acceptedGlobalPresets({ agent: 'codex' });
|
|
463
|
+
if (presets.length) {
|
|
464
|
+
console.log(`Codex preset rules [global]: ${presetRatchetPath({ agent: 'codex' })}`);
|
|
465
|
+
for (const p of presets) console.log(`${p.id} ${p.text}`);
|
|
466
|
+
console.log('Remove a preset with: sprag seed skip <id> --agent codex');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
447
469
|
} else if (sub === 'rm') {
|
|
448
470
|
const raw = args[2];
|
|
449
471
|
// Locate the index even when scope flags precede it.
|
package/src/commands/harness.js
CHANGED
|
@@ -337,7 +337,7 @@ export async function run({ args, hasFlag }) {
|
|
|
337
337
|
// rules, and a project ratchet inherits global anyway). Strictly opt-in:
|
|
338
338
|
// install/init never auto-injects rules.
|
|
339
339
|
const scope = parseHarnessScope(args.slice(2), 'global');
|
|
340
|
-
const r = harnessPull({ scope });
|
|
340
|
+
const r = await harnessPull({ scope });
|
|
341
341
|
console.log(`Curated preset rules → ${r.path} [${r.scope}]`);
|
|
342
342
|
if (r.added.length) {
|
|
343
343
|
console.log(`✅ ${r.added.length}/${r.presets} rule(s) registered:`);
|
|
@@ -366,7 +366,18 @@ export async function run({ args, hasFlag }) {
|
|
|
366
366
|
console.log('');
|
|
367
367
|
};
|
|
368
368
|
if (wantProject) print('project');
|
|
369
|
-
if (wantGlobal)
|
|
369
|
+
if (wantGlobal) {
|
|
370
|
+
print('global');
|
|
371
|
+
// Presets live in their own tool-owned file and are removed by id,
|
|
372
|
+
// so they are listed apart from the numbered rules `rm <N>` acts on.
|
|
373
|
+
const { acceptedGlobalPresets, presetRatchetPath } = await import('../preset-ratchet.js');
|
|
374
|
+
const presets = acceptedGlobalPresets();
|
|
375
|
+
if (presets.length) {
|
|
376
|
+
console.log(`📦 Preset rules [global] — ${presetRatchetPath()}\n`);
|
|
377
|
+
for (const p of presets) console.log(` ${p.id} ${p.text}`);
|
|
378
|
+
console.log('\nRemove a preset with: sprag seed skip <id>\n');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
370
381
|
console.log('Remove with: sprag harness rm [--global|--project] <N>');
|
|
371
382
|
console.log('Archive in bulk: sprag harness prune [--global] [--tag <t>] [--older-than <months>] [--dry-run]');
|
|
372
383
|
return;
|
package/src/commands/install.js
CHANGED
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
* a legacy command file is removed automatically. Cross-platform.
|
|
6
6
|
* sprag install # install/update the skill
|
|
7
7
|
* sprag install --force # overwrite existing skill file
|
|
8
|
-
* sprag install --
|
|
9
|
-
*
|
|
8
|
+
* sprag install --manual # ask before each optional step instead
|
|
9
|
+
*
|
|
10
|
+
* The default is automatic: nothing is asked, the bundled presets are
|
|
11
|
+
* registered globally, and Codex is set up too when ~/.codex exists. `--manual`
|
|
12
|
+
* restores the step-by-step questions (they still need a terminal attached).
|
|
13
|
+
* `--yes` and `--no-input` are accepted as the explicit spelling of the default.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
import { debug } from '../debug.js';
|
|
@@ -19,12 +23,13 @@ export async function run({ hasFlag }) {
|
|
|
19
23
|
const { userLanguage, languageDecided, setUserLanguage } = await import('../config.js');
|
|
20
24
|
const { canPrompt, confirm } = await import('../prompt.js');
|
|
21
25
|
const force = hasFlag('--force');
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
const
|
|
26
|
+
// Automatic unless asked otherwise. A question per step turned the install
|
|
27
|
+
// into a dozen answers before any work, and every answer has a sensible
|
|
28
|
+
// default: the harness and presets are what make the first session save
|
|
29
|
+
// tokens. `--manual` brings the questions back, and only with a terminal
|
|
30
|
+
// attached, so postinstall, CI and pipes never wait on input.
|
|
31
|
+
const manual = hasFlag('--manual') && !hasFlag('--yes') && !hasFlag('--no-input');
|
|
32
|
+
const interactive = manual && canPrompt();
|
|
28
33
|
|
|
29
34
|
// Output language, decided first because every line below it — and every
|
|
30
35
|
// briefing the hooks inject from here on — is written in it. Until now it
|
|
@@ -342,10 +347,37 @@ export async function run({ hasFlag }) {
|
|
|
342
347
|
// postinstall cannot hold a conversation, and a prompt here would be
|
|
343
348
|
// answered by whoever happens to be at the terminal for a set of rules
|
|
344
349
|
// they have not read. All the install does is say what is waiting.
|
|
350
|
+
// Presets now live in ratchet-preset.md. Moving the ones older versions
|
|
351
|
+
// appended to ratchet.md comes first, so they are not offered again below.
|
|
345
352
|
try {
|
|
346
|
-
const {
|
|
353
|
+
const { preparePresetRatchet } = await import('../preset-ratchet.js');
|
|
354
|
+
const p = preparePresetRatchet();
|
|
355
|
+
if (p.moved) {
|
|
356
|
+
console.log('');
|
|
357
|
+
console.log(lang === 'ko'
|
|
358
|
+
? ` preset: ratchet.md 에 있던 추천 룰 ${p.moved}건을 ${p.file} 로 옮겼습니다 (원본은 ratchet.md.bak-preset-migration).`
|
|
359
|
+
: ` preset: moved ${p.moved} bundled rule(s) from ratchet.md to ${p.file} (original kept as ratchet.md.bak-preset-migration).`);
|
|
360
|
+
}
|
|
361
|
+
} catch (e) {
|
|
362
|
+
debug('install:preset-ratchet', e); // the next session start retries
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const { pendingSeeds, acceptSeed } = await import('../seed-rules.js');
|
|
347
367
|
const pending = pendingSeeds();
|
|
348
|
-
if (pending.length > 0) {
|
|
368
|
+
if (pending.length > 0 && !manual) {
|
|
369
|
+
// Automatic install registers every preset globally. Global because a
|
|
370
|
+
// preset is a tool-wide rule, not a project's; `sprag seed` lists them
|
|
371
|
+
// and `sprag harness rm` removes any one afterwards.
|
|
372
|
+
let n = 0;
|
|
373
|
+
for (const p of pending) {
|
|
374
|
+
try { if (await acceptSeed(p.id, { scope: 'global' })) n += 1; } catch (e) { debug('install:seed-accept', e); }
|
|
375
|
+
}
|
|
376
|
+
console.log('');
|
|
377
|
+
console.log(lang === 'ko'
|
|
378
|
+
? ` seed: 추천 룰 ${n}건을 전체 프로젝트에 등록했습니다 (모델 피팅 + 래칫 프리셋). 목록: sprag seed`
|
|
379
|
+
: ` seed: registered ${n} recommended rule(s) for all projects (model-fitting + ratchet presets). List: sprag seed`);
|
|
380
|
+
} else if (pending.length > 0) {
|
|
349
381
|
console.log('');
|
|
350
382
|
console.log(lang === 'ko'
|
|
351
383
|
? ` seed: 추천 룰 ${pending.length}건이 대기 중입니다 (모델 피팅 + 래칫 프리셋).`
|
|
@@ -358,6 +390,70 @@ export async function run({ hasFlag }) {
|
|
|
358
390
|
debug('install:seed-offer', e); // optional feature; never fail install
|
|
359
391
|
}
|
|
360
392
|
|
|
393
|
+
// Codex, when it is installed on this machine. Without this a Codex user
|
|
394
|
+
// had to know to run `sprag install --agent codex` after the package
|
|
395
|
+
// install. Registration is all we can do: Codex still asks the user to
|
|
396
|
+
// trust the hooks in /hooks before they run, and that step stays theirs.
|
|
397
|
+
try {
|
|
398
|
+
const { codexUserDir } = await import('../agent.js');
|
|
399
|
+
const { existsSync } = await import('node:fs');
|
|
400
|
+
if (process.env.CTS_NO_CODEX !== '1' && existsSync(codexUserDir())) {
|
|
401
|
+
let proceed = true;
|
|
402
|
+
if (interactive) {
|
|
403
|
+
console.log('');
|
|
404
|
+
proceed = await confirm(lang === 'ko'
|
|
405
|
+
? ' codex: Codex 가 설치되어 있습니다. Codex 훅과 하네스도 함께 설정할까요?'
|
|
406
|
+
: ' codex: Codex is installed here. Set up its hooks and harness too?', { defaultValue: true });
|
|
407
|
+
}
|
|
408
|
+
if (proceed) {
|
|
409
|
+
const { configureCodexHooks } = await import('../codex-installer.js');
|
|
410
|
+
const { initCodexHarness } = await import('../codex-harness.js');
|
|
411
|
+
const { loadConfig } = await import('../config.js');
|
|
412
|
+
const { setupCodexPanel, describePanelSetup } = await import('../codex-panel-setup.js');
|
|
413
|
+
const config = loadConfig() || {};
|
|
414
|
+
// A manual install asks before touching the system package manager.
|
|
415
|
+
let env = process.env;
|
|
416
|
+
if (interactive && process.platform === 'darwin') {
|
|
417
|
+
const { tmuxAvailable } = await import('../codex-panel-setup.js');
|
|
418
|
+
if (!tmuxAvailable() && !await confirm(lang === 'ko'
|
|
419
|
+
? ' codex: tmux 가 없습니다. Homebrew 로 설치해 같은 창에 패널을 붙일까요?'
|
|
420
|
+
: ' codex: tmux is missing. Install it with Homebrew so the panel shares the Codex window?', { defaultValue: true })) {
|
|
421
|
+
env = { ...process.env, CTS_NO_TMUX: '1' };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let panel;
|
|
425
|
+
try { panel = setupCodexPanel({ config, env }); }
|
|
426
|
+
catch (e) {
|
|
427
|
+
debug('install:codex-panel', e);
|
|
428
|
+
const auto = process.platform === 'darwin' && config.codex?.panelAuto !== false;
|
|
429
|
+
panel = { panelAuto: auto, mode: auto ? 'window' : 'off' };
|
|
430
|
+
}
|
|
431
|
+
const hook = configureCodexHooks({ panelAuto: panel.panelAuto });
|
|
432
|
+
console.log('');
|
|
433
|
+
console.log(` codex: hooks ${hook.action} (${hook.file})`);
|
|
434
|
+
for (const line of describePanelSetup(panel, lang)) console.log(` codex: ${line}`);
|
|
435
|
+
if (process.env.CTS_NO_HARNESS !== '1') {
|
|
436
|
+
const h = initCodexHarness({ scope: 'global' });
|
|
437
|
+
console.log(` codex: harness ${h.file}`);
|
|
438
|
+
}
|
|
439
|
+
try { (await import('../preset-ratchet.js')).preparePresetRatchet({ agent: 'codex' }); } catch (e) { debug('install:codex-preset', e); }
|
|
440
|
+
if (!manual) {
|
|
441
|
+
const { pendingSeeds, acceptSeed } = await import('../seed-rules.js');
|
|
442
|
+
let n = 0;
|
|
443
|
+
for (const p of pendingSeeds({ agent: 'codex' })) {
|
|
444
|
+
try { if (await acceptSeed(p.id, { scope: 'global', agent: 'codex' })) n += 1; } catch (e) { debug('install:codex-seed', e); }
|
|
445
|
+
}
|
|
446
|
+
if (n) console.log(lang === 'ko' ? ` codex: 추천 룰 ${n}건 등록` : ` codex: registered ${n} recommended rule(s)`);
|
|
447
|
+
}
|
|
448
|
+
console.log(lang === 'ko'
|
|
449
|
+
? ' codex: Codex 에서 /hooks 를 열어 Sprag 훅을 신뢰해야 실행됩니다 (Codex 보안 정책이라 자동화할 수 없습니다).'
|
|
450
|
+
: " codex: open /hooks in Codex and trust the Sprag hooks; Codex requires that step and it cannot be automated.");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch (e) {
|
|
454
|
+
debug('install:codex', e); // optional; `sprag install --agent codex` still works
|
|
455
|
+
}
|
|
456
|
+
|
|
361
457
|
console.log('');
|
|
362
458
|
console.log('Open Claude Code in any directory and just mention:');
|
|
363
459
|
console.log(' "cache hit rate" / "1M context" / "5H cap" — the skill auto-activates.');
|
|
@@ -277,6 +277,10 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
277
277
|
// when the rendering differs, so the common case is a read and a string
|
|
278
278
|
// compare.
|
|
279
279
|
try { mrHook.syncAllFiles(); } catch (e) { debug('route-scan:sync-ratchet', e); }
|
|
280
|
+
// Same reason for the preset ratchet: an upgrade that rewords or drops a
|
|
281
|
+
// bundled rule reaches ratchet-preset.md here, without a reinstall. It
|
|
282
|
+
// also moves presets older versions appended to ratchet.md.
|
|
283
|
+
try { (await import('../preset-ratchet.js')).preparePresetRatchet(); } catch (e) { debug('route-scan:sync-preset', e); }
|
|
280
284
|
// Registered rules whose delegated-category error rate crossed the
|
|
281
285
|
// health threshold since promotion — the user approved these, so a
|
|
282
286
|
// status change must be briefed, not just written into the md file.
|
package/src/commands/seed.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* sprag seed accept <id> --global # register one rule
|
|
6
6
|
* sprag seed accept <id> --project # ... into this project only
|
|
7
7
|
* sprag seed accept all --global # when the user says "all of them"
|
|
8
|
-
* sprag seed skip <id> | skip all # never offer it again
|
|
8
|
+
* sprag seed skip <id> | skip all # never offer it again; for an
|
|
9
|
+
* # accepted preset, drop it from ratchet-preset.md
|
|
9
10
|
* sprag seed reset # make every preset pending again
|
|
10
11
|
*
|
|
11
12
|
* Scope is explicit on purpose, exactly as `harness promote` requires it: the
|
|
@@ -26,7 +27,7 @@ export async function run({ args, hasFlag, agent = 'claude', root: rootArg }) {
|
|
|
26
27
|
const scopeFlag = () => (hasFlag('--global') ? 'global' : hasFlag('--project') ? 'project' : null);
|
|
27
28
|
|
|
28
29
|
if (sub === 'reset') {
|
|
29
|
-
const n = seed.resetSeeds({ agent });
|
|
30
|
+
const n = await seed.resetSeeds({ agent });
|
|
30
31
|
console.log(ko
|
|
31
32
|
? `기록된 응답 ${n}건을 지웠습니다 — 프리셋 전체가 다시 제안 대상이 됩니다.`
|
|
32
33
|
: `Cleared ${n} recorded answer(s) — every preset is pending again.`);
|
|
@@ -46,9 +47,11 @@ export async function run({ args, hasFlag, agent = 'claude', root: rootArg }) {
|
|
|
46
47
|
|
|
47
48
|
if (sub === 'skip') {
|
|
48
49
|
for (const id of targets) {
|
|
49
|
-
const r = seed.skipSeed(id, { lang, root, agent });
|
|
50
|
+
const r = await seed.skipSeed(id, { lang, root, agent });
|
|
50
51
|
console.log(r
|
|
51
|
-
? (
|
|
52
|
+
? (r.removed
|
|
53
|
+
? (ko ? `프리셋 파일에서 뺐습니다: ${id}` : `removed from the preset file: ${id}`)
|
|
54
|
+
: (ko ? `건너뜀: ${id}` : `skipped: ${id}`))
|
|
52
55
|
: (ko ? `대기 중인 프리셋이 아닙니다: ${id}` : `not a pending preset: ${id}`));
|
|
53
56
|
}
|
|
54
57
|
return;
|
|
@@ -35,6 +35,29 @@ export async function run({ hasFlag, args = [] }) {
|
|
|
35
35
|
for (const item of r.kept) {
|
|
36
36
|
console.log(lang === 'ko' ? ` 유지: ${item}` : ` kept: ${item}`);
|
|
37
37
|
}
|
|
38
|
+
// The install now registers Codex too when it finds it, so removal has to
|
|
39
|
+
// undo that as well; otherwise Codex keeps calling a command that may no
|
|
40
|
+
// longer exist. Only our own entries go, exactly as `uninstall --agent codex`.
|
|
41
|
+
try {
|
|
42
|
+
const { codexHookStatus, configureCodexHooks } = await import('../codex-installer.js');
|
|
43
|
+
const { codexHarnessStatus, uninitCodexHarness } = await import('../codex-harness.js');
|
|
44
|
+
const hooked = codexHookStatus().some((h) => h.registered);
|
|
45
|
+
let harness = false;
|
|
46
|
+
try { harness = codexHarnessStatus({ scope: 'global' }).configured > 0; } catch { /* no AGENTS.md */ }
|
|
47
|
+
if (hooked || harness) {
|
|
48
|
+
const hook = configureCodexHooks({ remove: true });
|
|
49
|
+
const h = uninitCodexHarness({ scope: 'global' });
|
|
50
|
+
console.log(lang === 'ko'
|
|
51
|
+
? ` Codex: 훅 ${hook.action} (${hook.file}), 하네스 ${h.removed ? '제거' : '없음'}`
|
|
52
|
+
: ` Codex: hooks ${hook.action} (${hook.file}), harness ${h.removed ? 'removed' : 'absent'}`);
|
|
53
|
+
if (process.platform === 'darwin') {
|
|
54
|
+
try {
|
|
55
|
+
const { installPanelShell } = await import('../codex-panel-shell.js');
|
|
56
|
+
installPanelShell({ remove: true });
|
|
57
|
+
} catch (e) { /* a malformed or foreign block stays for the user to repair */ }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch { /* Codex state is optional; the Claude removal above already succeeded */ }
|
|
38
61
|
if (!purge) {
|
|
39
62
|
console.log(lang === 'ko'
|
|
40
63
|
? ` 기록된 절감액과 설정까지 지우려면 \`${CLI_NAME} uninstall --purge\` 를 실행하십시오.`
|
package/src/commands/upgrade.js
CHANGED
|
@@ -31,6 +31,17 @@ export async function run({ hasFlag, version }) {
|
|
|
31
31
|
const lang = userLanguage();
|
|
32
32
|
const cmd = upgradeCommand();
|
|
33
33
|
|
|
34
|
+
// pnpm and bun skip lifecycle scripts by default, and npm skips them under
|
|
35
|
+
// ignore-scripts, so the new copy's setup (Codex hooks, tmux panel) runs here.
|
|
36
|
+
// The install is idempotent, so npm running it twice changes nothing.
|
|
37
|
+
async function runSetup() {
|
|
38
|
+
console.log('$ sprag install');
|
|
39
|
+
const installCode = await runCommand('sprag install');
|
|
40
|
+
if (installCode !== 0) console.error(lang === 'ko'
|
|
41
|
+
? '설치 후 설정(sprag install)이 실패했습니다. 새 셸에서 sprag install 을 직접 실행하십시오.'
|
|
42
|
+
: 'Post-upgrade setup (sprag install) failed. Run sprag install in a fresh shell.');
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
if (hasFlag('--print')) {
|
|
35
46
|
console.log(cmd);
|
|
36
47
|
return;
|
|
@@ -55,6 +66,7 @@ export async function run({ hasFlag, version }) {
|
|
|
55
66
|
: `Migration command failed with exit code ${migrateCode}. Check the output above.`);
|
|
56
67
|
process.exit(migrateCode);
|
|
57
68
|
}
|
|
69
|
+
await runSetup();
|
|
58
70
|
console.log(lang === 'ko'
|
|
59
71
|
? '이전이 끝났습니다. 새 셸에서 sprag --version 으로 확인하십시오.'
|
|
60
72
|
: 'Migration done. Confirm with sprag --version in a fresh shell.');
|
|
@@ -86,6 +98,7 @@ export async function run({ hasFlag, version }) {
|
|
|
86
98
|
// still reports the old version. Refresh the cache against the new latest so
|
|
87
99
|
// the statusline chip clears on the next render instead of lingering.
|
|
88
100
|
await refreshUpdateState(s.latest);
|
|
101
|
+
await runSetup();
|
|
89
102
|
console.log(lang === 'ko'
|
|
90
103
|
? `설치가 끝났습니다. 새 셸에서 sprag --version 으로 ${s.latest} 인지 확인하십시오.`
|
|
91
104
|
: `Done. In a fresh shell, run sprag --version to confirm ${s.latest}.`);
|
package/src/harness-templates.js
CHANGED
|
@@ -40,6 +40,38 @@ export function modelRatchetImportLine(scope = 'project') {
|
|
|
40
40
|
return scope === 'global' ? '@~/.claude/ratchet-model.md' : '@.claude/ratchet-model.md';
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The preset ratchet: the bundled rules the user accepted globally. It is a
|
|
45
|
+
* tool-owned file like ratchet-model.md, regenerated whole from the recorded
|
|
46
|
+
* answers, so an upgrade can reword, add or drop presets without touching the
|
|
47
|
+
* rules the user wrote into ratchet.md. Presets are global-only, so only the
|
|
48
|
+
* global block imports it.
|
|
49
|
+
*/
|
|
50
|
+
export function presetRatchetImportLine() {
|
|
51
|
+
return '@~/.claude/ratchet-preset.md';
|
|
52
|
+
}
|
|
53
|
+
export const PRESET_RATCHET_IMPORT_RE = /^@~\/\.claude\/ratchet-preset\.md\s*$/m;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Render ratchet-preset.md. `rules` is [{ id, text }] in display order. The
|
|
57
|
+
* file is loaded into every session, so the header stays short.
|
|
58
|
+
*/
|
|
59
|
+
export function renderPresetRatchet(rules, { lang = 'en', agent = 'claude' } = {}) {
|
|
60
|
+
const ko = lang === 'ko' && agent !== 'codex';
|
|
61
|
+
const suffix = agent === 'codex' ? ' --agent codex' : '';
|
|
62
|
+
const head = ko
|
|
63
|
+
? `# Preset Ratchet Rules (sprag 관리)
|
|
64
|
+
|
|
65
|
+
sprag 에 동봉된 추천 룰 가운데 전역으로 등록한 것입니다. sprag 가 이 파일을 통째로 다시 쓰므로
|
|
66
|
+
직접 고치지 마십시오. 하나를 빼려면 \`sprag seed skip <id>${suffix}\`, 직접 쓰는 룰은 ratchet.md 에 둡니다.`
|
|
67
|
+
: `# Preset Ratchet Rules (managed by sprag)
|
|
68
|
+
|
|
69
|
+
Bundled rules you registered globally. Sprag rewrites this file whole, so do not
|
|
70
|
+
edit it. Drop one with \`sprag seed skip <id>${suffix}\`; your own rules belong in ratchet.md.`;
|
|
71
|
+
const body = rules.map((r) => `- [${r.id}] ${r.text}`).join('\n');
|
|
72
|
+
return `${head}\n\n## Rules\n\n${body}${body ? '\n' : ''}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
43
75
|
// Matches either scope's import line, so completeness checks don't need to know
|
|
44
76
|
// which scope wrote the block. `(?!-)` keeps the ratchet-model import from
|
|
45
77
|
// counting as the ratchet.md one.
|
|
@@ -118,7 +150,7 @@ ${HARNESS_SECTIONS[4].heading}
|
|
|
118
150
|
📥 ratchet 룰 로드 (이 줄들을 지우면 룰이 적용되지 않습니다):
|
|
119
151
|
|
|
120
152
|
${ratchetImportLine(scope)}
|
|
121
|
-
${modelRatchetImportLine(scope)}
|
|
153
|
+
${modelRatchetImportLine(scope)}${scope === 'global' ? `\n${presetRatchetImportLine()}` : ''}
|
|
122
154
|
${HARNESS_BLOCK_END}
|
|
123
155
|
`;
|
|
124
156
|
}
|
package/src/harness.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
appendRatchetRule,
|
|
22
22
|
RATCHET_IMPORT_RE,
|
|
23
23
|
MODEL_RATCHET_IMPORT_RE,
|
|
24
|
+
renderPresetRatchet,
|
|
24
25
|
} from './harness-templates.js';
|
|
25
26
|
import { routeWarningForStatusline } from './route-scan.js';
|
|
26
27
|
import { ruleHealthWarningForStatusline, modelRatchetPathFor, renderModelRatchet } from './model-rules.js';
|
|
@@ -265,6 +266,18 @@ export function harnessInit({ root = findProjectRoot(), force = false, scope = '
|
|
|
265
266
|
result.skipped.push(mrPath + ' (already exists)');
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
// ratchet-preset.md — tool-owned and imported by the global block only, so
|
|
270
|
+
// it is seeded here for the same reason as ratchet-model.md above.
|
|
271
|
+
if (scope === 'global') {
|
|
272
|
+
const prPath = join(homedir(), '.claude', 'ratchet-preset.md');
|
|
273
|
+
if (!existsSync(prPath)) {
|
|
274
|
+
try {
|
|
275
|
+
writeFileSync(prPath, renderPresetRatchet([], { lang: userLanguage() }));
|
|
276
|
+
result.wrote.push(prPath);
|
|
277
|
+
} catch { /* unwritable — the next session start renders it */ }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
268
281
|
return result;
|
|
269
282
|
}
|
|
270
283
|
|
|
@@ -347,7 +360,19 @@ export function harnessPromote(ruleText, { root = findProjectRoot(), scope = 'pr
|
|
|
347
360
|
* Deduped by rule text (ignoring the YYYY-MM-DD stamp) — idempotent.
|
|
348
361
|
* Returns { path, scope, added, skippedRules, presets }.
|
|
349
362
|
*/
|
|
350
|
-
export function harnessPull({ root = findProjectRoot(), scope = 'global', agent = 'claude' } = {}) {
|
|
363
|
+
export async function harnessPull({ root = findProjectRoot(), scope = 'global', agent = 'claude' } = {}) {
|
|
364
|
+
// Global presets live in ratchet-preset.md now, rendered from recorded
|
|
365
|
+
// answers, so pulling them globally is accepting every one of them.
|
|
366
|
+
if (scope === 'global') {
|
|
367
|
+
const { acceptedGlobalPresets, presetRatchetPath } = await import('./preset-ratchet.js');
|
|
368
|
+
const { pendingSeeds, acceptSeed } = await import('./seed-rules.js');
|
|
369
|
+
const added = [];
|
|
370
|
+
for (const s of pendingSeeds({ root, agent }).filter((x) => x.kind === 'ratchet')) {
|
|
371
|
+
if (await acceptSeed(s.id, { scope: 'global', root, agent })) added.push(s.ruleText);
|
|
372
|
+
}
|
|
373
|
+
const all = acceptedGlobalPresets({ agent }).length;
|
|
374
|
+
return { path: presetRatchetPath({ agent }), scope, added, skippedRules: all - added.length, presets: presetRuleEntries().length };
|
|
375
|
+
}
|
|
351
376
|
const presets = presetRules();
|
|
352
377
|
const rmPath = resolveRatchetPath(scope, root, agent);
|
|
353
378
|
const result = { path: rmPath, scope, added: [], skippedRules: 0, presets: presets.length };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preset-ratchet — the bundled ratchet presets the user accepted globally,
|
|
3
|
+
* kept in a file of their own.
|
|
4
|
+
*
|
|
5
|
+
* Presets used to be appended to ratchet.md next to the user's own rules. Once
|
|
6
|
+
* there, an upgrade could not reword a preset, drop one that turned out wrong,
|
|
7
|
+
* or tell which lines were ours: every line looked like the user's. This file
|
|
8
|
+
* holds only presets and is regenerated whole from the recorded answers in
|
|
9
|
+
* seed-state, the same way ratchet-model.md is regenerated from the rule
|
|
10
|
+
* registry. So an upgrade updates it by itself, `seed skip` removes one, and
|
|
11
|
+
* ratchet.md stays the user's.
|
|
12
|
+
*
|
|
13
|
+
* Claude Code ~/.claude/ratchet-preset.md (imported by the global harness block)
|
|
14
|
+
* Codex <CODEX_HOME>/ratchet-preset.md (injected by the SessionStart hook)
|
|
15
|
+
*
|
|
16
|
+
* Presets accepted into a project scope still go to that project's ratchet.md:
|
|
17
|
+
* a project rule is the user's placement decision, not a tool-wide default.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { codexUserDir } from './agent.js';
|
|
24
|
+
import { userLanguage } from './config.js';
|
|
25
|
+
import {
|
|
26
|
+
HARNESS_BLOCK_BEGIN,
|
|
27
|
+
HARNESS_BLOCK_END,
|
|
28
|
+
PRESET_RATCHET_IMPORT_RE,
|
|
29
|
+
presetRatchetImportLine,
|
|
30
|
+
renderPresetRatchet,
|
|
31
|
+
} from './harness-templates.js';
|
|
32
|
+
import { loadSeedState, ratchetPresets, saveSeedState } from './seed-rules.js';
|
|
33
|
+
|
|
34
|
+
export function presetRatchetPath({ agent = 'claude' } = {}) {
|
|
35
|
+
return agent === 'codex'
|
|
36
|
+
? join(codexUserDir(), 'ratchet-preset.md')
|
|
37
|
+
: join(homedir(), '.claude', 'ratchet-preset.md');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function globalRatchetPath(agent) {
|
|
41
|
+
return agent === 'codex' ? join(codexUserDir(), 'ratchet.md') : join(homedir(), '.claude', 'ratchet.md');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeIfChanged(file, text) {
|
|
45
|
+
let before = null;
|
|
46
|
+
try { before = readFileSync(file, 'utf8'); } catch { /* first write */ }
|
|
47
|
+
if (before === text) return false;
|
|
48
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
49
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
50
|
+
writeFileSync(tmp, text);
|
|
51
|
+
renameSync(tmp, file);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The accepted-global presets, in the bundled order, with this version's wording. */
|
|
56
|
+
export function acceptedGlobalPresets({ agent = 'claude', lang = userLanguage() } = {}) {
|
|
57
|
+
const { decided } = loadSeedState({ agent });
|
|
58
|
+
return ratchetPresets(lang, { agent })
|
|
59
|
+
.filter((p) => decided[p.id]?.action === 'accepted' && decided[p.id]?.scope === 'global')
|
|
60
|
+
.map((p) => ({ id: p.id, text: p.text }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Rewrite the preset file from the recorded answers. A preset that a newer
|
|
65
|
+
* release removed simply stops being rendered; its answer stays in seed-state
|
|
66
|
+
* so it is not offered again if it comes back under the same text.
|
|
67
|
+
*/
|
|
68
|
+
export function syncPresetRatchet({ agent = 'claude', lang = userLanguage() } = {}) {
|
|
69
|
+
const file = presetRatchetPath({ agent });
|
|
70
|
+
const rules = acceptedGlobalPresets({ agent, lang });
|
|
71
|
+
const changed = writeIfChanged(file, renderPresetRatchet(rules, { lang, agent }));
|
|
72
|
+
return { file, rules: rules.length, changed };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Move presets that earlier versions appended to the global ratchet.md into
|
|
77
|
+
* the preset file. A line counts as a preset only when its text, without the
|
|
78
|
+
* date stamp, equals a bundled rule in either language, so nothing the user
|
|
79
|
+
* wrote themselves is touched. The original is backed up once before the
|
|
80
|
+
* first change.
|
|
81
|
+
*/
|
|
82
|
+
export function migrateLegacyPresetRules({ agent = 'claude' } = {}) {
|
|
83
|
+
const file = globalRatchetPath(agent);
|
|
84
|
+
if (!existsSync(file)) return { file, moved: 0 };
|
|
85
|
+
const byText = new Map();
|
|
86
|
+
for (const lang of ['ko', 'en']) {
|
|
87
|
+
for (const p of ratchetPresets(lang, { agent })) byText.set(p.text.trim(), p.id);
|
|
88
|
+
}
|
|
89
|
+
const stamp = /^\d{4}-\d{2}-\d{2}(\s*\([^)]*\))?:\s*/;
|
|
90
|
+
const original = readFileSync(file, 'utf8');
|
|
91
|
+
const moved = [];
|
|
92
|
+
const kept = original.split('\n').filter((line) => {
|
|
93
|
+
const m = line.match(/^\s*-\s+(.*)$/);
|
|
94
|
+
if (!m) return true;
|
|
95
|
+
const id = byText.get(m[1].replace(stamp, '').trim());
|
|
96
|
+
if (!id) return true;
|
|
97
|
+
moved.push(id);
|
|
98
|
+
return false;
|
|
99
|
+
});
|
|
100
|
+
if (!moved.length) return { file, moved: 0 };
|
|
101
|
+
writeFileSync(`${file}.bak-preset-migration`, original);
|
|
102
|
+
writeIfChanged(file, kept.join('\n'));
|
|
103
|
+
const state = loadSeedState({ agent });
|
|
104
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
105
|
+
for (const id of moved) state.decided[id] = { action: 'accepted', at: today, scope: 'global', migrated: true };
|
|
106
|
+
saveSeedState(state, { agent });
|
|
107
|
+
return { file, moved: moved.length };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Add the preset import to a global harness block written before the file
|
|
112
|
+
* existed. Only the tool's own marked block is edited; a CLAUDE.md without the
|
|
113
|
+
* block is left alone, since the user chose not to install the harness there.
|
|
114
|
+
*/
|
|
115
|
+
export function ensurePresetImport() {
|
|
116
|
+
const file = join(homedir(), '.claude', 'CLAUDE.md');
|
|
117
|
+
let text;
|
|
118
|
+
try { text = readFileSync(file, 'utf8'); } catch { return { file, added: false }; }
|
|
119
|
+
const begin = text.indexOf(HARNESS_BLOCK_BEGIN);
|
|
120
|
+
const end = text.indexOf(HARNESS_BLOCK_END);
|
|
121
|
+
if (begin < 0 || end < begin) return { file, added: false };
|
|
122
|
+
if (PRESET_RATCHET_IMPORT_RE.test(text.slice(begin, end))) return { file, added: false };
|
|
123
|
+
writeFileSync(file, `${text.slice(0, end)}${presetRatchetImportLine()}\n${text.slice(end)}`);
|
|
124
|
+
return { file, added: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Everything a session start or an install does: migrate, render, and wire the import. */
|
|
128
|
+
export function preparePresetRatchet({ agent = 'claude', lang = userLanguage() } = {}) {
|
|
129
|
+
const migrated = migrateLegacyPresetRules({ agent });
|
|
130
|
+
const synced = syncPresetRatchet({ agent, lang });
|
|
131
|
+
const imported = agent === 'codex' ? { added: false } : ensurePresetImport();
|
|
132
|
+
return { ...synced, moved: migrated.moved, importAdded: imported.added };
|
|
133
|
+
}
|
package/src/seed-rules.js
CHANGED
|
@@ -177,6 +177,15 @@ export async function acceptSeed(id, { scope, root = findProjectRoot(), lang = u
|
|
|
177
177
|
if (!seed) return null;
|
|
178
178
|
|
|
179
179
|
if (seed.kind === 'ratchet') {
|
|
180
|
+
// A global preset goes to the tool-owned preset file, which is rendered
|
|
181
|
+
// from the recorded answer; a project one is the user's placement and is
|
|
182
|
+
// appended to that project's ratchet.md as before.
|
|
183
|
+
if (scope === 'global') {
|
|
184
|
+
recordDecision(id, 'accepted', { scope }, { agent });
|
|
185
|
+
const { syncPresetRatchet } = await import('./preset-ratchet.js');
|
|
186
|
+
const res = syncPresetRatchet({ agent, lang });
|
|
187
|
+
return { id, kind: 'ratchet', scope, path: res.file, rule: seed.ruleText };
|
|
188
|
+
}
|
|
180
189
|
const res = harnessPromote(seed.ruleText, { root, scope, agent });
|
|
181
190
|
recordDecision(id, 'accepted', { scope }, { agent });
|
|
182
191
|
return { id, kind: 'ratchet', scope, path: res.path, rule: seed.ruleText };
|
|
@@ -218,23 +227,39 @@ export async function acceptSeed(id, { scope, root = findProjectRoot(), lang = u
|
|
|
218
227
|
lastSeen: today,
|
|
219
228
|
});
|
|
220
229
|
const written = syncAllFiles();
|
|
221
|
-
recordDecision(id, 'accepted', { scope });
|
|
230
|
+
recordDecision(id, 'accepted', { scope }, { agent });
|
|
222
231
|
return { id, kind: 'model', scope, paths: written, rule: composeRuleText(entry.rule, entry, lang) };
|
|
223
232
|
}
|
|
224
233
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Record a "no" so the rule is never offered again (until `seed reset`).
|
|
236
|
+
*
|
|
237
|
+
* A preset already accepted globally can be skipped too: that is how one comes
|
|
238
|
+
* out of the preset file, which is rendered from these answers.
|
|
239
|
+
*/
|
|
240
|
+
export async function skipSeed(id, opts = {}) {
|
|
227
241
|
const seed = findSeed(id, opts);
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
242
|
+
if (seed) {
|
|
243
|
+
recordDecision(id, 'skipped', {}, { agent: opts.agent });
|
|
244
|
+
return { id, kind: seed.kind, rule: seed.ruleText };
|
|
245
|
+
}
|
|
246
|
+
const { decided } = loadSeedState({ agent: opts.agent });
|
|
247
|
+
const preset = ratchetPresets(opts.lang, { agent: opts.agent }).find((p) => p.id === id);
|
|
248
|
+
if (!preset || decided[id]?.action !== 'accepted' || decided[id]?.scope !== 'global') return null;
|
|
249
|
+
recordDecision(id, 'skipped', { was: 'accepted' }, { agent: opts.agent });
|
|
250
|
+
const { syncPresetRatchet } = await import('./preset-ratchet.js');
|
|
251
|
+
syncPresetRatchet({ agent: opts.agent, lang: opts.lang });
|
|
252
|
+
return { id, kind: 'ratchet', rule: preset.text, removed: true };
|
|
231
253
|
}
|
|
232
254
|
|
|
233
255
|
/** Forget every recorded answer — the full set becomes pending again. */
|
|
234
|
-
export function resetSeeds({ agent = 'claude' } = {}) {
|
|
256
|
+
export async function resetSeeds({ agent = 'claude' } = {}) {
|
|
235
257
|
const state = loadSeedState({ agent });
|
|
236
258
|
const count = Object.keys(state.decided).length;
|
|
237
259
|
saveSeedState({ decided: {} }, { agent });
|
|
260
|
+
// The preset file mirrors the answers, so forgetting them empties it.
|
|
261
|
+
const { syncPresetRatchet } = await import('./preset-ratchet.js');
|
|
262
|
+
syncPresetRatchet({ agent });
|
|
238
263
|
return count;
|
|
239
264
|
}
|
|
240
265
|
|