@ictechgy/context-guard 0.4.9 → 0.4.10

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.ko.md +41 -24
  3. package/README.md +66 -26
  4. package/docs/benchmark-fixtures/token-savings-12task-baseline.prompt.example.md +7 -0
  5. package/docs/benchmark-fixtures/token-savings-12task-contextguard.prompt.example.md +7 -0
  6. package/docs/benchmark-fixtures/token-savings-12task.tasks.example.json +182 -0
  7. package/docs/benchmark-fixtures/token-savings-12task.variants.example.json +10 -0
  8. package/docs/distribution.md +10 -7
  9. package/docs/experimental-benchmark-fixtures.md +8 -1
  10. package/package.json +3 -6
  11. package/packaging/homebrew/context-guard.rb.template +1 -1
  12. package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
  13. package/plugins/context-guard/README.ko.md +9 -6
  14. package/plugins/context-guard/README.md +21 -13
  15. package/plugins/context-guard/bin/context-guard +113 -26
  16. package/plugins/context-guard/bin/context-guard-artifact +542 -46
  17. package/plugins/context-guard/bin/context-guard-cache-score +380 -0
  18. package/plugins/context-guard/bin/context-guard-compress +146 -1
  19. package/plugins/context-guard/bin/context-guard-cost +783 -4
  20. package/plugins/context-guard/bin/context-guard-experiments +99 -18
  21. package/plugins/context-guard/bin/context-guard-failed-nudge +3 -0
  22. package/plugins/context-guard/bin/context-guard-filter +163 -7
  23. package/plugins/context-guard/bin/context-guard-guard-read +3 -0
  24. package/plugins/context-guard/bin/context-guard-pack +602 -43
  25. package/plugins/context-guard/bin/context-guard-rewrite-bash +3 -0
  26. package/plugins/context-guard/bin/context-guard-setup +165 -31
  27. package/plugins/context-guard/bin/context-guard-statusline +490 -283
  28. package/plugins/context-guard/bin/context-guard-statusline-merged +5 -0
  29. package/plugins/context-guard/bin/context-guard-tool-prune +241 -1
  30. package/plugins/context-guard/lib/context_guard_commands.py +206 -0
  31. package/plugins/context-guard/skills/setup/SKILL.md +1 -0
  32. package/context-guard-kit/README.md +0 -91
  33. package/context-guard-kit/benchmark_runner.py +0 -2401
  34. package/context-guard-kit/claude_transcript_cost_audit.py +0 -2346
  35. package/context-guard-kit/context_compress.py +0 -695
  36. package/context-guard-kit/context_escrow.py +0 -935
  37. package/context-guard-kit/context_filter.py +0 -637
  38. package/context-guard-kit/context_guard_cli.py +0 -325
  39. package/context-guard-kit/context_guard_diet.py +0 -1711
  40. package/context-guard-kit/context_pack.py +0 -2713
  41. package/context-guard-kit/cost_guard.py +0 -2349
  42. package/context-guard-kit/experimental_registry.py +0 -4348
  43. package/context-guard-kit/failed_attempt_nudge.py +0 -567
  44. package/context-guard-kit/guard_large_read.py +0 -690
  45. package/context-guard-kit/hook_secret_patterns.py +0 -43
  46. package/context-guard-kit/read_symbol.py +0 -483
  47. package/context-guard-kit/rewrite_bash_for_token_budget.py +0 -501
  48. package/context-guard-kit/sanitize_output.py +0 -725
  49. package/context-guard-kit/settings.example.json +0 -67
  50. package/context-guard-kit/setup_wizard.py +0 -2515
  51. package/context-guard-kit/statusline.sh +0 -362
  52. package/context-guard-kit/statusline_merged.sh +0 -157
  53. package/context-guard-kit/tool_schema_pruner.py +0 -837
  54. package/context-guard-kit/trim_command_output.py +0 -1449
@@ -21,6 +21,11 @@
21
21
  # (미지정 시 자기 옆 디렉토리만 사용; PATH 탐색 안 함)
22
22
  set -u
23
23
 
24
+ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
25
+ printf 'ContextGuard helper: context-guard-statusline-merged\n'
26
+ exit 0
27
+ fi
28
+
24
29
  statusline_input_tmp=''
25
30
 
