Fix fusion redirect classifier mishandling negation; name the plugin not the repo - #76
Merged
Conversation
…not the repo detect_fusion_redirect.py matched app-capability keywords with no regard for negation. A prompt saying "no Foundry app, no UI, no functions" registered UI and Foundry app as requests for those capabilities, so redirect came back false. The requests that most clearly belong to the Fusion plugin — the ones that explicitly rule out app capabilities — were the ones most likely to be kept here. Negated spans are now blanked out before app signals are matched, using cues like no/not/without/don't need across a comma or "or" series, stopping at sentence-ending punctuation so a later clause that genuinely asks for an app is unaffected. The verdict reports negated_app_signals so the reasoning stays inspectable rather than silently dropping matches. Caught by the fusion-redirect eval, which failed 0/5 trials. The agent behaved well in those runs — no app scaffolded, correct standalone workflow YAML, correct action IDs — but never named the Fusion plugin, because the classifier had told it this was app-shaped work. Also switched the advisory from the fusion-skills repo to the plugin itself: /plugin install crowdstrike-falcon-fusion plus the marketplace link. A GitHub repo is a detour for anyone who installs from the marketplace, and the repo name is not the product name. classify() now returns crowdstrike-falcon-fusion / crowdstrike-falcon-foundry as target. Five negation and naming tests added; verified all five fail against the pre-fix classifier. Two guard tests confirm the fix does not over-reach: an affirmative UI request, and an app request following a negated clause, both still stay here. pytest 89 passed, test-hooks.sh 182/182.
…equired The negation fix was necessary but not sufficient. The fusion-redirect eval still passed only 1 of 5 trials against it: three runs declined to scaffold an app but never mentioned the Fusion plugin, and one scaffolded an app anyway. Root cause: detect_fusion_redirect.py was never invoked by any hook. It shipped as a standalone script, so its verdict never reached the agent at runtime — flipping its logic changed nothing about behavior. The skill router now runs it on Foundry-matched prompts and injects a redirect advisory when it fires, replacing the generic "build a Foundry app" steer. The skill guidance was also too passive. A table row saying "advise the plugin" let the agent satisfy itself by not scaffolding while quietly authoring the workflow anyway — which is the worst outcome, since it hands over YAML with placeholder action IDs instead of pointing at a plugin that can discover real ones, validate against the schema, and import to the CID. The advisory now states that naming the plugin is required output, lists the three things a redirect response must contain, and says not to hand-write the YAML. Seven router tests added covering the redirect, the negated-capability case, and two guards against over-reach: a genuine app request still gets the normal advisory, and a bare Fusion request the router has no business in produces no output. Verified 5 of them fail against the unwired router. test-hooks.sh 190/190, pytest 89 passed, shellcheck clean. dev-workflow skill at 96% of its token budget.
Telling someone to install a plugin they already have is noise, so the router now checks installed_plugins.json and says "hand off to its workflows skill" instead. When it is absent, the install command and marketplace link stand — promoting the sibling plugin is the useful thing to do, since it is the tool that can actually finish a standalone Fusion workflow. Mirrors the check fusion-skills already does in its own bridge hook. Also corrected a misleading test comment. Test 10.8 asserts the router abstains on a bare "build a Fusion workflow" prompt, and I had described that as a coverage gap. It is not: the fusion plugin's own router matches that phrasing and routes to its workflows orchestrator. The two routers partition the space, and this redirect covers the ambiguous middle where someone says Foundry but describes work needing no app. test-hooks.sh 192/192, pytest 89 passed, shellcheck clean.
The Added entry still read "Cross-plugin redirect to fusion-skills" even though the body and the advisory itself now name the plugin. Kept the one remaining fusion-skills mention in the Changed entry, since that one describes what changed.
An unknown ref produced no output and exit 0, so it looked like the script had done nothing at all rather than reporting a bad argument. Three things combined to swallow it: rev-parse is muted by 2>/dev/null, git archive then writes nothing, and its failure is masked because it is piped into tar — pipefail does not help when the last command in the pipe succeeds. The script carried on with an empty baseline directory and exited cleanly. Now the ref is verified before any work happens, and the error names the bad ref, lists the release tags that do exist, and says what forms --ref accepts. Skipped for --no-skill, which has no baseline ref to check. Verified: a bad ref exits 1 with the message, a valid tag still extracts and proceeds, and a branch name still validates. test-hooks.sh 192/192, pytest 89 passed.
`verify-apps.sh` reported success against nothing. Pointed at a directory whose `run-*/` dirs hold no `manifest.yml`, discovery found zero apps and the script still wrote `phase1_completed: true` / `phase2_completed: true` and printed `Overall: 0/0 fully verified` — which reads as a pass. It then went on to log into the console and drive a browser phase with nothing in scope. Discovery now exits 1 when no apps are found, naming the directory it searched and pointing at `--green` / `--dir`, since the usual cause is verifying an A/B run with the default base directory. The advice loops iterated `seq 0 $((APP_COUNT - 1))`. With `APP_COUNT=0` that expands to `seq 0 -1`, which emits `0` and `-1` rather than nothing, so each loop body ran twice against nonexistent indices. `jq` returned `null` for both, and `null != "true"`, producing `2/0 apps failed to release` and `App null: null` scorecard rows — advice fabricated from absent data, recommending a release step that was never actually measured. All six loops now use the C-style form, which doesn't execute when the count is zero. Five tests cover the zero-app counting; the two that pin the phantom-count behavior were confirmed to fail against the `seq` form, reproducing the same `2`.
A/B run 4 of 5 generated `limit: '25'` in an extension's API call while the other four generated `limit: 25`. All five OpenAPI specs declare `limit` as `type: integer`, so the quoted value failed server-side schema validation with `got string want integer` and that extension never fetched data. `apiIntegration().execute()` types params as `Record<string, unknown>` (`foundry-js` `src/abstraction/api-integration.ts`), so a quoted number type-checks and builds cleanly — the mismatch only appears at runtime. The extension still renders, which makes it look like an API or credential error rather than a bug in the generated code. Verification scored that app's UI as passing. Adds the constraint to the API integration section with a correct/incorrect pair, and a Common Pitfalls entry for anyone scanning for gotchas. Also ignores stray `*.png`, since browser verification drops screenshots in whatever directory it runs from. Repo images live in `images/` and stay tracked via a negation.
mraible
force-pushed
the
fix-fusion-redirect-negation
branch
from
July 31, 2026 00:42
a7ee7b3 to
d64e481
Compare
mraible
enabled auto-merge (squash)
July 31, 2026 00:44
prvn
approved these changes
Jul 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
detect_fusion_redirect.pydecides whether a request belongs to the Falcon Fusion plugin instead of this one. It got that decision backwards on exactly the requests where it mattered most, and it turned out the script was never running in the first place.The classifier ignored negation
Capability keywords were matched with no regard for the words around them. A prompt saying "no Foundry app, no UI, no functions" registered
UIandFoundry appas requests for those capabilities, soredirectcame backfalse.The effect was precisely inverted: a request that explicitly rules out app capabilities is the clearest possible signal it belongs to the Fusion plugin, and those were the ones most likely to be kept here.
Negated spans are now blanked out before app signals are matched, using cues like
no/not/without/don't needacross a comma-or-orseries. Matching stops at sentence-ending punctuation, so a later clause that genuinely asks for an app still counts. The verdict reportsnegated_app_signalsso the reasoning stays inspectable rather than silently dropping matches.The script was never wired up
Fixing the logic changed nothing at runtime, because
detect_fusion_redirect.pyshipped as a standalone script that no hook invoked. The eval still passed only 1 of 5 trials against the corrected classifier.The skill router now runs it on Foundry-matched prompts and injects a redirect advisory. The wording adapts to whether the sibling plugin is already installed, since telling someone to install what they already have is noise.
The advisory was too easy to satisfy
A table row saying "advise the plugin" let the agent congratulate itself for not scaffolding an app while quietly authoring the workflow anyway. That's the worst available outcome: it hands over YAML with placeholder action IDs instead of pointing at the plugin that can discover real ones, validate against the schema, and import to the CID. Naming the plugin is now stated as required output, with the three things a redirect response must contain.
Name the plugin, not the repo
The advisory pointed at the
fusion-skillsGitHub repo. It now names the plugin —/plugin install crowdstrike-falcon-fusion, plus https://claude.com/plugins/crowdstrike-falcon-fusion. A repo link is a detour for anyone installing from the marketplace, and the repo name isn't the product name.classify()returnscrowdstrike-falcon-fusion/crowdstrike-falcon-foundryastarget.Test harness reported success against nothing
Also here, since it's what surfaced the query-parameter bug. Pointed at a directory whose
run-*/dirs contain nomanifest.yml,verify-apps.shdiscovered zero apps and still wrotephase1_completed: true/phase2_completed: true, printingOverall: 0/0 fully verified— which reads as a pass. It then drove the browser phase with nothing in scope. Discovery now exits 1, naming the directory it searched and pointing at--green/--dir.The advice loops iterated
seq 0 $((APP_COUNT - 1)). WithAPP_COUNT=0that expands toseq 0 -1, which emits0and-1rather than nothing, so each body ran twice against nonexistent indices.jqreturnednull,null != "true", and the output was2/0 apps failed to releaseplusApp null: nullscorecard rows — advice fabricated from absent data, recommending a release step that had never been measured. All six loops now use the C-style form, which doesn't execute when the count is zero.run-ab-test.shalso now fails loudly when--refnames a nonexistent baseline.These files (
verify-apps.sh,test-scorecard-parser.sh,run-ab-test.sh) don't ship with the plugin, so there's no CHANGELOG entry for them.Query parameter types in generated UI code
Found by the end-to-end verification below. One of five A/B runs generated
limit: '25'in its extension's API call while the other four generatedlimit: 25. All five OpenAPI specs declarelimitastype: integer, so the quoted value failed server-side schema validation withgot string want integerand that extension never fetched data.apiIntegration().execute()types params asRecord<string, unknown>, so a quoted number type-checks and builds cleanly — the mismatch only surfaces at runtime, and because the extension still renders, it reads as an API or credential error rather than a code bug. The UI skill now documents the constraint with a correct/incorrect pair, plus a Common Pitfalls entry.A/B against v1.3.0
5 runs per arm, same model throughout. Deploy rate 5/5 both arms, spec quality unchanged, zero anti-patterns, no reference-file reads. Tokens came in +4.7% (5.64M → 5.91M per run), which is inside the noise at this sample size — Welch t = 0.49, 95% CI spanning -970k to +1.5M. No measurable regression; no claim of improvement either.
End-to-end verification ran all five apps through install → UI render → workflow execution → uninstall against a live CID: 5/5 on every stage. Worth reading precisely — all five extensions rendered but none completed a successful data fetch. Four returned
401from the upstream API (expected with test credentials) and the fifth hit the schema error described above. So what's confirmed is that manifest sockets, CSP, entrypoint paths, and build output are all correct; a successful end-to-end data fetch is not part of this evidence.Tests
Negation, naming, and router tests were each confirmed to fail against the unfixed code before being kept — a passing test proves nothing until you've watched it fail. Guard tests cover over-reach in both directions: an affirmative UI request and an app request following a negated clause both still stay here, and a bare "Fusion workflow" request produces no output, correctly, since the Fusion plugin's own router matches that phrasing. The two routers partition the space; this redirect covers the ambiguous middle where someone says "Foundry" but describes work needing no app.
./test-hooks.sh192/192,pytest89 passed,./test-scorecard-parser.sh25 passed, shellcheck clean.Known limitation
The eval covering this can't validate the hook path. The Agent SDK doesn't load hooks for a local plugin, confirmed by neither advisory appearing anywhere in the eval transcript. So what's verified is that the advisory is correctly emitted; agent compliance with it is not. The eval is marked non-blocking in the evals repo with the same rationale, to be reverted once the harness loads hooks.