sprag-cli 3.63.0 → 3.64.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/bin/cli.js +3 -25
- package/docs/CODEX.ko.md +55 -1
- package/docs/CODEX.md +85 -3
- package/mod/sprag-delegation-meter/hooks/hooks.json +3 -0
- package/mod/sprag-delegation-meter/hooks/register.ts +197 -0
- package/mod/sprag-doc2md/hooks/hooks.json +3 -0
- package/mod/sprag-doc2md/hooks/register.ts +97 -0
- package/package.json +2 -2
- package/src/codex-brief.js +10 -1
- package/src/codex-delegation-diagnostics.js +42 -0
- package/src/codex-delegation.js +17 -0
- package/src/codex-doc2md-ledger.js +53 -8
- package/src/codex-doc2md.js +21 -4
- package/src/codex-doctor.js +9 -1
- package/src/codex-harness.js +2 -1
- package/src/codex-hooks.js +12 -3
- package/src/codex-installer.js +1 -0
- package/src/codex-ledger.js +25 -1
- package/src/codex-panel.js +33 -12
- package/src/codex-reply-language.js +43 -0
- package/src/codex-rule-health.js +23 -0
- package/src/commands/codex-delegate.js +16 -2
- package/src/commands/codex.js +40 -8
- package/src/commands/route-scan.js +2 -36
- package/src/commands/saved.js +10 -4
- package/src/commands/update-check.js +7 -4
- package/src/implementation-delegation.js +34 -0
- package/src/model-rules.js +3 -20
- package/src/reply-language.cjs +3 -2
- package/src/update-notice.js +37 -0
package/bin/cli.js
CHANGED
|
@@ -57,7 +57,7 @@ import { sessionCostAcross } from '../src/claude-price.js';
|
|
|
57
57
|
import { chipForIssues } from '../src/advice.js';
|
|
58
58
|
import { debug } from '../src/debug.js';
|
|
59
59
|
import { createArgs } from '../src/cli-args.js';
|
|
60
|
-
import {
|
|
60
|
+
import { readUpdateChip } from '../src/update-notice.js';
|
|
61
61
|
import { selectAgent } from '../src/agent.js';
|
|
62
62
|
|
|
63
63
|
const selection = (() => {
|
|
@@ -77,28 +77,6 @@ const PKG_VERSION = (() => {
|
|
|
77
77
|
|
|
78
78
|
const { getArg, hasFlag, numArg } = createArgs(args);
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
* Version/update state for the statusline chip. Reads a cache file and, when
|
|
82
|
-
* that cache has aged past the check interval, kicks a detached child to
|
|
83
|
-
* refresh it for a later render. Never awaits the network, never throws into
|
|
84
|
-
* the render: a registry outage must not cost the statusline its other chips.
|
|
85
|
-
*/
|
|
86
|
-
function readUpdateChip() {
|
|
87
|
-
try {
|
|
88
|
-
maybeSpawnUpdateCheck(PKG_VERSION);
|
|
89
|
-
const status = updateStatus(PKG_VERSION);
|
|
90
|
-
// When the registry was last asked, for the verbose "✓6h" on an up-to-date
|
|
91
|
-
// version. updateStatus() does not carry it, and a second read of the
|
|
92
|
-
// 200-byte state file is cheaper than a second status computation. With
|
|
93
|
-
// checks switched off the file is a leftover, so it says nothing.
|
|
94
|
-
const checkedAt = updateCheckDisabled() ? null : (Number(readUpdateState().checkedAt) || null);
|
|
95
|
-
return { ...status, checkedAt };
|
|
96
|
-
} catch (e) {
|
|
97
|
-
debug('update-check:chip', e);
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
80
|
/**
|
|
103
81
|
* Whether the table report may use ANSI codes. The formatter looked at NO_COLOR
|
|
104
82
|
* alone, so `--no-color` did nothing there and a piped report (`sprag | less`,
|
|
@@ -605,7 +583,7 @@ async function main() {
|
|
|
605
583
|
}
|
|
606
584
|
const { withBaseStatusline } = await import('../src/statusline-base.js');
|
|
607
585
|
console.log(withBaseStatusline(formatNoSession(
|
|
608
|
-
{ caps, model, effort, windowLabel, version: PKG_VERSION, update: readUpdateChip() },
|
|
586
|
+
{ caps, model, effort, windowLabel, version: PKG_VERSION, update: readUpdateChip(PKG_VERSION) },
|
|
609
587
|
{ color: colorOk, mode: labelMode },
|
|
610
588
|
)));
|
|
611
589
|
return;
|
|
@@ -892,7 +870,7 @@ async function main() {
|
|
|
892
870
|
cost,
|
|
893
871
|
options: { days, windowHours, windowLabel, version: PKG_VERSION },
|
|
894
872
|
// Cached-only; the background refresh it may trigger lands on a later render.
|
|
895
|
-
update: format === 'statusline' ? readUpdateChip() : null,
|
|
873
|
+
update: format === 'statusline' ? readUpdateChip(PKG_VERSION) : null,
|
|
896
874
|
lastActivity,
|
|
897
875
|
monthSpend: monthSpendInfo,
|
|
898
876
|
spikeReport,
|
package/docs/CODEX.ko.md
CHANGED
|
@@ -49,6 +49,7 @@ macOS zsh 환경에서는 설치가 tmux 설치 여부를 먼저 확인합니다
|
|
|
49
49
|
| seed | 호환되는 래칫 프리셋을 제안하고 응답을 별도 저장합니다 |
|
|
50
50
|
| 문서 변환 | PDF·Office·Figma 문서를 Markdown으로 변환하고 캐시 덮어쓰기를 막습니다 |
|
|
51
51
|
| 한국어 검사 | 파일 쓰기 후 문체 위반을 검사하며 block·warn·off를 지원합니다 |
|
|
52
|
+
| 답변 언어 검사 | Stop 훅이 현재 한국어 질문과 최종 답변을 대조합니다. 영어 답변 요청, Stop 재개, `korean reply off`는 제외합니다 |
|
|
52
53
|
| 상태 표시 | 별도 패널, tmux 패널, 일회성 상태줄을 제공합니다 |
|
|
53
54
|
| 브리핑·이력 | 최신 컨텍스트와 사용량 경고를 알리고 세션별로 저장합니다 |
|
|
54
55
|
| 위임 원장 | Sprag가 위임한 하위 실행의 토큰에 호출 경로의 단가를 적용해 예상 차액을 계산합니다. 직접 호출은 OpenAI 정가를, LiteLLM 공급자는 그 게이트웨이의 단가를 씁니다 |
|
|
@@ -240,6 +241,8 @@ sprag harness prune --global --older-than 6 --dry-run --agent codex
|
|
|
240
241
|
```sh
|
|
241
242
|
sprag doc2md install-converter --agent codex
|
|
242
243
|
sprag doc2md report.pdf --agent codex
|
|
244
|
+
sprag doc2md deck.pptx --agent codex --session SESSION_ID
|
|
245
|
+
sprag doc2md savings --agent codex --format json
|
|
243
246
|
sprag korean on --agent codex
|
|
244
247
|
sprag korean lint warn --agent codex
|
|
245
248
|
sprag cohesion on --agent codex
|
|
@@ -250,6 +253,25 @@ sprag cohesion on --agent codex
|
|
|
250
253
|
지원하는 라이브러리로 복사본을 편집한 뒤 다시 변환하십시오. 텍스트 추출만으로 차트·이미지·배치를
|
|
251
254
|
검증했다고 말할 수 없습니다. 실패와 잘린 결과는 그대로 보고해야 합니다.
|
|
252
255
|
|
|
256
|
+
`Doc2md saved`는 PPTX·DOCX·XLSX의 원본 XML과 준비된 Markdown을 비교한 추정치입니다.
|
|
257
|
+
각각 UTF-8 바이트 수를 4로 나눠 토큰 수를 추정하고, 그 차이에 해당 Codex 모델의 입력 단가를
|
|
258
|
+
적용합니다. 네이티브 첨부 비용이나 실제 청구액을 측정한 값은 아닙니다. 변환본이 더 크면
|
|
259
|
+
음수를 그대로 표시합니다. Claude 단가를 가져오지 않으며 모델, 프로바이더와 단가 출처를 기록합니다.
|
|
260
|
+
|
|
261
|
+
같은 문서를 다시 사용해도 절감액을 더하거나 단가를 바꾸지 않습니다. 원본이 바뀌면 해당 문서의
|
|
262
|
+
기존 기록을 교체합니다. PDF·Figma·구형 XLS, 잘린 변환본, 단가나 모델 정보가 없는 기록은
|
|
263
|
+
미산정으로 남깁니다. 과거 기록에 현재 기본 모델의 단가를 임의로 적용하지 않습니다.
|
|
264
|
+
수동 변환은 `--session` 또는 `CODEX_THREAD_ID`로 세션을 식별하며, 정보가 없으면 변환만 수행합니다.
|
|
265
|
+
|
|
266
|
+
패널은 `Doc2md saved`와 `Routing saved`를 설정 항목 앞에 함께 표시합니다. 위임 안내만
|
|
267
|
+
생성되고 자식이 실행되지 않으면 절감액은 `n/a`이며, 세션별 pending·bound·completed·unclaimed
|
|
268
|
+
건수로 진행 상태를 구분합니다. 한 요청의 T1/T2 선택지는 안내 한 건으로 계산합니다.
|
|
269
|
+
|
|
270
|
+
`sprag delegate doctor --agent codex --session SESSION_ID`는 모델 호출 없이 로컬 버전,
|
|
271
|
+
기능 플래그, 모델 카탈로그와 연결 기록을 확인합니다. v2 카탈로그와 기능 플래그가 다르면
|
|
272
|
+
진단 단서로만 알리고 설정은 바꾸지 않습니다. 실제 세션에 `spawn_agent`와 `wait_agent`가
|
|
273
|
+
있는지는 별도로 확인해야 하며, 도구가 없으면 메인 모델이 처리합니다.
|
|
274
|
+
|
|
253
275
|
훅은 지원되는 파일 읽기와 단순한 `cat`·`head`·`tail`, 인식 가능한 쓰기 대상을 처리합니다.
|
|
254
276
|
임의의 추출 스크립트나 복합 셸 명령을 모두 가로채지는 않습니다. `doc2md off`는 Codex의 자동
|
|
255
277
|
변환과 문서 쓰기 보호를 끕니다. 문체 설정은 공유하지만 기능을 켤 때는 Codex 훅을 먼저 등록합니다.
|
|
@@ -286,16 +308,48 @@ sprag capabilities --agent codex
|
|
|
286
308
|
|
|
287
309
|
## 제거와 업그레이드
|
|
288
310
|
|
|
311
|
+
패널은 새 버전이 있으면 사용량 앞에 알림을 표시하고, 백그라운드에서 버전 캐시를 갱신합니다.
|
|
312
|
+
일회성 상태줄은 캐시만 읽습니다. 대화형 SessionStart는 변경 내용을 알리고 설치 동의를 구합니다.
|
|
313
|
+
`sprag update-check --dismiss --agent codex`로 해당 버전의 알림을 끌 수 있습니다.
|
|
314
|
+
`CTS_NO_UPDATE_CHECK=1` 또는 `NO_UPDATE_NOTIFIER`는 확인 자체를 끕니다.
|
|
315
|
+
compaction, 하위 세션 시작, `codex exec`에는 업그레이드 질문을 넣지 않습니다.
|
|
316
|
+
|
|
289
317
|
```sh
|
|
290
318
|
sprag uninstall --agent codex
|
|
291
319
|
npm i -g sprag-cli --ignore-scripts
|
|
292
320
|
sprag install --agent codex
|
|
293
321
|
```
|
|
294
322
|
|
|
295
|
-
제거는 Codex
|
|
323
|
+
제거는 Codex 훅, 전역 하네스 블록, 관리 중인 zsh 함수를 대상으로 합니다. 새 셸을 열어야
|
|
324
|
+
이미 로드된 함수도 사라집니다. 프로젝트 하네스, 래칫 규칙, 백업과
|
|
296
325
|
공유 상태는 남습니다. 일반 npm 설치는 Claude postinstall을 실행하므로 Codex만 사용한다면
|
|
297
326
|
업그레이드에도 `--ignore-scripts`를 유지하십시오.
|
|
298
327
|
|
|
328
|
+
## 구현 분업
|
|
329
|
+
|
|
330
|
+
설계, 원인 진단, 스펙 확정과 최종 검증은 메인 모델이 맡습니다. 여러 파일을 고치는 구현은
|
|
331
|
+
설계를 확정한 뒤 T1 모델에 위임합니다. Claude는 기존처럼 `model: sonnet`을 쓰고,
|
|
332
|
+
Codex는 현재 부모 모델과 프로바이더의 명시적 T1 매핑을 우선합니다. 매핑이 없으면
|
|
333
|
+
자동 해석한 T1을 씁니다. 이는 역할 구분이며 두 모델의 성능이 같다는 뜻은 아닙니다.
|
|
334
|
+
|
|
335
|
+
```sh
|
|
336
|
+
sprag delegate implementation --agent codex --from PARENT_MODEL --provider PROVIDER --session PARENT_SESSION_ID
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
기본 설정 대신 실제 부모 세션의 정보를 넘깁니다. 자식에게 설계, 수정할 파일, 제약,
|
|
340
|
+
테스트 명령, 완료 기준과 모델 선택 이유를 전달하고, 반환된 route 줄을 메시지 끝에 붙입니다.
|
|
341
|
+
`fork_turns "none"`으로 생성하며 출력 상한은 8,000토큰입니다. 메인은 `wait_agent`로
|
|
342
|
+
완료를 기다린 뒤 diff와 실제 테스트 결과를 검증합니다. 상한은 지침이며 강제 제한은 아닙니다.
|
|
343
|
+
|
|
344
|
+
T1이 없거나 위임이 꺼져 있으면 메인이 처리합니다. haiku나 T2로 대체하지 않습니다.
|
|
345
|
+
이미 읽은 파일의 약 50줄 이하 수정도 메인에서 처리합니다. 위임 도구가 없으면 위 명령을
|
|
346
|
+
실행하지 않고 제약을 알립니다. 별도 Codex CLI로 우회하지 않습니다. 원장에는 부모, 모델,
|
|
347
|
+
프로바이더가 맞는 자식 실행만 집계하며, 실행되지 않은 안내는 절감으로 계산하지 않습니다.
|
|
348
|
+
|
|
349
|
+
공유 정책의 Codex 실패율도 규칙 목록, 패널, 브리핑에 표시합니다. 정책, 등급, 범위,
|
|
350
|
+
프로바이더와 부모·자식 모델별로 집계하며 Claude 통계에는 합치지 않습니다. 매핑을 검토할
|
|
351
|
+
동안 `sprag delegate shared off --agent codex`로 공유 위임을 끌 수 있습니다.
|
|
352
|
+
|
|
299
353
|
공식 인터페이스는 [Codex 훅](https://developers.openai.com/codex/hooks/)과
|
|
300
354
|
[AGENTS.md 안내](https://developers.openai.com/codex/guides/agents-md/),
|
|
301
355
|
[설정 참조](https://developers.openai.com/codex/config-reference/)에서 확인할 수 있습니다.
|
package/docs/CODEX.md
CHANGED
|
@@ -80,6 +80,7 @@ shared hooks needed by other features.
|
|
|
80
80
|
| Ratchet rules | Separate Codex files; global and project rules injected at SessionStart, including resume and compaction |
|
|
81
81
|
| Korean guidance / English cohesion | SessionStart injection using shared Sprag preferences |
|
|
82
82
|
| Korean write-time lint | PostToolUse feedback for `apply_patch` and recognized shell write targets; block, warn, and off modes |
|
|
83
|
+
| Reply language | Stop checks the current Korean prompt and final answer; `korean reply off` disables it, and explicit English requests and Stop continuations are excluded |
|
|
83
84
|
| Documents | Shared converter, prompt paths, supported file reads, and recognized document/cache write guards; `sprag doc2md <file> --agent codex` also works manually |
|
|
84
85
|
| Token report | Table, JSON, or CSV from local Codex rollout logs, with time and project filters |
|
|
85
86
|
| Statusline | `--statusline` or `--format statusline` renders the compact panel once, with wrapping and label/color preferences |
|
|
@@ -94,6 +95,7 @@ shared hooks needed by other features.
|
|
|
94
95
|
| Routing saved | Sprag-attributed child tokens priced by the path the call took (OpenAI list prices for direct calls, the gateway's `/model/info` prices for a LiteLLM provider); signed, so a pricier child is a loss; an estimate, not billed savings |
|
|
95
96
|
| Diagnostics | `doctor` checks user config, hook registration, harness, logs, panel activity, and preferences |
|
|
96
97
|
| Capability matrix | `capabilities` lists supported, partial, native, and unsupported areas without invoking a model |
|
|
98
|
+
| Updates | Live panels refresh the shared version cache in the background; one-shot statuslines read it offline; interactive SessionStart offers a consent-based Codex-only upgrade |
|
|
97
99
|
|
|
98
100
|
The document converter is shared with Claude Code and requires its optional
|
|
99
101
|
converter dependencies. Disable automatic Codex conversion and write guards with
|
|
@@ -120,11 +122,34 @@ history is not imported. Counts are sorted by frequency, then extension, with no
|
|
|
120
122
|
Claude pricing involved. Codex reads a neutral Markdown view without the shared
|
|
121
123
|
cache's Claude-based savings banner; the shared cache remains unchanged.
|
|
122
124
|
|
|
125
|
+
`Doc2md saved` is a separate Codex estimate for `.pptx`, `.docx`, and `.xlsx`:
|
|
126
|
+
Office body XML bytes divided by four, minus the prepared Markdown bytes divided
|
|
127
|
+
by four, multiplied by the recorded model's input-token price. This compares
|
|
128
|
+
XML extraction with Markdown, not native attachment billing or measured tokens.
|
|
129
|
+
Negative differences remain negative. The price comes from the active Codex
|
|
130
|
+
provider, never Claude's price table, and its source is recorded with the result.
|
|
131
|
+
Repeated use of the same source does not add another saving or reprice a saved
|
|
132
|
+
estimate. Changed sources replace their previous entry, just like document counts.
|
|
133
|
+
Clipped conversions, PDF/Figma/legacy XLS, missing prices, and old records without
|
|
134
|
+
model identity remain unpriced. Nothing is backfilled from default config.
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
sprag doc2md deck.pptx --agent codex --session SESSION_ID
|
|
138
|
+
sprag doc2md savings --agent codex --format json
|
|
139
|
+
sprag saved --agent codex
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Hooks use the current payload/rollout model and provider. Manual conversion can
|
|
143
|
+
use `--session` or the CLI's `CODEX_THREAD_ID`; without session identity, the
|
|
144
|
+
document is converted but the monetary estimate stays unavailable. No network
|
|
145
|
+
request is needed for conversion accounting; it uses a cached provider price.
|
|
146
|
+
|
|
123
147
|
## Diagnostics And Continuity
|
|
124
148
|
|
|
125
149
|
```sh
|
|
126
150
|
sprag doctor --agent codex
|
|
127
151
|
sprag doctor --agent codex --format json
|
|
152
|
+
sprag delegate doctor --agent codex --session SESSION_ID
|
|
128
153
|
sprag capabilities --agent codex
|
|
129
154
|
sprag brief --agent codex
|
|
130
155
|
sprag brief on --agent codex
|
|
@@ -142,6 +167,15 @@ proof of trust. Its config report excludes provider credentials and is not an
|
|
|
142
167
|
effective-config resolver: active profiles, project config, managed policy,
|
|
143
168
|
and invocation overrides can differ. Check Codex `/hooks` for approval.
|
|
144
169
|
|
|
170
|
+
`delegate doctor` reads local Codex version, feature flags, bundled model catalog
|
|
171
|
+
and Sprag offer/binding records without calling a model. A v2 catalog/feature
|
|
172
|
+
mismatch is reported only as a compatibility clue, not a diagnosis. Enabled
|
|
173
|
+
settings and registered hooks cannot prove the running model has `spawn_agent`
|
|
174
|
+
and `wait_agent`. Confirm those tools in the active session. The panel reports
|
|
175
|
+
pending, bound, completed and unclaimed offers separately, scoped to its session;
|
|
176
|
+
T1/T2 alternatives from one prompt count as one offer. Unclaimed offers are not
|
|
177
|
+
charged or credited as savings. Missing tools leave work on the main agent.
|
|
178
|
+
|
|
145
179
|
Briefing is enabled by default after installing and trusting the prompt hook.
|
|
146
180
|
It reads at most the latest 256 KiB of the current transcript and warns when
|
|
147
181
|
recorded input reaches 80% or 95% of the recorded context window, or a reported
|
|
@@ -304,6 +338,35 @@ booked as a loss and the entry is marked `aborted`.
|
|
|
304
338
|
|
|
305
339
|
## Harness Commands
|
|
306
340
|
|
|
341
|
+
### Implementation Split
|
|
342
|
+
|
|
343
|
+
The main agent retains design, diagnosis, specification and final verification.
|
|
344
|
+
After design is complete, multi-file implementation can use the resolved T1:
|
|
345
|
+
|
|
346
|
+
```sh
|
|
347
|
+
sprag delegate implementation --agent codex --from PARENT_MODEL --provider PROVIDER --session PARENT_SESSION_ID
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Use the active parent's identity, not the defaults in config. The installed
|
|
351
|
+
harness treats this policy as the user's delegation request. The resolver uses
|
|
352
|
+
an explicit T1 mapping first, otherwise automatic T1 resolution; Claude's
|
|
353
|
+
corresponding policy still uses `model: sonnet`. These are roles, not a claim of
|
|
354
|
+
equal model capability. It never falls back to T2. Disabled delegation or an
|
|
355
|
+
unresolved T1 leaves the work on the main agent. Small edits of roughly 50 lines
|
|
356
|
+
in already-read files also stay on the main agent.
|
|
357
|
+
|
|
358
|
+
The child receives a self-contained specification, owned files, constraints,
|
|
359
|
+
tests, completion criteria, the model-selection reason and an 8,000-output-token
|
|
360
|
+
cap. Use `fork_turns "none"` and end the child message with the returned route
|
|
361
|
+
line. Wait with `wait_agent` (at least 120 seconds per wait), then inspect its
|
|
362
|
+
diff and actual test results. Without native subagent tools, skip the resolver
|
|
363
|
+
and report the limitation; do not launch a second Codex CLI as a substitute.
|
|
364
|
+
The cap is guidance, not runtime enforcement. Accounting requires a child of
|
|
365
|
+
the recorded parent on the selected model/provider; an unused hint earns no
|
|
366
|
+
savings. Implementation requests are not automatically classified as cheap work.
|
|
367
|
+
|
|
368
|
+
### Ratchet Management
|
|
369
|
+
|
|
307
370
|
```sh
|
|
308
371
|
sprag harness init --agent codex # current project
|
|
309
372
|
sprag harness check --agent codex # project, or global fallback
|
|
@@ -463,6 +526,12 @@ each other:
|
|
|
463
526
|
rate and signed saving. A rule is flagged for review when the 95% lower bound
|
|
464
527
|
of its failure rate exceeds 20% over at least five runs, as on the Claude side.
|
|
465
528
|
|
|
529
|
+
Shared policies also report their Codex outcomes in the rule list, panel and
|
|
530
|
+
prompt briefing. Outcomes are grouped by policy, tier, scope, provider and
|
|
531
|
+
parent/child model pair; they are never imported into Claude's rule statistics.
|
|
532
|
+
Use `sprag delegate shared off --agent codex` to pause shared routing while
|
|
533
|
+
reviewing the mapping. Removing a common policy affects both agents.
|
|
534
|
+
|
|
466
535
|
Providers are kept separate. Negative values are losses: the child ran on a
|
|
467
536
|
pricier model than the parent, and the loss reduces the totals. Missing rates or usage produce unpriced entries, never invented zeroes. A priced
|
|
468
537
|
entry keeps its rate snapshot across later outages. Pending route records are
|
|
@@ -470,7 +539,8 @@ matched for seven days; saved ledger entries remain afterward. Run `--refresh`
|
|
|
470
539
|
after delegation to update the ledger without waiting for the next route scan.
|
|
471
540
|
|
|
472
541
|
Claude's ledger is never imported. Document conversion shows distinct document
|
|
473
|
-
counts
|
|
542
|
+
counts and the explicitly labelled Office XML-versus-Markdown estimate above,
|
|
543
|
+
not a billing measurement.
|
|
474
544
|
|
|
475
545
|
### Live Companion Panel
|
|
476
546
|
|
|
@@ -574,7 +644,9 @@ The inline compact panel reuses Claude's icon/narrow/text preferences and
|
|
|
574
644
|
segmented gauges. Chips wrap as units instead of losing their trailing values;
|
|
575
645
|
the pane grows with the content while keeping at least ten rows for Codex.
|
|
576
646
|
If the terminal is too small, a remaining-row count marks hidden content.
|
|
577
|
-
Cache reuse is shown in tokens, not an inferred dollar saving.
|
|
647
|
+
Cache reuse is shown in tokens, not an inferred dollar saving. `Doc2md saved` and
|
|
648
|
+
`Routing saved` stay together before preferences, even when unavailable; missing
|
|
649
|
+
prices or execution evidence are labelled instead of being reported as $0. Document counts
|
|
578
650
|
and format breakdowns come from Codex's own document records and are labelled
|
|
579
651
|
`Codex total`; Claude-based dollar estimates are not attributed to Codex.
|
|
580
652
|
Unavailable cost and limit placeholders are hidden in compact mode. Cache timer
|
|
@@ -615,6 +687,7 @@ are skipped, and incomplete/copied/reset logs can only give best-effort totals.
|
|
|
615
687
|
Reports show **cost and exact cache TTL as unavailable** (`null` in JSON). They do not
|
|
616
688
|
apply Anthropic prices, infer subscription charges, or claim Claude's cache TTL.
|
|
617
689
|
Document counts are not a measurement of Codex billing or avoided tokens.
|
|
690
|
+
The separate Doc2md estimate is a bytes-based comparison, not tokenizer output.
|
|
618
691
|
|
|
619
692
|
## Limits And Removal
|
|
620
693
|
|
|
@@ -634,7 +707,9 @@ Rate-limit gauges use Codex's recorded values only. Run
|
|
|
634
707
|
sprag uninstall --agent codex
|
|
635
708
|
```
|
|
636
709
|
|
|
637
|
-
Removal takes out Sprag's Codex hooks
|
|
710
|
+
Removal takes out Sprag's Codex hooks, global harness block and managed zsh
|
|
711
|
+
wrapper. Open a fresh shell afterward; an already-loaded shell function remains
|
|
712
|
+
in the old shell. It keeps project
|
|
638
713
|
harnesses, ratchet rules, backups, other hooks, and shared Sprag state.
|
|
639
714
|
`--purge` is intentionally unsupported for Codex. Remove a project block with
|
|
640
715
|
`sprag harness uninit --agent codex --project` in that project.
|
|
@@ -643,6 +718,13 @@ Normal npm upgrades still run the package's Claude postinstall. Codex-only users
|
|
|
643
718
|
should keep using `npm i -g sprag-cli --ignore-scripts` followed by
|
|
644
719
|
`sprag install --agent codex` when upgrading.
|
|
645
720
|
|
|
721
|
+
The panel places an available update before usage metrics. Dismissing a version
|
|
722
|
+
with `sprag update-check --dismiss --agent codex` hides its chip and session
|
|
723
|
+
offer until a newer version appears. `CTS_NO_UPDATE_CHECK=1` or
|
|
724
|
+
`NO_UPDATE_NOTIFIER` disables checks. Compaction, subagent starts and `codex exec`
|
|
725
|
+
do not ask to upgrade. SessionStart checks at most every three hours; the live
|
|
726
|
+
panel uses the shared 24-hour interval and never waits for a network response.
|
|
727
|
+
|
|
646
728
|
## Official References
|
|
647
729
|
|
|
648
730
|
- [Codex hooks, event schemas, and trust](https://developers.openai.com/codex/hooks/)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { EngineInterface, Register, TurnUsage } from 'claude-code'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sprag-delegation-meter: what each delegated subagent actually spent.
|
|
5
|
+
*
|
|
6
|
+
* sprag's delegation ledger prices a run after the fact, from the transcript
|
|
7
|
+
* it left. This mod reads the same numbers while the run happens, from the
|
|
8
|
+
* API's own usage: agent.spawn says which model the subagent got and what its
|
|
9
|
+
* parent runs on, every turn.step of the subagent's loop reports one
|
|
10
|
+
* request's tokens, and turn.complete closes a turn of that loop.
|
|
11
|
+
*
|
|
12
|
+
* Each run is kept in $.store under a key of its own, `run:<session>:<agent>`,
|
|
13
|
+
* so two sessions never overwrite each other's runs. The mod only observes: no
|
|
14
|
+
* spawn, request or turn is changed, and sprag's transcript-based ledger keeps
|
|
15
|
+
* running beside it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type Usage = { input: number; output: number; cacheRead: number; cacheWrite: number }
|
|
19
|
+
|
|
20
|
+
type Run = {
|
|
21
|
+
sessionId: string
|
|
22
|
+
agentId: string
|
|
23
|
+
description: string
|
|
24
|
+
subagentType: string
|
|
25
|
+
/** The Agent tool's `model` parameter as given; null when the call named none. */
|
|
26
|
+
requestedModel: string | null
|
|
27
|
+
/** What the spawn resolved; null when this module never saw the spawn. */
|
|
28
|
+
model: string | null
|
|
29
|
+
parentModel: string | null
|
|
30
|
+
/** The model the API last reported answering, by its own id. */
|
|
31
|
+
answeredBy: string | null
|
|
32
|
+
startedAt: number
|
|
33
|
+
endedAt: number | null
|
|
34
|
+
durationMs: number
|
|
35
|
+
steps: number
|
|
36
|
+
turns: number
|
|
37
|
+
usage: Usage
|
|
38
|
+
lastReason: string | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Families as sprag's modelFamily() names them: versions are noise in a log line.
|
|
42
|
+
const FAMILIES = ['fable', 'mythos', 'opus', 'sonnet', 'haiku']
|
|
43
|
+
|
|
44
|
+
const family = (model: string) => FAMILIES.find(f => model.toLowerCase().includes(f)) ?? model
|
|
45
|
+
|
|
46
|
+
const zero = (): Usage => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 })
|
|
47
|
+
|
|
48
|
+
const fromApi = (u: TurnUsage): Usage => ({
|
|
49
|
+
input: u.input_tokens,
|
|
50
|
+
output: u.output_tokens,
|
|
51
|
+
cacheRead: u.cache_read_input_tokens,
|
|
52
|
+
cacheWrite: u.cache_creation_input_tokens,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const add = (into: Usage, more: Usage) => {
|
|
56
|
+
into.input += more.input
|
|
57
|
+
into.output += more.output
|
|
58
|
+
into.cacheRead += more.cacheRead
|
|
59
|
+
into.cacheWrite += more.cacheWrite
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tokens = (n: number) =>
|
|
63
|
+
n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1_000 ? `${(n / 1_000).toFixed(1)}k` : String(n)
|
|
64
|
+
|
|
65
|
+
const OUTCOME: Record<string, string> = { answer: '완료', aborted: '중단', refusal: '거절', error: '실패' }
|
|
66
|
+
|
|
67
|
+
// Runs in flight by agent id, and per run the requests of the turn still open.
|
|
68
|
+
// A run leaves the map once its turn is saved, so the map holds only what is
|
|
69
|
+
// running and does not grow with the session. $.store has the run from then
|
|
70
|
+
// on: a subagent resumed later (SendMessage starts another turn of the same
|
|
71
|
+
// loop) or met again after a module reload continues from its saved totals.
|
|
72
|
+
const runs = new Map<string, Run>()
|
|
73
|
+
const openTurn = new Map<string, { usage: Usage; steps: number }>()
|
|
74
|
+
|
|
75
|
+
const storeKey = (sessionId: string, agentId: string) => `run:${sessionId}:${agentId}`
|
|
76
|
+
|
|
77
|
+
const isRun = (value: unknown, agentId: string): value is Run =>
|
|
78
|
+
typeof value === 'object' && value !== null
|
|
79
|
+
&& (value as Run).agentId === agentId
|
|
80
|
+
&& typeof (value as Run).usage === 'object' && (value as Run).usage !== null
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The run for an agent id: the one in flight, else the saved one, else a new
|
|
84
|
+
* one. A subagent's first request can arrive before the spawn hook resumes
|
|
85
|
+
* after `await next(e)`, so a step may be what creates the run.
|
|
86
|
+
*
|
|
87
|
+
* The session id is read per run and never cached in the module: the module
|
|
88
|
+
* outlives /clear, /resume and /branch, each of which moves the conversation
|
|
89
|
+
* to another session id without loading the module again.
|
|
90
|
+
*/
|
|
91
|
+
async function runFor($: EngineInterface, agentId: string): Promise<Run> {
|
|
92
|
+
const held = runs.get(agentId)
|
|
93
|
+
if (held) return held
|
|
94
|
+
const sessionId = await $.session.id()
|
|
95
|
+
let saved: unknown
|
|
96
|
+
try {
|
|
97
|
+
saved = await $.store.get(storeKey(sessionId, agentId))
|
|
98
|
+
} catch {
|
|
99
|
+
// An unreadable store costs this run only its earlier turns.
|
|
100
|
+
}
|
|
101
|
+
// A copy, since the totals are added to in place.
|
|
102
|
+
const run: Run = isRun(saved, agentId) ? JSON.parse(JSON.stringify(saved)) : {
|
|
103
|
+
sessionId,
|
|
104
|
+
agentId,
|
|
105
|
+
description: '',
|
|
106
|
+
subagentType: '',
|
|
107
|
+
requestedModel: null,
|
|
108
|
+
model: null,
|
|
109
|
+
parentModel: null,
|
|
110
|
+
answeredBy: null,
|
|
111
|
+
startedAt: await $.clock.now(),
|
|
112
|
+
endedAt: null,
|
|
113
|
+
durationMs: 0,
|
|
114
|
+
steps: 0,
|
|
115
|
+
turns: 0,
|
|
116
|
+
usage: zero(),
|
|
117
|
+
lastReason: null,
|
|
118
|
+
}
|
|
119
|
+
runs.set(agentId, run)
|
|
120
|
+
return run
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const register: Register = on => {
|
|
124
|
+
// A meter must never stop what it measures, so every hook here fails open:
|
|
125
|
+
// a hook that breaks before its next() is skipped and the event goes on, and
|
|
126
|
+
// one that breaks after it keeps the result next() settled to. That is the
|
|
127
|
+
// engine's default already; each .catch states it, and answers the same way
|
|
128
|
+
// when the hook is reached from beneath its own $ call (a re-entry).
|
|
129
|
+
on('agent.spawn', async ($, e, next) => {
|
|
130
|
+
const spawned = await next(e)
|
|
131
|
+
if (spawned.deny !== undefined || !spawned.agentId) return spawned
|
|
132
|
+
const run = await runFor($, spawned.agentId)
|
|
133
|
+
run.description = e.description
|
|
134
|
+
run.subagentType = e.subagentType
|
|
135
|
+
run.requestedModel = e.model ?? null
|
|
136
|
+
run.model = spawned.model
|
|
137
|
+
run.parentModel = e.parentModel || null
|
|
138
|
+
// A workflow can start dozens of agents; one line each would bury the transcript.
|
|
139
|
+
if (!e.workflow) {
|
|
140
|
+
const parent = run.parentModel ? ` (메인 ${family(run.parentModel)})` : ''
|
|
141
|
+
$.ui.log(`🔀 [sprag] "${e.description}" → ${family(spawned.model)} 위임${parent}`)
|
|
142
|
+
}
|
|
143
|
+
return spawned
|
|
144
|
+
}).catch(($, e, next) => next(e))
|
|
145
|
+
|
|
146
|
+
on('turn.step', async function* ($, e, next) {
|
|
147
|
+
const result = yield* next(e)
|
|
148
|
+
if (e.agentId && result.usage) {
|
|
149
|
+
const run = await runFor($, e.agentId)
|
|
150
|
+
const turn = openTurn.get(e.agentId) ?? { usage: zero(), steps: 0 }
|
|
151
|
+
add(turn.usage, fromApi(result.usage))
|
|
152
|
+
turn.steps += 1
|
|
153
|
+
openTurn.set(e.agentId, turn)
|
|
154
|
+
run.answeredBy = result.usage.model
|
|
155
|
+
}
|
|
156
|
+
return result
|
|
157
|
+
}).catch(async function* ($, e, next) {
|
|
158
|
+
return yield* next(e)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
on('turn.complete', async ($, e, next) => {
|
|
162
|
+
if (!e.agentId) return next(e)
|
|
163
|
+
const run = await runFor($, e.agentId)
|
|
164
|
+
const turn = openTurn.get(e.agentId)
|
|
165
|
+
openTurn.delete(e.agentId)
|
|
166
|
+
// The steps' own counts are the measurement. The turn's sum stands in only
|
|
167
|
+
// when no step was seen, e.g. the module reloaded during the turn.
|
|
168
|
+
if (turn && turn.steps > 0) {
|
|
169
|
+
add(run.usage, turn.usage)
|
|
170
|
+
run.steps += turn.steps
|
|
171
|
+
} else if (e.usage) {
|
|
172
|
+
add(run.usage, fromApi(e.usage))
|
|
173
|
+
run.answeredBy = e.usage.model
|
|
174
|
+
}
|
|
175
|
+
run.turns += 1
|
|
176
|
+
run.durationMs += e.durationMs
|
|
177
|
+
run.endedAt = await $.clock.now()
|
|
178
|
+
run.lastReason = e.reason
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await $.store.set(storeKey(run.sessionId, run.agentId), run)
|
|
182
|
+
runs.delete(e.agentId)
|
|
183
|
+
} catch {
|
|
184
|
+
// A store that refuses the write leaves only this run unrecorded. It
|
|
185
|
+
// stays in the map, so its next turn still adds to these totals.
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const model = family(run.model ?? run.answeredBy ?? 'unknown')
|
|
189
|
+
const u = run.usage
|
|
190
|
+
$.ui.log(
|
|
191
|
+
`🔀 [sprag] ${model} 위임 ${OUTCOME[e.reason] ?? e.reason} · ${(run.durationMs / 1000).toFixed(1)}초`
|
|
192
|
+
+ ` · 요청 ${run.steps}회 · 입력 ${tokens(u.input)} · 캐시 읽기 ${tokens(u.cacheRead)}`
|
|
193
|
+
+ ` · 캐시 쓰기 ${tokens(u.cacheWrite)} · 출력 ${tokens(u.output)}`,
|
|
194
|
+
)
|
|
195
|
+
return next(e)
|
|
196
|
+
}).catch(($, e, next) => next(e))
|
|
197
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { EngineInterface, Register } from 'claude-code'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sprag-doc2md: the mod form of sprag's PreToolUse(Read) hook.
|
|
5
|
+
*
|
|
6
|
+
* The settings hook can only refuse the Read and tell Claude where the
|
|
7
|
+
* converted Markdown is, which costs one more tool call per document. A mod
|
|
8
|
+
* sits in front of the call, so it hands the same call on with the path
|
|
9
|
+
* swapped: Claude asked for deck.pptx and reads deck.md, and the permission
|
|
10
|
+
* check still runs on the path that is actually read.
|
|
11
|
+
*
|
|
12
|
+
* Whenever this mod does not redirect (not a document, conversion refused or
|
|
13
|
+
* failed, sprag not on PATH) the call goes on untouched, and sprag's settings
|
|
14
|
+
* hook beneath it explains the refusal exactly as it does today. After a
|
|
15
|
+
* redirect that hook sees a .md path and stays silent, so the two never
|
|
16
|
+
* answer the same Read twice.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Mirrors TARGET_EXTENSIONS in src/doc2md.cjs. A hooks module has no Node, so
|
|
20
|
+
// it cannot load that file; test/mod-install.test.js holds the two in step.
|
|
21
|
+
const TARGET = /\.(pptx|xlsx|xls|pdf|docx|fig)$/i
|
|
22
|
+
|
|
23
|
+
// The CLI's name, as CLI_NAME in src/cli-name.js spells it. The mod cannot
|
|
24
|
+
// import that file either; test/mod-install.test.js holds the two in step.
|
|
25
|
+
const CLI = 'sprag'
|
|
26
|
+
|
|
27
|
+
// The converter gives up at 120 s on its own; the margin covers CLI start-up.
|
|
28
|
+
// Time spent in $.process.run does not count against the hook's 10 s budget,
|
|
29
|
+
// so the Read waits as long as the conversion does, as it does under the
|
|
30
|
+
// settings hook today. The status line says so while it lasts.
|
|
31
|
+
const CONVERT_TIMEOUT_MS = 150_000
|
|
32
|
+
|
|
33
|
+
// What `sprag doc2md <path>` prints on success. Parsing human output is a
|
|
34
|
+
// prototype shortcut: a `--json` flag on that command should replace it.
|
|
35
|
+
const CONVERTED = /^✓ (cached|converted): (.+)$/m
|
|
36
|
+
|
|
37
|
+
type Converted = { cacheFile: string; isCached: boolean }
|
|
38
|
+
|
|
39
|
+
const baseName = (path: string) => path.split(/[\\/]/).pop() ?? path
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Runs the conversion the settings hook would run, ledger entry included.
|
|
43
|
+
* Answers null on any failure so the caller passes the event on unchanged.
|
|
44
|
+
*
|
|
45
|
+
* A first conversion can take a minute, and nothing else on screen says the
|
|
46
|
+
* Read is waiting on it, so the status line under the prompt names the file
|
|
47
|
+
* until the CLI returns. A status line rather than a transcript line: whether
|
|
48
|
+
* the file is cached is unknown until the CLI answers, and a log line on every
|
|
49
|
+
* Read would double the lines for the cached ones too.
|
|
50
|
+
*/
|
|
51
|
+
async function convert($: EngineInterface, path: string): Promise<Converted | null> {
|
|
52
|
+
$.ui.status(`doc2md: ${baseName(path)} 변환 중…`)
|
|
53
|
+
try {
|
|
54
|
+
const run = await $.process.run([CLI, 'doc2md', path], { timeoutMs: CONVERT_TIMEOUT_MS })
|
|
55
|
+
if (run.exitCode !== 0) return null
|
|
56
|
+
const match = CONVERTED.exec(run.stdout)
|
|
57
|
+
if (!match) return null
|
|
58
|
+
return { cacheFile: match[2].trim(), isCached: match[1] === 'cached' }
|
|
59
|
+
} catch {
|
|
60
|
+
return null
|
|
61
|
+
} finally {
|
|
62
|
+
$.ui.status(undefined)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function logRedirect($: EngineInterface, from: string, doc: Converted) {
|
|
67
|
+
$.ui.log(`doc2md: ${baseName(from)} 대신 변환본을 읽습니다${doc.isCached ? ' (캐시)' : ''}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const register: Register = on => {
|
|
71
|
+
// Both hooks fail open, which is the point of the .catch on each: this mod
|
|
72
|
+
// saves a round trip and guards nothing, so a hook that breaks hands the
|
|
73
|
+
// event on as it came and sprag's settings hook beneath answers as before.
|
|
74
|
+
// Where the hook had already called next, next(e) replays that call.
|
|
75
|
+
on('tool.call', { tool: 'Read' }, async ($, e, next) => {
|
|
76
|
+
if (!TARGET.test(e.file_path)) return next(e)
|
|
77
|
+
const doc = await convert($, e.file_path)
|
|
78
|
+
if (!doc) return next(e)
|
|
79
|
+
logRedirect($, e.file_path, doc)
|
|
80
|
+
// Line and page ranges addressed the binary and mean nothing in the
|
|
81
|
+
// Markdown (`pages` applies to PDFs only), so the whole file is read.
|
|
82
|
+
const { offset: _offset, limit: _limit, pages: _pages, ...call } = e
|
|
83
|
+
return next({ ...call, file_path: doc.cacheFile })
|
|
84
|
+
}).catch(($, e, next) => next(e))
|
|
85
|
+
|
|
86
|
+
// An @-mentioned document is attached whole, before any tool call, so the
|
|
87
|
+
// Read hook above never sees it. Redirect the attachment the same way.
|
|
88
|
+
on('prompt.mention', { path: TARGET }, async ($, e, next) => {
|
|
89
|
+
const doc = await convert($, e.path)
|
|
90
|
+
if (!doc) return next(e)
|
|
91
|
+
logRedirect($, e.path, doc)
|
|
92
|
+
// A mention carries no `pages` (PromptMentionInput has mention, path,
|
|
93
|
+
// offset, limit and agentId), so only the line range is dropped here.
|
|
94
|
+
const { offset: _offset, limit: _limit, ...mention } = e
|
|
95
|
+
return next({ ...mention, path: doc.cacheFile })
|
|
96
|
+
}).catch(($, e, next) => next(e))
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.64.0",
|
|
4
4
|
"description": "Lower your Claude Code bill without changing how you work: simple tasks go to cheaper models, documents go in as compact Markdown, and the statusline shows spend and savings live. Works with Codex too.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"bin/",
|
|
23
23
|
"src/",
|
|
24
24
|
"mod/*/.claude-plugin/plugin.json",
|
|
25
|
-
"mod/*/hooks
|
|
25
|
+
"mod/*/hooks/**",
|
|
26
26
|
"presets/",
|
|
27
27
|
"examples/",
|
|
28
28
|
"README.md",
|
package/src/codex-brief.js
CHANGED
|
@@ -7,7 +7,7 @@ import { writeViaTmp } from './state-file.js';
|
|
|
7
7
|
import { readCodexSnapshot } from './codex-parser.js';
|
|
8
8
|
import { loadCodexModelRules } from './codex-delegation.js';
|
|
9
9
|
import { loadCodexLedger } from './codex-ledger.js';
|
|
10
|
-
import { codexRulesInReview } from './codex-rule-health.js';
|
|
10
|
+
import { codexRulesInReview, codexSharedRuleHealth } from './codex-rule-health.js';
|
|
11
11
|
|
|
12
12
|
const RESET_TOLERANCE_S = 300;
|
|
13
13
|
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
@@ -88,6 +88,15 @@ export function runCodexBrief({ sessionId, transcriptPath, cwd, root: projectRoo
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
} catch { /* An unreadable rule file or ledger only skips this line. */ }
|
|
91
|
+
try {
|
|
92
|
+
for (const { rule, health, from, model, provider } of codexSharedRuleHealth({ root: cwd || projectRoot || snapshot.projectDir, dir })) {
|
|
93
|
+
if (health.status !== 'review') continue;
|
|
94
|
+
issues.push({ key: 'rule-health', tier: 1,
|
|
95
|
+
signature: `shared-rule-health:${JSON.stringify([rule.sharedSignature, rule.scope, rule.targetRoot, provider, from, model])}`,
|
|
96
|
+
level: 'warning', message: `Codex shared policy ${rule.tier} ${rule.category} (${provider}: ${from} -> ${model}) failed ${health.errs} of ${health.runs} measured runs.`,
|
|
97
|
+
advice: 'Review: sprag delegate rules --agent codex. Pause shared routing: sprag delegate shared off --agent codex. Common policy removal affects both agents.' });
|
|
98
|
+
}
|
|
99
|
+
} catch { /* Shared health is independent of Codex-only rule parsing. */ }
|
|
91
100
|
const next = issues.filter((issue) => !active.includes(issue.signature) &&
|
|
92
101
|
!(issue.key === 'context' && issue.tier === 1 && active.includes('context:2')));
|
|
93
102
|
// Missing or stale measurements do not resolve an earlier warning. A fresh
|