@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 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 플러그인으로 먼저 시작할 수 있으며, 한 번 설치한 뒤 프로젝트별로 명시적으로 활성화하고 필요하면 되돌릴 수 있습니다. 출력 축약, 심볼 단위 읽기, 반복 실패 알림, 민감정보 패턴 가림, 사용량 측정 가드레일은 로컬 헬퍼 명령과 brief 모드 안내 규칙 스니펫을 통해 다른 에이전트에서도 재사용할 수 있습니다.
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 사용자는 플러그인으로 시작하는 것이 가장 빠릅니다. 설치 후에는 같은 로컬 우선 가드레일을 다음 방식으로 다른 AI 코딩·도구 에이전트에서도 재사용할 수 있습니다.
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
- 보고서를 읽을 때는 먼저 claim boundary를 확인하세요.
372
+ 보고서를 읽을 때는 먼저 주장 범위를 확인하세요.
373
373
 
374
374
  - 성공한 기준/변형 실행은 실제 토큰과 `cost_usd + external_cost_usd` 기준으로 비교하고, 바이트 감소는 간접 증거로만 기록합니다.
375
375
  - 토큰 절감 주장은 대응 태스크 양쪽 모두에 `primary_tokens_measured`가 있을 때만 계산합니다.
376
- - `matched_pair_evidence`는 성공한 task bucket을 transform, 측정 가능 여부, quality gate, claim boundary와 연결하므로 절감 문구를 쓰기 전에 먼저 확인해야 합니다.
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, claim boundary가 포함되어 provider-measured matched-task evidence 없이는 hosted API token/cost savings claim으로 쓰지 않도록 합니다. `experiments enable`은 의도만 기록하며 helper를 실행하거나 명시 flag를 대체하거나 exact receipt/re-expand evidence 없는 content replacement를 허용하지 않습니다.
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
- - caller-supplied learned candidate emitter를 넘어서는 learned/synthetic compressor 실행 또는 생성형 replacement, caller-supplied visual evidence-pack emitter를 넘어서는 생성형 crop/OCR 또는 visual-token pruning runtime, 명시적 local metrics 기록을 넘어서는 self-hosted KV/latent optimization, one-shot literal-loopback local proxy MVP를 넘어서는 external/daemon/credential-bearing proxy forwarding runtime. 자세한 내용은 [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 절감 주장이나 더 넓은 런타임 기능 주장으로 승격할 수 있습니다.
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 management toolkit for AI coding and tool-using agents. It ships as a Claude Code plugin first: install it once, enable it per project, and roll it back when needed.
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 helps trim noisy output, steer agents toward symbol-level reads, nudge repeated failures, redact secret-like patterns, and measure usage. The same guardrails extend to other agents through local helper commands and advisory brief-mode rule snippets.
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; configuration changes happen only when you run an explicit setup command.
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. Use `context-guard doctor` or `context-guard setup --verify` for a read-only health check before applying setup. `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.
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 instead of executable Python, and the macOS visibility helper is discovered 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.
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 path to value for Claude users. After installation, the same local-first guardrails can be reused by other AI coding and tool-using agents through:
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 lower model prices by itself. It reduces avoidable context before it reaches an AI coding agent, then gives you signals to measure whether the change helped.
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 sits next to prompt compression. Its main job is simpler: **do not send unnecessary files, logs, or output in the first place**.
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, and benchmark evidence you measure yourself. |
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 the evidence a reviewer needs: 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 any token or cost savings.
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 the project has tracked, not committed features. Nothing here ships unless documented elsewhere in the repository.
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.11",
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",
@@ -37,5 +37,5 @@
37
37
  "gated-experiments",
38
38
  "future-roadmap"
39
39
  ],
40
- "version": "0.4.11"
40
+ "version": "0.4.12"
41
41
  }
@@ -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
- MAX_COMMAND_MANIFEST_BYTES = 128 * 1024
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 _manifest_open_flags() -> int | None:
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 _read_manifest_source(path: Path) -> str | None:
69
- flags = _manifest_open_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 > MAX_COMMAND_MANIFEST_BYTES:
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, MAX_COMMAND_MANIFEST_BYTES + 1 - total))
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 > MAX_COMMAND_MANIFEST_BYTES:
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 _literal_manifest_assignments(source: str) -> dict[str, Any] | None:
100
- try:
101
- tree = ast.parse(source)
102
- except SyntaxError:
103
- return None
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
- target: str | None = None
109
- value: ast.expr | None = None
110
- if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
111
- target = node.target.id
112
- value = node.value
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
- values[target] = ast.literal_eval(value)
122
- except (SyntaxError, ValueError):
123
- return None
124
- return values
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
- source = _read_manifest_source(path)
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
- return _literal_manifest_assignments(source)
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 append_csv(csv_path: Path, claude_ver: str, result: RunResult, *, skip_existing: bool = False) -> bool:
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
- if skip_existing and (result.task_id, result.variant) in _read_existing_keys_unlocked(csv_path):
1435
- return False
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
- skip_keys = existing_keys(args.csv) if args.resume else set()
3789
- runnable_targets = [
3790
- (task, variant)
3791
- for task, variant in targets
3792
- if (task.id, variant.name) not in skip_keys
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) in skip_keys:
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 skip_keys:
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(args.csv, claude_ver, result, skip_existing=args.resume)
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) in skip_keys:
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(args.csv, claude_ver, result, skip_existing=args.resume)
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