@ictechgy/context-guard 0.4.0 → 0.4.3

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 (45) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.ko.md +61 -32
  3. package/README.md +90 -22
  4. package/context-guard-kit/README.md +39 -26
  5. package/context-guard-kit/benchmark_runner.py +273 -8
  6. package/context-guard-kit/claude_transcript_cost_audit.py +325 -12
  7. package/context-guard-kit/context_compress.py +153 -1
  8. package/context-guard-kit/context_filter.py +446 -0
  9. package/context-guard-kit/context_guard_cli.py +3 -0
  10. package/context-guard-kit/context_guard_diet.py +677 -2
  11. package/context-guard-kit/context_pack.py +1694 -2
  12. package/context-guard-kit/cost_guard.py +1870 -0
  13. package/context-guard-kit/setup_wizard.py +820 -29
  14. package/context-guard-kit/trim_command_output.py +396 -45
  15. package/docs/benchmark-fixtures/learned-compression.tasks.example.json +24 -0
  16. package/docs/benchmark-fixtures/learned-compression.variants.example.json +10 -0
  17. package/docs/benchmark-fixtures/visual-ocr.tasks.example.json +24 -0
  18. package/docs/benchmark-fixtures/visual-ocr.variants.example.json +10 -0
  19. package/docs/benchmark-workflow-examples.md +40 -0
  20. package/docs/benchmark-workflows/context-pack-byte-proxy.example.json +169 -0
  21. package/docs/benchmark-workflows/measured-token-workflow.example.json +170 -0
  22. package/docs/benchmark-workflows/provider-cache-telemetry.example.json +170 -0
  23. package/docs/cache-diagnostics-schema.md +75 -0
  24. package/docs/cache-diagnostics.example.json +116 -0
  25. package/docs/cache-diagnostics.schema.json +460 -0
  26. package/docs/distribution.md +4 -2
  27. package/docs/experimental-benchmark-fixtures.md +36 -0
  28. package/package.json +11 -2
  29. package/packaging/homebrew/context-guard.rb.template +3 -2
  30. package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
  31. package/plugins/context-guard/README.ko.md +21 -13
  32. package/plugins/context-guard/README.md +24 -10
  33. package/plugins/context-guard/bin/context-guard +3 -0
  34. package/plugins/context-guard/bin/context-guard-audit +325 -12
  35. package/plugins/context-guard/bin/context-guard-bench +273 -8
  36. package/plugins/context-guard/bin/context-guard-compress +153 -1
  37. package/plugins/context-guard/bin/context-guard-cost +1870 -0
  38. package/plugins/context-guard/bin/context-guard-diet +677 -2
  39. package/plugins/context-guard/bin/context-guard-filter +446 -0
  40. package/plugins/context-guard/bin/context-guard-pack +1694 -2
  41. package/plugins/context-guard/bin/context-guard-setup +820 -29
  42. package/plugins/context-guard/bin/context-guard-trim-output +396 -45
  43. package/plugins/context-guard/brief/README.md +10 -3
  44. package/plugins/context-guard/skills/optimize/SKILL.md +5 -2
  45. package/plugins/context-guard/skills/setup/SKILL.md +3 -1
@@ -13,7 +13,7 @@ import importlib.machinery
13
13
  import importlib.util
14
14
  import json
15
15
  import os
16
- from pathlib import PurePosixPath
16
+ from pathlib import Path, PurePosixPath
17
17
  import queue
18
18
  import re
19
19
  import shlex
@@ -33,6 +33,8 @@ MAX_RUNNER_SUMMARY_ITEMS_LIMIT = 100
33
33
  DEFAULT_TIMEOUT_SECONDS = 600
34
34
  MAX_TIMEOUT_SECONDS = 86_400
35
35
  TIMEOUT_EXIT_CODE = 124
36
+ DEFAULT_ARTIFACT_RECEIPT_MAX_BYTES = 10_000_000
37
+ MAX_ARTIFACT_RECEIPT_MAX_BYTES = 100_000_000
36
38
 
37
39
 
38
40
  def bounded_int(value: object, default: int, minimum: int, maximum: int) -> int:
@@ -180,6 +182,166 @@ def load_line_sanitizer(show_paths: bool) -> object:
180
182
  return FallbackLineSanitizer(show_paths=show_paths, diagnostic=diagnostic)
181
183
 
182
184
 
