@ictechgy/context-guard 0.4.11 → 0.4.12
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/CHANGELOG.md +4 -0
- package/README.ko.md +19 -12
- package/README.md +11 -11
- package/package.json +1 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/bin/context-guard +42 -46
- package/plugins/context-guard/bin/context-guard-audit +3 -3
- package/plugins/context-guard/bin/context-guard-bench +136 -16
- package/plugins/context-guard/bin/context-guard-cache-score +29 -2
- package/plugins/context-guard/bin/context-guard-compress +89 -27
- package/plugins/context-guard/bin/context-guard-filter +88 -18
- package/plugins/context-guard/bin/context-guard-pack +28 -2
- package/plugins/context-guard/bin/context-guard-read-symbol +27 -0
- package/plugins/context-guard/bin/context-guard-sanitize-output +169 -6
- package/plugins/context-guard/bin/context-guard-setup +21 -5
- package/plugins/context-guard/bin/context-guard-tool-prune +48 -10
- package/plugins/context-guard/bin/context-guard-trim-output +109 -52
- package/plugins/context-guard/lib/context_guard_command_manifest_loader.py +123 -0
- package/plugins/context-guard/lib/context_guard_commands.py +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ All notable changes for the ContextGuard plugin are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.4.12] - 2026-06-22
|
|
8
|
+
|
|
9
|
+
- Published the post-merge README, Korean README, and GitHub Pages copy polish into the npm/package metadata so package consumers see the same setup, packaging, helper-trust, and conservative savings-claim guidance as the product site.
|
|
10
|
+
|
|
7
11
|
## [0.4.11] - 2026-06-21
|
|
8
12
|
|
|
9
13
|
- Hardened token-savings advisory surfaces with cache-score amortization risk accounting, tool-prune deferred-schema proxy accounting, and benchmark measurement-baseline contracts while preserving claim-safe boundaries.
|
package/README.ko.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# ContextGuard
|
|
2
2
|
|
|
3
|
-
ContextGuard는 AI 코딩·도구 에이전트를 위한 로컬 우선 컨텍스트 관리 도구 모음입니다. Claude Code 플러그인으로 먼저 시작할 수 있으며, 한 번 설치한 뒤 프로젝트별로 명시적으로 활성화하고 필요하면 되돌릴 수 있습니다. 출력 축약, 심볼 단위
|
|
3
|
+
ContextGuard는 AI 코딩·도구 에이전트를 위한 로컬 우선 컨텍스트 관리 도구 모음입니다. Claude Code 플러그인으로 먼저 시작할 수 있으며, 한 번 설치한 뒤 프로젝트별로 명시적으로 활성화하고 필요하면 되돌릴 수 있습니다. 출력 축약, 심볼 단위 읽기 유도, 반복 실패 알림, 민감정보 패턴 가림, 사용량 측정 가드레일은 로컬 헬퍼 명령과 brief 모드 안내 스니펫을 통해 다른 에이전트에서도 재사용할 수 있습니다.
|
|
4
4
|
|
|
5
5
|
- 영문 문서: [`README.md`](README.md)
|
|
6
6
|
- HTML 랜딩 페이지: [GitHub Pages](https://ictechgy.github.io/context-guard/) ([소스](docs/index.html))
|
|
7
7
|
|
|
8
8
|
## 한눈에 보기
|
|
9
9
|
|
|
10
|
-
설치와 활성화는 의도적으로 분리되어 있습니다. 설치만 하면 로컬 헬퍼나 Claude 플러그인 스킬이 준비될 뿐이며, 설정 파일은 사용자가 `setup`을 명시적으로 실행할 때만
|
|
10
|
+
설치와 활성화는 의도적으로 분리되어 있습니다. 설치만 하면 로컬 헬퍼나 Claude 플러그인 스킬이 준비될 뿐이며, 설정 파일은 사용자가 `setup`을 명시적으로 실행할 때만 기록됩니다.
|
|
11
11
|
|
|
12
12
|
| 쓰는 도구 | 설치 | 활성화 |
|
|
13
13
|
| --- | --- | --- |
|
|
@@ -27,15 +27,15 @@ context-guard setup --agent claude --scope user --verify --json # 읽기 전용
|
|
|
27
27
|
context-guard setup --agent claude --scope user --plan
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
기본값은 프로젝트 단위 설정입니다. 사용자 단위 설정은 명시적으로 선택해야 하며, 실제 변경을 적용하려면 `--yes`와 명시적인 `--agent`가 필요합니다. 지원되는 사용자 단위 변경은 백업과 되돌리기 기록을 남기며, 패키지 설치 중에는 실행되지 않습니다. `setup`은 먼저 패키지/체크아웃 내부 헬퍼를 찾습니다. 신뢰할 수 있는 설치임을 확인한 경우에만 `--allow-path-helper-fallback`으로 `PATH` 헬퍼 대체 경로를 허용하세요.
|
|
30
|
+
기본값은 프로젝트 단위 설정입니다. 사용자 단위 설정은 명시적으로 선택해야 하며, 실제 변경을 적용하려면 `--yes`와 명시적인 `--agent`가 필요합니다. 지원되는 사용자 단위 변경은 백업과 되돌리기 기록을 남기며, 패키지 설치 중에는 실행되지 않습니다. 적용 전에는 `context-guard doctor` 또는 `context-guard setup --verify`로 읽기 전용 상태를 먼저 확인하세요. `setup`은 먼저 패키지/체크아웃 내부 헬퍼를 찾습니다. 신뢰할 수 있는 설치임을 확인한 경우에만 `--allow-path-helper-fallback`으로 `PATH` 헬퍼 대체 경로를 허용하세요.
|
|
31
31
|
|
|
32
|
-
배포와 헬퍼 신뢰 경계도 보수적입니다. npm은 canonical `context-guard`/`context-guard-*` bin 링크만 노출하고 legacy `claude-*` 래퍼는 경로 기반 마이그레이션용 패키지 파일로만 남깁니다. 명령 매니페스트는 실행 가능한 Python이 아니라 literal 데이터로만 읽으며, macOS visibility 헬퍼는 번들/resource/실행 파일 기준 경로나 absolute explicit override만 사용하고 최소 환경으로 실행합니다. 현재 작업 디렉터리, 상대 override, symlink 헬퍼, 임의 `PATH`, 상위 셸 환경은 기본적으로 신뢰하지 않습니다.
|
|
32
|
+
배포와 헬퍼 신뢰 경계도 보수적입니다. npm은 canonical `context-guard`/`context-guard-*` bin 링크만 노출하고 legacy `claude-*` 래퍼는 경로 기반 마이그레이션용 패키지 파일로만 남깁니다. 명령 매니페스트는 실행 가능한 Python이 아니라 literal 데이터로만 읽으며, macOS visibility 헬퍼는 번들/resource/실행 파일 기준 경로나 absolute explicit override만 사용하고 최소 환경으로 실행합니다. 현재 작업 디렉터리, 상대 override, symlink 헬퍼, 임의 `PATH`, 불필요한 상위 셸 환경은 기본적으로 신뢰하지 않습니다.
|
|
33
33
|
|
|
34
34
|
ContextGuard는 절감 수치를 과장하지 않습니다. 흔히 컨텍스트를 불필요하게 키우는 원인을 줄이고, 실제 전후 비교 결과는 각자의 작업에서 측정할 수 있도록 벤치마크 도구를 제공합니다. 저장소마다 효과는 달라질 수 있으며, 고정된 토큰·비용 절감률은 보장하지 않습니다.
|
|
35
35
|
|
|
36
36
|
## Claude Code 우선, 다른 에이전트도 함께
|
|
37
37
|
|
|
38
|
-
Claude Code 사용자는 플러그인으로 시작하는 것이 가장 빠릅니다.
|
|
38
|
+
Claude Code 사용자는 플러그인으로 시작하는 것이 가장 빠릅니다. 설치한 뒤에는 같은 로컬 우선 가드레일을 다음 방식으로 다른 AI 코딩·도구 에이전트에서도 재사용할 수 있습니다.
|
|
39
39
|
|
|
40
40
|
- **로컬 헬퍼 명령**(`context-guard-*`)은 특정 에이전트에 묶이지 않은 일반 셸 명령으로 실행됩니다.
|
|
41
41
|
- **brief 모드 스니펫**은 에이전트의 지시 파일(`AGENTS.md`, `GEMINI.md`, `.cursorrules`, Copilot 지시 파일 등)에 마커 블록으로 설치하고, 블록을 지우면 제거됩니다.
|
|
@@ -71,7 +71,7 @@ ContextGuard는 모델 단가 자체를 낮추는 도구가 아닙니다. AI 코
|
|
|
71
71
|
|
|
72
72
|
## 캐시·압축 도구와의 차이
|
|
73
73
|
|
|
74
|
-
ContextGuard는 provider 캐시, semantic cache, 프롬프트 압축 도구를 대체하지 않습니다. 핵심 역할은 더 단순합니다. **불필요한 파일·로그·출력이 에이전트 컨텍스트에 들어가기 전에
|
|
74
|
+
ContextGuard는 provider 캐시, semantic cache, 프롬프트 압축 도구를 대체하지 않습니다. 핵심 역할은 더 단순합니다. **불필요한 파일·로그·출력이 에이전트 컨텍스트에 들어가기 전에 줄어들도록 돕는 것**입니다.
|
|
75
75
|
|
|
76
76
|
| 도구 유형 | 줄이는 방식 | ContextGuard와의 관계 |
|
|
77
77
|
| --- | --- | --- |
|
|
@@ -87,11 +87,11 @@ ContextGuard는 provider 캐시, semantic cache, 프롬프트 압축 도구를
|
|
|
87
87
|
| --- | --- | --- |
|
|
88
88
|
| 압축 우선 | 모델에 이미 선택된 텍스트를 줄이며, 경우에 따라 손실형 변환을 사용합니다. | ContextGuard는 손실형 단방향 압축보다 로컬 보관본 저장과 정확한 줄·패턴 재조회를 선호하므로, 원본을 다시 가져올 수 있습니다. |
|
|
89
89
|
| 여러 에이전트의 간결 출력 규칙 | 여러 에이전트에 brief 모드 출력 규칙을 한꺼번에 설치합니다. | ContextGuard는 안내용 brief 모드 스니펫과 dry-run 에이전트 간 설정을 제공합니다. 프로젝트별 opt-in이며, 절감을 보장하지 않습니다. |
|
|
90
|
-
| ContextGuard | 불필요한 파일·로그·출력이 컨텍스트에 들어가기 전에 줄어들도록 돕고 보수적으로 측정합니다. | 로컬 가드레일, 되돌릴 수 있는 로컬 보관본·재조회, 직접 측정하는 벤치마크
|
|
90
|
+
| ContextGuard | 불필요한 파일·로그·출력이 컨텍스트에 들어가기 전에 줄어들도록 돕고 보수적으로 측정합니다. | 로컬 가드레일, 되돌릴 수 있는 로컬 보관본·재조회, 직접 측정하는 벤치마크 근거를 제공합니다. |
|
|
91
91
|
|
|
92
92
|
## brief 모드 (안내용)
|
|
93
93
|
|
|
94
|
-
brief 모드는 코딩 에이전트가 군더더기를 줄이도록 요청하되, 리뷰에 필요한 증거(파일 경로, 명령, 명령 출력과 오류, 코드 블록, 검증 상태, 변경 파일, 남은 과제, 주의사항)는
|
|
94
|
+
brief 모드는 코딩 에이전트가 군더더기를 줄이도록 요청하되, 리뷰에 필요한 증거(파일 경로, 명령, 명령 출력과 오류, 코드 블록, 검증 상태, 변경 파일, 남은 과제, 주의사항)는 유지하도록 돕는 에이전트 중립·안내용 규칙 스니펫 모음입니다. 강제가 아니라 최선 노력 안내이며, 토큰·비용 절감을 **보장하지 않습니다.**
|
|
95
95
|
|
|
96
96
|
사전 정의된 세 레벨이 [`plugins/context-guard/brief/`](plugins/context-guard/brief/)에 포함됩니다: `lite`, `standard`, `ultra`. 각 레벨은 에이전트 규칙·지시 파일(`AGENTS.md`, `CLAUDE.md`, Cursor 규칙 파일, Copilot 지시 등)에 들어가는 마커 구분 블록입니다. `context-guard setup --agent codex --scope project --brief-mode standard --plan`으로 미리 보고, 적용은 `--yes`로 다시 실행하며, 제거는 `--brief-mode off`를 사용하세요. 자세한 내용은 [`plugins/context-guard/brief/README.md`](plugins/context-guard/brief/README.md)를 참고하세요.
|
|
97
97
|
|
|
@@ -369,11 +369,11 @@ JSON 출력에는 여러 증거 surface가 포함될 수 있습니다.
|
|
|
369
369
|
--ledger-jsonl bench/cost-shift.jsonl --report-json bench/report.json
|
|
370
370
|
```
|
|
371
371
|
|
|
372
|
-
보고서를 읽을 때는 먼저
|
|
372
|
+
보고서를 읽을 때는 먼저 주장 범위를 확인하세요.
|
|
373
373
|
|
|
374
374
|
- 성공한 기준/변형 실행은 실제 토큰과 `cost_usd + external_cost_usd` 기준으로 비교하고, 바이트 감소는 간접 증거로만 기록합니다.
|
|
375
375
|
- 토큰 절감 주장은 대응 태스크 양쪽 모두에 `primary_tokens_measured`가 있을 때만 계산합니다.
|
|
376
|
-
- `matched_pair_evidence`는 성공한 task bucket을 transform, 측정 가능 여부, quality gate,
|
|
376
|
+
- `matched_pair_evidence`는 성공한 task bucket을 transform, 측정 가능 여부, quality gate, 주장 범위와 연결하므로 절감 문구를 쓰기 전에 먼저 확인해야 합니다.
|
|
377
377
|
- `default_matrix`는 같은 대응 evidence를 기반으로 trimming, artifact escrow, tool pruning, cache advice, adaptive-k, optional compression을 `default-on`, `advisory`, `experimental`, `reject/rework`로 분류합니다. 이 matrix는 report 전용이며 runtime default를 바꾸거나 hosted token/cost 절감 주장을 허용하지 않습니다.
|
|
378
378
|
- `public_claim_readiness`는 release/public claim의 최종 gate입니다. matched successful task, provider-measured primary token/cost, quality non-inferiority, shifted-cost accounting, 명시적 confidence/failure note, complete provider-export provenance가 모두 통과해야 `claim_allowed=true`가 되며, 그렇지 않은 hosted savings claim은 금지됩니다.
|
|
379
379
|
- `wall_time_seconds`, `provider_cached_tokens`, `provider_cached_tokens_measured`는 진단용 텔레메트리이며, ContextGuard가 직접 만든 토큰·비용 절감 증거로 보지 않습니다.
|
|
@@ -414,7 +414,7 @@ local-proxy 예시는 side effect 기준으로 나뉩니다.
|
|
|
414
414
|
- `--diagnostic-ledger-jsonl`을 지정하면 successful forwarded request 뒤에만 shifted-cost 진단 row를 append하며 raw header, request body, response body, hosted-savings evidence를 저장하지 않습니다.
|
|
415
415
|
- `plan local-proxy-external-forwarding`은 dry-run design gate일 뿐입니다. explicit external intent, design ack, HTTPS host allowlist, threat model note, credential redaction policy, provider-evidence boundary를 요구하지만 listener 시작, DNS lookup, external service call, traffic forwarding, credential persistence, external proxy forwarding runtime 제공, hosted savings claim을 하지 않습니다.
|
|
416
416
|
|
|
417
|
-
기본적으로 프로젝트 설정은 `.context-guard/experiments.json`에 저장됩니다. 명시적인 프로젝트 로컬 재정의가 필요할 때만 `--config <path>`를 사용하세요. 실험 메타데이터에는 risk level, gate requirement, explicit command/flag surface,
|
|
417
|
+
기본적으로 프로젝트 설정은 `.context-guard/experiments.json`에 저장됩니다. 명시적인 프로젝트 로컬 재정의가 필요할 때만 `--config <path>`를 사용하세요. 실험 메타데이터에는 risk level, gate requirement, explicit command/flag surface, 주장 범위가 포함되어 provider-measured matched-task evidence 없이는 hosted API token/cost savings claim으로 쓰지 않도록 합니다. `experiments enable`은 의도만 기록하며 helper를 실행하거나 명시 flag를 대체하거나 exact receipt/re-expand evidence 없는 content replacement를 허용하지 않습니다.
|
|
418
418
|
|
|
419
419
|
| 안전성 checker/planner/runtime | 출력하는 것 | 넘지 않는 경계 |
|
|
420
420
|
| --- | --- | --- |
|
|
@@ -428,7 +428,14 @@ local-proxy 예시는 side effect 기준으로 나뉩니다.
|
|
|
428
428
|
|
|
429
429
|
아래 항목은 프로젝트가 기록해 둔 방향일 뿐, 약속된 기능이 아닙니다. 저장소의 다른 문서에 명시되지 않는 한 아직 제공 기능이 아닙니다.
|
|
430
430
|
|
|
431
|
-
|
|
431
|
+
ContextGuard는 아직 다음 기능을 제공하지 않습니다.
|
|
432
|
+
|
|
433
|
+
- caller-supplied learned candidate emitter를 넘어서는 learned/synthetic compressor 실행 또는 생성형 replacement
|
|
434
|
+
- caller-supplied visual evidence-pack emitter를 넘어서는 생성형 crop/OCR 또는 visual-token pruning runtime
|
|
435
|
+
- 명시적 local metrics 기록을 넘어서는 self-hosted KV/latent optimization
|
|
436
|
+
- one-shot literal-loopback local proxy MVP를 넘어서는 external/daemon/credential-bearing proxy forwarding runtime
|
|
437
|
+
|
|
438
|
+
자세한 내용은 [experimental token-reduction radar](research/experimental-token-reduction-radar.md)와 [fixture-only experimental benchmark starters](docs/experimental-benchmark-fixtures.md)를 참고하세요. 이 항목들은 later-roadmap gate를 통과하기 전까지 제공 기능이 아닙니다. matched successful task, failure-rate guardrail, human-correction tracking, shifted-cost accounting, provider가 측정한 token/cost evidence와 별도 future PR gate가 있어야 hosted API 절감 주장이나 더 넓은 런타임 기능 주장으로 승격할 수 있습니다.
|
|
432
439
|
|
|
433
440
|
## 저장소 구조
|
|
434
441
|
|
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# ContextGuard
|
|
2
2
|
|
|
3
|
-
ContextGuard is a local-first context
|
|
3
|
+
ContextGuard is a local-first context-management toolkit for AI coding and tool-using agents. It starts with a Claude Code plugin: install it once, enable it explicitly per project, and roll it back when needed.
|
|
4
4
|
|
|
5
|
-
It
|
|
5
|
+
It trims noisy output, guides agents toward symbol-level reads, flags repeated failures, redacts secret-like patterns, and measures usage. The same guardrails are reusable by other agents through local helper commands and advisory brief-mode snippets.
|
|
6
6
|
|
|
7
7
|
- Korean documentation: [`README.ko.md`](README.ko.md)
|
|
8
8
|
- Static landing page: [GitHub Pages](https://ictechgy.github.io/context-guard/) ([source](docs/index.html))
|
|
9
9
|
|
|
10
10
|
## TL;DR
|
|
11
11
|
|
|
12
|
-
Installation and activation are deliberately separate. Installing ContextGuard only makes local helpers or Claude plugin skills available;
|
|
12
|
+
Installation and activation are deliberately separate. Installing ContextGuard only makes local helpers or Claude plugin skills available; it does not write configuration until you run an explicit setup command.
|
|
13
13
|
|
|
14
14
|
| If you use... | Install | Activate |
|
|
15
15
|
| --- | --- | --- |
|
|
@@ -29,15 +29,15 @@ context-guard setup --agent claude --scope user --verify --json # read-only use
|
|
|
29
29
|
context-guard setup --agent claude --scope user --plan
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Project scope is the default. User-level setup is opt-in, requires an explicit agent for writes, records backups and rollback metadata, and never runs during package installation.
|
|
32
|
+
Project scope is the default. User-level setup is opt-in, requires an explicit agent for writes, records backups and rollback metadata, and never runs during package installation. Before applying setup, use `context-guard doctor` or `context-guard setup --verify` for a read-only health check. `doctor` reports next commands and makes no changes. Setup looks for bundled or checkout-local helpers first; it does not trust arbitrary `PATH` helpers unless you explicitly pass `--allow-path-helper-fallback` for a known-good install.
|
|
33
33
|
|
|
34
|
-
Distribution and helper trust boundaries are conservative too: npm exposes only canonical `context-guard`/`context-guard-*` bin links, legacy `claude-*` wrappers remain package files for path-based migration, command manifests are treated as literal data
|
|
34
|
+
Distribution and helper trust boundaries are conservative too: npm exposes only canonical `context-guard`/`context-guard-*` bin links, legacy `claude-*` wrappers remain package files for path-based migration, command manifests are treated as literal data rather than executable Python, and the macOS visibility helper is discovered only from bundled/resource/executable-relative paths or an absolute explicit override with a minimal child environment. Current working directories, relative overrides, symlinked helpers, arbitrary `PATH`, and ambient shell environment are not trusted by default.
|
|
35
35
|
|
|
36
36
|
ContextGuard is intentionally conservative about savings claims. It reduces common sources of context bloat and provides benchmark tooling so you can measure before-and-after results on your own tasks. It does **not** promise a fixed token or cost reduction for every repository.
|
|
37
37
|
|
|
38
38
|
## Claude Code first, other agents too
|
|
39
39
|
|
|
40
|
-
ContextGuard ships as a Claude Code plugin first, which is still the fastest
|
|
40
|
+
ContextGuard ships as a Claude Code plugin first, which is still the fastest starting point for Claude users. After installation, the same local-first guardrails can be reused by other AI coding and tool-using agents through:
|
|
41
41
|
|
|
42
42
|
- **Local helper commands** (`context-guard-*`) that run as plain shell commands, independent of any specific agent.
|
|
43
43
|
- **Advisory brief-mode rule snippets** that you install into an agent's own instruction file (`AGENTS.md`, `GEMINI.md`, `.cursorrules`, Copilot instructions, and similar rule files) and remove by deleting the marker-delimited block.
|
|
@@ -58,7 +58,7 @@ Current setup surfaces:
|
|
|
58
58
|
|
|
59
59
|
## How ContextGuard reduces token waste
|
|
60
60
|
|
|
61
|
-
ContextGuard does not
|
|
61
|
+
ContextGuard does not change model prices. It reduces avoidable context before it reaches an AI coding agent, then gives you signals to measure whether the change helped.
|
|
62
62
|
|
|
63
63
|
| Waste path | ContextGuard guardrail |
|
|
64
64
|
| --- | --- |
|
|
@@ -73,7 +73,7 @@ ContextGuard does not lower model prices by itself. It reduces avoidable context
|
|
|
73
73
|
|
|
74
74
|
## How it fits with caching and compression tools
|
|
75
75
|
|
|
76
|
-
ContextGuard complements provider and semantic caches, and
|
|
76
|
+
ContextGuard complements provider and semantic caches, and works alongside prompt compression. Its main job is simpler: **do not send unnecessary files, logs, or output in the first place**.
|
|
77
77
|
|
|
78
78
|
| Tool category | Saves by | ContextGuard relationship |
|
|
79
79
|
| --- | --- | --- |
|
|
@@ -89,11 +89,11 @@ Related patterns that informed the design:
|
|
|
89
89
|
| --- | --- | --- |
|
|
90
90
|
| Compression-first | Shortening text already selected for the model, often with lossy transforms. | ContextGuard prefers local artifact storage with exact slice retrieval over lossy one-way compression, so you can get the original back. |
|
|
91
91
|
| Terse-output rulesets across agents | Installing brief-mode output rules into many agents at once. | ContextGuard offers advisory brief-mode snippets and dry-run cross-agent setup — opt-in per project, no guaranteed savings claimed. |
|
|
92
|
-
| ContextGuard | Avoiding unnecessary files, logs, and output before they enter context, with conservative measurement. | Local guardrails, reversible artifacts and retrieval,
|
|
92
|
+
| ContextGuard | Avoiding unnecessary files, logs, and output before they enter context, with conservative measurement. | Local guardrails, reversible artifacts and retrieval, plus benchmark evidence you measure yourself. |
|
|
93
93
|
|
|
94
94
|
## Brief mode (advisory)
|
|
95
95
|
|
|
96
|
-
Brief mode is a set of agent-neutral, advisory rule snippets that ask a coding agent to cut filler while preserving
|
|
96
|
+
Brief mode is a set of agent-neutral, advisory rule snippets that ask a coding agent to cut filler while preserving reviewer evidence: file paths, commands, command output and errors, code blocks, verification status, changed files, known gaps, and caveats. It is best-effort guidance, not enforcement, and does **not** guarantee token or cost savings.
|
|
97
97
|
|
|
98
98
|
Three deterministic levels ship under [`plugins/context-guard/brief/`](plugins/context-guard/brief/): `lite`, `standard`, and `ultra`. Each level is a single marker-delimited block for an agent's rule/instruction file (for example `AGENTS.md`, `CLAUDE.md`, a Cursor rules file, or Copilot instructions). Manage it through setup with `context-guard setup --agent codex --scope project --brief-mode standard --plan`, rerun with `--yes` to apply, and use `--brief-mode off` to remove the managed block. See [`plugins/context-guard/brief/README.md`](plugins/context-guard/brief/README.md).
|
|
99
99
|
|
|
@@ -475,7 +475,7 @@ Shipped experimental checker/planner surfaces, plus explicit local context-diff,
|
|
|
475
475
|
|
|
476
476
|
## What is not yet shipped
|
|
477
477
|
|
|
478
|
-
These are directions
|
|
478
|
+
These are tracked directions, not committed features. Nothing here ships unless another repository document says it does.
|
|
479
479
|
|
|
480
480
|
ContextGuard does not yet ship:
|
|
481
481
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ictechgy/context-guard",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "ContextGuard CLI helpers for keeping AI coding agent context focused and local-first.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/ictechgy/context-guard#readme",
|
|
@@ -9,7 +9,6 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
|
-
import ast
|
|
13
12
|
from pathlib import Path
|
|
14
13
|
import subprocess
|
|
15
14
|
import stat
|
|
@@ -19,25 +18,13 @@ from typing import Any, NoReturn
|
|
|
19
18
|
COMMAND_NAME = "context-guard"
|
|
20
19
|
PACKAGE_NAME = "@ictechgy/context-guard"
|
|
21
20
|
MAX_VERSION_METADATA_BYTES = 64 * 1024
|
|
22
|
-
|
|
21
|
+
MAX_MANIFEST_HELPER_BYTES = 128 * 1024
|
|
23
22
|
ALLOWED_FIRST_ABSOLUTE_SYMLINKS = {
|
|
24
23
|
"tmp": Path("/private/tmp"),
|
|
25
24
|
"var": Path("/private/var"),
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
MANIFEST_LOAD_ERROR: str | None = None
|
|
29
|
-
COMMAND_MANIFEST_LITERAL_NAMES = {
|
|
30
|
-
"IMPLEMENTATION_PAIRS",
|
|
31
|
-
"HELPER_PAIRS",
|
|
32
|
-
"NPM_BINS",
|
|
33
|
-
"NPM_BIN_PATHS",
|
|
34
|
-
"DISPATCHER_SUBCOMMANDS",
|
|
35
|
-
"LEGACY_WRAPPERS",
|
|
36
|
-
"ENTRYPOINT_SMOKE_CASES",
|
|
37
|
-
"PLUGIN_ENTRYPOINTS",
|
|
38
|
-
"DISPATCHER_SMOKE_CASES",
|
|
39
|
-
"EXPECTED_COMMAND_PACK_FILES",
|
|
40
|
-
}
|
|
41
28
|
|
|
42
29
|
|
|
43
30
|
def _manifest_candidates(script_dir: Path) -> tuple[Path, ...]:
|
|
@@ -52,7 +39,15 @@ def _manifest_candidates(script_dir: Path) -> tuple[Path, ...]:
|
|
|
52
39
|
return ()
|
|
53
40
|
|
|
54
41
|
|
|
55
|
-
def
|
|
42
|
+
def _manifest_helper_candidates(script_dir: Path) -> tuple[Path, ...]:
|
|
43
|
+
if script_dir.name == "context-guard-kit":
|
|
44
|
+
return (script_dir / "context_guard_command_manifest_loader.py",)
|
|
45
|
+
if script_dir.name == "bin":
|
|
46
|
+
return (script_dir.parent / "lib" / "context_guard_command_manifest_loader.py",)
|
|
47
|
+
return ()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _trusted_source_open_flags() -> int | None:
|
|
56
51
|
if not hasattr(os, "O_NOFOLLOW"):
|
|
57
52
|
return None
|
|
58
53
|
flags = os.O_RDONLY | os.O_NOFOLLOW
|
|
@@ -65,25 +60,25 @@ def _manifest_open_flags() -> int | None:
|
|
|
65
60
|
return flags
|
|
66
61
|
|
|
67
62
|
|
|
68
|
-
def
|
|
69
|
-
flags =
|
|
63
|
+
def _read_trusted_helper_source(path: Path) -> str | None:
|
|
64
|
+
flags = _trusted_source_open_flags()
|
|
70
65
|
if flags is None:
|
|
71
66
|
return None
|
|
72
67
|
fd = -1
|
|
73
68
|
try:
|
|
74
69
|
fd = os.open(path, flags)
|
|
75
70
|
st = os.fstat(fd)
|
|
76
|
-
if not stat.S_ISREG(st.st_mode) or st.st_size >
|
|
71
|
+
if not stat.S_ISREG(st.st_mode) or st.st_size > MAX_MANIFEST_HELPER_BYTES:
|
|
77
72
|
return None
|
|
78
73
|
chunks: list[bytes] = []
|
|
79
74
|
total = 0
|
|
80
75
|
while True:
|
|
81
|
-
chunk = os.read(fd, min(64 * 1024,
|
|
76
|
+
chunk = os.read(fd, min(64 * 1024, MAX_MANIFEST_HELPER_BYTES + 1 - total))
|
|
82
77
|
if not chunk:
|
|
83
78
|
break
|
|
84
79
|
chunks.append(chunk)
|
|
85
80
|
total += len(chunk)
|
|
86
|
-
if total >
|
|
81
|
+
if total > MAX_MANIFEST_HELPER_BYTES:
|
|
87
82
|
return None
|
|
88
83
|
return b"".join(chunks).decode("utf-8")
|
|
89
84
|
except (OSError, UnicodeDecodeError):
|
|
@@ -96,39 +91,40 @@ def _read_manifest_source(path: Path) -> str | None:
|
|
|
96
91
|
pass
|
|
97
92
|
|
|
98
93
|
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
values: dict[str, Any] = {}
|
|
105
|
-
for node in tree.body:
|
|
106
|
-
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
94
|
+
def _load_manifest_helper() -> dict[str, Any] | None:
|
|
95
|
+
script_dir = Path(__file__).resolve().parent
|
|
96
|
+
for candidate in _manifest_helper_candidates(script_dir):
|
|
97
|
+
source = _read_trusted_helper_source(candidate)
|
|
98
|
+
if source is None:
|
|
107
99
|
continue
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
elif isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
114
|
-
target = node.targets[0].id
|
|
115
|
-
value = node.value
|
|
116
|
-
if target is None:
|
|
117
|
-
return None
|
|
118
|
-
if target not in COMMAND_MANIFEST_LITERAL_NAMES or value is None:
|
|
119
|
-
return None
|
|
100
|
+
namespace: dict[str, Any] = {
|
|
101
|
+
"__builtins__": __builtins__,
|
|
102
|
+
"__file__": str(candidate),
|
|
103
|
+
"__name__": "_context_guard_command_manifest_loader",
|
|
104
|
+
}
|
|
120
105
|
try:
|
|
121
|
-
|
|
122
|
-
except
|
|
123
|
-
|
|
124
|
-
|
|
106
|
+
exec(compile(source, str(candidate), "exec"), namespace)
|
|
107
|
+
except Exception:
|
|
108
|
+
continue
|
|
109
|
+
if callable(namespace.get("read_manifest_source")) and callable(namespace.get("literal_command_manifest_from_source")):
|
|
110
|
+
return namespace
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
COMMAND_MANIFEST_LOADER = _load_manifest_helper()
|
|
125
115
|
|
|
126
116
|
|
|
127
117
|
def _load_manifest_from_path(path: Path) -> dict[str, Any] | None:
|
|
128
|
-
|
|
118
|
+
if COMMAND_MANIFEST_LOADER is None:
|
|
119
|
+
return None
|
|
120
|
+
source = COMMAND_MANIFEST_LOADER["read_manifest_source"](path)
|
|
129
121
|
if source is None:
|
|
130
122
|
return None
|
|
131
|
-
|
|
123
|
+
try:
|
|
124
|
+
values = COMMAND_MANIFEST_LOADER["literal_command_manifest_from_source"](source)
|
|
125
|
+
except ValueError:
|
|
126
|
+
return None
|
|
127
|
+
return values
|
|
132
128
|
|
|
133
129
|
|
|
134
130
|
def _coerce_helper_subcommands(value: Any) -> dict[str, tuple[str, ...]] | None:
|
|
@@ -145,14 +145,14 @@ class PromptCacheAudit:
|
|
|
145
145
|
|
|
146
146
|
def observe(self, root: Any) -> None:
|
|
147
147
|
self.sampled_records += 1
|
|
148
|
+
if len(self.samples) >= PROMPT_AUDIT_MAX_RECORDS:
|
|
149
|
+
self.capped_records += 1
|
|
150
|
+
return
|
|
148
151
|
segments, bytes_sampled, redactions, collection_capped = prompt_segments_for_record(root)
|
|
149
152
|
if collection_capped:
|
|
150
153
|
self.prompt_collection_capped_records += 1
|
|
151
154
|
if not segments:
|
|
152
155
|
return
|
|
153
|
-
if len(self.samples) >= PROMPT_AUDIT_MAX_RECORDS:
|
|
154
|
-
self.capped_records += 1
|
|
155
|
-
return
|
|
156
156
|
self.analyzed_prompt_records += 1
|
|
157
157
|
self.total_segments += len(segments)
|
|
158
158
|
self.total_bytes_sampled += bytes_sampled
|
|
@@ -1429,10 +1429,84 @@ def run_fixture(task: TaskFixture, variant: Variant, claude_bin: str,
|
|
|
1429
1429
|
)
|
|
1430
1430
|
|
|
1431
1431
|
|
|
1432
|
-
def
|
|
1432
|
+
def csv_file_stamp_unlocked(csv_path: Path) -> tuple[int, int, int, int] | None:
|
|
1433
|
+
try:
|
|
1434
|
+
fd = _open_regular_no_symlink(csv_path)
|
|
1435
|
+
except FileNotFoundError:
|
|
1436
|
+
return None
|
|
1437
|
+
try:
|
|
1438
|
+
st = os.fstat(fd)
|
|
1439
|
+
return (int(st.st_dev), int(st.st_ino), int(st.st_size), int(st.st_mtime_ns))
|
|
1440
|
+
finally:
|
|
1441
|
+
os.close(fd)
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def refresh_existing_key_cache_unlocked(
|
|
1445
|
+
csv_path: Path,
|
|
1446
|
+
existing_key_cache: set[tuple[str, str]],
|
|
1447
|
+
existing_key_cache_stamp: dict[str, tuple[int, int, int, int] | None] | None,
|
|
1448
|
+
) -> None:
|
|
1449
|
+
current_stamp = csv_file_stamp_unlocked(csv_path)
|
|
1450
|
+
if existing_key_cache_stamp is not None and existing_key_cache_stamp.get("stamp") == current_stamp:
|
|
1451
|
+
return
|
|
1452
|
+
refreshed = _read_existing_keys_unlocked(csv_path)
|
|
1453
|
+
existing_key_cache.clear()
|
|
1454
|
+
existing_key_cache.update(refreshed)
|
|
1455
|
+
if existing_key_cache_stamp is not None:
|
|
1456
|
+
existing_key_cache_stamp["stamp"] = current_stamp
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
def resume_key_present(
|
|
1460
|
+
csv_path: Path,
|
|
1461
|
+
key: tuple[str, str],
|
|
1462
|
+
existing_key_cache: set[tuple[str, str]],
|
|
1463
|
+
existing_key_cache_stamp: dict[str, tuple[int, int, int, int] | None] | None,
|
|
1464
|
+
) -> bool:
|
|
1465
|
+
if not _csv_exists_no_follow(csv_path):
|
|
1466
|
+
existing_key_cache.clear()
|
|
1467
|
+
if existing_key_cache_stamp is not None:
|
|
1468
|
+
existing_key_cache_stamp["stamp"] = None
|
|
1469
|
+
return False
|
|
1470
|
+
with csv_file_lock(csv_path, create_parent=False):
|
|
1471
|
+
refresh_existing_key_cache_unlocked(csv_path, existing_key_cache, existing_key_cache_stamp)
|
|
1472
|
+
return key in existing_key_cache
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def resume_runnable_targets(
|
|
1476
|
+
csv_path: Path,
|
|
1477
|
+
targets: list[tuple[TaskFixture, Variant]],
|
|
1478
|
+
*,
|
|
1479
|
+
resume: bool,
|
|
1480
|
+
existing_key_cache: set[tuple[str, str]],
|
|
1481
|
+
existing_key_cache_stamp: dict[str, tuple[int, int, int, int] | None] | None,
|
|
1482
|
+
) -> list[tuple[TaskFixture, Variant]]:
|
|
1483
|
+
if not resume:
|
|
1484
|
+
return list(targets)
|
|
1485
|
+
return [
|
|
1486
|
+
(task, variant)
|
|
1487
|
+
for task, variant in targets
|
|
1488
|
+
if not resume_key_present(csv_path, (task.id, variant.name), existing_key_cache, existing_key_cache_stamp)
|
|
1489
|
+
]
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
def append_csv(
|
|
1493
|
+
csv_path: Path,
|
|
1494
|
+
claude_ver: str,
|
|
1495
|
+
result: RunResult,
|
|
1496
|
+
*,
|
|
1497
|
+
skip_existing: bool = False,
|
|
1498
|
+
existing_key_cache: set[tuple[str, str]] | None = None,
|
|
1499
|
+
existing_key_cache_stamp: dict[str, tuple[int, int, int, int] | None] | None = None,
|
|
1500
|
+
) -> bool:
|
|
1433
1501
|
with csv_file_lock(csv_path, create_parent=True):
|
|
1434
|
-
|
|
1435
|
-
|
|
1502
|
+
key = (result.task_id, result.variant)
|
|
1503
|
+
if skip_existing:
|
|
1504
|
+
if existing_key_cache is not None:
|
|
1505
|
+
refresh_existing_key_cache_unlocked(csv_path, existing_key_cache, existing_key_cache_stamp)
|
|
1506
|
+
if key in existing_key_cache:
|
|
1507
|
+
return False
|
|
1508
|
+
elif key in _read_existing_keys_unlocked(csv_path):
|
|
1509
|
+
return False
|
|
1436
1510
|
flags = os.O_CREAT | os.O_APPEND | os.O_WRONLY
|
|
1437
1511
|
fd = _open_regular_no_symlink(csv_path, flags, 0o600, create_parent=True)
|
|
1438
1512
|
try:
|
|
@@ -1486,6 +1560,10 @@ def append_csv(csv_path: Path, claude_ver: str, result: RunResult, *, skip_exist
|
|
|
1486
1560
|
finally:
|
|
1487
1561
|
if fd != -1:
|
|
1488
1562
|
os.close(fd)
|
|
1563
|
+
if existing_key_cache is not None:
|
|
1564
|
+
existing_key_cache.add(key)
|
|
1565
|
+
if existing_key_cache_stamp is not None:
|
|
1566
|
+
existing_key_cache_stamp["stamp"] = csv_file_stamp_unlocked(csv_path)
|
|
1489
1567
|
return True
|
|
1490
1568
|
|
|
1491
1569
|
|
|
@@ -1644,10 +1722,16 @@ def _csv_exists_no_follow(csv_path: Path) -> bool:
|
|
|
1644
1722
|
|
|
1645
1723
|
def existing_keys(csv_path: Path) -> set[tuple[str, str]]:
|
|
1646
1724
|
"""이미 적재된 (task_id, variant) 조합. resume 시 skip 판정에 사용."""
|
|
1725
|
+
keys, _stamp = existing_keys_snapshot(csv_path)
|
|
1726
|
+
return keys
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def existing_keys_snapshot(csv_path: Path) -> tuple[set[tuple[str, str]], tuple[int, int, int, int] | None]:
|
|
1730
|
+
"""Loaded resume keys plus the CSV stamp observed under the same lock."""
|
|
1647
1731
|
if not _csv_exists_no_follow(csv_path):
|
|
1648
|
-
return set()
|
|
1732
|
+
return set(), None
|
|
1649
1733
|
with csv_file_lock(csv_path, create_parent=False):
|
|
1650
|
-
return _read_existing_keys_unlocked(csv_path)
|
|
1734
|
+
return _read_existing_keys_unlocked(csv_path), csv_file_stamp_unlocked(csv_path)
|
|
1651
1735
|
|
|
1652
1736
|
|
|
1653
1737
|
def read_csv_rows(csv_path: Path) -> list[dict[str, str]]:
|
|
@@ -3785,16 +3869,23 @@ def main() -> int:
|
|
|
3785
3869
|
print("no (task, variant) targets matched the filters", file=sys.stderr)
|
|
3786
3870
|
return 1
|
|
3787
3871
|
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3872
|
+
if args.resume:
|
|
3873
|
+
skip_keys, skip_keys_loaded_stamp = existing_keys_snapshot(args.csv)
|
|
3874
|
+
skip_keys_stamp = {"stamp": skip_keys_loaded_stamp}
|
|
3875
|
+
else:
|
|
3876
|
+
skip_keys = set()
|
|
3877
|
+
skip_keys_stamp = None
|
|
3878
|
+
runnable_targets = resume_runnable_targets(
|
|
3879
|
+
args.csv,
|
|
3880
|
+
targets,
|
|
3881
|
+
resume=args.resume,
|
|
3882
|
+
existing_key_cache=skip_keys,
|
|
3883
|
+
existing_key_cache_stamp=skip_keys_stamp,
|
|
3884
|
+
)
|
|
3794
3885
|
if args.evidence_jsonl is not None:
|
|
3795
3886
|
if args.dry_run:
|
|
3796
3887
|
for task, variant in targets:
|
|
3797
|
-
if (task.id, variant.name)
|
|
3888
|
+
if args.resume and resume_key_present(args.csv, (task.id, variant.name), skip_keys, skip_keys_stamp):
|
|
3798
3889
|
print(f"skip {task.id}/{variant.name} (already in {args.csv})")
|
|
3799
3890
|
continue
|
|
3800
3891
|
print(f"evidence replay dry-run: {task.id}/{variant.name} <- {args.evidence_jsonl}")
|
|
@@ -3802,18 +3893,33 @@ def main() -> int:
|
|
|
3802
3893
|
return 0
|
|
3803
3894
|
csv_had_preexisting_content = file_has_content_no_follow(args.csv)
|
|
3804
3895
|
evidence_rows = read_evidence_jsonl(args.evidence_jsonl)
|
|
3896
|
+
runnable_targets = resume_runnable_targets(
|
|
3897
|
+
args.csv,
|
|
3898
|
+
targets,
|
|
3899
|
+
resume=args.resume,
|
|
3900
|
+
existing_key_cache=skip_keys,
|
|
3901
|
+
existing_key_cache_stamp=skip_keys_stamp,
|
|
3902
|
+
)
|
|
3805
3903
|
evidence_by_key = validate_evidence_coverage(evidence_rows, runnable_targets)
|
|
3904
|
+
runnable_keys = {(task.id, variant.name) for task, variant in runnable_targets}
|
|
3806
3905
|
claude_ver = "evidence-replay"
|
|
3807
3906
|
completed = 0
|
|
3808
3907
|
replay_rows_written: list[EvidenceReplayRow] = []
|
|
3809
3908
|
for task, variant in targets:
|
|
3810
|
-
if (task.id, variant.name) in
|
|
3909
|
+
if args.resume and (task.id, variant.name) not in runnable_keys:
|
|
3811
3910
|
print(f"skip {task.id}/{variant.name} (already in {args.csv})")
|
|
3812
3911
|
continue
|
|
3813
3912
|
evidence = evidence_by_key[(task.id, variant.name)]
|
|
3814
3913
|
print(f"replay {task.id}/{variant.name} ...", flush=True)
|
|
3815
3914
|
result = run_evidence_fixture(task, variant, evidence)
|
|
3816
|
-
wrote = append_csv(
|
|
3915
|
+
wrote = append_csv(
|
|
3916
|
+
args.csv,
|
|
3917
|
+
claude_ver,
|
|
3918
|
+
result,
|
|
3919
|
+
skip_existing=args.resume,
|
|
3920
|
+
existing_key_cache=skip_keys if args.resume else None,
|
|
3921
|
+
existing_key_cache_stamp=skip_keys_stamp,
|
|
3922
|
+
)
|
|
3817
3923
|
if wrote:
|
|
3818
3924
|
replay_rows_written.append(evidence)
|
|
3819
3925
|
if args.ledger_jsonl is not None:
|
|
@@ -3846,6 +3952,13 @@ def main() -> int:
|
|
|
3846
3952
|
print(f"completed {completed} run(s); results in {args.csv}")
|
|
3847
3953
|
return 0
|
|
3848
3954
|
|
|
3955
|
+
runnable_targets = resume_runnable_targets(
|
|
3956
|
+
args.csv,
|
|
3957
|
+
targets,
|
|
3958
|
+
resume=args.resume,
|
|
3959
|
+
existing_key_cache=skip_keys,
|
|
3960
|
+
existing_key_cache_stamp=skip_keys_stamp,
|
|
3961
|
+
)
|
|
3849
3962
|
placeholder_targets = [
|
|
3850
3963
|
f"{task.id}/{variant.name}"
|
|
3851
3964
|
for task, variant in runnable_targets
|
|
@@ -3873,7 +3986,7 @@ def main() -> int:
|
|
|
3873
3986
|
|
|
3874
3987
|
completed = 0
|
|
3875
3988
|
for task, variant in targets:
|
|
3876
|
-
if (task.id, variant.name)
|
|
3989
|
+
if args.resume and resume_key_present(args.csv, (task.id, variant.name), skip_keys, skip_keys_stamp):
|
|
3877
3990
|
print(f"skip {task.id}/{variant.name} (already in {args.csv})")
|
|
3878
3991
|
continue
|
|
3879
3992
|
print(f"run {task.id}/{variant.name} ...", flush=True)
|
|
@@ -3882,7 +3995,14 @@ def main() -> int:
|
|
|
3882
3995
|
# 깎고, (b) --resume 이 그 (task, variant) 를 skip 해 실제 측정값이 영구 누락된다.
|
|
3883
3996
|
wrote = True
|
|
3884
3997
|
if not args.dry_run:
|
|
3885
|
-
wrote = append_csv(
|
|
3998
|
+
wrote = append_csv(
|
|
3999
|
+
args.csv,
|
|
4000
|
+
claude_ver,
|
|
4001
|
+
result,
|
|
4002
|
+
skip_existing=args.resume,
|
|
4003
|
+
existing_key_cache=skip_keys if args.resume else None,
|
|
4004
|
+
existing_key_cache_stamp=skip_keys_stamp,
|
|
4005
|
+
)
|
|
3886
4006
|
if wrote and args.ledger_jsonl is not None:
|
|
3887
4007
|
append_cost_shift_ledger(args.ledger_jsonl, claude_ver, result)
|
|
3888
4008
|
completed += 1
|