@ictechgy/context-guard 0.4.1 → 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.
- package/CHANGELOG.md +9 -0
- package/README.ko.md +61 -32
- package/README.md +90 -22
- package/context-guard-kit/README.md +39 -26
- package/context-guard-kit/benchmark_runner.py +273 -8
- package/context-guard-kit/claude_transcript_cost_audit.py +325 -12
- package/context-guard-kit/context_compress.py +153 -1
- package/context-guard-kit/context_filter.py +446 -0
- package/context-guard-kit/context_guard_cli.py +3 -0
- package/context-guard-kit/context_guard_diet.py +677 -2
- package/context-guard-kit/context_pack.py +1694 -2
- package/context-guard-kit/cost_guard.py +1870 -0
- package/context-guard-kit/setup_wizard.py +820 -29
- package/context-guard-kit/trim_command_output.py +396 -45
- package/docs/benchmark-fixtures/learned-compression.tasks.example.json +24 -0
- package/docs/benchmark-fixtures/learned-compression.variants.example.json +10 -0
- package/docs/benchmark-fixtures/visual-ocr.tasks.example.json +24 -0
- package/docs/benchmark-fixtures/visual-ocr.variants.example.json +10 -0
- package/docs/benchmark-workflow-examples.md +40 -0
- package/docs/benchmark-workflows/context-pack-byte-proxy.example.json +169 -0
- package/docs/benchmark-workflows/measured-token-workflow.example.json +170 -0
- package/docs/benchmark-workflows/provider-cache-telemetry.example.json +170 -0
- package/docs/cache-diagnostics-schema.md +75 -0
- package/docs/cache-diagnostics.example.json +116 -0
- package/docs/cache-diagnostics.schema.json +460 -0
- package/docs/distribution.md +4 -2
- package/docs/experimental-benchmark-fixtures.md +36 -0
- package/package.json +11 -2
- package/packaging/homebrew/context-guard.rb.template +3 -2
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +21 -13
- package/plugins/context-guard/README.md +24 -10
- package/plugins/context-guard/bin/context-guard +3 -0
- package/plugins/context-guard/bin/context-guard-audit +325 -12
- package/plugins/context-guard/bin/context-guard-bench +273 -8
- package/plugins/context-guard/bin/context-guard-compress +153 -1
- package/plugins/context-guard/bin/context-guard-cost +1870 -0
- package/plugins/context-guard/bin/context-guard-diet +677 -2
- package/plugins/context-guard/bin/context-guard-filter +446 -0
- package/plugins/context-guard/bin/context-guard-pack +1694 -2
- package/plugins/context-guard/bin/context-guard-setup +820 -29
- package/plugins/context-guard/bin/context-guard-trim-output +396 -45
- package/plugins/context-guard/brief/README.md +10 -3
- package/plugins/context-guard/skills/optimize/SKILL.md +5 -2
- 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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
784
|
+
add(f"- line_capped: true\n")
|
|
576
785
|
if raw_output.get("redacted_lines"):
|
|
577
|
-
|
|
786
|
+
add(f"- redacted_lines: {raw_output.get('redacted_lines')}\n")
|
|
578
787
|
if isinstance(budget, dict):
|
|
579
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
808
|
+
add("\n## runner_failure_summary\n")
|
|
596
809
|
for runner, items in sorted(runner_summary.items()):
|
|
597
|
-
|
|
810
|
+
add(f"- runner={runner}\n")
|
|
598
811
|
if isinstance(items, list):
|
|
599
812
|
for item in items:
|
|
600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
836
|
+
add(f"\n## {title}\n")
|
|
624
837
|
for value in values:
|
|
625
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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.
|