185
+ def load_artifact_store_module() -> object:
186
+ """Load the adjacent artifact store without importing by package name.
187
+
188
+ The plugin ships helper scripts as sibling executable files, so the trim
189
+ wrapper must resolve both source-tree (`context_escrow.py`) and packaged
190
+ (`context-guard-artifact`) names.
191
+ """
192
+ script_dir = os.path.dirname(os.path.abspath(__file__))
193
+ load_errors: list[str] = []
194
+ for name in ("context_escrow.py", "context-guard-artifact", "claude-token-artifact"):
195
+ candidate = os.path.join(script_dir, name)
196
+ if not os.path.exists(candidate):
197
+ continue
198
+ try:
199
+ loader = importlib.machinery.SourceFileLoader(f"_context_guard_artifact_{os.getpid()}", candidate)
200
+ spec = importlib.util.spec_from_loader(loader.name, loader)
201
+ if spec is None:
202
+ continue
203
+ module = importlib.util.module_from_spec(spec)
204
+ loader.exec_module(module)
205
+ return module
206
+ except Exception as exc:
207
+ load_errors.append(f"{os.path.basename(candidate)} failed to load: {exc.__class__.__name__}: {exc}")
208
+ continue
209
+ diagnostic = "; ".join(load_errors) if load_errors else "artifact store not found next to trim wrapper"
210
+ raise RuntimeError(diagnostic)
211
+
212
+
213
+ def store_sanitized_artifact_receipt(
214
+ *,
215
+ sanitized_text: str,
216
+ command: list[str],
217
+ args: argparse.Namespace,
218
+ line_sanitizer: object,
219
+ redacted_lines: int,
220
+ ) -> dict[str, object]:
221
+ """Store exact sanitized output using the existing artifact receipt format."""
222
+ artifact = load_artifact_store_module()
223
+ max_bytes = bounded_int(
224
+ getattr(args, "artifact_max_bytes", DEFAULT_ARTIFACT_RECEIPT_MAX_BYTES),
225
+ DEFAULT_ARTIFACT_RECEIPT_MAX_BYTES,
226
+ 1,
227
+ MAX_ARTIFACT_RECEIPT_MAX_BYTES,
228
+ )
229
+ content_bytes = len(sanitized_text.encode("utf-8", errors="replace"))
230
+ if content_bytes > max_bytes:
231
+ return {
232
+ "stored": False,
233
+ "error": "sanitized_output_exceeds_artifact_max_bytes",
234
+ "bytes": content_bytes,
235
+ "max_bytes": max_bytes,
236
+ "exact_reexpand": {"available": False, "reason": "artifact size cap exceeded"},
237
+ }
238
+
239
+ directory = artifact.normalize_allowed_first_absolute_symlink(Path(args.artifact_dir).expanduser())
240
+ content_sha = hashlib.sha256(sanitized_text.encode("utf-8", errors="replace")).hexdigest()
241
+ preview = command_preview(command, line_sanitizer, args.max_line_chars)
242
+ id_basis = json.dumps(
243
+ {
244
+ "content_sha256": content_sha,
245
+ "command_preview": preview,
246
+ "input_truncated": False,
247
+ "producer": "context-guard-trim-output",
248
+ },
249
+ sort_keys=True,
250
+ )
251
+ artifact_id = hashlib.sha256(id_basis.encode("utf-8")).hexdigest()[:20]
252
+ content_path, meta_path = artifact.artifact_paths(directory, artifact_id)
253
+ total_lines = sanitized_text.count("\n") + (1 if sanitized_text and not sanitized_text.endswith("\n") else 0)
254
+ content_type = artifact.classify_content_type(sanitized_text)
255
+ strategy = artifact.recommended_strategy(content_type)
256
+ metadata: dict[str, object] = {
257
+ "artifact_id": artifact_id,
258
+ "created_at": int(time.time()),
259
+ "command_preview": preview,
260
+ "content_type": content_type,
261
+ "input": {
262
+ "bytes_read": content_bytes,
263
+ "truncated": False,
264
+ "max_bytes": max_bytes,
265
+ "source": "context-guard-trim-output:sanitized-output",
266
+ },
267
+ "stored_output": {
268
+ "bytes": content_bytes,
269
+ "lines": total_lines,
270
+ "sha256": content_sha,
271
+ "content_file": content_path.name,
272
+ "metadata_file": meta_path.name,
273
+ "scope": "sanitized_full_output",
274
+ },
275
+ "digest": artifact.build_digest(sanitized_text, artifact_id=artifact_id, redacted_lines=redacted_lines),
276
+ "retrieval": {
277
+ "strategy": strategy,
278
+ "deterministic": True,
279
+ "hints": artifact.build_retrieval_hints(
280
+ artifact_id,
281
+ sanitized_text,
282
+ content_type=content_type,
283
+ strategy=strategy,
284
+ total_lines=total_lines,
285
+ ),
286
+ },
287
+ }
288
+ artifact.shrink_digest_for_metadata_cap(metadata)
289
+ artifact.write_private_text(content_path, sanitized_text)
290
+ artifact.write_private_text(meta_path, artifact.metadata_json_text(metadata))
291
+ receipt = artifact.receipt_for(metadata)
292
+ query_line_cap = int(getattr(artifact, "MAX_QUERY_LINES", 5_000))
293
+ query_char_cap = 1_000_000
294
+ content_chars = len(sanitized_text)
295
+ exact_reexpand: dict[str, object] = {
296
+ "available": False,
297
+ "scope": "sanitized_full_output",
298
+ "sha256": content_sha,
299
+ "bytes": content_bytes,
300
+ "lines": total_lines,
301
+ "reason": "artifact query cap exceeded; use retrieval hints for exact slices",
302
+ }
303
+ if total_lines <= query_line_cap and content_chars <= query_char_cap:
304
+ raw_artifact_dir = str(getattr(args, "artifact_dir", ".context-guard/artifacts"))
305
+ dir_flags = ""
306
+ if raw_artifact_dir != ".context-guard/artifacts":
307
+ dir_flags = f" --dir {shlex.quote(raw_artifact_dir)}"
308
+ line_flags = ""
309
+ if total_lines > 0:
310
+ line_flags = f" --lines 1:{total_lines} --max-lines {max(1, total_lines)}"
311
+ exact_reexpand = {
312
+ "available": True,
313
+ "scope": "sanitized_full_output",
314
+ "sha256": content_sha,
315
+ "bytes": content_bytes,
316
+ "lines": total_lines,
317
+ "cli": (
318
+ f"context-guard-artifact{dir_flags} get {artifact_id}{line_flags} "
319
+ f"--max-chars {max(1, content_chars)}"
320
+ ),
321
+ }
322
+ receipt["exact_reexpand"] = exact_reexpand
323
+ return receipt
324
+
325
+
326
+ def capture_sanitized_artifact_line(
327
+ *,
328
+ capture_enabled: bool,
329
+ sanitized_line: str,
330
+ artifact_lines: list[str],
331
+ capture_bytes: int,
332
+ capture_overflow: bool,
333
+ max_bytes: int,
334
+ ) -> tuple[int, bool]:
335
+ if not capture_enabled or capture_overflow:
336
+ return capture_bytes, capture_overflow
337
+ source_bytes = len(sanitized_line.encode("utf-8", errors="replace"))
338
+ if capture_bytes + source_bytes <= max_bytes:
339
+ artifact_lines.append(sanitized_line)
340
+ return capture_bytes + source_bytes, False
341
+ artifact_lines.clear()
342
+ return capture_bytes, True
343
+
344
+
183
345
  def unique_keep_order(lines: Iterable[str]) -> list[str]:
