diff --git a/.gitignore b/.gitignore index e90ea75..eb35c02 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,8 @@ __pycache__/ .pytest_cache/ .coverage .venv/ + +# Browser verification drops screenshots wherever it is run from. +# Repo images live in images/ and stay tracked. +*.png +!images/*.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bc9f77..b6edad8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,23 +11,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this - **LogScale query recipe** — Complete `NGSIEM.start_search()` / `get_search_status()` pattern for querying LogScale from Foundry functions. Documents the `search-all` repository requirement (specific repo names cause 403), the `search=` keyword requirement (FalconPy documents `body=` but its guard never honors it — see [falconpy#1491](https://github.com/CrowdStrike/falconpy/issues/1491)), the `resources` vs `body` response-key asymmetry between `start_search` and `get_search_status`, and clarifies that `NGSIEM` is the query class while `FoundryLogScale` is ingestion-only. Adds `humio-auth-proxy:read` to the scope reference table, verified against a live CID. - **Function I/O schema requirements** — Functions called from workflows must be created with `--input-schema` and `--output-schema`. Schemas bind only at creation time; the CLI writes `null` for both without these flags, even when `--wf-expose` is set. Functions without a response schema produce no visible output in Fusion actions. - **Workflow deletion warning** — Documents that deleting a workflow and recreating it with the same name causes `409 name must be unique for an app` followed by `400 dependent artifact failed`, blocking all further deploys. Recovery requires a fresh app. -- **Cross-plugin redirect to fusion-skills** — The development-workflow orchestrator now recognizes standalone Falcon Fusion workflow requests (trigger + actions, no UI/function/collection/API integration) and advises the `crowdstrike-falcon-fusion` plugin instead of scaffolding a Foundry app. Adds `detect_fusion_redirect.py` classifier with unit tests. +- **Cross-plugin redirect to the Falcon Fusion plugin** — The development-workflow orchestrator now recognizes standalone Falcon Fusion workflow requests (trigger + actions, no UI/function/collection/API integration) and advises the `crowdstrike-falcon-fusion` plugin instead of scaffolding a Foundry app. Adds `detect_fusion_redirect.py` classifier with unit tests. - **GraphQL APIs use case** — Integrate GraphQL APIs (Falcon Identity Protection, GitHub, Snyk) into Foundry apps using FalconPy or HTTP POST. Covers zero-arg auth for Falcon GraphQL endpoints and the security tradeoff of env vars vs API integrations for third-party APIs. - **`scripts/action_search.py`** — API-based action discovery script that works in headless/CI environments where the CLI's interactive `actions view` prompt fails. Uses FalconPy with FQL fuzzy matching and prints action IDs with `version_constraint` values. - **CLI guard for `actions view` / `triggers view`** — Hook now catches missing `--no-prompt` on these commands to prevent TTY hangs. - **`foundry apps list` in prerequisite check** — New CLI 2.0.2 command that lists all deployed apps on the CID from any directory. Added to Step 3 to help avoid name collisions. - **Collection description validation constraints** — Documents the 3–500 character length limit, alphanumeric-start requirement, and allowed character set for collection descriptions. - **Function logs in testing-patterns reference** — Added function logs (viewing in UI and Advanced Event Search) to the reference table entry for testing patterns. +- **Query parameter type matching for API integrations** — Documents that `apiIntegration().execute()` types params as `Record`, so a quoted number like `limit: '25'` passes type-checking and fails server-side with `got string want integer`. The extension still renders, so the failure reads as an API or credential error rather than a code bug. - **Content regression tests** — `tests/test_skill_content.py` guards critical documentation (LogScale recipe, schema requirements, workflow deletion warning) against accidental removal. ### Changed +- **Fusion redirect names the plugin, not the repo** — The cross-plugin advisory pointed users at the `fusion-skills` GitHub repo. It now names the plugin (`crowdstrike-falcon-fusion`) with the `/plugin install` command and the marketplace link, since most users install from the marketplace and a repo detour is confusing to anyone unfamiliar with GitHub. `detect_fusion_redirect.py` reports `target` as `crowdstrike-falcon-fusion` / `crowdstrike-falcon-foundry` rather than the repo names. - **Gemini CLI → Antigravity CLI** — Google transitioned Gemini CLI to Antigravity CLI (binary: `agy`). Updated README with new command, skills paths (`~/.gemini/antigravity-cli/skills/` for user scope, `.agents/skills/` for workspace scope). Removed `GEMINI.md` since we never shipped Gemini CLI support; Antigravity reads `AGENTS.md` directly. - **Codex docs link** — Updated from `developers.openai.com/codex/skills` to `learn.chatgpt.com/docs/build-skills`. - **Renamed Python scripts to snake_case** — `scripts/adapt-spec-for-foundry.py` → `adapt_spec_for_foundry.py` and `scripts/test-adapt-spec.py` → `test_adapt_spec.py`, matching the repo's `snake_case` lint convention and allowing the test to import the module directly. The PreToolUse hook and all skill docs reference the new names; no behavior changed. If you invoked the old path directly in your own tooling, update it to the underscore name. ### Fixed +- **Fusion redirect was never wired to a hook** — `detect_fusion_redirect.py` shipped as a standalone script that nothing invoked, so its verdict never reached the agent at runtime. The `fusion-redirect` eval passed only 1 of 5 trials: in three runs the agent declined to scaffold an app but never mentioned the Fusion plugin, and in one it scaffolded an app anyway. The skill router now runs the classifier on Foundry-matched prompts and injects an explicit redirect advisory when it fires. The advisory in `development-workflow` also states that naming the plugin is *required output* — declining to scaffold is only half a redirect — and that hand-writing the workflow YAML defeats the purpose, since the Fusion plugin discovers real action IDs, validates against the platform schema, and imports to the CID. +- **Fusion redirect classifier mishandled negation** — `detect_fusion_redirect.py` matched app-capability keywords without regard to negation, so a prompt saying "no Foundry app, no UI, no functions" registered `UI` and `Foundry app` as *requests* for those capabilities and suppressed the redirect. Standalone Fusion workflow requests that explicitly ruled out app capabilities — the clearest possible case for redirecting — were the ones most likely to be kept in this plugin. Negated spans are now stripped before app signals are matched, and the verdict reports `negated_app_signals` so the reasoning stays visible. Caught by the `fusion-redirect` eval, which failed 0/5 trials before this fix. - **Removed "delete and re-create" advice** — The old guidance for fixing missing `workflow_integration` said to delete and recreate the function. This is technically correct (schemas only bind at creation), but was misleading about workflows: you must never delete and recreate a *workflow* to refresh a binding. Both skills now give consistent guidance — recreate the function, update the workflow YAML reference in place. - **Action discovery guidance** — Updated all `actions view` examples to include `--no-prompt` and pointed to `action_search.py` as the primary fallback. The CLI ignores `--no-prompt` for these commands (tracked upstream), so the script is the reliable path. - **Alert and detection query routing (population vs. enrich)** — The orchestrator and workflows skills now distinguish two cases. Fetching a *population* the workflow doesn't already have ("summarize all high-severity alerts") goes to a source-of-truth API — a native platform action (e.g. Cases → Search Cases) first, or a FalconPy `Alerts`/`Detects` function when none fits — since an Event Query against NG-SIEM can silently return nothing (repo contents are connector-dependent). *Enriching* a detection the workflow already holds (query by its ID) stays an Event Query, as does historical/aggregate telemetry. New reference [event-query-vs-api.md](skills/workflows-development/references/event-query-vs-api.md); the functions-falcon-api example keeps the verified `severity_name` + `created_timestamp` FQL filter. diff --git a/hooks/foundry-skill-router.sh b/hooks/foundry-skill-router.sh index f4fdc26..e0ff547 100755 --- a/hooks/foundry-skill-router.sh +++ b/hooks/foundry-skill-router.sh @@ -57,6 +57,33 @@ case "$HOOK_EVENT" in # Write marker so PreToolUse hook knows to inject advisory context echo "$$" > "$MARKER" + # Standalone Fusion workflow? Advise the sibling plugin instead of + # steering into app scaffolding. Without this the classifier is never + # consulted at runtime and the agent tends to quietly author the workflow + # itself, never telling the user a better-suited plugin exists. + REDIRECT_SCRIPT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/scripts/detect_fusion_redirect.py" + if [ -f "$REDIRECT_SCRIPT" ]; then + VERDICT=$(printf '%s' "$USER_PROMPT" | python3 "$REDIRECT_SCRIPT" 2>/dev/null || true) + if echo "$VERDICT" | grep -q '"redirect": true'; then + # If the sibling plugin is already installed, say "use it" rather than + # "install it". Best-effort: the file may be absent, in which case we + # fall back to the install wording. Mirrors the check in + # fusion-skills' fusion-foundry-bridge.sh. + FUSION_HINT="Advise the crowdstrike-falcon-fusion plugin — /plugin install crowdstrike-falcon-fusion, or https://claude.com/plugins/crowdstrike-falcon-fusion." + if [ -f "$HOME/.claude/plugins/installed_plugins.json" ] && + grep -q "crowdstrike-falcon-fusion" "$HOME/.claude/plugins/installed_plugins.json" 2>/dev/null; then + FUSION_HINT="The crowdstrike-falcon-fusion plugin is already installed — hand off to its workflows skill." + fi + jq -n --arg hint "$FUSION_HINT" '{ + hookSpecificOutput: { + hookEventName: "UserPromptSubmit", + additionalContext: ("STANDALONE FUSION WORKFLOW DETECTED: this request looks like a trigger plus actions that already exist in the CID — no UI, function, collection, or API integration to build. It does NOT need a Foundry app. " + $hint + " Do NOT scaffold a Foundry app. Naming the plugin is required output — declining to scaffold is only half the redirect, and hand-writing the workflow YAML yourself defeats the purpose since that plugin discovers real action IDs, validates against the platform schema, and imports to the CID. This detection is advisory: if the request genuinely needs an app capability built, proceed with crowdstrike-falcon-foundry:development-workflow instead.") + } + }' + exit 0 + fi + fi + jq -n '{ hookSpecificOutput: { hookEventName: "UserPromptSubmit", diff --git a/run-ab-test.sh b/run-ab-test.sh index b21398d..7d925da 100755 --- a/run-ab-test.sh +++ b/run-ab-test.sh @@ -63,6 +63,25 @@ RED_DIR="$AB_RESULTS_DIR/red-runs" GREEN_DIR="$AB_RESULTS_DIR/green-runs" BASELINE_JSON="$AB_RESULTS_DIR/baseline.json" +# ── Validate the baseline ref up front ─────────────────────── +# Without this an unknown ref fails silently: 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 runs on with an empty baseline and exits 0 having printed nothing, +# which reads as "the script did nothing" rather than "that ref is not here". +if [ "$NO_SKILL" != "1" ]; then + if ! git -C "$REPO_ROOT" rev-parse --verify --quiet "${BASELINE_REF}^{commit}" >/dev/null 2>&1; then + echo "ERROR: baseline ref '$BASELINE_REF' does not exist in $REPO_ROOT" >&2 + echo "" >&2 + echo " Release tags available here:" >&2 + git -C "$REPO_ROOT" tag --list 'v*' --sort=-v:refname 2>/dev/null | head -5 | sed 's/^/ /' >&2 + echo "" >&2 + echo " Pass one of the above with --ref, or a branch or commit SHA." >&2 + exit 1 + fi +fi + + # Delete Foundry apps from a phase directory cleanup_phase_apps() { local phase_dir="$1" diff --git a/scripts/detect_fusion_redirect.py b/scripts/detect_fusion_redirect.py index a28fb42..02d61b5 100644 --- a/scripts/detect_fusion_redirect.py +++ b/scripts/detect_fusion_redirect.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 """Classify whether a request is a standalone Fusion workflow (redirect to -fusion-skills) or a Falcon Foundry app (handle here in foundry-skills). +the crowdstrike-falcon-fusion plugin) or a Falcon Foundry app (handle here). A standalone Fusion workflow needs only a trigger plus actions that already exist in the CID — no UI, no serverless function, no collection, no custom API integration to be built, no manifest.yml. Those app-only capabilities are what keep a request in foundry-skills. When none are present, the request belongs to -the sibling fusion-skills (crowdstrike-falcon-fusion) plugin. +the sibling crowdstrike-falcon-fusion plugin. This is a heuristic used to *advise* a redirect; it never blocks. The routing decision ultimately rests with the orchestrator skill (development-workflow), @@ -68,19 +68,54 @@ def _matches(patterns, text): return hits +# Negation cues that *withdraw* a capability rather than request it. A prompt +# saying "no UI, no functions" is describing what it does NOT need, so those +# words must not count as app signals — the naive read inverts the verdict and +# keeps a standalone-workflow request in foundry-skills. +_NEGATION_CUE = ( + r"(?:no|not|without|excluding|skip|omit|" + r"(?:do(?:n't|es not|esn't| not)? (?:need|want|require))|" + r"(?:no need for)|(?:isn't|is not|aren't|are not))" +) + +# A negation scopes over the capabilities that follow it, including across a +# comma/"or"/"and" series: "no UI, no functions" and "without a UI or function". +# Stop at sentence-ending punctuation so a later clause isn't swallowed. +_NEGATED_SPAN = re.compile( + rf"\b{_NEGATION_CUE}\b[^.;!?\n]*", + re.IGNORECASE, +) + + +def _strip_negated_spans(text): + """Blank out spans where capabilities are explicitly ruled out. + + Replaces with spaces rather than deleting so surrounding word boundaries + survive and offsets stay comparable. + """ + return _NEGATED_SPAN.sub(lambda m: " " * len(m.group(0)), text or "") + + def classify(request): """Classify a natural-language request. Returns a dict: redirect - True if it looks like a standalone Fusion workflow - target - "fusion-skills" when redirecting, else "foundry-skills" + target - "crowdstrike-falcon-fusion" when redirecting, else + "crowdstrike-falcon-foundry" is_workflow - whether the request mentions a workflow/automation app_signals - app-capability patterns that matched (block redirect) reason - one-line human-readable explanation """ text = request or "" workflow_hits = _matches(WORKFLOW_PATTERNS, text) - app_hits = _matches(APP_CAPABILITY_PATTERNS, text) + # Match app signals only against text with negated spans removed, so + # "no UI, no functions" reads as an absence of those capabilities. + affirmative = _strip_negated_spans(text) + app_hits = _matches(APP_CAPABILITY_PATTERNS, affirmative) + negated_app_hits = [ + p for p in _matches(APP_CAPABILITY_PATTERNS, text) if p not in app_hits + ] is_workflow = bool(workflow_hits) # Redirect only when it's clearly a workflow AND carries no app-only signal. @@ -96,14 +131,15 @@ def classify(request): else: reason = ( "Standalone Fusion workflow (trigger + existing actions, no app " - "capability) — advise fusion-skills (crowdstrike-falcon-fusion)." + "capability) — advise the crowdstrike-falcon-fusion plugin." ) return { "redirect": redirect, - "target": "fusion-skills" if redirect else "foundry-skills", + "target": "crowdstrike-falcon-fusion" if redirect else "crowdstrike-falcon-foundry", "is_workflow": is_workflow, "app_signals": app_hits, + "negated_app_signals": negated_app_hits, "reason": reason, } diff --git a/skills/development-workflow/SKILL.md b/skills/development-workflow/SKILL.md index fd1e449..4a3dc08 100644 --- a/skills/development-workflow/SKILL.md +++ b/skills/development-workflow/SKILL.md @@ -62,23 +62,49 @@ Security review → security-patterns E2E testing / Playwright → e2e-testing Standalone Fusion workflow (no app — trigger + existing actions only) -└── Advise fusion-skills — see Cross-Plugin Advisory +└── Advise the Falcon Fusion plugin — see Cross-Plugin Advisory ``` ## Cross-Plugin Advisory (Fusion vs. Foundry) A Falcon Fusion workflow can be authored **standalone** (no app wrapper) when it only needs a trigger plus actions that already exist in the CID. That is the -sibling **fusion-skills** (`crowdstrike-falcon-fusion`) plugin's job, not this one. +sibling **Falcon Fusion** plugin's job (`crowdstrike-falcon-fusion`), not this one. | Situation | Action | |-----------|--------| -| Just a workflow: trigger + existing actions, no UI/function/collection/manifest | **Advise fusion-skills** (`claude plugin install crowdstrike-falcon-fusion`). Do NOT scaffold a Foundry app. | +| Just a workflow: trigger + existing actions, no UI/function/collection/manifest | **Advise the Falcon Fusion plugin** — see the required response below. Do NOT scaffold a Foundry app. | | Workflow needs a UI, function, collection, or custom API integration to be BUILT | **Proceed here** — that's a Foundry app; use the App Creation Flow. | | A workflow *inside* an app you're already building | **Proceed here** — use `workflows-development`. | -If every action already exists and there's no UI/function/collection, redirect. -Detection is advisory, never blocking. +> **⚠️ MUST NOT silently do the Fusion plugin's job.** The common failure is to +> recognize no app is needed, then hand the user workflow YAML anyway without +> ever telling them a better-suited plugin exists. Declining to scaffold is only +> half the redirect — **naming the plugin is required output.** + +When redirecting, your response MUST contain all three: + +1. A statement that this needs no Foundry app +2. The plugin name **`crowdstrike-falcon-fusion`** written out +3. How to get it: `/plugin install crowdstrike-falcon-fusion` or + https://claude.com/plugins/crowdstrike-falcon-fusion + +Name the plugin, not a GitHub repo — most users install from the marketplace and +a repo link is a detour. + +**Do not hand-write the workflow YAML in a redirect.** Producing the artifact +yourself defeats the purpose: the Fusion plugin discovers real action IDs from +the live API, validates against the platform schema, and imports and releases to +the CID. A YAML block with placeholder action IDs is strictly worse than sending +the user somewhere that can finish the job. Offer a one-line sketch of the shape +if it helps, then redirect. + +A negated capability is not a request for it. "no UI", "without a function", +"I don't need a collection" all mean the request is *smaller*, not larger — so +they push **toward** redirecting, never away. If every action already exists and +there's no UI/function/collection to build, redirect. The routing decision is +yours; `scripts/detect_fusion_redirect.py` is available as a heuristic +cross-check and never blocks. ## App Creation Flow diff --git a/skills/ui-development/SKILL.md b/skills/ui-development/SKILL.md index 099aeea..0f82ab9 100644 --- a/skills/ui-development/SKILL.md +++ b/skills/ui-development/SKILL.md @@ -173,6 +173,16 @@ const body = result.resources?.[0]?.response_body; const status = result.resources?.[0]?.status_code; ``` +Query and path parameters must match the types declared in the OpenAPI spec. `execute()` types params as `Record`, so a quoted number compiles and ships fine, then fails server-side schema validation at runtime with `got string want integer`. The extension still renders, so this looks like an API or credential error rather than a bug in your code. + +```javascript +// spec declares: - name: limit / schema: { type: integer } +request: { params: { query: { limit: 25 } } } // ✅ number +request: { params: { query: { limit: '25' } } } // ❌ got string want integer +``` + +Check the `schema.type` of each parameter in the spec before passing it. Numbers and booleans are unquoted; only `type: string` parameters take quotes. + ### Collection Operations ```javascript @@ -330,6 +340,7 @@ Run `foundry ui extensions list-sockets` to get the current list of available so - **Expecting backend to work with `foundry ui run`.** The dev server only serves UI — deploy backend capabilities first. - **Shoelace dialogs/drawers white in dark mode.** Override `--sl-panel-background-color` and `--sl-color-neutral-0` with `var(--ground-floor)`. See [references/shoelace-reference.md](references/shoelace-reference.md). - **Using Tailwind arbitrary values with prebuilt toucan CSS.** Values like `max-h-[400px]` require JIT compilation. Use inline styles instead when using the prebuilt `tailwind-toucan-base/index.css`. +- **Quoting numeric query parameters.** `execute()` accepts `Record`, so `limit: '25'` passes type-checking and fails server-side with `got string want integer`. Match the `schema.type` declared in the OpenAPI spec — the extension still renders, so this reads as an API error rather than a code bug. - **Missing CSP for Shoelace icons.** The Foundry CSP only allows `assets.foundry.crowdstrike.com`. If using `setBasePath()` with `cdn.jsdelivr.net`, you must add it to `connect-src` and `img-src` in the manifest's `content_security_policy`. Alternatively, copy icon assets to your `dist/` folder and set a relative base path to avoid CDN dependencies entirely. ## Reading Guide diff --git a/test-hooks.sh b/test-hooks.sh index 9ca81a9..6a7662d 100755 --- a/test-hooks.sh +++ b/test-hooks.sh @@ -1385,6 +1385,46 @@ OUTPUT=$(cat "$ENV_TMP") assert_empty "$OUTPUT" "9.10 does NOT set FOUNDRY_UI_HEADLESS_MODE for 2.0.1+" rm -rf "$FAKE_BIN" "$ENV_TMP" +# ---------- 10. Fusion redirect wiring (UserPromptSubmit) ---------- +# +# The classifier existed but no hook invoked it, so its verdict never reached +# the agent. The fusion-redirect eval passed only 1/5 trials: the agent declined +# to scaffold but never named the plugin, and once scaffolded an app anyway. + +printf "\n${BOLD}10. Fusion redirect wiring${RESET}\n" + +OUTPUT=$(echo '{"hook_event_name":"UserPromptSubmit","prompt":"Create a Falcon Fusion workflow — no Foundry app, no UI, no functions. When a critical detection fires, contain the host and post to Slack. Use actions that already exist in my CID."}' | CLAUDE_PLUGIN_ROOT="$(pwd)" "$HOOK" 2>&1) +assert_contains "$OUTPUT" "STANDALONE FUSION WORKFLOW DETECTED" "10.1 standalone fusion prompt → redirect advisory" +assert_contains "$OUTPUT" "crowdstrike-falcon-fusion" "10.2 advisory names the plugin" +assert_contains "$OUTPUT" "Naming the plugin is required output" "10.3 advisory makes naming mandatory" +assert_not_contains "$OUTPUT" "IMMEDIATELY invoke" "10.4 does not also steer into app scaffolding" + +# A genuine app request must NOT be redirected — guards against over-reach. +OUTPUT=$(echo '{"hook_event_name":"UserPromptSubmit","prompt":"Create a Foundry app with a UI extension and a workflow that shows detections."}' | CLAUDE_PLUGIN_ROOT="$(pwd)" "$HOOK" 2>&1) +assert_not_contains "$OUTPUT" "STANDALONE FUSION" "10.5 app request → no fusion redirect" +assert_contains "$OUTPUT" "FOUNDRY PLUGIN DETECTED" "10.6 app request → normal foundry advisory" + +# Negated capabilities must not block the redirect (the original bug). Note the +# prompt must still name a Foundry noun for the router to engage at all — a bare +# "Fusion workflow" request never reaches this plugin's router, which is correct: +# the user is already asking for Fusion, not Foundry. +OUTPUT=$(echo '{"hook_event_name":"UserPromptSubmit","prompt":"Build a foundry workflow to automate host containment on detection. I do not need a UI or a function."}' | CLAUDE_PLUGIN_ROOT="$(pwd)" "$HOOK" 2>&1) +assert_contains "$OUTPUT" "STANDALONE FUSION" "10.7 negated capabilities still redirect" + +# Wording adapts to whether the sibling plugin is already installed, so we don't +# tell users to install what they have. Assert on the stable half of the message. +OUTPUT=$(echo '{"hook_event_name":"UserPromptSubmit","prompt":"Create a foundry workflow — no app, no UI, no functions. Contain the host on critical detection using existing actions."}' | CLAUDE_PLUGIN_ROOT="$(pwd)" "$HOOK" 2>&1) +assert_contains "$OUTPUT" "crowdstrike-falcon-fusion" "10.9 advisory names the plugin either way" +assert_contains "$OUTPUT" "Do NOT scaffold a Foundry app" "10.10 advisory forbids scaffolding either way" + +# A bare Fusion request (no Foundry noun) is not this router's business — the +# crowdstrike-falcon-fusion plugin's own router matches "build a Fusion workflow" +# and routes to its workflows orchestrator. The two routers partition the space; +# this redirect exists for the ambiguous middle, where someone says "Foundry" but +# describes work that needs no app. +OUTPUT=$(echo '{"hook_event_name":"UserPromptSubmit","prompt":"Build a Fusion workflow to contain a host on detection."}' | CLAUDE_PLUGIN_ROOT="$(pwd)" "$HOOK" 2>&1) +assert_empty "$OUTPUT" "10.8 bare Fusion request → router abstains" + # ---------- Cleanup and Summary ---------- cleanup diff --git a/test-scorecard-parser.sh b/test-scorecard-parser.sh index e9c5b07..ea8e873 100755 --- a/test-scorecard-parser.sh +++ b/test-scorecard-parser.sh @@ -43,6 +43,22 @@ assert_eq() { fi } +# ── count_over_apps: matches the advice loops in verify-apps.sh ── +# The C-style form must not iterate when APP_COUNT is 0. The earlier +# `seq 0 $((APP_COUNT - 1))` form emitted "0" and "-1", so a zero-app +# report counted two phantom failures against a total of 0 ("2/0 apps +# failed to release") and printed "App null: null" scorecard rows. +count_over_apps() { + local app_count="$1" json="$2" field="$3" expected="$4" + local n=0 + for ((idx=0; idx&2 + printf " %d run directory(ies) were found, but none contained a manifest.yml.\n\n" "${#RUN_DIRS[@]}" >&2 + printf " Verifying A/B test runs? Use the matching flag:\n" >&2 + printf " %s --green # /tmp/foundry-skill-ab/green-runs\n" "$0" >&2 + printf " %s --dir # any other directory\n\n" "$0" >&2 + exit 1 +fi + # ── Phase 1 summary table ── printf "${BLUE}==========================================${RESET}\n" printf "${BLUE} Phase 1 Summary${RESET}\n" @@ -290,7 +302,7 @@ print_phase1_advice() { # Analyze spec issues across all apps local combobox_count=0 local textbox_count=0 - for idx in $(seq 0 $((APP_COUNT - 1))); do + for ((idx=0; idx