@ictechgy/context-guard 0.5.1 → 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 +21 -0
- package/README.ko.md +13 -1
- package/README.md +13 -1
- package/docs/distribution.md +2 -2
- package/package.json +2 -1
- package/packaging/homebrew/context-guard.rb.template +5 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +11 -1
- package/plugins/context-guard/README.md +13 -2
- package/plugins/context-guard/bin/context-guard-artifact +1 -1
- package/plugins/context-guard/bin/context-guard-cost +327 -0
- package/plugins/context-guard/bin/context-guard-pack +626 -3
- package/plugins/context-guard/bin/context-guard-setup +89 -12
- package/plugins/context-guard/bin/context-guard-task-memory +635 -0
- package/plugins/context-guard/lib/context_guard_commands.py +21 -1
- package/plugins/context-guard/lib/context_pack_git_boundary.py +19 -0
- package/plugins/context-guard/lib/context_pack_identity.py +115 -0
- package/plugins/context-guard/lib/context_pack_receipts.py +9 -0
- package/plugins/context-guard/lib/context_pack_rendering.py +12 -0
- package/plugins/context-guard/lib/context_pack_scanning.py +10 -0
- package/plugins/context-guard/lib/context_pack_selection.py +6 -0
|
@@ -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
|
|