184
346
  seen: set[str] = set()
185
347
  out: list[str] = []
@@ -557,55 +719,106 @@ def build_digest_payload(
557
719
  return payload
558
720
 
559
721
 
722
+ def markdown_artifact_receipt_lines(artifact_receipt: dict[str, object]) -> list[str]:
723
+ lines = [
724
+ "- artifact_receipt: "
725
+ f"stored={str(artifact_receipt.get('stored')).lower()} "
726
+ f"id={artifact_receipt.get('artifact_id') or artifact_receipt.get('error')}\n"
727
+ ]
728
+ exact = artifact_receipt.get("exact_reexpand")
729
+ if isinstance(exact, dict) and exact.get("cli"):
730
+ lines.append(f"- exact_reexpand: `{exact.get('cli')}`\n")
731
+ return lines
732
+
733
+
734
+ def compact_markdown_artifact_receipt(payload: dict[str, object], max_chars: int) -> str:
735
+ artifact_receipt = payload.get("artifact_receipt")
736
+ if not isinstance(artifact_receipt, dict) or max_chars <= 0:
737
+ return ""
738
+
739
+ full = "".join(markdown_artifact_receipt_lines(artifact_receipt))
740
+ if len(full) <= max_chars:
741
+ return full
742
+
743
+ artifact_id = artifact_receipt.get("artifact_id") or artifact_receipt.get("error")
744
+ stored = str(artifact_receipt.get("stored")).lower()
745
+ exact = artifact_receipt.get("exact_reexpand")
746
+ exact_available = ""
747
+ if isinstance(exact, dict) and "available" in exact:
748
+ exact_available = f" exact_available={str(exact.get('available')).lower()}"
749
+
750
+ candidates = [
751
+ f"- artifact_receipt: stored={stored} id={artifact_id}{exact_available}; raise --max-chars for full exact_reexpand\n",
752
+ f"- artifact_receipt: stored={stored} id={artifact_id}{exact_available}\n",
753
+ f"- artifact_receipt: id={artifact_id}\n",
754
+ ]
755
+ for candidate in candidates:
756
+ if len(candidate) <= max_chars:
757
+ return candidate
758
+ return ""
759
+
760
+
560
761
  def render_digest_markdown(payload: dict[str, object], max_chars: int) -> str:
561
762
  raw_output = payload.get("raw_output", {})
562
763
  budget = payload.get("budget", {})
563
764
  lines: list[str] = []
765
+ non_receipt_lines: list[str] = []
766
+
767
+ def add(line: str, *, receipt: bool = False) -> None:
768
+ lines.append(line)
769
+ if not receipt:
770
+ non_receipt_lines.append(line)
771
+
564
772
  lines.append("[context-guard-kit] semantic digest\n")
565
- lines.append(f"- status: {payload.get('status')}\n")
566
- lines.append(f"- exit_code: {payload.get('exit_code')}\n")
567
- lines.append(f"- timed_out: {str(payload.get('timed_out')).lower()}\n")
773
+ non_receipt_lines.append("[context-guard-kit] semantic digest\n")
774
+ add(f"- status: {payload.get('status')}\n")
775
+ add(f"- exit_code: {payload.get('exit_code')}\n")
776
+ add(f"- timed_out: {str(payload.get('timed_out')).lower()}\n")
568
777
  if isinstance(raw_output, dict):
569
- lines.append(
778
+ add(
570
779
  "- raw_output: "
571
780
  f"{raw_output.get('lines')} lines/{raw_output.get('chars')} chars"
572
781
  f" (visible={raw_output.get('visible_chars')}, truncated={str(raw_output.get('truncated')).lower()})\n"
573
782
  )
574
783
  if raw_output.get("line_capped"):
575
- lines.append(f"- line_capped: true\n")
784
+ add(f"- line_capped: true\n")
576
785
  if raw_output.get("redacted_lines"):
577
- lines.append(f"- redacted_lines: {raw_output.get('redacted_lines')}\n")
786
+ add(f"- redacted_lines: {raw_output.get('redacted_lines')}\n")
578
787
  if isinstance(budget, dict):
579
- lines.append(
788
+ add(
580
789
  "- budget: "
581
790
  f"{budget.get('max_lines')} lines/{budget.get('max_chars')} chars/"
582
791
  f"line={budget.get('max_line_chars')} chars\n"
583
792
  )
584
793
  if payload.get("command_preview"):
585
- lines.append(f"- command: `{payload.get('command_preview')}`\n")
794
+ add(f"- command: `{payload.get('command_preview')}`\n")
795
+ artifact_receipt = payload.get("artifact_receipt")
796
+ if isinstance(artifact_receipt, dict):
797
+ for line in markdown_artifact_receipt_lines(artifact_receipt):
798
+ add(line, receipt=True)
586
799
  failure_signature = payload.get("failure_signature")
587
800
  if isinstance(failure_signature, dict):
588
- lines.append(
801
+ add(
589
802
  "- failure_signature: "
590
803
  f"{failure_signature.get('hash')} ({failure_signature.get('source')})\n"
591
804
  )
592
805
 
593
806
  runner_summary = payload.get("runner_failure_summary")
594
807
  if isinstance(runner_summary, dict) and runner_summary:
595
- lines.append("\n## runner_failure_summary\n")
808
+ add("\n## runner_failure_summary\n")
596
809
  for runner, items in sorted(runner_summary.items()):
597
- lines.append(f"- runner={runner}\n")
810
+ add(f"- runner={runner}\n")
598
811
  if isinstance(items, list):
599
812
  for item in items:
600
- lines.append(f" - {item}\n")
813
+ add(f" - {item}\n")
601
814
 
602
815
  duplicate_line_groups = payload.get("duplicate_line_groups")
603
816
  if isinstance(duplicate_line_groups, list) and duplicate_line_groups:
604
- lines.append("\n## duplicate_line_groups\n")
817
+ add("\n## duplicate_line_groups\n")
605
818
  for group in duplicate_line_groups:
606
819
  if not isinstance(group, dict):
607
820
  continue
608
- lines.append(
821
+ add(
609
822
  "- "
610
823
  f"count={group.get('count')} "
611
824
  f"first_line={group.get('first_line')} "
@@ -620,9 +833,9 @@ def render_digest_markdown(payload: dict[str, object], max_chars: int) -> str:
620
833
  ]:
621
834
  values = payload.get(key)
622
835
  if isinstance(values, list) and values:
623
- lines.append(f"\n## {title}\n")
836
+ add(f"\n## {title}\n")
624
837
  for value in values:
625
- lines.append(f"- {value}\n")
838
+ add(f"- {value}\n")
626
839
 
627
840
  text = "".join(lines)
628
841
  output, capped = cap_text(text, max_chars)
@@ -631,6 +844,21 @@ def render_digest_markdown(payload: dict[str, object], max_chars: int) -> str:
631
844
  marker = "[context-guard-kit] digest capped by --max-chars.\n"
632
845
  if max_chars <= len(marker):
633
846
  return marker[:max_chars]
847
+ reserved_receipt = compact_markdown_artifact_receipt(payload, max_chars - len(marker))
848
+ if reserved_receipt:
849
+ head_budget = max_chars - len(marker) - len(reserved_receipt)
850
+ head = ""
851
+ if head_budget > 0:
852
+ non_receipt_text = "".join(non_receipt_lines)
853
+ text_cap_marker = f"\n[context-guard-kit] text capped: {len(non_receipt_text)} chars total\n"
854
+ if len(non_receipt_text) <= head_budget:
855
+ head = non_receipt_text
856
+ elif head_budget > len(text_cap_marker):
857
+ keep = head_budget - len(text_cap_marker)
858
+ head = non_receipt_text[:keep].rstrip() + text_cap_marker
859
+ if head and not head.endswith("\n"):
860
+ head += "\n"
861
+ return head + reserved_receipt + marker
634
862
  output, _ = cap_text(text, max_chars - len(marker))
635
863
  return output + marker
636
864
 
@@ -662,6 +890,35 @@ def render_digest_json(payload: dict[str, object], max_chars: int) -> str:
662
890
  return output
663
891
  return dumps(candidates[-1])
664
892
 
893
+ def compact_artifact_receipt(*, include_exact_reexpand: bool) -> dict[str, object] | None:
894
+ artifact_receipt = payload.get("artifact_receipt")
895
+ if not isinstance(artifact_receipt, dict):
896
+ return None
897
+ compact: dict[str, object] = {}
898
+ for key in ("stored", "artifact_id", "error", "bytes", "max_bytes"):
899
+ if key in artifact_receipt:
900
+ compact[key] = artifact_receipt[key]
901
+ stored_output = artifact_receipt.get("stored_output")
902
+ if isinstance(stored_output, dict):
903
+ compact["stored_output"] = {
904
+ key: stored_output[key]
905
+ for key in ("scope", "bytes", "lines", "sha256")
906
+ if key in stored_output
907
+ }
908
+ exact = artifact_receipt.get("exact_reexpand")
909
+ if include_exact_reexpand and isinstance(exact, dict):
910
+ compact["exact_reexpand"] = {
911
+ key: exact[key]
912
+ for key in ("available", "scope", "sha256", "bytes", "lines", "cli", "reason")
913
+ if key in exact
914
+ }
915
+ return compact
916
+
917
+ def attach_artifact_receipt(candidate: dict[str, object], artifact_receipt: dict[str, object] | None) -> dict[str, object]:
918
+ if artifact_receipt is not None:
919
+ candidate["artifact_receipt"] = artifact_receipt
920
+ return candidate
921
+
665
922
  output = dumps(payload)
666
923
  if len(output) <= max_chars:
667
924
  return output
@@ -696,37 +953,57 @@ def render_digest_json(payload: dict[str, object], max_chars: int) -> str:
696
953
  "exit_code": failure_signature.get("exit_code"),
697
954
  "timed_out": failure_signature.get("timed_out"),
698
955
  }
956
+ compact_receipt = compact_artifact_receipt(include_exact_reexpand=True)
957
+ minimal_receipt = compact_artifact_receipt(include_exact_reexpand=False)
699
958
 
700
959
  return first_fitting(
701
960
  [
702
- {
703
- "tool": payload.get("tool"),
704
- "digest_version": payload.get("digest_version"),
705
- "digest_capped": True,
706
- "status": payload.get("status"),
707
- "exit_code": payload.get("exit_code"),
708
- "timed_out": payload.get("timed_out"),
709
- "failure_signature": compact_signature,
710
- "raw_output": payload.get("raw_output"),
711
- "budget": payload.get("budget"),
712
- "next_queries": ["Raise --max-chars or inspect a narrower command for details."],
713
- },
714
- {
715
- "digest_capped": True,
716
- "status": payload.get("status"),
717
- "exit_code": payload.get("exit_code"),
718
- "timed_out": payload.get("timed_out"),
719
- "failure_signature": compact_signature,
720
- "raw_output": payload.get("raw_output"),
721
- "next_queries": ["Raise --max-chars or inspect a narrower command for details."],
722
- },
723
- {
724
- "digest_capped": True,
725
- "status": payload.get("status"),
726
- "exit_code": payload.get("exit_code"),
727
- "timed_out": payload.get("timed_out"),
728
- "failure_signature": compact_signature,
729
- },
961
+ attach_artifact_receipt(
962
+ {
963
+ "tool": payload.get("tool"),
964
+ "digest_version": payload.get("digest_version"),
965
+ "digest_capped": True,
966
+ "status": payload.get("status"),
967
+ "exit_code": payload.get("exit_code"),
968
+ "timed_out": payload.get("timed_out"),
969
+ "failure_signature": compact_signature,
970
+ "raw_output": payload.get("raw_output"),
971
+ "budget": payload.get("budget"),
972
+ "next_queries": ["Raise --max-chars or inspect a narrower command for details."],
973
+ },
974
+ compact_receipt,
975
+ ),
976
+ attach_artifact_receipt(
977
+ {
978
+ "digest_capped": True,
979
+ "status": payload.get("status"),
980
+ "exit_code": payload.get("exit_code"),
981
+ "timed_out": payload.get("timed_out"),
982
+ "failure_signature": compact_signature,
983
+ "raw_output": payload.get("raw_output"),
984
+ "next_queries": ["Raise --max-chars or inspect a narrower command for details."],
985
+ },
986
+ compact_receipt,
987
+ ),
988
+ attach_artifact_receipt(
989
+ {
990
+ "digest_capped": True,
991
+ "status": payload.get("status"),
992
+ "exit_code": payload.get("exit_code"),
993
+ "timed_out": payload.get("timed_out"),
994
+ "failure_signature": compact_signature,
995
+ },
996
+ compact_receipt,
997
+ ),
998
+ attach_artifact_receipt(
999
+ {
1000
+ "digest_capped": True,
1001
+ "status": payload.get("status"),
1002
+ "exit_code": payload.get("exit_code"),
1003
+ "timed_out": payload.get("timed_out"),
1004
+ },
1005
+ minimal_receipt,
1006
+ ),
730
1007
  {"digest_capped": True},
731
1008
  ]
732
1009
  )
@@ -931,9 +1208,40 @@ def main() -> int:
931
1208
  "(default: off; formats: markdown, json)"
932
1209
  ),
933
1210
  )
1211
+ parser.add_argument(
1212
+ "--artifact-receipt",
1213
+ action="store_true",
1214
+ help=(
1215
+ "with --digest, store the exact sanitized full output as a local "
1216
+ "context-guard-artifact receipt and include re-expand metadata"
1217
+ ),
1218
+ )
1219
+ parser.add_argument(
1220
+ "--artifact-dir",
1221
+ default=".context-guard/artifacts",
1222
+ help="artifact receipt directory used by --artifact-receipt (default: .context-guard/artifacts)",
1223
+ )
1224
+ parser.add_argument(
1225
+ "--artifact-max-bytes",
1226
+ type=int,
1227
+ default=DEFAULT_ARTIFACT_RECEIPT_MAX_BYTES,
1228
+ help=(
1229
+ "maximum sanitized output bytes eligible for --artifact-receipt "
1230
+ f"(default: {DEFAULT_ARTIFACT_RECEIPT_MAX_BYTES}, max: {MAX_ARTIFACT_RECEIPT_MAX_BYTES})"
1231
+ ),
1232
+ )
934
1233
  parser.add_argument("command", nargs=argparse.REMAINDER)
935
1234
  args = parser.parse_args()
936
1235
  normalize_budgets(args)
1236
+ args.artifact_max_bytes = bounded_int(
1237
+ args.artifact_max_bytes,
1238
+ DEFAULT_ARTIFACT_RECEIPT_MAX_BYTES,
1239
+ 1,
1240
+ MAX_ARTIFACT_RECEIPT_MAX_BYTES,
1241
+ )
1242
+ if args.artifact_receipt and args.digest == "off":
1243
+ print("trim_command_output.py: --artifact-receipt requires --digest markdown or --digest json", file=sys.stderr)
1244
+ return 2
937
1245
 
938
1246
  command = args.command
939
1247
  if command and command[0] == "--":
@@ -971,6 +1279,9 @@ def main() -> int:
971
1279
  line_sanitizer = load_line_sanitizer(args.show_paths)
972
1280
  duplicate_tracker = DuplicateLineTracker()
973
1281
  redacted_lines = 0
1282
+ artifact_lines: list[str] = []
1283
+ artifact_capture_bytes = 0
1284
+ artifact_capture_overflow = False
974
1285
 
975
1286
  if proc.stdout is None:
976
1287
  print("trim_command_output.py: subprocess produced no stdout pipe", file=sys.stderr)
@@ -987,6 +1298,14 @@ def main() -> int:
987
1298
  visible_source, redacted = line_sanitizer.sanitize(line) # type: ignore[attr-defined]
988
1299
  if redacted:
989
1300
  redacted_lines += 1
1301
+ artifact_capture_bytes, artifact_capture_overflow = capture_sanitized_artifact_line(
1302
+ capture_enabled=args.artifact_receipt,
1303
+ sanitized_line=visible_source,
1304
+ artifact_lines=artifact_lines,
1305
+ capture_bytes=artifact_capture_bytes,
1306
+ capture_overflow=artifact_capture_overflow,
1307
+ max_bytes=args.artifact_max_bytes,
1308
+ )
990
1309
  visible_line, line_capped = cap_line(visible_source, args.max_line_chars)
991
1310
  any_line_capped = any_line_capped or line_capped
992
1311
  visible_chars += len(visible_line)
@@ -1009,6 +1328,14 @@ def main() -> int:
1009
1328
  visible_source, redacted = line_sanitizer.sanitize(line) # type: ignore[attr-defined]
1010
1329
  if redacted:
1011
1330
  redacted_lines += 1
1331
+ artifact_capture_bytes, artifact_capture_overflow = capture_sanitized_artifact_line(
1332
+ capture_enabled=args.artifact_receipt,
1333
+ sanitized_line=visible_source,
1334
+ artifact_lines=artifact_lines,
1335
+ capture_bytes=artifact_capture_bytes,
1336
+ capture_overflow=artifact_capture_overflow,
1337
+ max_bytes=args.artifact_max_bytes,
1338
+ )
1012
1339
  visible_line, line_capped = cap_line(visible_source, args.max_line_chars)
1013
1340
  any_line_capped = any_line_capped or line_capped
1014
1341
  visible_chars += len(visible_line)
@@ -1040,6 +1367,30 @@ def main() -> int:
1040
1367
  line_sanitizer=line_sanitizer,
1041
1368
  duplicate_line_groups=duplicate_tracker.as_list(),
1042
1369
  )
1370
+ if args.artifact_receipt:
1371
+ if artifact_capture_overflow:
1372
+ payload["artifact_receipt"] = {
1373
+ "stored": False,
1374
+ "error": "sanitized_output_exceeds_artifact_max_bytes",
1375
+ "max_bytes": args.artifact_max_bytes,
1376
+ "exact_reexpand": {"available": False, "reason": "artifact size cap exceeded"},
1377
+ }
1378
+ else:
1379
+ try:
1380
+ payload["artifact_receipt"] = store_sanitized_artifact_receipt(
1381
+ sanitized_text="".join(artifact_lines),
1382
+ command=command,
1383
+ args=args,
1384
+ line_sanitizer=line_sanitizer,
1385
+ redacted_lines=redacted_lines,
1386
+ )
1387
+ except Exception as exc:
1388
+ payload["artifact_receipt"] = {
1389
+ "stored": False,
1390
+ "error": "artifact_receipt_unavailable",
1391
+ "reason": f"{exc.__class__.__name__}: {exc}",
1392
+ "exact_reexpand": {"available": False, "reason": "artifact receipt unavailable"},
1393
+ }
1043
1394
  if args.digest == "json":
1044
1395
  sys.stdout.write(render_digest_json(payload, args.max_chars))
1045
1396
  else:
@@ -48,9 +48,16 @@ A brief-mode response must still include, whenever relevant:
48
48
  Brief mode is installed by copying the marker-delimited block from a level file into the
49
49
  target agent's rule/instruction file (for example a repo `AGENTS.md`, `CLAUDE.md`,
50
50
  `.cursorrules`, or `.github/copilot-instructions.md`). The cross-agent setup planner is the
