Skip to content

feat: behavior-vs-drift guard + anchor-aware heals + signed releases (v0.1.3) - #6

Merged
Desperado merged 1 commit into
mainfrom
feat/moat-guard-anchor-trust
Jul 9, 2026
Merged

feat: behavior-vs-drift guard + anchor-aware heals + signed releases (v0.1.3)#6
Desperado merged 1 commit into
mainfrom
feat/moat-guard-anchor-trust

Conversation

@Desperado

Copy link
Copy Markdown
Contributor

Four moat features — the ones that make 9lives hard to copy and safe to trust.

1. 🛡️ Behavior-vs-drift guard (the trust feature)

A failing assertion means the app behaved differently — not that a selector moved. The old code routed assertion failures with an expected/received message to a Tier 2 rewrite, which would edit the assertion to make the test pass — silently masking a real regression. Now:

  • assertion failures → flagged "possible real bug", handed to a human, spec left untouched;
  • opt back in with NINELIVES_HEAL_ASSERTIONS=1 if you really want an assertion update proposed.

Live proof:

🐾 run 1/3 → failure: assertion_failed (.ok) → tier3_human
🐾 possible real bug — an assertion failed (behavior changed, not a selector).
   9lives won't rewrite assertions to force a pass.

2. ⚓ Anchor-aware Tier 1

Re-identify the element by anchors in stability order (testid > id > aria-label > text > class) and record which anchor won (metadata.anchor, and "Re-found via testid" in the report) — so a heal is explainable and the durable identity is preferred over fragile copy/CSS.

3. 📣 Footer growth CTA

Sharper self-healing pitch in the PR-comment footer. No client telemetry (the README promise stands); install attribution comes from the 9lives.run edge redirect, server-side.

4. 🔏 Signed, provable releases (release.yml)

  • PyPI PEP 740 attestations (attestations: true) — signed build provenance on PyPI, OIDC only.
  • Sigstore-signed wheel + sdist, .sigstore bundles attached to the GitHub release.
  • Best-effort CycloneDX SBOM attached (never blocks a release).

Bump to 0.1.3. 4 new tests (guard default + opt-in, anchor recording, stable-anchor preference); suite 32 green, ruff clean. Ship: merge → tag v0.1.3git tag -f v1 && git push -f origin v1.

…(v0.1.3)

Moat features:

1. Behavior-vs-drift guard — a failing assertion means the app's behavior
   changed, not that a selector moved. 9lives no longer routes assertion
   failures to a Tier 2 rewrite (which would force the test green and hide a
   real bug); it flags them as a possible real bug and hands off to a human.
   Opt back in with NINELIVES_HEAL_ASSERTIONS=1.

2. Anchor-aware Tier 1 — try re-identifying anchors in stability order
   (testid > id > aria-label > text > class) and record WHICH anchor re-found
   the element (metadata.anchor + 'Re-found via <anchor>' in the report).

3. Footer growth CTA — sharper self-healing pitch in the PR-comment footer
   (still no client telemetry; install analytics come from the 9lives.run edge).

4. Trust primitives in release.yml — PyPI PEP 740 attestations, Sigstore-signed
   wheel+sdist attached to the GitHub release, and a best-effort CycloneDX SBOM.

Bump to 0.1.3. 4 new tests (guard default + opt-in, anchor recording, stable-
anchor preference); suite 32 green, ruff clean. Verified end-to-end: an
assertion failure is flagged 'possible real bug' and the spec is left untouched;
selector drift still heals via Tier 1.
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@sigilix

sigilix Bot commented Jul 9, 2026

Copy link
Copy Markdown

Sigilix Overview

Effort: 4/5 (large)

Quality gates

  • ✅ PR title follows convention
  • ✅ PR description is complete
  • ℹ️ PR is linked to an issue — No Closes #N / Closes SIG-N keyword found in PR body or commit messages.

Summary — latest push

Introduces a behavior-vs-drift guard that routes assertion failures to a human by default instead of auto-rewriting them, adds anchor-aware Tier 1 healing that re-identifies elements using a stability-ordered priority (testid > id > aria-label > text > class), and hardens the release pipeline with PEP 740 attestations, Sigstore signing, and CycloneDX SBOM generation. The specialist review flagged two P1 logic defects in the new guard implementation and a testing gap that must be resolved before merge.

Important files