26
31
  statusline_tmp_base() {
@@ -23,14 +23,20 @@ from typing import Any, NoReturn
23
23
 
24
24
  TOOL_NAME = "context-guard-tool-prune"
25
25
  SCHEMA_VERSION = "contextguard.tool-prune.v1"
26
+ DEFER_SCHEMA_VERSION = "contextguard.tool-prune.defer.v1"
26
27
  DEFAULT_STORE_DIR = ".context-guard/tool-prune"
27
28
  DEFAULT_TOP = 5
29
+ DEFAULT_CORE_TOP = 3
30
+ DEFAULT_DEFERRED_TOP = 20
31
+ DEFAULT_NAMESPACE_TOP = 20
28
32
  DEFAULT_BUDGET_BYTES = 12_000
29
33
  DEFAULT_MAX_CATALOG_BYTES = 1_000_000
30
34
  DEFAULT_MAX_OUTPUT_BYTES = 65_536
31
35
  DEFAULT_MAX_PAYLOAD_BYTES = 1_048_576
32
36
  DEFAULT_MAX_RECEIPT_BYTES = 16_384
33
37
  MAX_TOP = 200
38
+ MAX_DEFERRED_TOP = 1_000
39
+ MAX_NAMESPACE_TOP = 200
34
40
  MAX_LABEL_CHARS = 160
35
41
  MAX_DESCRIPTION_CHARS = 360
36
42
  MAX_OMITTED_TOOLS = 30
@@ -94,13 +100,17 @@ def byte_len_json(data: Any) -> int:
94
100
  return byte_len_text(json_bytes(data))
95
101
 
96
102
 
103
+ def proxy_tokens(chars: int) -> int:
104
+ return max(0, (int(chars) + TOKEN_PROXY_CHARS_PER_TOKEN - 1) // TOKEN_PROXY_CHARS_PER_TOKEN)
105
+
106
+
97
107
  def sha256_text(text: str) -> str:
98
108
  return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
99
109
 
100
110
 
101
111
  def bounded_int(value: object, *, default: int, minimum: int, maximum: int, name: str) -> int:
102
112
  try:
103
- number = int(value)
113
+ number = int(default if value is None else value)
104
114
  except (TypeError, ValueError, OverflowError):
105
115
  fail(f"{name} must be an integer")
106
116
  if number < minimum:
@@ -583,6 +593,86 @@ def selected_tool_record(cand: Candidate, receipt_id: str, budget_left: int, *,
583
593
  return record, 0
584
594
 
585
595
 
596
+ def deferred_tool_record(cand: Candidate, receipt_id: str, *, store_dir: str) -> dict[str, Any]:
597
+ return {
598
+ "name": cand.name,
599
+ "server": cand.server,
600
+ "score": cand.score,
601
+ "rank": cand.rank,
602
+ "description": cand.description,
603
+ "schema_bytes": byte_len_json(cand.schema),
604
+ "reason": "deferred_after_core_top",
605
+ "retrieval": retrieval_command(receipt_id, store_dir=store_dir, tool_name=cand.name),
606
+ }
607
+
608
+
609
+ def namespace_records(
610
+ ranked: list[Candidate],
611
+ core_names: set[str],
612
+ deferred_names: set[str],
613
+ receipt_id: str,
614
+ *,
615
+ store_dir: str,
616
+ namespace_top: int,
617
+ ) -> tuple[list[dict[str, Any]], int]:
618
+ grouped: dict[str, dict[str, Any]] = {}
619
+ for cand in ranked:
620
+ namespace = cand.server or "local"
621
+ item = grouped.setdefault(
622
+ namespace,
623
+ {
624
+ "namespace": namespace,
625
+ "tool_count": 0,
626
+ "core_count": 0,
627
+ "listed_deferred_count": 0,
628
+ "sample_tools": [],
629
+ "retrieval": retrieval_command(receipt_id, store_dir=store_dir),
630
+ },
631
+ )
632
+ item["tool_count"] += 1
633
+ if cand.name in core_names:
634
+ item["core_count"] += 1
635
+ if cand.name in deferred_names:
636
+ item["listed_deferred_count"] += 1
637
+ samples = item["sample_tools"]
638
+ if isinstance(samples, list) and len(samples) < 8:
639
+ samples.append(cand.name)
640
+ records = sorted(grouped.values(), key=lambda item: (-int(item["listed_deferred_count"]), str(item["namespace"])))
641
+ return records[:namespace_top], max(0, len(records) - namespace_top)
642
+
643
+
644
+ def build_receipt_and_payload(ranked: list[Candidate], safe_query: str, total_redactions: int, *, store_dir_arg: str, max_payload_bytes: int, max_receipt_bytes: int) -> tuple[str, dict[str, Any], dict[str, Any], Path, Path, Path, int, int]:
645
+ payload_without_id = build_payload("pending", ranked, safe_query, total_redactions)
646
+ receipt_id = build_receipt_id(payload_without_id)
647
+ payload = build_payload(receipt_id, ranked, safe_query, total_redactions)
648
+ payload_text = json_bytes(payload, indent=2) + "\n"
649
+ payload_bytes = byte_len_text(payload_text)
650
+ if payload_bytes > max_payload_bytes:
651
+ fail(f"payload exceeds --max-payload-bytes: {payload_bytes} > {max_payload_bytes}")
652
+ payload_sha = sha256_text(payload_text.rstrip("\n"))
653
+
654
+ store_dir, receipt_path, payload_path = store_paths(store_dir_arg, receipt_id)
655
+ receipt = {
656
+ "tool": TOOL_NAME,
657
+ "schema_version": SCHEMA_VERSION,
658
+ "receipt_id": receipt_id,
659
+ "created_at_unix": int(time.time()),
660
+ "path": display_path(receipt_path),
661
+ "payload_path": display_path(payload_path),
662
+ "payload_sha256": payload_sha,
663
+ "payload_bytes": payload_bytes,
664
+ "contains": "compact_metadata_plus_sanitized_payload",
665
+ "tool_count": len(ranked),
666
+ "tools": [cand.name for cand in ranked[:50]],
667
+ "tools_truncated": len(ranked) > 50,
668
+ "retrieval_hint": retrieval_command(receipt_id, store_dir=store_dir_arg, tool_name="<name>"),
669
+ }
670
+ receipt_size = byte_len_text(json_bytes(receipt, indent=2) + "\n")
671
+ if receipt_size > max_receipt_bytes:
672
+ fail(f"receipt exceeds --max-receipt-bytes: {receipt_size} > {max_receipt_bytes}")
673
+ return receipt_id, payload, receipt, store_dir, receipt_path, payload_path, payload_bytes, receipt_size
674
+
675
+
586
676
  def shrink_result_for_output(result: dict[str, Any], max_output_bytes: int) -> str:
587
677
  candidate = json_bytes(result, indent=2) + "\n"
588
678
  if byte_len_text(candidate) <= max_output_bytes:
@@ -591,6 +681,7 @@ def shrink_result_for_output(result: dict[str, Any], max_output_bytes: int) -> s
591
681
  result = json.loads(json_bytes(result))
592
682
  omitted = result.get("omitted_tools")
593
683
  while isinstance(omitted, list) and len(omitted) > 0:
684
+ # The list is halved on each pass, so even a one-item list converges.
594
685
  keep = max(0, len(omitted) // 2)
595
686
  result["omitted_tools"] = omitted[:keep]
596
687
  result["omitted_tools_truncated"] = True
@@ -699,6 +790,138 @@ def select_catalog(args: argparse.Namespace) -> str:
699
790
  return rendered
700
791
 
701
792
 
793
+ def defer_report(args: argparse.Namespace) -> str:
794
+ max_catalog_bytes = bounded_int(args.max_catalog_bytes, default=DEFAULT_MAX_CATALOG_BYTES, minimum=1, maximum=100_000_000, name="--max-catalog-bytes")
795
+ max_output_bytes = bounded_int(args.max_output_bytes, default=DEFAULT_MAX_OUTPUT_BYTES, minimum=1, maximum=10_000_000, name="--max-output-bytes")
796
+ max_payload_bytes = bounded_int(args.max_payload_bytes, default=DEFAULT_MAX_PAYLOAD_BYTES, minimum=1, maximum=100_000_000, name="--max-payload-bytes")
797
+ max_receipt_bytes = bounded_int(args.max_receipt_bytes, default=DEFAULT_MAX_RECEIPT_BYTES, minimum=1, maximum=10_000_000, name="--max-receipt-bytes")
798
+ core_top = bounded_int(args.core_top, default=DEFAULT_CORE_TOP, minimum=1, maximum=MAX_TOP, name="--core-top")
799
+ deferred_top = bounded_int(args.deferred_top, default=DEFAULT_DEFERRED_TOP, minimum=0, maximum=MAX_DEFERRED_TOP, name="--deferred-top")
800
+ namespace_top = bounded_int(args.namespace_top, default=DEFAULT_NAMESPACE_TOP, minimum=0, maximum=MAX_NAMESPACE_TOP, name="--namespace-top")
801
+ budget_bytes = bounded_int(args.budget_bytes, default=DEFAULT_BUDGET_BYTES, minimum=0, maximum=100_000_000, name="--budget-bytes")
802
+
803
+ text = read_limited_path(Path(args.catalog), max_catalog_bytes) if args.catalog else read_limited_stdin(max_catalog_bytes)
804
+ raw, redactions = parse_catalog_text(text)
805
+ raw_query = args.query or ""
806
+ safe_query, query_redactions = redact_string(raw_query)
807
+ total_redactions = redactions + query_redactions
808
+ ranked = rank_candidates(normalize_catalog(raw), raw_query)
809
+ (
810
+ receipt_id,
811
+ payload,
812
+ receipt,
813
+ store_dir,
814
+ receipt_path,
815
+ payload_path,
816
+ payload_bytes,
817
+ receipt_size,
818
+ ) = build_receipt_and_payload(
819
+ ranked,
820
+ safe_query,
821
+ total_redactions,
822
+ store_dir_arg=args.store_dir,
823
+ max_payload_bytes=max_payload_bytes,
824
+ max_receipt_bytes=max_receipt_bytes,
825
+ )
826
+
827
+ core_candidates = ranked[:core_top]
828
+ deferred_candidates = ranked[core_top:core_top + deferred_top]
829
+ core_tools: list[dict[str, Any]] = []
830
+ core_schema_bytes = 0
831
+ for cand in core_candidates:
832
+ record, used = selected_tool_record(cand, receipt_id, budget_bytes - core_schema_bytes, store_dir=args.store_dir)
833
+ core_schema_bytes += used
834
+ core_tools.append(record)
835
+ deferred_tools = [deferred_tool_record(cand, receipt_id, store_dir=args.store_dir) for cand in deferred_candidates]
836
+ core_names = {cand.name for cand in core_candidates}
837
+ deferred_names = {cand.name for cand in deferred_candidates}
838
+ deferred_namespaces, deferred_namespaces_truncated_count = namespace_records(
839
+ ranked,
840
+ core_names,
841
+ deferred_names,
842
+ receipt_id,
843
+ store_dir=args.store_dir,
844
+ namespace_top=namespace_top,
845
+ )
846
+ all_schema_bytes = sum(byte_len_json(cand.schema) for cand in ranked)
847
+ tool_stub_report_bytes = byte_len_json(core_tools) + byte_len_json(deferred_tools)
848
+ result = {
849
+ "tool": TOOL_NAME,
850
+ "schema_version": DEFER_SCHEMA_VERSION,
851
+ "mode": "defer-report",
852
+ "query": safe_query,
853
+ "core_top": core_top,
854
+ "deferred_top": deferred_top,
855
+ "namespace_top": namespace_top,
856
+ "candidate_count": len(ranked),
857
+ "native_provider_integration": False,
858
+ "core_tools": core_tools,
859
+ "deferred_tools": deferred_tools,
860
+ "listed_deferred_count": len(deferred_tools),
861
+ "total_deferred_count": max(0, len(ranked) - core_top),
862
+ "deferred_tools_truncated_count": max(0, len(ranked) - core_top - len(deferred_tools)),
863
+ "deferred_namespaces": deferred_namespaces,
864
+ "deferred_namespaces_truncated_count": deferred_namespaces_truncated_count,
865
+ "receipt": {
866
+ **receipt,
867
+ "bytes": receipt_size,
868
+ },
869
+ "token_proxy": {
870
+ "measurement": "estimated",
871
+ "method": "char4_proxy",
872
+ "chars_per_token": TOKEN_PROXY_CHARS_PER_TOKEN,
873
+ "all_schema_bytes": all_schema_bytes,
874
+ "tool_stub_report_bytes": tool_stub_report_bytes,
875
+ "all_schema_tokens_estimated": proxy_tokens(all_schema_bytes),
876
+ "tool_stub_report_tokens_estimated": proxy_tokens(tool_stub_report_bytes),
877
+ "claim_boundary": "proxy_only_not_provider_billed_tokens",
878
+ },
879
+ "provider_patterns": [
880
+ {
881
+ "provider": "openai",
882
+ "pattern": "Keep only core tool schemas inline; retrieve deferred schemas through app/tool-search plumbing or the local receipt before invoking a deferred tool.",
883
+ "native_provider_integration": False,
884
+ },
885
+ {
886
+ "provider": "anthropic",
887
+ "pattern": "Keep stable, frequently used tool definitions in the cacheable prefix; treat deferred tools as application-managed retrieval, not Claude-native lazy loading.",
888
+ "native_provider_integration": False,
889
+ },
890
+ {
891
+ "provider": "gemini",
892
+ "pattern": "Group large tool catalogs by namespace and load only the task-relevant subset before the model call; verify any platform-native tool retrieval separately.",
893
+ "native_provider_integration": False,
894
+ },
895
+ ],
896
+ "claim_boundary": {
897
+ "advisory_only": True,
898
+ "native_provider_integration": False,
899
+ "provider_tool_search_configured": False,
900
+ "hosted_api_token_or_cost_savings_claim_allowed": False,
901
+ "requires_provider_measured_matched_tasks_for_savings_claims": True,
902
+ },
903
+ "redaction": {"redacted_values": total_redactions},
904
+ "caveats": [
905
+ "Deferred loading is an application strategy report, not a native provider integration.",
906
+ "Token proxy values are char/4 estimates over sanitized local JSON, not billed provider tokens.",
907
+ "Use receipt get commands to retrieve full sanitized schemas before using deferred tools.",
908
+ ],
909
+ }
910
+ rendered = json_bytes(result, indent=2) + "\n"
911
+ if byte_len_text(rendered) > max_output_bytes:
912
+ fail(f"defer report exceeds --max-output-bytes: {byte_len_text(rendered)} > {max_output_bytes}")
913
+
914
+ # Only write after every size gate has passed, so failures leave no success receipt.
915
+ ensure_private_dir(store_dir)
916
+ written_payload_bytes = write_private_json_atomic(payload_path, payload, max_bytes=max_payload_bytes, label="payload")
917
+ if written_payload_bytes != payload_bytes:
918
+ fail("payload byte size changed during write")
919
+ written_receipt_bytes = write_private_json_atomic(receipt_path, receipt, max_bytes=max_receipt_bytes, label="receipt")
920
+ if written_receipt_bytes != receipt_size:
921
+ fail("receipt byte size changed during write")
922
+ return rendered
923
+
924
+
702
925
  def payload_path_from_receipt(store_dir: Path, receipt_id: str, receipt: dict[str, Any]) -> Path:
703
926
  expected_name = f"{receipt_id}.payload.json"
704
927
  raw = str(receipt.get("payload_path") or "")
@@ -803,6 +1026,20 @@ def build_parser() -> argparse.ArgumentParser:
803
1026
  select.add_argument("--store-dir", default=DEFAULT_STORE_DIR, help=f"receipt/payload directory (default: {DEFAULT_STORE_DIR})")
804
1027
  select.add_argument("--json", action="store_true", help="emit JSON (default and only stable output contract)")
805
1028
 
1029
+ defer = sub.add_parser("defer-report", help="split a local catalog into core inline tools plus deferred receipt-backed tools")
1030
+ defer.add_argument("--catalog", help="catalog JSON path; stdin is used when omitted")
1031
+ defer.add_argument("--query", default="", help="task query used for lexical ranking")
1032
+ defer.add_argument("--core-top", default=DEFAULT_CORE_TOP, help=f"number of core inline tools (default: {DEFAULT_CORE_TOP})")
1033
+ defer.add_argument("--deferred-top", default=DEFAULT_DEFERRED_TOP, help=f"number of deferred tool stubs to list (default: {DEFAULT_DEFERRED_TOP})")
1034
+ defer.add_argument("--namespace-top", default=DEFAULT_NAMESPACE_TOP, help=f"number of deferred namespace summaries to list (default: {DEFAULT_NAMESPACE_TOP})")
1035
+ defer.add_argument("--budget-bytes", default=DEFAULT_BUDGET_BYTES, help=f"inline core schema byte budget (default: {DEFAULT_BUDGET_BYTES})")
1036
+ defer.add_argument("--max-catalog-bytes", default=DEFAULT_MAX_CATALOG_BYTES, help=f"maximum catalog JSON bytes (default: {DEFAULT_MAX_CATALOG_BYTES})")
1037
+ defer.add_argument("--max-output-bytes", default=DEFAULT_MAX_OUTPUT_BYTES, help=f"maximum rendered defer JSON bytes (default: {DEFAULT_MAX_OUTPUT_BYTES})")
1038
+ defer.add_argument("--max-payload-bytes", default=DEFAULT_MAX_PAYLOAD_BYTES, help=f"maximum sanitized payload bytes (default: {DEFAULT_MAX_PAYLOAD_BYTES})")
1039
+ defer.add_argument("--max-receipt-bytes", default=DEFAULT_MAX_RECEIPT_BYTES, help=f"maximum compact receipt bytes (default: {DEFAULT_MAX_RECEIPT_BYTES})")
1040
+ defer.add_argument("--store-dir", default=DEFAULT_STORE_DIR, help=f"receipt/payload directory (default: {DEFAULT_STORE_DIR})")
1041
+ defer.add_argument("--json", action="store_true", help="emit JSON (default and only stable output contract)")
1042
+
806
1043
  get = sub.add_parser("get", help="retrieve a full sanitized schema from a receipt payload")
807
1044
  get.add_argument("receipt_id", help="receipt id returned by select")
808
1045
  get.add_argument("--tool", help="tool name to retrieve; omit to list available names")
@@ -821,6 +1058,9 @@ def main(argv: list[str] | None = None) -> int:
821
1058
  if args.command == "select":
822
1059
  sys.stdout.write(select_catalog(args))
823
1060
  return 0
1061
+ if args.command == "defer-report":
1062
+ sys.stdout.write(defer_report(args))
1063
+ return 0
824
1064
  if args.command == "get":
825
1065
  sys.stdout.write(get_schema(args))
826
1066
  return 0
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env python3
2
+ """Canonical ContextGuard command/package manifest.
3
+
4
+ This module is intentionally side-effect free. It centralizes command, copy,
5
+ package, and smoke-test inventories that were previously repeated across the
6
+ runtime dispatcher, release gates, sync tooling, smoke tests, and unit tests.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ IMPLEMENTATION_PAIRS: tuple[tuple[str, str], ...] = (
13
+ ("context_guard_cli.py", "context-guard"),
14
+ ("cost_guard.py", "context-guard-cost"),
15
+ ("cache_score.py", "context-guard-cache-score"),
16
+ ("benchmark_runner.py", "context-guard-bench"),
17
+ ("context_escrow.py", "context-guard-artifact"),
18
+ ("context_compress.py", "context-guard-compress"),
19
+ ("context_pack.py", "context-guard-pack"),
20
+ ("context_filter.py", "context-guard-filter"),
21
+ ("tool_schema_pruner.py", "context-guard-tool-prune"),
22
+ ("claude_transcript_cost_audit.py", "context-guard-audit"),
23
+ ("context_guard_diet.py", "context-guard-diet"),
24
+ ("experimental_registry.py", "context-guard-experiments"),
25
+ ("failed_attempt_nudge.py", "context-guard-failed-nudge"),
26
+ ("guard_large_read.py", "context-guard-guard-read"),
27
+ ("read_symbol.py", "context-guard-read-symbol"),
28
+ ("rewrite_bash_for_token_budget.py", "context-guard-rewrite-bash"),
29
+ ("sanitize_output.py", "context-guard-sanitize-output"),
30
+ ("setup_wizard.py", "context-guard-setup"),
31
+ ("statusline.sh", "context-guard-statusline"),
32
+ ("statusline_merged.sh", "context-guard-statusline-merged"),
33
+ ("trim_command_output.py", "context-guard-trim-output"),
34
+ )
35
+
36
+ HELPER_PAIRS: tuple[tuple[str, str], ...] = (
37
+ ("hook_secret_patterns.py", "lib/hook_secret_patterns.py"),
38
+ ("context_guard_commands.py", "lib/context_guard_commands.py"),
39
+ )
40
+
41
+ NPM_BINS: tuple[str, ...] = (
42
+ "context-guard",
43
+ "context-guard-cost",
44
+ "context-guard-cache-score",
45
+ "context-guard-bench",
46
+ "context-guard-artifact",
47
+ "context-guard-compress",
48
+ "context-guard-pack",
49
+ "context-guard-filter",
50
+ "context-guard-tool-prune",
51
+ "context-guard-audit",
52
+ "context-guard-diet",
53
+ "context-guard-experiments",
54
+ "context-guard-failed-nudge",
55
+ "context-guard-guard-read",
56
+ "context-guard-read-symbol",
57
+ "context-guard-rewrite-bash",
58
+ "context-guard-sanitize-output",
59
+ "context-guard-setup",
60
+ "context-guard-statusline",
61
+ "context-guard-statusline-merged",
62
+ "context-guard-trim-output",
63
+ )
64
+ NPM_BIN_PATHS: dict[str, str] = {
65
+ name: f"plugins/context-guard/bin/{name}" for name in NPM_BINS
66
+ }
67
+
68
+ DISPATCHER_SUBCOMMANDS: dict[str, tuple[str, ...]] = {
69
+ "setup": ("context-guard-setup",),
70
+ "doctor": ("context-guard-setup", "--verify"),
71
+ "audit": ("context-guard-audit",),
72
+ "diet": ("context-guard-diet",),
73
+ "experiments": ("context-guard-experiments",),
74
+ "scan": ("context-guard-diet", "scan"),
75
+ "trim-output": ("context-guard-trim-output",),
76
+ "trim": ("context-guard-trim-output",),
77
+ "sanitize-output": ("context-guard-sanitize-output",),
78
+ "sanitize": ("context-guard-sanitize-output",),
79
+ "filter": ("context-guard-filter",),
80
+ "artifact": ("context-guard-artifact",),
81
+ "pack": ("context-guard-pack",),
82
+ "tool-prune": ("context-guard-tool-prune",),
83
+ "compress": ("context-guard-compress",),
84
+ "cost": ("context-guard-cost",),
85
+ "route-advisor": ("context-guard-cost", "route-advisor"),
86
+ "route": ("context-guard-cost", "route-advisor"),
87
+ "cache-score": ("context-guard-cache-score",),
88
+ "bench": ("context-guard-bench",),
89
+ "read-symbol": ("context-guard-read-symbol",),
90
+ "rewrite-bash": ("context-guard-rewrite-bash",),
91
+ "guard-read": ("context-guard-guard-read",),
92
+ "failed-nudge": ("context-guard-failed-nudge",),
93
+ "statusline": ("context-guard-statusline",),
94
+ "statusline-merged": ("context-guard-statusline-merged",),
95
+ }
96
+
97
+ LEGACY_WRAPPERS: tuple[str, ...] = (
98
+ "claude-read-symbol",
99
+ "claude-sanitize-output",
100
+ "claude-token-artifact",
101
+ "claude-token-audit",
102
+ "claude-token-bench",
103
+ "claude-token-diet",
104
+ "claude-token-failed-nudge",
105
+ "claude-token-guard-read",
106
+ "claude-token-rewrite-bash",
107
+ "claude-token-setup",
108
+ "claude-token-statusline",
109
+ "claude-token-statusline-merged",
110
+ "claude-trim-output",
111
+ )
112
+
113
+ ENTRYPOINT_SMOKE_CASES: dict[str, dict[str, Any]] = {
114
+ "context-guard": {"args": ["--version"], "mode": "text"},
115
+ "context-guard-read-symbol": {"args": ["--help"], "mode": "text"},
116
+ "context-guard-sanitize-output": {"args": ["--help"], "mode": "text"},
117
+ "context-guard-artifact": {"args": ["--help"], "mode": "text"},
118
+ "context-guard-audit": {"args": ["--help"], "mode": "text"},
119
+ "context-guard-bench": {"args": ["--help"], "mode": "text"},
120
+ "context-guard-compress": {"args": ["--help"], "mode": "text"},
121
+ "context-guard-cost": {"args": ["--help"], "mode": "text"},
122
+ "context-guard-cache-score": {"args": ["--help"], "mode": "text"},
123
+ "context-guard-pack": {"args": ["--help"], "mode": "text"},
124
+ "context-guard-tool-prune": {"args": ["--help"], "mode": "text"},
125
+ "context-guard-diet": {"args": ["--help"], "mode": "text"},
126
+ "context-guard-experiments": {"args": ["--help"], "mode": "text"},
127
+ "context-guard-failed-nudge": {"args": [], "mode": "hook-json"},
128
+ "context-guard-filter": {"args": ["--help"], "mode": "text"},
129
+ "context-guard-guard-read": {"args": [], "mode": "hook-json"},
130
+ "context-guard-rewrite-bash": {"args": [], "mode": "hook-json"},
131
+ "context-guard-setup": {"args": ["--help"], "mode": "text"},
132
+ "context-guard-statusline": {"args": [], "mode": "statusline"},
133
+ "context-guard-statusline-merged": {"args": [], "mode": "statusline"},
134
+ "context-guard-trim-output": {"args": ["--help"], "mode": "text"},
135
+ # Legacy wrappers kept so existing automation does not break during the rebrand.
136
+ "claude-read-symbol": {"args": ["--help"], "mode": "text"},
137
+ "claude-sanitize-output": {"args": ["--help"], "mode": "text"},
138
+ "claude-token-artifact": {"args": ["--help"], "mode": "text"},
139
+ "claude-token-audit": {"args": ["--help"], "mode": "text"},
140
+ "claude-token-bench": {"args": ["--help"], "mode": "text"},
141
+ "claude-token-diet": {"args": ["--help"], "mode": "text"},
142
+ "claude-token-failed-nudge": {"args": [], "mode": "hook-json"},
143
+ "claude-token-guard-read": {"args": [], "mode": "hook-json"},
144
+ "claude-token-rewrite-bash": {"args": [], "mode": "hook-json"},
145
+ "claude-token-setup": {"args": ["--help"], "mode": "text"},
146
+ "claude-token-statusline": {"args": [], "mode": "statusline"},
147
+ "claude-token-statusline-merged": {"args": [], "mode": "statusline"},
148
+ "claude-trim-output": {"args": ["--help"], "mode": "text"},
149
+ }
150
+
151
+ PLUGIN_ENTRYPOINTS: tuple[str, ...] = (
152
+ "claude-read-symbol",
153
+ "claude-sanitize-output",
154
+ "claude-token-artifact",
155
+ "claude-token-audit",
156
+ "claude-token-bench",
157
+ "claude-token-diet",
158
+ "claude-token-failed-nudge",
159
+ "claude-token-guard-read",
160
+ "claude-token-rewrite-bash",
161
+ "claude-token-setup",
162
+ "claude-token-statusline",
163
+ "claude-token-statusline-merged",
164
+ "claude-trim-output",
165
+ "context-guard",
166
+ "context-guard-artifact",
167
+ "context-guard-audit",
168
+ "context-guard-bench",
169
+ "context-guard-compress",
170
+ "context-guard-cost",
171
+ "context-guard-cache-score",
172
+ "context-guard-diet",
173
+ "context-guard-experiments",
174
+ "context-guard-failed-nudge",
175
+ "context-guard-filter",
176
+ "context-guard-guard-read",
177
+ "context-guard-pack",
178
+ "context-guard-read-symbol",
179
+ "context-guard-rewrite-bash",
180
+ "context-guard-sanitize-output",
181
+ "context-guard-setup",
182
+ "context-guard-statusline",
183
+ "context-guard-statusline-merged",
184
+ "context-guard-tool-prune",
185
+ "context-guard-trim-output",
186
+ )
187
+
188
+ DISPATCHER_SMOKE_CASES: tuple[dict[str, Any], ...] = (
189
+ {"entrypoint": "context-guard", "args": ["experiments", "list", "--json"], "mode": "json"},
190
+ {"entrypoint": "context-guard", "args": ["cost", "--help"], "mode": "text"},
191
+ {"entrypoint": "context-guard", "args": ["route-advisor", "--help"], "mode": "text"},
192
+ {"entrypoint": "context-guard", "args": ["cache-score", "--help"], "mode": "text"},
193
+ {"entrypoint": "context-guard-pack", "args": ["suggest", "--help"], "mode": "text"},
194
+ {"entrypoint": "context-guard-pack", "args": ["auto", "--help"], "mode": "text"},
195
+ )
196
+
197
+
198
+ def expected_command_pack_files() -> tuple[str, ...]:
199
+ # npm packages ship the plugin-local executable/helper copies only. The
200
+ # checkout-local ``context-guard-kit`` files remain the source of truth for
201
+ # maintainers and are kept byte-synchronized with these packaged copies by
202
+ # ``scripts/sync_plugin_copies.py`` and ``scripts/prepublish_check.py``.
203
+ files = {f"plugins/context-guard/bin/{bin_name}" for _kit_name, bin_name in IMPLEMENTATION_PAIRS}
204
+ files.update(f"plugins/context-guard/{plugin_rel}" for _kit_name, plugin_rel in HELPER_PAIRS)
205
+ files.update(f"plugins/context-guard/bin/{wrapper}" for wrapper in LEGACY_WRAPPERS)
206
+ return tuple(sorted(files))
@@ -40,3 +40,4 @@ Safety:
40
40
  - Prefer project-local `.claude/settings.json`.
41
41
  - `context-guard-setup --verify` is a local read-only health check and never applies settings.
42
42
  - Setup's post-apply scan is local, read-only, and prints a summary only; it does not mutate settings.
43
+ - Setup should use packaged/check-out helper paths by default; only pass `--allow-path-helper-fallback` when the user explicitly trusts a PATH-installed ContextGuard helper set.
@@ -1,91 +0,0 @@
1
- # ContextGuard Kit
2
-
3
- Claude Code CLI 컨텍스트 낭비를 줄이기 위한 실험용 도구 모음입니다. 모두 Python/Bash 표준 기능만 사용합니다.
4
-
5
- ## 구성
6
-
7
- - `statusline.sh` — context/cost/model을 상태표시줄에 표시합니다.
8
- - `trim_command_output.py` — 긴 명령 출력을 head/tail/error와 pytest/Jest/Vitest/Go/Rust 실패 요약 중심으로 축약하고 원래 종료 코드를 보존합니다.
9
- - `rewrite_bash_for_token_budget.py` — Claude Code `PreToolUse` hook에서 test/build/lint 명령을 wrapper로 감쌉니다.
10
- - `claude_transcript_cost_audit.py` — `~/.claude/projects` JSONL transcript에서 usage/cost/cache 필드와 캐시 친화적 프롬프트 배치 신호를 집계하고 `--recommend`로 절감 액션을 제안합니다.
11
- - `context_guard_diet.py` — 프로젝트 `.claude/settings.json`의 deny/hook/statusline, 여러 AI 에이전트 규칙 파일의 컨텍스트 비대화, 로컬 context-exclusion 추천, structural-waste 진단을 스캔합니다.
12
- - `guard_large_read.py` — Claude Code `PreToolUse` Read hook에서 큰 파일 전체 읽기를 막고 symbol/line-range 읽기로 유도합니다.
13
- - `read_symbol.py` — Python/JS/TS/Go/Rust 파일에서 지정 symbol 주변만 출력합니다.
14
- - `sanitize_output.py` — `rg`/`grep`/`git diff` 같은 검색·diff 출력에서 자격 증명처럼 보이는 값을 가리고 head/anchor/tail로 축약합니다.
15
- - `context_escrow.py` — 큰 명령 출력을 정제한 뒤 로컬 artifact로 저장하고 line/pattern query로 다시 조회합니다.
16
- - `context_pack.py` — 우선순위가 있는 로컬 파일 근거를 바이트 예산 안의 Markdown context pack으로 조립하고, 로컬 query/diff/output 신호에서 build manifest를 추천합니다.
17
- - `context_filter.py` — 사용자 소유 JSON DSL로 성공 출력 라인 필터를 적용하되, 보호해야 할 실패 출력은 원문 그대로 통과시킵니다.
18
- - `tool_schema_pruner.py` — 로컬 tool/MCP catalog를 top-k schema 자문 리포트로 줄이고, 전체 정제된 schema는 receipt/payload로 재조회할 수 있게 합니다.
19
- - `benchmark_runner.py` — 고정 task/variant fixture로 A/B token/cost 절감 benchmark, cost-shift ledger, report를 생성합니다.
20
- - `setup_wizard.py` — 설치 후 project-local `.claude/settings.json`을 대화형으로 선택하고 병합합니다.
21
- - `failed_attempt_nudge.py` — 반복 Bash 실패 시 `/clear`/`/compact`와 전략 전환을 짧게 권유합니다.
22
- - `settings.example.json` — project `.claude/settings.json` 예시입니다.
23
-
24
- ## 빠른 실험
25
-
26
- ```bash
27
- python3 context-guard-kit/trim_command_output.py --max-lines 80 -- bash -lc 'seq 1 1000; echo FAIL test_x >&2; exit 1'
28
- python3 context-guard-kit/trim_command_output.py --max-lines 80 -- pytest tests -q
29
- python3 context-guard-kit/claude_transcript_cost_audit.py ~/.claude/projects --top 10 --recommend
30
- python3 context-guard-kit/setup_wizard.py
31
- python3 context-guard-kit/context_guard_diet.py scan . --json
32
- python3 context-guard-kit/context_guard_diet.py structural-waste . --tool-catalog tools.json --log-path .claude --json
33
- python3 context-guard-kit/context_filter.py validate --config .context-guard/filter-dsl.json --json
34
- python3 context-guard-kit/context_filter.py run --config .context-guard/filter-dsl.json -- git status --short
35
- python3 context-guard-kit/read_symbol.py path/to/file.py TargetSymbol
36
- long-command 2>&1 | python3 context-guard-kit/context_escrow.py store --command "long-command" --json
37
- python3 context-guard-kit/context_escrow.py get <artifact_id> --lines 1:80
38
- python3 context-guard-kit/context_pack.py suggest --root . --query "failing tests review" --diff HEAD --manifest-out suggested-pack.json --budget-bytes 12000 --json
39
- python3 context-guard-kit/context_pack.py build --root . --manifest suggested-pack.json --budget-bytes 12000 --json
40
- python3 context-guard-kit/context_pack.py slice --root . --path README.md --lines 1:40 --json
41
- python3 context-guard-kit/tool_schema_pruner.py select --catalog tools.json --query "review failing tests" --top 5 --budget-bytes 12000 --json
42
- python3 context-guard-kit/tool_schema_pruner.py get <receipt_id> --tool read_file --json
43
- python3 context-guard-kit/benchmark_runner.py --tasks bench/tasks.json --variants bench/variants.json --csv bench/results.csv --ledger-jsonl bench/cost-shift.jsonl --report-json bench/report.json
44
- python3 context-guard-kit/sanitize_output.py -- rg -n "TOKEN|SECRET" .
45
- python3 context-guard-kit/sanitize_output.py -- git diff
46
- ```
47
-
48
- `trim_command_output.py`는 output이 budget을 넘을 때 runner별 failure summary를 먼저 보여줍니다. 예를 들어 pytest node id, Jest/Vitest 실패 파일/테스트, `go test`의 실패 test와 `_test.go:line`, `cargo test` panic 위치를 짧게 보존해 Claude가 전체 로그를 다시 읽지 않고도 다음에 수정할 파일을 고를 수 있게 합니다. head/tail 로그 대신 더 작은 의미 요약만 필요하면 `--digest markdown` 또는 `--digest json`을 추가하세요. digest mode는 status, exit code, truncation count, runner failure facts, 정제된 failure signature, 중복 라인 그룹, 대표 라인, redaction count, 다음 query 제안을 남깁니다. digest mode에 `--artifact-receipt`를 더하면 sanitized 전체 output을 로컬 `context-guard-artifact` receipt로 보관하고, 출력된 `context-guard-artifact get ...` 명령으로 누락된 부분을 정확히 다시 조회할 수 있습니다. 감싼 명령은 기본 600초 후 timeout 처리되며(`--timeout-seconds`로 조정), 가능한 환경에서는 process group까지 종료한 뒤 124를 반환합니다. ANSI color code는 제거하며, 절대경로는 기본적으로 `basename#path:<hash>`로 익명화합니다. 로컬 디버깅에서 원문 절대경로가 꼭 필요하면 `--show-paths`를 추가하세요.
49
-
50
- `context_escrow.py`는 대용량 output을 Claude context에 그대로 넣지 않고 `.context-guard/artifacts` 아래 `0o600` 파일로 저장합니다. 저장 전에 sanitizer를 적용해 secret/path 노출을 줄이고, receipt에는 `artifact_id`, line/byte count, 줄 번호가 포함된 top-error receipt, 중복 라인 그룹, 대표 head/tail, 정제된 bounded `suggested_queries`와 `get --lines`/`get --pattern` query 예시만 출력합니다. suggested `--lines START:END` query에 `--max-lines`가 함께 있으면 이는 해당 line range의 반환 cap일 뿐 selector를 넓히는 옵션이 아닙니다. `get`과 `list`는 legacy 기본 위치인 `.claude-token-optimizer/artifacts`도 함께 읽어 리브랜딩 전 receipt를 계속 조회할 수 있습니다. 저장된 artifact는 sanitize된 사본이며, 필요할 때만 `get <artifact_id> --lines 10:40`처럼 정확한 범위를 조회하세요. 파이프라인 저장은 capture/query 용도이므로 producer 명령의 exit code가 필요한 release check에서는 shell `pipefail`/별도 `$?` 저장을 쓰거나 `trim_command_output.py -- ...`로 감싸세요.
51
-
52
-
53
- `context_pack.py auto`는 `suggest`와 `build`를 한 번에 합성해 build-compatible manifest와 예산 기반 Markdown pack을 함께 만듭니다. `auto --explain`은 manifest, pack 본문, receipt, byte budget을 바꾸지 않고 결정적 로컬 선택/build 이유를 JSON 또는 텍스트로 짧게 보여줍니다. JSON explain에는 bounded `repo_map`도 포함되어 sampled byte/token-proxy tree, category-only secret risk summary, signature-first hints, explain-only graph rank, 기존 `slice`/symbol 재조회 힌트를 제공합니다. 이 repo-map은 네트워크·모델 호출·임베딩 없이 로컬 표준 라이브러리 휴리스틱만 쓰며, pack 선택/본문/receipt를 바꾸지 않고 provider token 또는 savings claim으로 해석하면 안 됩니다. `context_pack.py suggest`는 `--query`, `--diff`, 반복 `--files`, 가림 처리한 `--output`, `--test-output`에서 build-compatible manifest 후보를 만듭니다. 모두 `--root` 아래 로컬 파일과 `git diff`만 읽고, 네트워크·모델 호출·임베딩·provider 비용 추정은 하지 않습니다. `context_pack.py build`는 여러 로컬 파일 source를 우선순위와 줄 범위에 따라 정렬하고, 렌더링된 UTF-8 byte budget 안에서 Markdown context pack을 만듭니다. 포함·부분 포함·누락 source, 누락 사유, `.context-guard/packs` bounded receipt, 그리고 `slice --lines` 정확 재조회 명령을 JSON으로 남깁니다. pack 본문과 receipt를 만들기 전에 sanitizer를 적용하며, token 값은 관측값이 아닌 추정 proxy로만 표시합니다.
54
-
55
- `context_filter.py`는 opt-in declarative output filter helper입니다. filter JSON은 사용자가 package code 밖(예: `.context-guard/filter-dsl.json`)에 두고 `validate`로 검증한 뒤 `run --config ... -- <command>`로 적용합니다. invalid config, no-match, filter error, empty output, protected `git`/test/lint/`gh` failure는 원래 command stdout/stderr와 exit code를 passthrough합니다. filtered mode는 stdout+stderr를 합친 line에 filter를 적용해 stdout으로 쓰고, passthrough mode는 stdout/stderr stream을 그대로 보존합니다. `--json-report`는 stdout을 command/filter output 전용으로 두기 위해 stderr에만 diagnostic JSON을 쓰지만, protected nonzero passthrough에서는 stderr 원문 보존을 위해 report를 생략합니다. token/cost 절감 수치는 측정 claim이 아니라 local presentation 변화로만 다루세요.
56
-
57
- `tool_schema_pruner.py`는 provider-neutral tool/MCP catalog helper입니다. `select`는 task query와 lexical overlap으로 top-k tool을 고르고, inline schema는 `--budget-bytes` 안에만 넣으며, compact receipt와 별도 sanitized payload를 `.context-guard/tool-prune`에 기록합니다. `get`은 payload size/SHA-256을 검증한 뒤 전체 정제 schema를 반환합니다. 이 helper는 MCP 설정을 바꾸지 않으며, token 절감은 측정값이 아니라 추정 proxy로만 표현합니다.
58
-
59
- `context_compress.py --protected-policy`는 기본 압축 동작을 바꾸지 않고 code fence, diff, identifier, numeric constant, hash, path, stack frame, quoted string, JSON key 같은 보호-zone class/count 정책 메타데이터를 추가합니다. 보호-zone 정책은 semantic/paraphrase rewrite를 금지하고 structural dedupe/window/truncate 및 artifact retrieval만 허용합니다. raw span은 receipt에 저장하지 않으며, lossy structural transform에는 정확 재조회가 필요하다는 hint를 남깁니다.
60
-
61
- `cost_guard.py compile`은 section manifest의 `protected`, `semantic_sensitive`, `protected_zone_classes`, `content_type`, `volatile`, `ttl`, `bytes` 필드를 읽어 `protected_zone_policy`와 `transform_policy`를 출력합니다. `protected=true`와 `volatile=true`가 같이 있으면 volatile이 cache ordering을 tail 쪽으로 보내고, protection은 transform/retrieval 정책만 제어합니다. 대용량 protected section은 local artifact retrieval을 권고하지만 provider prompt cache를 대체한다고 주장하지 않습니다.
62
-
63
- `experimental_registry.py` provides the `context-guard experiments` metadata surface. It is default-off and project-local: `enable`/`disable` update `.context-guard/experiments.json` only, while existing helper behavior still requires explicit flags. The registry marks the receipt-backed output trim path (`trim_command_output.py --digest markdown|json --artifact-receipt`) and protected-zone policy path (`context_compress.py --protected-policy`, plus `cost_guard.py compile` protected section metadata) as available explicit-flag experiments. It also exposes `experimental_registry.py plan context-diff-compaction` as a read-only dry-run planner and `experimental_registry.py emit context-diff-compaction --receipt-id ... --reexpand-command ...` as an explicit local runtime: `plan` summarizes diff files/hunks without replacement text, while `emit` outputs only caller-supplied compact replacement text when reviewable hunks, exact local artifact content matching the input diff, valid re-expand metadata, and a smaller replacement are present; it verifies local receipt content but still does not execute the re-expand command or claim hosted savings. `experimental_registry.py plan visual-crop-ocr` is a shipped dry-run metadata planner for full visual evidence receipts plus crop/OCR fixture notes, and `experimental_registry.py emit visual-crop-ocr` is an explicit local evidence-pack runtime for caller-supplied crop/OCR evidence. The visual lane never captures screenshots, crops images, parses screenshots, runs OCR, calls OCR/image services, writes evidence files, emits replacement evidence, or claims hosted savings. `experimental_registry.py plan learned-compression` is a deny-by-default dry-run safety checker for sanitized trusted prose plus exact fallback handles, and `experimental_registry.py emit learned-compression` is an explicit local candidate emitter for caller-supplied compact prose with verified exact fallback content. The learned lane never runs learned/synthetic compressor execution, embeddings, model calls, rerankers, subprocesses, external services, or generated replacement text, and it never claims hosted savings. `experimental_registry.py plan self-hosted-metrics-ledger` is a dry-run preview for explicit local/model-server latency, memory, quality, energy, throughput, and local-cost sidecar evidence; the dry-run preview does not write a ledger. `experimental_registry.py record self-hosted-metrics-ledger --ledger-jsonl ...` explicitly writes one local JSONL sidecar row and still never turns self-hosted telemetry into hosted API token/cost savings claims. `experimental_registry.py plan local-proxy` is a localhost-only dry-run advisory plan, `experimental_registry.py plan local-proxy-external-forwarding` is a design-only dry-run gate for future external forwarding that requires explicit intent, HTTPS allowlist, threat model notes, credential redaction policy, and provider-evidence boundary while performing no DNS lookup, external service call, or traffic forwarding, `experimental_registry.py record local-proxy-runtime-gate --ledger-jsonl ...` is an explicit local gate-row runtime that starts no listener, forwards no traffic, and performs no DNS lookup, and `experimental_registry.py serve local-proxy` is an explicit one-shot loopback forwarding MVP: it requires `--runtime-gate-ack --forwarding-gate-ack --once`, literal loopback bind/target IPs, no hostname DNS targets, nonzero ports, byte/time limits, and credential-free requests; it persists no API keys, supports no external forwarding or CONNECT/TLS proxying, and makes no hosted savings claim. Optional `--diagnostic-ledger-jsonl` appends one shifted-cost diagnostic row only after a successful forwarded request, without raw headers/bodies or hosted-savings evidence. External proxy forwarding runtime remains non-shipped. Other roadmap lanes remain advisory until separate runtime gates exist.
64
-
65
- `benchmark_runner.py`는 `research/benchmark-plan.md`의 고정 task/variant 실험을 실행합니다. `variant_prompt_files`는 선택된 task/variant를 필터링한 뒤 필요한 file-backed prompt만 읽으므로 선택하지 않은 fixture의 누락 파일이 선택된 실행을 깨지 않습니다. `--ledger-jsonl`은 subagent·artifact 등 외부 실행 표면으로 옮겨간 token/cost와 run별 측정 가능 여부를 남기고, 선택적 `self_hosted_metrics` provider payload는 run별 sidecar로만 기록합니다. `--report-json`은 baseline 대비 실제 token/cost 절감과 proxy byte 감소를 분리한 A/B report를 생성하며, `self_hosted_metrics`는 CSV/report 요약에 접지 않습니다. Report의 `matched_pair_evidence`는 성공한 baseline/variant task bucket을 transform, quality gate, 측정 가능 여부, claim boundary와 연결하므로 절감 주장을 쓰기 전에 이 항목을 확인하세요.
66
-
67
- `../research/experimental-token-reduction-radar.md`는 learned compression, generated crop/OCR/visual-token pruning, self-hosted KV/latent inference optimization 같은 선택적 미래 실험을 문서화한 gate입니다. `../docs/experimental-benchmark-fixtures.md`에는 fixture-only task/variant 시작 예시가 있습니다. 이 radar와 fixture는 hosted API token/cost 절감을 보장하지 않습니다. 현재 제공되는 helper surface는 명시적 local context-diff emit, visual evidence-pack emit, learned candidate emit, self-hosted metrics record, local proxy gate record, one-shot literal-loopback local proxy serve, design-only external-forwarding plan 같은 좁은 local surface뿐이며, hosted API token/cost 절감 주장은 provider가 측정한 matched-task 근거가 있을 때만 허용합니다. Radar의 later-roadmap gate는 neural/semantic compression, trust-tiered injection-aware compression, generated visual-token reduction, broader external/daemon/hostname-DNS/credential-bearing local proxy forwarding constraints를 별도 미래 PR이 gate를 통과하기 전까지 experimental/non-shipped로 유지합니다.
68
-
69
- `claude_transcript_cost_audit.py --recommend`의 기본 출력은 공유 시 안전하도록 transcript 경로를 `basename#hash`, 명령을 `command#hash` 형태로 익명화합니다. 로컬 원문 식별자가 꼭 필요할 때만 `--show-paths` 또는 `--show-commands`를 추가하세요.
70
- 대용량/손상 transcript 방어를 위해 파일 단위 `--max-file-bytes`, JSONL record 단위 `--max-line-bytes` 제한도 기본 적용되며, 건너뛴 항목은 skip count와 warning으로 표시됩니다. JSON summary/feasibility 출력의 `cache_friendliness`는 제한된 정제 segment hash로 안정적인 prefix와 volatile prefix/tail 신호를 비교하는 휴리스틱입니다. `cache_layout_advice`는 그 신호를 긴 세션 분리, prefix 안정화, diet 점검 같은 순위화된 확인/실험으로 연결하지만, 관측 issue와 가설/입증 cause를 분리합니다. `--feasibility-json`은 macOS-visible prototype 같은 consumer가 안정적인 top-level field에만 바인딩하도록 `mac_visibility` 계약도 함께 제공합니다. 원문 prompt text는 출력하지 않고, provider cache token field와 historical token total은 ContextGuard가 만든 토큰 절감 또는 live headroom 증거가 아니라 별도 진단 텔레메트리로 해석하세요.
71
-
72
- `context_guard_diet.py scan`은 항상 로컬에서만 읽는 read-only 스캐너입니다. 기본 출력은 project root를 익명화하고 상대경로 중심으로 보고합니다. `--top`은 보고서의 context-like file 목록과 context-exclusion recommendation 목록에 공통으로 적용됩니다. `--show-paths`는 로컬/비공개 디버깅에서만 쓰세요.
73
-
74
- `context_guard_diet.py structural-waste`는 opt-in read-only 구조 진단입니다. context/rule file의 중복 rule unit, stale Python import 후보, unused skill 후보, MCP/tool schema 과다, local JSON/JSONL log의 반복 file read·중복 tool call을 bounded scan으로 보고합니다. 네트워크 호출이나 삭제/수정은 하지 않고, 기본 출력은 raw prompt/tool input/command를 출력하지 않으며 secret-shaped path component를 redaction합니다. import/skill 결과는 동적 사용을 놓칠 수 있는 advisory 후보로만 다루세요.
75
-
76
- `context_pack.py suggest`가 쓰는 manifest는 그대로 `context_pack.py build --manifest suggested-pack.json`에 넣을 수 있습니다. `context_pack.py build`의 retrieval command는 path/root를 안전하게 표시할 수 있을 때만 출력됩니다. 안전하지 않으면 pack 본문과 JSON source metadata에 `retrieval_omitted_reason`을 기록합니다. `token_proxy`는 렌더링된 pack 문자 수를 `chars_div_4`로 나눈 추정치이며, provider가 실제로 청구/소모한 token 측정값이 아닙니다.
77
-
78
- `setup_wizard.py`는 설치 후 한 번 실행하는 설정 마법사입니다. 터미널에서 실행하면 deny rules, statusline, Bash trim/sanitize hook, large Read guard, 반복 실패 nudge, model/effort defaults를 project-local `.claude/settings.json`에 병합합니다. 비대화형 환경에서는 `--verify`로 읽기 전용 상태 점검을 하고, `--plan`으로 미리 본 뒤, `--yes`로 추천값을 적용하세요. Codex/Gemini/Cursor 같은 rule-file 에이전트에는 `--brief-mode lite|standard|ultra`로 권고 brief 스니펫을 설치·교체하고, `--brief-mode off`로 제거할 수 있습니다. 설정을 적용하면 read-only `context_guard_diet.py scan` 요약을 자동으로 출력해 남은 gap을 확인할 수 있습니다. 반복 실패 nudge가 방해되는 프로젝트는 `--no-failed-attempt-nudge`로, post-setup scan이 불필요한 자동화는 `--no-diet-scan`으로 제외할 수 있습니다.
79
-
80
- `guard_large_read.py`는 opt-in Read hook입니다. 큰 파일 전체를 Claude context에 넣기 전에 progressive read ladder를 반환해 `rg -n` 검색, `read_symbol.py` symbol slice, 작은 `offset`/`limit` Read 순서로 좁히게 합니다. Python/JS/TS/Go/Rust/Markdown 파일은 bounded prefix에서 top-level outline과 line estimate도 함께 보여줍니다. 같은 oversized file fingerprint를 반복해서 읽으려 하면 repeated-read dedup 힌트를 추가해 이전 ladder를 재사용하게 합니다. `CONTEXT_GUARD_READ_GUARD=0`으로 로컬에서 일시 비활성화할 수 있습니다.
81
-
82
- `failed_attempt_nudge.py`는 같은 Bash 실패 방향이 두 번 반복되면 `/clear`/`/compact` 힌트를 주고, 세 번 이상 반복되면 strategy-switch signal을 추가해 동일 명령 재시도 대신 다른 가설·더 작은 재현·수정 후 재검증으로 전환하게 합니다. recommended setup에서는 기본으로 켜지며, 실행을 막지 않고 짧은 추가 컨텍스트만 주입합니다.
83
-
84
- `sanitize_output.py`는 grep/diff output을 Claude에게 보여주기 전에 secret-like line, Authorization header, private key block, API token, credential URL을 `[REDACTED]`로 바꾸고, 긴 결과는 head / grep·diff·security anchor / tail만 남깁니다. 명령을 감싸는 wrapper mode는 원래 종료 코드를 보존합니다. stdin pipe도 지원하지만 producer exit code는 shell `pipefail` 없이는 알 수 없으므로 자동화에는 `python3 .../sanitize_output.py -- rg ...`처럼 wrapper mode를 선호하세요. 절대경로는 기본 익명화되고 로컬 디버깅에서만 `--show-paths`를 쓰세요. `rewrite_bash_for_token_budget.py` hook은 단일 argv 형태의 `rg`, `grep`, `git grep`, `git diff`, `git show`, `git log -p`를 자동으로 이 sanitizer에 감쌉니다.
85
-
86
- Claude Code에 적용하려면 `settings.example.json`을 `.claude/settings.json`으로 복사하되, 먼저 작은 repo에서 quoting/종료 코드를 확인하세요.
87
-
88
-
89
- ## License
90
-
91
- Copyright 2026 jinhongan. Licensed under the Apache License 2.0. See the repository [LICENSE](../LICENSE) and [NOTICE](../NOTICE).