51
- intended automation for this; per the project safety rules it stays dry-run first, writes
52
- only local files, backs up before changing anything, and applies only with explicit
53
- approval.
51
+ intended automation for this:
52
+
53
+ ```bash
54
+ context-guard setup --agent codex --scope project --brief-mode standard --plan
55
+ context-guard setup --agent codex --scope project --brief-mode standard --yes
56
+ context-guard setup --agent codex --scope project --brief-mode off --yes
57
+ ```
58
+
59
+ Per the project safety rules it stays dry-run first, writes only local files, backs up
60
+ existing rule files before changing anything, and applies only with explicit approval.
54
61
 
55
62
  Each block is wrapped in stable markers:
56
63
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Diagnose and reduce Claude Code token usage for a project or session using context hygiene, model and effort routing, MCP minimization, output trimming/sanitizing, subagent discipline, and measurement. Use when the user asks to lower Claude Code token usage, cost, context bloat, or usage-limit burn.
3
3
  argument-hint: [project/session symptoms]
4
- allowed-tools: Bash(context-guard-setup *), Bash(context-guard-audit *), Bash(context-guard-diet scan *), Bash(context-guard-read-symbol *), Bash(context-guard-artifact store *), Bash(context-guard-artifact get *), Bash(context-guard-artifact list *), Bash(context-guard-statusline)
4
+ allowed-tools: Bash(context-guard-setup *), Bash(context-guard-audit *), Bash(context-guard-diet scan *), Bash(context-guard-diet structural-waste *), Bash(context-guard-read-symbol *), Bash(context-guard-artifact store *), Bash(context-guard-artifact get *), Bash(context-guard-artifact list *), Bash(context-guard-statusline)
5
5
  ---