File Score Notes Next step
src/ninelives/healing/strategy.py 5/5 Adds the behavior-vs-drift guard that defaults assertion failures to TIER3_HUMAN, requiring an explicit NINELIVES_HEAL_ASSERTIONS=1 opt-in to route them to Tier 2. Fix the P1 logic bug where the _is_value_change check is still gated behind the opt-in, meaning even with the env var set, non-value-change assertion failures will silently fall through to TIER3_HUMAN instead of being explicitly handled.
src/ninelives/cli.py 5/5 Adds the CLI branch that prints the 'possible real bug' warning when an assertion failure hits the human tier, and imports the new FailureType enum. Fix the P1 logic bug where the new failure.failure_type == FailureType.ASSERTION_FAILED check is nested inside the else block for HealingTier.TIER2_AI_SUGGEST, meaning it only executes when the strategy did NOT select Tier 2, creating a mismatch between the strategy decision and the CLI's messaging.
src/ninelives/healing/tier1.py 4/5 Refactors Tier 1 healing to return the winning anchor type alongside the alternative selector, and reorders the anchor extraction to prefer stable identifiers over fragile ones. Add a test verifying that when no stable anchors match but a class anchor does, the heal still succeeds and reports 'class' as the anchor, ensuring the reordered fallback logic works end-to-end.
tests/test_healing.py 4/5 Adds four new tests covering the assertion guard default, the opt-in behavior, anchor recording, and stable-anchor preference. Add a test for the P1 logic defect where _is_value_change returns False even when NINELIVES_HEAL_ASSERTIONS=1, to lock down the expected strategy selection for non-value-change assertion failures under the opt-in.
.github/workflows/release.yml 3/5 Adds PEP 740 attestations, Sigstore signing, and best-effort CycloneDX SBOM generation to the release workflow. Verify the SBOM generation step correctly handles the case where dist/*.whl matches zero files, ensuring the uv pip install command doesn't fail or install the wrong artifact.

Sequence diagram

sequenceDiagram
    participant Test as Test Runner
    participant Strategy as HealingStrategy
    participant CLI as CLI Heal Loop
    participant Tier1 as Tier1 Healer
    participant Human as Human Reviewer
    Test->>Strategy: select_strategy(failure)
    alt Failure is ASSERTION_FAILED
        Strategy->>CLI: TIER3_HUMAN (default) or TIER2_AI_SUGGEST (opt-in)
        CLI->>Human: Flag 'possible real bug'
    else Failure is LOCATOR_NOT_FOUND
        Strategy->>Tier1: TIER1_AUTO
        Tier1->>Tier1: Re-find via anchor priority
        Tier1-->>CLI: HealingResult with anchor metadata
    end
Loading

Confidence: 2/5

Two P1 logic defects in the new assertion guard implementation cause a mismatch between the strategy selection and the CLI's conditional messaging, risking silent misrouting of failures.

  • Fix the P1 logic bug in strategy.py lines 208-210 where _is_value_change is still gated behind the opt-in, meaning non-value-change assertion failures are silently dropped even when the user explicitly opts in.
  • Fix the P1 logic bug in cli.py lines 197-203 where the failure.failure_type == FailureType.ASSERTION_FAILED check is nested inside the else block, so the 'possible real bug' message only prints when Tier 2 was NOT selected, contradicting the strategy's intent.
  • Add a test case for the assertion opt-in path where _is_value_change returns False, ensuring the strategy correctly falls back to TIER3_HUMAN instead of silently bypassing the guard.
  • Verify the SBOM generation step in release.yml handles the dist/*.whl glob safely when no wheel is present, given continue-on-error: true may mask an incomplete SBOM.

Suggested labels: bug breaking-change


Posted · 1cba234 · 3 findings — View review
Proof: 3 model-only
runner-verified = CI receipt · reproduced = sandbox observed diff · grounded = deterministic detector/worker-token · model-only = model judgment only
Dismiss @sigilix dismiss <reason> (not-a-bug | bad-anchor | already-covered | too-minor | wrong-context) · Re-run /sigilix review · Review #1

@sigilix sigilix Bot added the enhancement New feature or request label Jul 9, 2026
- name: GitHub release (artifacts + signatures + SBOM)
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 LOGICGROUNDED SBOM failure blocks release despite 'best-effort' comment

The GitHub release step references the literal path 9lives.cdx.json, but that file is only created if the preceding SBOM step succeeds. Because shopt -s nullglob only affects glob expansions, a missing SBOM file causes gh release create to fail, contradicting the SBOM step's continue-on-error: true and the 'best-effort; never blocks a release' comment. Change the literal path to a glob so nullglob can skip it when the SBOM step fails.

Example:

SBOM step fails (e.g., cyclonedx-bom install error).
Release step runs: gh release create v0.1.3 --generate-notes dist/*.whl dist/*.tar.gz dist/*.sigstore* 9lives.cdx.json
Result: gh: 9lives.cdx.json: no such file or directory
The release is not created despite the 'best-effort; never blocks a release' intent.

Current:

          attestations: true

Proposed:

            dist/*.whl dist/*.tar.gz dist/*.sigstore* *.cdx.json
Suggested change
run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes
dist/*.whl dist/*.tar.gz dist/*.sigstore* *.cdx.json
More Info
  • Threat model: A transient failure in the CycloneDX SBOM generator (network, tool install, etc.) prevents the release from being published, even though the build and signatures are ready.
  • Specific code citations: The SBOM step at .github/workflows/release.yml uses continue-on-error: true and writes 9lives.cdx.json; the release step on the next block passes that literal filename to gh release create.
  • Existing protections: continue-on-error: true on the SBOM step allows the job to continue, but the release step has no conditional guard for the literal file path.
  • Proposed mitigation: Change the literal 9lives.cdx.json to a glob like *.cdx.json so shopt -s nullglob skips it when the SBOM step fails, or build the file list conditionally before calling gh release create.
  • Alternative mitigations considered: Moving the SBOM generation into a separate job would also isolate failures, but is broader than needed for this one-line fix.
  • Severity calibration: Score 4 because it breaks the release pipeline on a non-essential step, which is a real operational failure under plausible conditions (SBOM tool flakiness), but it does not affect runtime correctness or security.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release.yml
Line: 27

Comment:
**SBOM failure blocks release despite 'best-effort' comment**

The GitHub release step references the literal path `9lives.cdx.json`, but that file is only created if the preceding SBOM step succeeds. Because `shopt -s nullglob` only affects glob expansions, a missing SBOM file causes `gh release create` to fail, contradicting the SBOM step's `continue-on-error: true` and the 'best-effort; never blocks a release' comment. Change the literal path to a glob so nullglob can skip it when the SBOM step fails.

Example:
SBOM step fails (e.g., cyclonedx-bom install error).
Release step runs: gh release create v0.1.3 --generate-notes dist/*.whl dist/*.tar.gz dist/*.sigstore* 9lives.cdx.json
Result: gh: 9lives.cdx.json: no such file or directory
The release is not created despite the 'best-effort; never blocks a release' intent.

Threat model:
A transient failure in the CycloneDX SBOM generator (network, tool install, etc.) prevents the release from being published, even though the build and signatures are ready.

Specific code citations:
The SBOM step at `.github/workflows/release.yml` uses `continue-on-error: true` and writes `9lives.cdx.json`; the release step on the next block passes that literal filename to `gh release create`.

Existing protections:
`continue-on-error: true` on the SBOM step allows the job to continue, but the release step has no conditional guard for the literal file path.

Proposed mitigation:
Change the literal `9lives.cdx.json` to a glob like `*.cdx.json` so `shopt -s nullglob` skips it when the SBOM step fails, or build the file list conditionally before calling `gh release create`.

Alternative mitigations considered:
Moving the SBOM generation into a separate job would also isolate failures, but is broader than needed for this one-line fix.

Severity calibration:
Score 4 because it breaks the release pipeline on a non-essential step, which is a real operational failure under plausible conditions (SBOM tool flakiness), but it does not affect runtime correctness or security.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Confirmed real (P1) — fixed in #7. The literal 9lives.cdx.json is now *.cdx.json so nullglob drops it when the best-effort SBOM step fails. v0.1.3 only shipped because that step happened to succeed. Good catch.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — the P1 is resolved. The fix in #7 changed the literal 9lives.cdx.json path in .github/workflows/release.yml:27 to *.cdx.json, so shopt -s nullglob (already set earlier in the step) correctly drops the argument when the best-effort SBOM step fails and produces no file. v0.1.3 only shipped cleanly because the SBOM step happened to succeed; without the glob, any SBOM failure would have crashed gh release create despite continue-on-error: true. No further action needed on this finding.

Comment thread tests/test_healing.py
)
result = asyncio.run(tier1_healer.heal(failure))
assert result.success
assert result.metadata.get("anchor") == "testid"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 TESTINGGROUNDED test_tier1_records_winning_anchor does not exercise anchor tracking because the selector is not parsed

The test test_tier1_records_winning_anchor uses #login-btn as the failed_selector, but _extract_identifiers only matches attribute selectors like id='login-btn' — it does not parse CSS id selectors (#id). Therefore _find_alternative_selector will return None, the anchor tracking path is never executed, and the test will either fail (because result.success is False) or exercise a different code path that does not record an anchor. The test does not verify the intended anchor-recording behavior.

Example:

failed_selector = '#login-btn'
page_html = '<button id="login-btn-v2">Sign in</button>'

Extracted identifiers: []  (no attribute match)
→ _find_alternative_selector returns None
→ Tier1 falls through to _try_transformations or _no_heal_result
→ anchor metadata never set, test fails

Suggested fix:

use 'id="login-btn"' as the failed_selector, or update _extract_identifiers to parse '#id' selectors
Detailed reasoning

The fix: either change the test's selector to id='login-btn' so the extraction logic recognizes it, or extend _extract_identifiers to handle CSS #id selectors. The test should still assert that the anchor is 'id' and the changes message includes re-found via id.

More Info
  • Threat model: A future change to the anchor tracking code could break the feature without this test catching it, because the test does not actually exercise the anchor path.
  • Specific code citations: _extract_identifiers in src/ninelives/healing/tier1.py only looks for id='...' attribute selectors; the test uses #login-btn.
  • Existing protections: No other test covers anchor tracking for CSS id selectors.
  • Proposed mitigation: Change the test's failed_selector to id='login-btn' so the extraction logic recognizes it and the anchor tracking path is exercised.
  • Alternative mitigations considered: Alternatively, the extraction logic could be extended to handle #id selectors. The test should then be updated to reflect that the anchor is still 'id'.
  • Severity calibration: The feature's anchor tracking is not properly tested for a common selector form, leaving a gap in regression coverage. Score 4 because the test as written is misleading and would fail.
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/test_healing.py
Line: 419

Comment:
**test_tier1_records_winning_anchor does not exercise anchor tracking because the selector is not parsed**

The test `test_tier1_records_winning_anchor` uses `#login-btn` as the `failed_selector`, but `_extract_identifiers` only matches attribute selectors like `id='login-btn'` — it does not parse CSS id selectors (`#id`). Therefore `_find_alternative_selector` will return `None`, the anchor tracking path is never executed, and the test will either fail (because `result.success` is `False`) or exercise a different code path that does not record an anchor. The test does not verify the intended anchor-recording behavior.

The fix: either change the test's selector to `id='login-btn'` so the extraction logic recognizes it, or extend `_extract_identifiers` to handle CSS `#id` selectors. The test should still assert that the anchor is `'id'` and the changes message includes `re-found via id`.

Example:
failed_selector = '#login-btn'
page_html = '<button id="login-btn-v2">Sign in</button>'

Extracted identifiers: []  (no attribute match)
→ _find_alternative_selector returns None
→ Tier1 falls through to _try_transformations or _no_heal_result
→ anchor metadata never set, test fails

Threat model:
A future change to the anchor tracking code could break the feature without this test catching it, because the test does not actually exercise the anchor path.

Specific code citations:
`_extract_identifiers` in `src/ninelives/healing/tier1.py` only looks for `id='...'` attribute selectors; the test uses `#login-btn`.

Existing protections:
No other test covers anchor tracking for CSS id selectors.

Proposed mitigation:
Change the test's `failed_selector` to `id='login-btn'` so the extraction logic recognizes it and the anchor tracking path is exercised.

Alternative mitigations considered:
Alternatively, the extraction logic could be extended to handle `#id` selectors. The test should then be updated to reflect that the anchor is still `'id'`.

Severity calibration:
The feature's anchor tracking is not properly tested for a common selector form, leaving a gap in regression coverage. Score 4 because the test as written is misleading and would fail.

Suggested fix shape:
use 'id="login-btn"' as the failed_selector, or update _extract_identifiers to parse '#id' selectors

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissing as a false positive. _extract_identifiers does parse CSS #id selectors — id_match = re.search(r"#([a-zA-Z][\w-]*)", selector) (tier1.py:98) matches #login-btn. The path is exercised: this test passes asserting result.metadata['anchor'] == 'id' and 're-found via id', and the pre-existing test_tier1_heals_moved_id_from_page_snapshot also uses #login-btn and passes. The #(\w+) you may have seen is a separate TRANSFORMATIONS entry, not the extractor.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retracting — the finding was a false positive. _extract_identifiers at tier1.py:98 uses re.search(r"#([a-zA-Z][\w-]*)", selector), which correctly parses CSS #id selectors like #login-btn. The #(\w+) pattern is a separate TRANSFORMATIONS entry (tier1.py:24), not the extractor. test_tier1_records_winning_anchor exercises the anchor-tracking path as intended: _extract_identifiers yields ("id", "login-btn"), _find_in_html resolves login-btn-v2, and the test's assertions on metadata["anchor"] and changes_made are valid. The pre-existing test_tier1_heals_moved_id_from_page_snapshot also uses #login-btn and passes, confirming the path works.

Learned something new

"For detecting CSS id selectors, use the extractor _extract_identifiers which handles #id selectors via r"#([a-zA-Z][\w-]*)" pattern, not the TRANSFORMATIONS entry which uses #(\w+)"
Undo: reply @sigilix forget to this comment.

Comment thread src/ninelives/cli.py
@@ -194,7 +194,14 @@ def _heal_loop(spec: Path, working_spec: Path, *, auto_apply: bool, max_iteratio
elif tier == HealingTier.TIER2_AI_SUGGEST:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 LOGICGROUNDED Tier 3 handler silently treats all failure types the same when NINELIVES_HEAL_ASSERTIONS is not set

The _heal_loop function now branches on failure.failure_type == FailureType.ASSERTION_FAILED to print a special message, but the else branch prints a generic message for all other failure types. If a new FailureType is added in the future, it will silently fall into the generic message without any explicit handling, which could mask a missing branch. The fix is to add an explicit elif for each known failure type or a final else that raises an error for unknown types.

Example:

A new FailureType.TIMEOUT is added to the enum. The select_strategy method returns HealingTier.TIER3_HUMAN for it. The _heal_loop handler prints the generic message instead of a timeout-specific message, and no one notices the missing branch.

Suggested fix:

if failure.failure_type == FailureType.ASSERTION_FAILED:
    ...
elif failure.failure_type == FailureType.LOCATOR_NOT_FOUND:
    print(f"{PAW} this failure needs a human ({tier.value}) — no automatic fix attempted.")
else:
    raise ValueError(f"Unknown failure type: {failure.failure_type}")
More Info
  • Threat model: A future developer adds a new FailureType enum value and forgets to update this handler. The new failure type will be silently treated as a generic human-needed failure, potentially hiding a bug or missing a special handling path.
  • Specific code citations: src/ninelives/cli.py line 194-214: the else branch in _heal_loop that handles HealingTier.TIER3_HUMAN and other tiers. src/ninelives/healing/strategy.py lines 199-212: the select_strategy method that returns HealingTier.TIER3_HUMAN for FailureType.ASSERTION_FAILED when the opt-in is not set.
  • Existing protections: The select_strategy method explicitly handles FailureType.ASSERTION_FAILED and FailureType.LOCATOR_NOT_FOUND, but the consumer in _heal_loop does not mirror this exhaustiveness.
  • Proposed mitigation: Add an explicit elif failure.failure_type == FailureType.LOCATOR_NOT_FOUND: branch before the else, or use a match statement to ensure all arms are covered.
  • Alternative mitigations considered: A match statement would provide exhaustiveness checking at the language level, but a simple elif chain is sufficient for the current two-arm enum.
  • Severity calibration: This is a maintainability issue, not a runtime bug. The current code works correctly for the two existing failure types, but the lack of exhaustiveness makes future changes error-prone. Score 3 reflects the moderate risk of silent misbehavior when the enum grows.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ninelives/cli.py
Line: 194

Comment:
**Tier 3 handler silently treats all failure types the same when NINELIVES_HEAL_ASSERTIONS is not set**

The `_heal_loop` function now branches on `failure.failure_type == FailureType.ASSERTION_FAILED` to print a special message, but the `else` branch prints a generic message for all other failure types. If a new `FailureType` is added in the future, it will silently fall into the generic message without any explicit handling, which could mask a missing branch. The fix is to add an explicit `elif` for each known failure type or a final `else` that raises an error for unknown types.

Example:
A new FailureType.TIMEOUT is added to the enum. The select_strategy method returns HealingTier.TIER3_HUMAN for it. The _heal_loop handler prints the generic message instead of a timeout-specific message, and no one notices the missing branch.

Threat model:
A future developer adds a new `FailureType` enum value and forgets to update this handler. The new failure type will be silently treated as a generic human-needed failure, potentially hiding a bug or missing a special handling path.

Specific code citations:
`src/ninelives/cli.py` line 194-214: the `else` branch in `_heal_loop` that handles `HealingTier.TIER3_HUMAN` and other tiers. `src/ninelives/healing/strategy.py` lines 199-212: the `select_strategy` method that returns `HealingTier.TIER3_HUMAN` for `FailureType.ASSERTION_FAILED` when the opt-in is not set.

Existing protections:
The `select_strategy` method explicitly handles `FailureType.ASSERTION_FAILED` and `FailureType.LOCATOR_NOT_FOUND`, but the consumer in `_heal_loop` does not mirror this exhaustiveness.

Proposed mitigation:
Add an explicit `elif failure.failure_type == FailureType.LOCATOR_NOT_FOUND:` branch before the `else`, or use a match statement to ensure all arms are covered.

Alternative mitigations considered:
A `match` statement would provide exhaustiveness checking at the language level, but a simple `elif` chain is sufficient for the current two-arm enum.

Severity calibration:
This is a maintainability issue, not a runtime bug. The current code works correctly for the two existing failure types, but the lack of exhaustiveness makes future changes error-prone. Score 3 reflects the moderate risk of silent misbehavior when the enum grows.

Suggested fix shape:
if failure.failure_type == FailureType.ASSERTION_FAILED:
    ...
elif failure.failure_type == FailureType.LOCATOR_NOT_FOUND:
    print(f"{PAW} this failure needs a human ({tier.value}) — no automatic fix attempted.")
else:
    raise ValueError(f"Unknown failure type: {failure.failure_type}")

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissing. The generic "needs a human" message for all non-assertion Tier-3 types (navigation, flow-changed) is the intended graceful fallback — only the assertion case warrants the special "possible real bug" wording. The suggested fix (raise on an unknown FailureType) would make the CLI crash on a failure it could otherwise report cleanly — strictly worse UX for an end-user tool. If we grow the enum and want per-type copy, that's a deliberate follow-up, not an exhaustiveness guard that aborts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, the original finding was wrong. The code is intentionally designed this way.

Looking at the full picture:

  1. The strategy selector already gates exhaustiveness. select_strategy in src/ninelives/healing/strategy.py:191-208 explicitly handles every FailureType that should route to Tier 1 or Tier 2. The remaining types (NAVIGATION_FAILED, FLOW_CHANGED, NETWORK_ERROR, SYNTAX_ERROR, UNKNOWN) all fall through to TIER3_HUMAN — that's the correct, deliberate default.

  2. The CLI's else branch is the Tier 3 handler. At src/ninelives/cli.py:194-205, the if failure.failure_type == FailureType.ASSERTION_FAILED check carves out the one special message ("possible real bug — behavior changed, not a selector"), and the else prints the generic "needs a human" message for all other Tier 3 types. That's the right UX: only assertions get the extra warning about hidden bugs; everything else that escalates to a human is reported cleanly without crashing.

  3. raise on an unknown FailureType would be strictly worse. The CLI is an end-user tool. Crashing on a failure it could report is bad UX. The graceful fallback is the feature, not a bug.

  4. If the enum grows, the strategy selector is the right place to add per-type routing, and the CLI message can get per-type copy as a deliberate follow-up. An exhaustiveness guard that aborts is not appropriate here.

Retracting the finding. The else branch at cli.py:197 is the intended graceful fallback for all non-assertion Tier 3 failures.

@Desperado
Desperado merged commit 8a40d8d into main Jul 9, 2026
5 checks passed
@Desperado
Desperado deleted the feat/moat-guard-anchor-trust branch July 9, 2026 22:53
Desperado added a commit that referenced this pull request Jul 9, 2026
…he release (#7)

Sigilix (PR #6, P1): gh release create referenced the literal 9lives.cdx.json,
but shopt -s nullglob only skips *globs* — a missing SBOM (its step is
continue-on-error) would make gh release create fail with 'no such file',
contradicting the best-effort intent. Use *.cdx.json so nullglob drops it
when absent. The v0.1.3 run only survived because the SBOM step happened to
succeed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant