@ictechgy/context-guard 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes for the ContextGuard plugin are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.7.0] - 2026-08-22
8
+
9
+ - Added a zero-provider-context advisory mode for WeightClass-style routing.
10
+ Small tasks bypass without standing instructions, while larger tasks select
11
+ only locally eligible log trimming, symbol slicing, adaptive packing, or
12
+ cached graph expansion candidates that pass the configured gross-byte floor
13
+ and local-overhead budget. Those gates do not guarantee provider token or
14
+ cost savings. The bounded sample harness now counterbalances arm order,
15
+ charges preprocessing per advisory run, rejects ambiguous usage and cache
16
+ accounting, and keeps invalid historical measurements excluded.
17
+ - Kept live Claude collection behind safe mode, an empty tool surface, trusted
18
+ executable ancestry, a minimal non-redirectable environment, exact quality
19
+ checks, and explicit provider-egress confirmation. Live Codex collection
20
+ fails closed before local or provider action until the subscription CLI has
21
+ a preventive no-tools mode; provider-free Codex planning remains available.
22
+ - Hardened Homebrew formula verification through an isolated temporary tap,
23
+ preserved pre-existing installation state, rendered release-safe formula
24
+ syntax, and registered the formula template as an explicit support surface.
25
+
7
26
  ## [0.6.0] - 2026-08-21
8
27
 
9
28
  - Added a provider-free `context-guard-receipt evaluate phase` surface with
package/README.ko.md CHANGED
@@ -230,7 +230,7 @@ plugin 배치에서는 기존 Bash trim 동작을 유지하고 setup이 referenc
230
230
  사용할 수 없다고 알립니다.
231
231
 
232
232
  ```bash
233
- npm install --save-exact @ictechgy/context-guard@0.6.0
233
+ npm install --save-exact @ictechgy/context-guard@0.7.0
234
234
  ./node_modules/.bin/context-guard setup --root . --agent claude --scope project --bash-reference-v1 --plan
235
235
  ./node_modules/.bin/context-guard setup --root . --agent claude --scope project --bash-reference-v1 --yes
236
236
  ```
@@ -422,10 +422,13 @@ context-guard-pack auto --root . --query "checkout retry 수정" --diff worktree
422
422
  ```bash
423
423
  ./plugins/context-guard/bin/context-guard route-advisor --workload workload.json --json
424
424
  ./plugins/context-guard/bin/context-guard-cost route-advisor --feature batch_api=true --feature structured_outputs=true --json < workload.json
425
+ ./plugins/context-guard/bin/context-guard cost advisory --workload advisory-workload.json --json
425
426
  ```
426
427
 
427
428
  `context-guard route-advisor`는 로컬 passive advisor입니다. caller가 제공한 workload JSON, provider feature 선언, usage telemetry, 외부·로컬 shifted cost를 읽고 total-cost accounting, batchability blocker, batch API·prompt-cache prefix 보존·structured outputs·저비용 모델 평가 같은 route 후보를 출력합니다. queue를 시작하거나 provider를 호출하거나 pricing 문서를 새로 가져오지 않으며, provider feature는 caller-supplied 또는 unknown/recheck-required로 표시합니다. 추천은 후보일 뿐입니다. hosted token/cost 절감을 주장하려면 matched successful task, 비열등 quality gate, shifted-cost evidence가 필요합니다.
428
429
 
430
+ `context-guard cost advisory`는 WeightClass/router용 zero-persistent-context gate입니다. 닫힌 숫자·불리언 capability 신호만 받고, 모든 경로에서 provider context를 빈 값으로 유지하며, 작거나 순이익이 없는 작업을 bypass하고, cached positive replacement 근거가 있을 때만 graph를 허용합니다. 자세한 계약은 [WeightClass advisory mode](https://github.com/ictechgy/context-guard/blob/main/docs/weightclass-advisory-mode.md)를 참고하세요.
431
+
429
432
  ### 선택한 로컬 텍스트를 보수적으로 압축하기
430
433
 
431
434
  ```bash
package/README.md CHANGED
@@ -243,7 +243,7 @@ keeps the existing Bash trim behavior and setup reports the reference route as
243
243
  unavailable.
244
244
 
245
245
  ```bash
246
- npm install --save-exact @ictechgy/context-guard@0.6.0
246
+ npm install --save-exact @ictechgy/context-guard@0.7.0
247
247
  ./node_modules/.bin/context-guard setup --root . --agent claude --scope project --bash-reference-v1 --plan
248
248
  ./node_modules/.bin/context-guard setup --root . --agent claude --scope project --bash-reference-v1 --yes
249
249
  ```
@@ -459,10 +459,13 @@ The packer uses deterministic standard-library heuristics only: no network, mode
459
459
  ```bash
460
460
  ./plugins/context-guard/bin/context-guard route-advisor --workload workload.json --json
461
461
  ./plugins/context-guard/bin/context-guard-cost route-advisor --feature batch_api=true --feature structured_outputs=true --json < workload.json
462
+ ./plugins/context-guard/bin/context-guard cost advisory --workload advisory-workload.json --json
462
463
  ```
463
464
 
464
465
  `context-guard route-advisor` is a local, passive advisor. It reads caller-supplied workload JSON, provider feature declarations, usage telemetry, and shifted external/local costs, then emits total-cost accounting, batchability blockers, and candidate routes such as batch API, prompt-cache prefix preservation, structured outputs, or cheaper-model evaluation. It does not start a queue, call providers, refresh pricing docs, or treat bundled provider feature knowledge as authoritative; unknown or caller-supplied features are marked recheck-required. Treat recommendations as candidates only. Hosted token or cost savings claims require matched successful tasks, non-inferior quality, and shifted-cost evidence.
465
466
 
467
+ `context-guard cost advisory` is the zero-persistent-context WeightClass/router gate. It accepts only closed numeric and boolean capability signals, returns an empty provider context on every path, bypasses small or non-profitable work, and permits graph only for cached positive replacement evidence. See [WeightClass advisory mode](https://github.com/ictechgy/context-guard/blob/main/docs/weightclass-advisory-mode.md).
468
+
466
469
  ### Compress selected local text conservatively
467
470
 
468
471
  ```bash
@@ -31,14 +31,14 @@ Project scope is the default. `context-guard doctor` and `context-guard setup --
31
31
 
32
32
  `bash_reference_v1` is a narrower distribution than the ordinary CLI/plugin.
33
33
  It accepts only an exact project-local npm topology: root
34
- `@ictechgy/context-guard@0.6.0` declares
34
+ `@ictechgy/context-guard@0.7.0` declares
35
35
  `@ictechgy/context-guard-receipt: 0.2.1`, and the installed Receipt inventory
36
36
  must match the SHA-256 trust anchor embedded in the root policy. Hoisted and
37
37
  nested npm dependency layouts are supported; global npm, `npx`, Homebrew,
38
38
  source-checkout, arbitrary `PATH`, and marketplace-plugin layouts are refused.
39
39
 
40
40
  ```bash
41
- npm install --save-exact @ictechgy/context-guard@0.6.0
41
+ npm install --save-exact @ictechgy/context-guard@0.7.0
42
42
  ./node_modules/.bin/context-guard setup --root . --agent claude --scope project --bash-reference-v1 --plan
43
43
  ./node_modules/.bin/context-guard setup --root . --agent claude --scope project --bash-reference-v1 --yes
44
44
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ictechgy/context-guard",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "ContextGuard CLI helpers for keeping AI coding agent context focused and local-first.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/ictechgy/context-guard#readme",
@@ -1,3 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
1
4
  # Homebrew formula draft for ContextGuard.
2
5
  # Replace URL/SHA/tag during release packaging; do not publish with SHA placeholder.
3
6
  class ContextGuard < Formula
@@ -30,6 +33,7 @@ class ContextGuard < Formula
30
33
  test do
31
34
  assert_match version.to_s, shell_output("#{bin}/context-guard --version")
32
35
  mkdir testpath/"project"
33
- system bin/"context-guard", "setup", "--root", testpath/"project", "--agent", "codex", "--scope", "project", "--plan"
36
+ system bin/"context-guard", "setup", "--root", testpath/"project",
37
+ "--agent", "codex", "--scope", "project", "--plan"
34
38
  end
35
39
  end
@@ -37,5 +37,5 @@
37
37
  "gated-experiments",
38
38
  "future-roadmap"
39
39
  ],
40
- "version": "0.6.0"
40
+ "version": "0.7.0"
41
41
  }
@@ -48,7 +48,7 @@ npx @ictechgy/context-guard --version
48
48
 
49
49
  짧은 `bash_reference_v1` Bash 출력 경로는 이 marketplace plugin/소스 배치에서
50
50
  의도적으로 활성화되지 않습니다. 정확한 프로젝트 로컬
51
- `@ictechgy/context-guard@0.6.0` npm 설치와
51
+ `@ictechgy/context-guard@0.7.0` npm 설치와
52
52
  `@ictechgy/context-guard-receipt@0.2.1` 의존성이 필요하며, 그 뒤
53
53
  `setup --agent claude --scope project --bash-reference-v1`로 명시적으로
54
54
  활성화해야 합니다. Plugin setup은 동작하지 않는 reference flag를 설치하지 않고
@@ -84,6 +84,7 @@ context-guard-compress --json < large-output.txt
84
84
  context-guard cost preflight --request request.json --budget-krw 3000 --json
85
85
  context-guard cost observe --usage usage.json --json
86
86
  context-guard route-advisor --workload workload.json --json
87
+ context-guard cost advisory --workload advisory-workload.json --json
87
88
  context-guard-trim-output --max-lines 120 -- npm test
88
89
  context-guard-read-symbol path/to/file.py TargetSymbol
89
90
  context-guard-sanitize-output -- rg -n "TOKEN|SECRET" .
@@ -48,8 +48,8 @@ npx @ictechgy/context-guard --version
48
48
 
49
49
  The compact `bash_reference_v1` Bash-output route is intentionally not
50
50
  available from this marketplace-plugin/source layout. It requires an exact
51
- project-local npm installation of `@ictechgy/context-guard@0.5.0` and its
52
- `@ictechgy/context-guard-receipt@0.2.0` dependency, then explicit
51
+ project-local npm installation of `@ictechgy/context-guard@0.7.0` and its
52
+ `@ictechgy/context-guard-receipt@0.2.1` dependency, then explicit
53
53
  `setup --agent claude --scope project --bash-reference-v1`. Plugin setup keeps
54
54
  legacy trimming and warns instead of installing a no-op reference flag. See the
55
55
  repository distribution guide for activation, disablement, seven-day handle,
@@ -88,6 +88,7 @@ context-guard-compress --json --type prose --mode readable < sanitized-prose.txt
88
88
  context-guard cost preflight --request request.json --budget-krw 3000 --json
89
89
  context-guard cost observe --usage usage.json --json
90
90
  context-guard route-advisor --workload workload.json --json
91
+ context-guard cost advisory --workload advisory-workload.json --json
91
92
  context-guard-trim-output --max-lines 120 -- npm test
92
93
  context-guard-read-symbol path/to/file.py TargetSymbol
93
94
  context-guard-sanitize-output -- rg -n "TOKEN|SECRET" .
@@ -91,6 +91,37 @@ ROUTE_STRUCTURED_TASK_KINDS = {
91
91
  "batch_eval",
92
92
  "eval",
93
93
  }