6
6
 
7
7
  # ContextGuard
@@ -15,12 +15,13 @@ Use this order:
15
15
  - For first-time setup, run `context-guard-setup --plan` and offer `context-guard-setup --yes` for recommended project-local settings.
16
16
  - If transcript files are available, run `context-guard-audit ~/.claude/projects --top 20 --recommend`.
17
17
  - For project configuration/context bloat, run `context-guard-diet scan .`.
18
+ - For structural waste such as duplicate rules, stale import candidates, oversized tool schemas, or repeated reads in local logs, run `context-guard-diet structural-waste . --json`.
18
19
  2. Identify the largest bucket:
19
20
  - stale conversation history -> recommend `/clear` between unrelated tasks and focused `/compact` for long tasks.
20
21
  - startup context -> prune `CLAUDE.md`, move long workflows to skills, disable unused MCP servers.
21
22
  - large file reads -> use `context-guard-read-symbol` and the example Read guard before whole-file context.
22
23
  - very large logs that may need later exact slices -> store sanitized output with `context-guard-artifact store` and query only needed lines/patterns.
23
- - noisy command output -> use `context-guard-trim-output` wrappers or the example PreToolUse hook.
24
+ - noisy command output -> use `context-guard-trim-output` wrappers or the example PreToolUse hook; use `context-guard-filter` only with an explicit user-owned config for stable successful command shapes.
24
25
  - grep/diff output with possible secrets -> use `context-guard-sanitize-output` or the example Bash hook.
25
26
  - expensive reasoning -> route default work to `sonnet` and lower `/effort`; reserve Opus/`opusplan` for planning.
26
27
  - noisy exploration -> keep it local: use `rg`, `context-guard-read-symbol`, artifact queries, or a bounded subagent only when parallel value justifies the multiplier.
@@ -37,11 +38,13 @@ Useful local commands provided by this plugin:
37
38
  context-guard-audit ~/.claude/projects --top 20 --recommend
38
39
  context-guard-setup --plan
39
40
  context-guard-diet scan .
41
+ context-guard-diet structural-waste . --json
40
42
  context-guard-read-symbol path/to/file.py TargetSymbol
41
43
  context-guard-artifact store --command "long-command" --json < large.log
42
44
  context-guard-artifact get <artifact_id> --lines 1:80
43
45
  context-guard-trim-output --max-lines 120 -- npm test
44
46
  context-guard-sanitize-output -- rg -n "TOKEN|SECRET" .
47
+ context-guard-filter validate --config .context-guard/filter-dsl.json
45
48
  context-guard-statusline
46
49
  ```
47
50
 
@@ -10,9 +10,10 @@ Goal: help the user configure this plugin without memorizing helper commands.
10
10
 
11
11
  Default flow:
12
12
 
13
- 1. Run a read-only plan first:
13
+ 1. Run a read-only health check and plan first:
14
14
 
15
15
  ```bash
16
+ context-guard-setup --verify
16
17
  context-guard-setup --plan
17
18
  ```
18
19
 
@@ -37,4 +38,5 @@ Safety:
37
38
 
38
39
  - Do not modify global `~/.claude/settings.json`.
39
40
  - Prefer project-local `.claude/settings.json`.
41
+ - `context-guard-setup --verify` is a local read-only health check and never applies settings.
40
42
  - Setup's post-apply scan is local, read-only, and prints a summary only; it does not mutate settings.