Repository navigation
fix(agentx): pin the draft and its acceptance when the baseline simulates acceptance - #1885
Conversation
…ates acceptance A launch that simulates speculative-decoding acceptance (SGLANG_SIMULATE_ACC_LEN, a --speculative-config with rejection_sample_method "synthetic" or a synthetic_acceptance_length, or an explicit acceptance-length flag) fixes an acceptance length that is only valid for the draft it was set for. A candidate that changed the draft length, method or the simulated value was measured against that stale acceptance and could keep a fake gain. _build_variant_yaml, which composes every benchmarked candidate (explore variants, integrate_patch, conc_sweep, specialist rebench) over its base, now composes the base launch alongside the candidate and refuses the candidate when the base simulates acceptance and the candidate's draft or acceptance settings differ from it: the pinned flags, the pinned --speculative-config keys and every SGLANG_SIMULATE_ACC_* env, whether added, changed, removed, unset or dropped by a replacing argument string. run_grid records the refusal as error_class speculative_decoding_pinned without launching the server. Other knobs, including --speculative-config keys that do not describe the draft, stay open, and a base that verifies a real draft pins nothing. Co-Authored-By: Claude <noreply@anthropic.com>
…ls in one step Keeps _build_variant_yaml's cognitive complexity where it was and tightens its function-lines baseline entry to the shorter body. Co-Authored-By: Claude <noreply@anthropic.com>
A base that verifies its draft for real pinned nothing, so a candidate could add SGLANG_SIMULATE_ACC_LEN or a synthetic --speculative-config and be graded on an acceptance length it set rather than measured. Such a candidate is now refused with speculative_decoding_pinned as well. Co-Authored-By: Claude <noreply@anthropic.com>
8f1265a to
80c2f11
Compare
Code metrics gate: PASSED with waived findings (
|
| Dimension | Limit | New | Worse | Baseline out of date | Baseline entries | vs base |
|---|---|---|---|---|---|---|
| Cyclomatic complexity | fails > 20 | 0 | 0 | 0 | 72 | +0 |
| Cognitive complexity | fails > 30 | 0 | 0 | 0 | 284 | +0 |
| Function length (lines) | fails > 80 | 0 | 0 | 0 | 450 | +0 |
| Nested block depth | fails > 5 | 0 | 0 | 0 | 27 | +0 |
| Module length (lines) | warns > 800, fails > 1200 | 0 | 1 | 0 | 53 | +0 |
| Duplicated lines per file | fails > 0 | 0 | 0 | 0 | 118 | +0 |
| Dead code (vulture) | fails > 0 | 0 | 0 | 0 | 2 | +0 |
| Check | Findings | Effect |
|---|---|---|
| Comments | 0 | |
| English only | 0 | |
| Production code imports test code | 0 | |
| Repeated literals | 0 | |
| A1 Private name of another package | 0 | |
| A8 Flag argument | 0 | |
| A9 Tuple return | 0 | |
| A10 Raw vocabulary comparison | 0 | |
| A11 Hidden global write | 0 | |
| A12 Pure forwarder | 0 | |
| A12 Single-implementation abstraction | 0 |
Worse than the baseline (waived by the baseline-raise label): 1
| Unit | Dimension | Baseline | Now |
|---|---|---|---|
src/hyperloom/orchestrator/actions/executors/_grid_runner.py:1 |
Module length (lines) | 2247 | 2248 |
How to fix
- New or worse: split the unit (extract a function, a class, a module) until it is back under the limit, or no worse than its recorded value. The baseline cannot absorb a new violation: CI refuses any baseline entry added or raised relative to the base branch.
- Baseline out of date: something improved. Run
python scripts/code_metrics.py --update-baselineand commitscripts/code_metrics_baseline.txt; the command only lowers or removes entries. - Override: the
baseline-raisePR label waives new, worse and baseline-growth findings and the failing design checks (they stay listed here). Use it only with the reason in the PR description; it waives nothing else. - Tools for a local run:
pip install ruff==0.16.2 complexipy==8.0.1 vulture==2.16andnpm install -g jscpd@5.4.1(or pointCODE_METRICS_JSCPDat another jscpd command, such asnpx --yes jscpd@<version>).
96 findings in files this change does not touch
Outside this change (informational): 96
| Unit | Dimension | Baseline | Now |
|---|---|---|---|
src/hyperloom/orchestrator/enablement/recipe/projections.py project_launch_evidence |
Cognitive complexity | absent | 36 |
src/hyperloom/orchestrator/state/shared_state.py resolve_graded_comparison |
Cognitive complexity | absent | 34 |
src/hyperloom_kb/collect/mapping.py _experience |
Cognitive complexity | absent | 31 |
src/hyperloom_kb/schema.py FieldDeclaration.__post_init__ |
Cognitive complexity | absent | 32 |
src/kernelforge/cli/forge_loop_command.py _prepare_measurement_driver |
Cognitive complexity | absent | 36 |
src/kernelforge/cli/forge_loop_command.py _read_kb_warm_start |
Cognitive complexity | absent | 40 |
src/kernelforge/loop/runner.py IterationLoop._collect_session_candidate |
Cognitive complexity | absent | 44 |
src/kernelforge/loop/runner.py IterationLoop._rule_on_attempt |
Cognitive complexity | absent | 38 |
src/hyperloom_kb/tests/test_cli.py |
Duplicated lines per file | absent | 18 |
src/hyperloom_kb/tests/test_global_identity_and_pull.py |
Duplicated lines per file | absent | 13 |
src/hyperloom_kb/tests/test_local_state.py |
Duplicated lines per file | absent | 18 |
src/hyperloom/inference_optimizer/cli/__init__.py _reexport_session_environment |
Function length (lines) | absent | 112 |
src/hyperloom/inference_optimizer/cli/__init__.py _resolve_agent_lineup |
Function length (lines) | absent | 89 |
src/hyperloom/inference_optimizer/cli/__init__.py _resolve_run_topology |
Function length (lines) | absent | 92 |
src/hyperloom/inference_optimizer/cli/__init__.py _resume_session |
Function length (lines) | absent | 171 |
src/hyperloom/inference_optimizer/cli/__init__.py _seed_fresh_session |
Function length (lines) | absent | 183 |
src/hyperloom/inference_optimizer/multi_node/cli.py _restart_multinode |
Function length (lines) | absent | 107 |
src/hyperloom/inference_optimizer/multi_node/scripts/launch_multinode.py _build_parser |
Function length (lines) | absent | 84 |
src/hyperloom/inference_optimizer/multi_node/scripts/launch_multinode.py _spawn_rank_actors |
Function length (lines) | absent | 90 |
src/hyperloom_kb/http_service.py RequestHandler._dispatch |
Function length (lines) | absent | 91 |
src/hyperloom_kb/sync.py GlobalSync.pull |
Function length (lines) | absent | 86 |
src/kernelforge/cli/forge_loop_command.py _RunReporter.build_result |
Function length (lines) | absent | 91 |
src/kernelforge/cli/forge_loop_command.py _prepare_measurement_driver |
Function length (lines) | absent | 140 |
src/kernelforge/cli/forge_loop_command.py _read_kb_warm_start |
Function length (lines) | absent | 154 |
src/kernelforge/loop/knowledge_integration.py _measure_warmstart_field |
Function length (lines) | absent | 83 |
...and 71 more; the job log lists every row.
Verdict computed by the gate scripts in
/home/runner/work/_temp/code-metrics-gate, not by this tree's copy.
Thresholds and sources: [tool.hyperloom.code_metrics] in pyproject.toml. Tools: complexipy 8.0.1, jscpd 5.4.1, ruff 0.16.2, vulture 2.16.
…h after its runtime override A --speculative-config.<key> flag merges over the JSON object, so it is folded into the same pinned keys; numbers are compared by value, so 3.61 and 3.610 agree. The check now runs after the runtime override is applied, which is the launch that is benchmarked. Co-Authored-By: Claude <noreply@anthropic.com>
xiaofei-zheng
left a comment
There was a problem hiding this comment.
What this PR does
When a session's base launch simulates speculative-decoding acceptance, that acceptance length is only valid for the draft it was set for, so a candidate that changed the draft was benchmarked against a stale acceptance and its gain was fake. #1696 pinned this on a launch path #1784 removed. This PR adds one pure rule, _spec_decode_pin.refuse_unpinned_speculative_change, called from _grid_runner._build_variant_yaml — the single composer every benchmarked candidate passes through — which compares the composed base launch against the composed candidate and raises before boot. run_grid records it as status: failed, error_class: speculative_decoding_pinned, naming each changed setting, and attaches no launch evidence.
Blocking issues
1. The agent's reference doc documents only half the rule, and its untouched half now instructs the agent into a guaranteed refusal.
src/hyperloom/orchestrator/prompts/references/speculative_decoding.md:75-87 conditions the whole pin on "When the session's base launch simulates acceptance". src/hyperloom/orchestrator/actions/executors/_spec_decode_pin.py:224-229 has a second branch the doc never mentions: when the base does not simulate, any candidate that turns simulated acceptance on is refused.
That branch contradicts pre-existing text in the same file, which this PR does not touch:
- lines 29-32: "Do not invent, round, or adjust an AL /
SGLANG_SIMULATE_ACC_LEN/synthetic_acceptance_lengthvalue to taste. If synthetic acceptance is used, its value must be the golden AL looked up for the exact gamma/draft length of the variant." - lines 37-39: "If the variant changes a draft-length / gamma / method knob, the AL reference changes with it and must be re-looked-up from the golden YAML."
Both now describe an action the code always refuses: with a simulating base the draft is pinned, so there is nothing to re-look-up; with a non-simulating base the AL may not be set at all.
Trigger — a session whose base launch carries no speculative decoding, the normal starting point for the enablement flow this doc exists for. A specialist proposes enabling EAGLE3 with the golden AL correctly looked up for its own draft length:
refuse_unpinned_speculative_change(
"--max-num-seqs 128 --gpu-memory-utilization 0.92",
{},
'--max-num-seqs 128 --gpu-memory-utilization 0.92 --speculative-config '
'{"method":"eagle3","model":"/models/MiniMax-M3-eagle3","num_speculative_tokens":3,'
'"rejection_sample_method":"synthetic","synthetic_acceptance_length":2.78}',
{},
) # -> SpeculativeDecodingPinnedErrorThe SGLang equivalent (--speculative-algorithm EAGLE --speculative-num-steps 3 plus SGLANG_SIMULATE_ACC_LEN=2.78 on a base with neither) raises the same way. The round is spent on speculative_decoding_pinned, and the agent's own spec gives it no basis to propose something different next round.
Please state the non-simulating branch in the reference doc together with what the agent should do instead, and reconcile lines 29-32 and 37-39 with it.
2. The refusal asserts something false about a base that runs no speculative decoding.
src/hyperloom/orchestrator/actions/executors/_spec_decode_pin.py:226-229 raises "the base launch verifies its draft for real, and this candidate switches to simulated acceptance, whose length is set rather than measured. Keep real verification." That branch is taken for every base _simulates_acceptance returns False for, which includes a base with no speculative decoding at all — there is no draft being verified, and "Keep real verification" names an action that does not exist there.
This message is the only feedback the agent receives: VariantResult.error → state/failure_evidence.render_failure_line → loop/conversation.py:172, which excerpts the last 120 characters, i.e. exactly the false prescription. Please distinguish "the base verifies its draft for real" from "the base runs no speculative decoding", and name the available next step in each case.
Checked: _build_variant_yaml (the base_args/base_envs composition, the call site, the getattr(exc, "error_class") abort mapping, the never-launched set in _attach_grid_launch_evidence); _spec_decode_pin.py in full; compose_server_args / remove_server_args / _shell_safe_dedupe / tokenize_server_args_preserving_json; seal_server_argv; explore.py's no-op probe and its run_grid call sites; state/failure_evidence.py and loop/conversation.py; speculative_decoding.md in full; the eight other open PRs touching _grid_runner.py.
Verified clean and not reported: the effective_remove refactor is behaviour-preserving (remove_server_args collects specs into sets, so the dropped dict.fromkeys dedup is inert, and base_remove=[] under a replacing base yields the old list); _shell_safe_dedupe and _last_flag_values are both last-wins, so the judged args match the launched ones; the dotted --speculative-config.<key> merge wins over the JSON object in both token orderings; 3.61/3.610 and int-vs-str env values agree; a damaged candidate JSON fails closed; the secondary _build_variant_yaml call sites only run after the first build succeeded.
Ran: refuse_unpinned_speculative_change directly against the head module on 9 constructed launches (no-op, non-string env values, compact / shell-quoted / spaced JSON, a spaced-JSON method change, 3.61 vs 3.610, and the two enablement cases above). The repo's pytest suite does not run on this box (kernelforge/loop/editable_repo.py imports fcntl); CI at head 14e7297 is green on all 12 shards, both coverage jobs, code-metrics and pr-hygiene.
Base: 190604fb9d6b7c213c8e83cd8ff918b138fff59e | Head: 14e7297909e292eb17b77a31df07b64de6f979f2
|
Follow-up on the review above. This one is about the shape of the rule rather than a defect in what is written, so it is separate from the two blocking items — but it is the reason those two items are awkward to fix in isolation. The predicate is not the invariantThe invariant this PR is defending is: a simulated acceptance length must correspond to the What the code enforces is: the candidate's pinned keys must equal the base's. The second is strictly stronger. Ran the three shapes against
The third row is the move Rows 2 and 3 are byte-identical inputs. The code cannot tell them apart, because it never consults the golden table. Equality is standing in for a lookup it cannot perform, and the cost is that the draft dimension — the main speculative-decoding tuning axis — is frozen for the whole session whenever the base simulates acceptance. The non-simulating branch addresses a different claim
What that branch actually asserts is "an AL that is set rather than measured cannot support a gain". If that holds, it holds for the base too, and every session already running on Root cause: golden AL has no owner in this repo
That leaves the rule with two sources: the prompt asks the agent to look it up, and this module hard-blocks on equality because nothing can check whether it did. Suggest giving it one owner — a module over
Two things fall out. The gate stops being stricter than the invariant, and If that is too large for this PR, the smaller step is to drop the non-simulating branch and loosen the predicate to "the draft and its AL must move together", which still refuses row 1 — the case that motivated the PR — without refusing row 3. The two blocking items above stand either way: whatever predicate ships has to be the one the reference doc and the refusal message describe. |
…he reference A base that does not simulate acceptance carries no simulated length a new draft could go stale against, so a candidate enabling synthetic acceptance on it is no longer refused (the refusal also misdescribed a base running no speculative decoding as verifying a real draft). The reference doc states both halves of the rule, and its golden-AL instructions no longer tell the agent to re-look-up an AL for a draft the pin refuses to change. Co-Authored-By: Claude <noreply@anthropic.com>
|
Heads-up from #1904 (fixes #1901): it is the real-draft counterpart to this PR. #1885 pins the draft when the base simulates acceptance. #1904 validates a speculative-decoding change in a synthetic session against the acceptance its own GSM8K eval measured on real text, which catches a vLLM |
|
Re-reviewed at My follow-up comment argued that branch away too broadly, and the deletion went one case further than it should have. Correcting that. Where my argument was wrongI treated My reductio — "if a set length cannot support a gain, the base is equally unsound" — only holds for the third row, where there is no acceptance figure on either side. For the first two the base's acceptance is measured, so a candidate that switches to a set length is comparing a declared number against a measured one. That is the same fake gain this PR exists to stop, pointed the other way. The regressionTwo cases in
Both were refused at Suggested split — three states, not two
Rows 2 and 3 are separable without a golden-AL lookup: Overlap with #1904@zoroyihan7's note says a simulated-acceptance round reports the same acceptance on the benchmark and the eval, so it passes #1904's cross-check and "stays yours to pin". With row 2 open, that round is no longer pinned here either — #1885 returns early because the base does not simulate, and #1904 passes it because both sides report the declared value. Worth confirming between the two PRs which one owns it; on the file lists there is no conflict, so this is about coverage, not about a rebase. |
…al draft's Dropping the reverse rule also let a candidate declare an acceptance length on a base that verifies a real draft, comparing a declared figure with a measured one. Only a base with no speculative decoding, which has no acceptance to compare against, may now enable simulated acceptance; the refusal says why. Co-Authored-By: Claude <noreply@anthropic.com>
Addressed at 49d243b: the reference doc now states all three base states, the refusal message distinguishes them, and the real-draft case is refused again.
|
Re-reviewed at What this PR doesWhen a session's base launch simulates speculative-decoding acceptance, that acceptance length is only valid for the draft it was looked up for, so a candidate that changed the draft was benchmarked against a stale acceptance and its gain was fake. #1696 pinned this on a launch path #1784 removed. This PR adds one pure rule, Blocking issuesNone. Both items from my first review are fixed, and so is the regression the second iteration introduced. Verified the three states against
The last row is the boundary Also confirmed the two claims this iteration added to the description and the reference doc: on a simulating base, a dotted Checked: LGTM |
xiaofei-zheng
left a comment
There was a problem hiding this comment.
No blocking issues at 49d243b. The three base states (simulated acceptance / real-draft verification / no speculative decoding) are each verified against the module directly, the reference doc and the refusal message state the same rule the code enforces, and the description tracks the current head. Full verification detail in the comment above.
…te (#1885) Acceptance refuses, with speculative_decoding_pinned, an AgentX candidate that changes the draft or acceptance settings of a parent that simulates acceptance, or that starts simulating, through main's refuse_unpinned_speculative_change. Interface-Change: https://global.primus-safe.amd.com/board/boards/hyperloom-multibranch/issues/48 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Description: what and why
When a session's base launch simulates speculative-decoding acceptance instead of verifying a real draft (
SGLANG_SIMULATE_ACC_LEN, a vLLM--speculative-configwithrejection_sample_method: "synthetic"or asynthetic_acceptance_length, or an explicit acceptance-length flag such as ATOM's--spec-decode-acceptance-length), the acceptance length is only valid for the draft it was set for (InferenceXgolden_al_distributionkeys it by model, method and draft length). A candidate that changed the draft length, the method or the simulated value was benchmarked against that stale acceptance, so its "gain" was fake. Production examples: GLM-5.2 session b81d60f8 kept a variant moving EAGLE steps 5 -> 3 whileSGLANG_SIMULATE_ACC_LEN=3.61stayed (+5.8% KEEP that does not exist); MiniMax-M3 specialists proposednum_speculative_tokens4/5 with a stale or inventedsynthetic_acceptance_length, stopped only by an orchestrator alert.History: [Feat] Deliver config levers to AgentX agentic recipes; stop injecting launcher defaults into them #1696 pinned these knobs on the AgentX agentic-recipe launch path; refactor(agentx): remove the agentic-recipe launch path #1784 removed that path and the pins with it. The generic launcher path (
{framework}_{gpu}.sh+--server-args/--extra-env), which every AgentX session uses now, never had them; the rule only survived as prompt text inreferences/speculative_decoding.md.Fix: one owner,
_spec_decode_pin.refuse_unpinned_speculative_change(pure), called from_grid_runner._build_variant_yaml, the function that composes every benchmarked candidate over its base (explore variants, integrate_patch_bench_patch, conc_sweep arms, specialist rebench, and the explore no-op probe). conc_sweep composes each arm's full args over the session base YAML with an emptybase_extra_args, so its arms are judged against the base YAML. Specialist rebench passes the candidate's server args asbase_extra_args, so only its env changes are judged there; a specialist candidate that is kept still goes through integrate_patch_bench_patch, which judges its server args. It now composes the base launch (inherited args + KEEP stack, i.e. what a no-op variant launches) alongside the candidate and, when the base simulates acceptance, refuses a candidate whose pinned settings differ, judged on the composed result:--speculative-num-steps,--speculative-num-draft-tokens,--speculative-eagle-topk,--speculative-algorithm,--speculative-draft-model-path,--speculative-dspark-block-size,--num-speculative-tokens,--method,--draft-model, any--spec-decode-acceptance*/--speculative-acceptance*flag, the--speculative-configkeysmethod,model,num_speculative_tokens,rejection_sample_method,synthetic_acceptance_length,draft_sample_method, and everySGLANG_SIMULATE_ACC_*env. Adding, changing, removing, unsetting, or a replacing argument string that drops them all count.--speculative-config.<key>flags merge over the JSON object (the dotted key wins), so both spellings land in the same pinned keys; numbers are compared by value, so3.61and3.610(or3and3.0) agree. The candidate is judged after itsruntime_overrideis applied, i.e. on the launch that is benchmarked.--speculative-configkeys that do not describe the draft (e.g.attention_backend), as long as the variant restates the pinned keys (a--speculative-configreplaces the whole object) or sets the key alone with the dotted form.SGLANG_SIMULATE_ACC_TOKEN_MODE=real-draft-tokenwithoutSGLANG_SIMULATE_ACC_LEN, Kimirejection_sample_method: block) leaves the draft open, but its acceptance is measured, so a candidate that switches to simulated acceptance (addsSGLANG_SIMULATE_ACC_LENor a synthetic--speculative-config) is refused: a declared acceptance is not comparable with a measured one. The message says the base measures a real draft and to keep real verification.The reference doc (
speculative_decoding.md) states all three cases, and its golden-AL instructions now say the AL is looked up for the launch it is set on: with a simulating base the draft is pinned, so there is nothing to re-look-up. Checking the AL a candidate sets against the golden table itself (which would let a simulating base change its draft together with a looked-up AL) needs an owner forgolden_al_distribution/, which no code reads today; that is left to a follow-up, and until then the pin is equality with the base.run_gridrecords the refusal asstatus: failed,error_class: speculative_decoding_pinnedwith a message naming each changed setting (old -> new), never launches the server for it, and attaches no launch evidence. The reference doc tells the agent the refusal exists.Enablement: the pin is relative to the launch the candidate is composed on. An enablement round's candidates are composed on the operator's requested launch, so if that launch simulates acceptance, an enablement candidate that changes the draft is refused as well; otherwise nothing is pinned. This is deliberate: the enabled config becomes the session baseline, and letting enablement change the draft under a stale simulated acceptance would make the baseline itself fake.
Linked issue(s): none filed; follows up [Feat] Deliver config levers to AgentX agentic recipes; stop injecting launcher defaults into them #1696 / refactor(agentx): remove the agentic-recipe launch path #1784.
Tests: added
src/hyperloom/orchestrator/actions/executors/tests/test_spec_decode_pin.py(35 tests): the pure rule (synthetic vLLM/DSpark/SGLang/ATOM bases refuse a change to the draft length, acceptance, method, a config that drops the draft or removes it, an acceptance env changed/unset/added, and a dotted--speculative-config.<key>over the JSON; other knobs, a non-pinned--speculative-configkey, dotted keys restating the JSON,3.61vs3.610, real-draft and block-verify bases are allowed; a real-draft base refuses a candidate that switches to simulated acceptance (block-verify -> synthetic, real-draft-token ->SGLANG_SIMULATE_ACC_LEN); a base with no speculative decoding allows enabling it with synthetic or simulated acceptance), the choke point (_build_variant_yamlrefuses inherited-config changes, replace-mode drops,remove_args, a replacing stack's draft, an env unset; leaves a real-draft base open) and the wiring (run_gridrecordsspeculative_decoding_pinnedwithout reaching Magpie). The wiring tests failed on the unmodified_grid_runner.py. Local (Windows):test_spec_decode_pin.py+inference_optimizer/tests/test_grid_moe_runner_backend_guard.py-> 40 passed;ruff check/ruff format --check(0.16.2) clean on touched files. CI green.Size/complexity triggers crossed:
_build_variant_yamlgets shorter (138 -> 137 lines, baseline tightened) with unchanged cyclomatic (19) and cognitive (37) complexity;run_gridunchanged. Code-metrics note:_grid_runner.py(already above the module-lines limit, 2247 on main) grows by one net line to 2248: the import of the new owner module and the new error class in_attach_grid_launch_evidence's never-launched set, offset by the shorter_build_variant_yaml. The rule itself lives in a new module so the backlog module only gains wiring; thebaseline-raiselabel is applied for that one line.If this simplifies or refactors: not a refactor;
effective_removeis folded into the base layer that both compositions share, with the same removal semantics (a replacing base still applies only the variant's removals).Observable effect: in a session whose base launch simulates acceptance, an explore variant, integrate_patch bench or conc_sweep arm (and a specialist rebench's env) that changes the draft or the simulated acceptance now fails before boot with
error_class: speculative_decoding_pinnedand a message naming the changed settings, instead of being benchmarked and possibly KEPT on a fake gain. Sessions whose base verifies a real draft can still tune the draft, but a candidate that turns simulated acceptance on is refused with the same error class; sessions with no speculative decoding can enable it either way. A resumed session whose KEEP stack already changed the draft keeps that stack as its base (the pin compares candidates to the stack they run on).Breaking changes: no
PR addresses single concern: yes
Root cause is upstream (Magpie/TraceLens/GEAK/IntelliKit/AgentKernelArena), ticket filed: no, the rule belongs to Hyperloom's candidate composition.
🤖 Generated with Claude Code