94
+ ADVISORY_WORKLOAD_SCHEMA_VERSION = "contextguard.advisory-workload.v1"
95
+ ADVISORY_DECISION_SCHEMA_VERSION = "contextguard.advisory-decision.v1"
96
+ ADVISORY_MAX_INTEGER = 1_000_000_000
97
+ ADVISORY_TOP_LEVEL_KEYS = {"invocation", "limits", "schema_version", "signals", "vendor"}
98
+ ADVISORY_INVOCATION_KEYS = {
99
+ "explicit_wrappers_available",
100
+ "hooks_available",
101
+ "host_tool_surface_equal_to_control",
102
+ "rules_loaded",
103
+ "safe_mode",
104
+ "skills_loaded",
105
+ }
106
+ ADVISORY_SIGNAL_INTEGER_KEYS = {
107
+ "candidate_context_bytes",
108
+ "estimated_local_overhead_ms",
109
+ "graph_candidate_bytes",
110
+ "graph_candidate_count",
111
+ "graph_replacement_bytes",
112
+ "largest_file_bytes",
113
+ "log_bytes",
114
+ "selected_file_count",
115
+ "task_prompt_bytes",
116
+ }
117
+ ADVISORY_SIGNAL_KEYS = ADVISORY_SIGNAL_INTEGER_KEYS | {"repo_map_cached"}
118
+ ADVISORY_LIMIT_KEYS = {
119
+ "inline_log_bytes",
120
+ "max_local_overhead_ms",
121
+ "minimum_gross_context_savings_bytes",
122
+ "pack_bytes",
123
+ "symbol_slice_bytes",
124
+ }
94
125
  ALLOWED_FIRST_COMPONENT_SYMLINKS = {
95
126
  "tmp": Path("/private/tmp"),
96
127
  "var": Path("/private/var"),
@@ -272,6 +303,33 @@ def load_json_input(path: str, *, max_bytes: int = DEFAULT_MAX_BYTES) -> tuple[A
272
303
  return data, truncated
273
304
 
274
305
 
306
+ def reject_duplicate_json_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
307
+ result: dict[str, Any] = {}
308
+ for key, value in pairs:
309
+ if key in result:
310
+ fail("duplicate JSON key is not allowed")
311
+ result[key] = value
312
+ return result
313
+
314
+
315
+ def load_advisory_json_input(path: str, *, max_bytes: int) -> Any:
316
+ text, truncated = read_text_path(path, max_bytes=max_bytes)
317
+ if truncated:
318
+ fail("JSON input exceeded max bytes")
319
+ try:
320
+ return json.loads(
321
+ text,
322
+ parse_constant=reject_json_constant,
323
+ object_pairs_hook=reject_duplicate_json_pairs,
324
+ )
325
+ except json.JSONDecodeError as exc:
326
+ fail(f"invalid JSON input at line {exc.lineno}: {exc.msg}")
327
+ except ValueError as exc:
328
+ if isinstance(exc, CostGuardError):
329
+ raise
330
+ fail(f"invalid JSON input: {exc}")
331
+
332
+
275
333
  def secret_count_in_text(text: str) -> int:
276
334
  return sum(1 for _ in SECRET_RE.finditer(text))
277
335
 
@@ -2532,6 +2590,80 @@ def route_recommendations(
2532
2590
  return recs
2533
2591
 
2534
2592
 
2593
+ def advisory_plain_object(value: Any, label: str, expected_keys: set[str]) -> dict[str, Any]:
2594
+ if type(value) is not dict:
2595
+ fail(f"{label} must be a plain JSON object")
2596
+ if set(value) != expected_keys:
2597
+ fail(f"{label} has missing or unknown fields")
2598
+ return value
2599
+
2600
+
2601
+ def advisory_bool(value: Any, label: str) -> bool:
2602
+ if type(value) is not bool:
2603
+ fail(f"{label} must be boolean")
2604
+ return value
2605
+
2606
+
2607
+ def advisory_integer(value: Any, label: str, *, positive: bool = False) -> int:
2608
+ if type(value) is not int:
2609
+ fail(f"{label} must be an integer")
2610
+ lower_bound = 1 if positive else 0
2611
+ if value < lower_bound or value > ADVISORY_MAX_INTEGER:
2612
+ relation = "> 0" if positive else ">= 0"
2613
+ fail(f"{label} must be {relation} and bounded")
2614
+ return value
2615
+
2616
+
2617
+ def advisory_result(
2618
+ *,
2619
+ activation_status: str,
2620
+ decision: str,
2621
+ reason: str,
2622
+ measurement_eligible: bool,
2623
+ capabilities: dict[str, bool],
2624
+ selected_features: dict[str, bool],
2625
+ actions: list[dict[str, Any]],
2626
+ control_bytes: int,
2627
+ treatment_bytes: int,
2628
+ minimum_savings: int,
2629
+ local_overhead_ms: int,
2630
+ max_local_overhead_ms: int,
2631
+ graph_replacement_delta_bytes: int,
2632
+ ) -> dict[str, Any]:
2633
+ gross_saved = max(0, control_bytes - treatment_bytes)
2634
+ return {
2635
+ "schema_version": ADVISORY_DECISION_SCHEMA_VERSION,
2636
+ "mode": "router_advisory",
2637
+ "activation_status": activation_status,
2638
+ "decision": decision,
2639
+ "reason": reason,
2640
+ "provider_context": "",
2641
+ "provider_context_bytes": 0,
2642
+ "persistent_writes_allowed": False,
2643
+ "receipts_enabled": False,
2644
+ "measurement_eligible": measurement_eligible,
2645
+ "capabilities": capabilities,
2646
+ "selected_features": selected_features,
2647
+ "actions": actions,
2648
+ "accounting": {
2649
+ "control_candidate_context_bytes": control_bytes,
2650
+ "estimated_treatment_context_bytes": treatment_bytes,
2651
+ "estimated_gross_context_saved_bytes": gross_saved,
2652
+ "minimum_gross_context_savings_bytes": minimum_savings,
2653
+ "estimated_local_overhead_ms": local_overhead_ms,
2654
+ "max_local_overhead_ms": max_local_overhead_ms,
2655
+ "graph_replacement_delta_bytes": graph_replacement_delta_bytes,
2656
+ },
2657
+ "claim_boundary": {
2658
+ "provider_token_or_cost_savings_claim_allowed": False,
2659
+ "requires_paired_provider_measurement": True,
2660
+ "requires_non_inferior_quality": True,
2661
+ "task_content_accepted": False,
2662
+ "persistent_context_allowed": False,
2663
+ },
2664
+ }
2665
+
2666
+
2535
2667
  def route_advisor_command(args: argparse.Namespace) -> int:
2536
2668
  workload_raw, _truncated = load_json_input(args.workload, max_bytes=args.max_bytes)
2537
2669
  workload = require_json_object(workload_raw.get("workload") if isinstance(workload_raw, dict) and isinstance(workload_raw.get("workload"), dict) else workload_raw, "workload")
@@ -2599,6 +2731,182 @@ def route_advisor_command(args: argparse.Namespace) -> int:
2599
2731
  return 0
2600
2732
 
2601
2733
 
2734
+ def advisory_decision(raw: Any) -> dict[str, Any]:
2735
+ workload = advisory_plain_object(raw, "advisory workload", ADVISORY_TOP_LEVEL_KEYS)
2736
+ if workload["schema_version"] != ADVISORY_WORKLOAD_SCHEMA_VERSION:
2737
+ fail("advisory workload schema version is unsupported")
2738
+ vendor = workload["vendor"]
2739
+ if type(vendor) is not str or vendor not in {"claude", "codex"}:
2740
+ fail("advisory vendor must be claude or codex")
2741
+ invocation = advisory_plain_object(
2742
+ workload["invocation"], "advisory invocation", ADVISORY_INVOCATION_KEYS
2743
+ )
2744
+ invocation_values = {
2745
+ key: advisory_bool(invocation[key], f"advisory invocation.{key}")
2746
+ for key in sorted(ADVISORY_INVOCATION_KEYS)
2747
+ }
2748
+ if invocation_values["safe_mode"] and vendor != "claude":
2749
+ fail("advisory safe_mode is only valid for claude")
2750
+ signals = advisory_plain_object(
2751
+ workload["signals"], "advisory signals", ADVISORY_SIGNAL_KEYS
2752
+ )
2753
+ signal_values = {
2754
+ key: advisory_integer(signals[key], f"advisory signals.{key}")
2755
+ for key in sorted(ADVISORY_SIGNAL_INTEGER_KEYS)
2756
+ }
2757
+ signal_values["repo_map_cached"] = advisory_bool(
2758
+ signals["repo_map_cached"], "advisory signals.repo_map_cached"
2759
+ )
2760
+ limits = advisory_plain_object(
2761
+ workload["limits"], "advisory limits", ADVISORY_LIMIT_KEYS
2762
+ )
2763
+ limit_values = {
2764
+ key: advisory_integer(
2765
+ limits[key],
2766
+ f"advisory limits.{key}",
2767
+ positive=key in {"inline_log_bytes", "pack_bytes", "symbol_slice_bytes"},
2768
+ )
2769
+ for key in sorted(ADVISORY_LIMIT_KEYS)
2770
+ }
2771
+
2772
+ control_bytes = signal_values["candidate_context_bytes"]
2773
+ if signal_values["largest_file_bytes"] > control_bytes:
2774
+ fail("largest_file_bytes exceeds candidate_context_bytes")
2775
+ if signal_values["log_bytes"] > control_bytes:
2776
+ fail("log_bytes exceeds candidate_context_bytes")
2777
+ if signal_values["graph_replacement_bytes"] > control_bytes:
2778
+ fail("graph_replacement_bytes exceeds candidate_context_bytes")
2779
+ graph_count = signal_values["graph_candidate_count"]
2780
+ graph_candidate_bytes = signal_values["graph_candidate_bytes"]
2781
+ graph_replacement_bytes = signal_values["graph_replacement_bytes"]
2782
+ if graph_count == 0 and (graph_candidate_bytes or graph_replacement_bytes):
2783
+ fail("graph bytes require at least one graph candidate")
2784
+ if graph_count > 0 and not (graph_candidate_bytes and graph_replacement_bytes):
2785
+ fail("graph candidates require nonzero candidate and replacement bytes")
2786
+
2787
+ hooks_effective = bool(
2788
+ invocation_values["hooks_available"]
2789
+ and not (vendor == "claude" and invocation_values["safe_mode"])
2790
+ )
2791
+ persistent_context_absent = not (
2792
+ invocation_values["rules_loaded"] or invocation_values["skills_loaded"]
2793
+ )
2794
+ capabilities = {
2795
+ "hooks_effective": hooks_effective,
2796
+ "explicit_wrappers_available": invocation_values["explicit_wrappers_available"],
2797
+ "persistent_context_absent": persistent_context_absent,
2798
+ "host_tool_surface_equal_to_control": invocation_values[
2799
+ "host_tool_surface_equal_to_control"
2800
+ ],
2801
+ }
2802
+ no_features = {
2803
+ "adaptive": False,
2804
+ "graph": False,
2805
+ "symbol": False,
2806
+ "trim_output": False,
2807
+ }
2808
+ result_common = {
2809
+ "capabilities": capabilities,
2810
+ "control_bytes": control_bytes,
2811
+ "minimum_savings": limit_values["minimum_gross_context_savings_bytes"],
2812
+ "local_overhead_ms": signal_values["estimated_local_overhead_ms"],
2813
+ "max_local_overhead_ms": limit_values["max_local_overhead_ms"],
2814
+ }
2815
+ if not persistent_context_absent:
2816
+ return advisory_result(
2817
+ activation_status="bypass", decision="bypass",
2818
+ reason="persistent_context_loaded", measurement_eligible=False,
2819
+ selected_features=no_features, actions=[], treatment_bytes=control_bytes,
2820
+ graph_replacement_delta_bytes=0, **result_common,
2821
+ )
2822
+ if not invocation_values["host_tool_surface_equal_to_control"]:
2823
+ return advisory_result(
2824
+ activation_status="bypass", decision="bypass",
2825
+ reason="host_tool_surface_mismatch", measurement_eligible=False,
2826
+ selected_features=no_features, actions=[], treatment_bytes=control_bytes,
2827
+ graph_replacement_delta_bytes=0, **result_common,
2828
+ )
2829
+
2830
+ candidates: list[dict[str, Any]] = []
2831
+ log_bytes = signal_values["log_bytes"]
2832
+ inline_log_bytes = limit_values["inline_log_bytes"]
2833
+ if log_bytes > inline_log_bytes:
2834
+ candidates.append({
2835
+ "decision": "trim_output", "reason": "log_savings",
2836
+ "treatment_bytes": control_bytes - (log_bytes - inline_log_bytes),
2837
+ "features": {**no_features, "trim_output": True},
2838
+ "actions": [{"kind": "trim_output", "max_inline_bytes": inline_log_bytes}],
2839
+ "graph_replacement_delta_bytes": 0,
2840
+ })
2841
+ largest_file_bytes = signal_values["largest_file_bytes"]
2842
+ symbol_slice_bytes = limit_values["symbol_slice_bytes"]
2843
+ if largest_file_bytes > symbol_slice_bytes:
2844
+ candidates.append({
2845
+ "decision": "symbol_slice", "reason": "symbol_slice_savings",
2846
+ "treatment_bytes": control_bytes - (largest_file_bytes - symbol_slice_bytes),
2847
+ "features": {**no_features, "symbol": True},
2848
+ "actions": [{"kind": "symbol_slice", "max_bytes": symbol_slice_bytes}],
2849
+ "graph_replacement_delta_bytes": 0,
2850
+ })
2851
+ if signal_values["selected_file_count"] >= 3 and control_bytes > limit_values["pack_bytes"]:
2852
+ graph_net_saved = 0
2853
+ graph_selected = False
2854
+ if (
2855
+ signal_values["repo_map_cached"] and graph_count > 0
2856
+ and graph_replacement_bytes > graph_candidate_bytes
2857
+ ):
2858
+ graph_net_saved = graph_replacement_bytes - graph_candidate_bytes
2859
+ graph_selected = True
2860
+ pack_bytes = limit_values["pack_bytes"]
2861
+ candidates.append({
2862
+ "decision": "adaptive_pack", "reason": "context_pack_savings",
2863
+ "treatment_bytes": pack_bytes,
2864
+ "features": {**no_features, "adaptive": True, "graph": graph_selected},
2865
+ "actions": [{
2866
+ "kind": "context_pack", "budget_bytes": limit_values["pack_bytes"],
2867
+ "adaptive": True, "symbol": False, "graph": graph_selected,
2868
+ }],
2869
+ "graph_replacement_delta_bytes": graph_net_saved,
2870
+ })
2871
+ candidate = min(candidates, key=lambda item: item["treatment_bytes"]) if candidates else None
2872
+ if candidate is None or (
2873
+ control_bytes - int(candidate["treatment_bytes"])
2874
+ < limit_values["minimum_gross_context_savings_bytes"]
2875
+ ):
2876
+ return advisory_result(
2877
+ activation_status="bypass", decision="bypass", reason="below_break_even",
2878
+ measurement_eligible=True, selected_features=no_features, actions=[],
2879
+ treatment_bytes=control_bytes, graph_replacement_delta_bytes=0, **result_common,
2880
+ )
2881
+ if signal_values["estimated_local_overhead_ms"] > limit_values["max_local_overhead_ms"]:
2882
+ return advisory_result(
2883
+ activation_status="bypass", decision="bypass",
2884
+ reason="local_overhead_budget_exceeded", measurement_eligible=True,
2885
+ selected_features=no_features, actions=[], treatment_bytes=control_bytes,
2886
+ graph_replacement_delta_bytes=0, **result_common,
2887
+ )
2888
+ if not invocation_values["explicit_wrappers_available"]:
2889
+ return advisory_result(
2890
+ activation_status="inactive", decision="bypass",
2891
+ reason="explicit_wrappers_unavailable", measurement_eligible=False,
2892
+ selected_features=no_features, actions=[], treatment_bytes=control_bytes,
2893
+ graph_replacement_delta_bytes=0, **result_common,
2894
+ )
2895
+ return advisory_result(
2896
+ activation_status="active", decision=str(candidate["decision"]),
2897
+ reason=str(candidate["reason"]), measurement_eligible=True,
2898
+ selected_features=dict(candidate["features"]), actions=list(candidate["actions"]),
2899
+ treatment_bytes=int(candidate["treatment_bytes"]),
2900
+ graph_replacement_delta_bytes=int(candidate["graph_replacement_delta_bytes"]), **result_common,
2901
+ )
2902
+
2903
+
2904
+ def advisory_command(args: argparse.Namespace) -> int:
2905
+ workload = load_advisory_json_input(args.workload, max_bytes=args.max_bytes)
2906
+ emit(advisory_decision(workload), json_mode=args.json)
2907
+ return 0
2908
+
2909
+
2602
2910
  def usage_int(data: dict[str, Any], key: str) -> int:
2603
2911
  value = data.get(key, 0)
2604
2912
  try:
@@ -3044,6 +3352,12 @@ def emit(data: dict[str, Any], *, json_mode: bool) -> None:
3044
3352
  f"candidates={routing.get('candidate_count', 0)} conditional={routing.get('conditional_count', 0)} "
3045
3353
  f"total_with_shift=${total.get('total_cost_with_shift_usd', 0)}"
3046
3354
  )
3355
+ elif mode == "router_advisory":
3356
+ accounting = data.get("accounting", {}) if isinstance(data.get("accounting"), dict) else {}
3357
+ print(
3358
+ f"{TOOL_NAME}: advisory {data.get('decision', 'bypass')} "
3359
+ f"gross_context_bytes={accounting.get('estimated_gross_context_saved_bytes', 0)}"
3360
+ )
3047
3361
  else:
3048
3362
  summary = data.get("summary", {}) if isinstance(data.get("summary"), dict) else {}
3049
3363
  print(f"{TOOL_NAME}: ledger entries={summary.get('entries', 0)}")
@@ -3113,6 +3427,19 @@ def build_parser() -> argparse.ArgumentParser:
3113
3427
  add_common_cost_args(route)
3114
3428
  route.set_defaults(func=route_advisor_command)
3115
3429
 
3430
+ advisory = sub.add_parser(
3431
+ "advisory",
3432
+ help="plan zero-persistent-context WeightClass/router advisory actions",
3433
+ description=(
3434
+ "select a zero-provider-instruction bypass or explicit local wrapper from "
3435
+ "closed numeric capability signals; never reads task text or project files"
3436
+ ),
3437
+ )
3438
+ advisory.add_argument("--workload", default="-", help="closed advisory workload JSON path, or '-' for stdin")
3439
+ advisory.add_argument("--max-bytes", type=int, default=DEFAULT_MAX_BYTES, help=f"maximum advisory JSON bytes (default: {DEFAULT_MAX_BYTES})")
3440
+ advisory.add_argument("--json", action="store_true", help="emit machine-readable JSON")
3441
+ advisory.set_defaults(func=advisory_command)
3442
+
3116
3443
  return parser
3117
3444
 
3118
3445