Skip to content

fix(sage): explain the discriminated review-failure causes - #7686

Merged
bolichen97 merged 1 commit into
mainfrom
fix/sage-cause-translator-7242
Sep 2, 2026
Merged

fix(sage): explain the discriminated review-failure causes#7686
bolichen97 merged 1 commit into
mainfrom
fix/sage-cause-translator-7242

Conversation

@chenmingwei23

@chenmingwei23 chenmingwei23 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

PR #7240 discriminated Code Review Sage's failure causes in the backend, so a
host that cannot spawn a reviewer now says so instead of reporting an
untriageable "produced no result record". The frontend cause translator
(website/src/apps/code-review-sage/lib/format.ts, failureReason) has no
branch for any of the new strings, so each one reaches the run card as raw
lowercase backend English:

  • the reviewer cannot run: no kiro-cli executable was found on this host (the reviewer session is driven by kiro-cli - install it or add it to PATH)
  • the reviewer cannot run: the ACP runtime (kiro_crew.acp.runtime) is not importable in this install
  • the reviewer never ran: its agent runtime is unavailable on this host
  • review wrote a result record but never completed the review

Why it matters

These are exactly the failures a reader most needs help with: the review did not
fail, it never ran, and the fix is on the host. Rendering the driver's own
sentence leaves that unsaid in every language, and leaves a non-English reader
with untranslated English on the one card that is supposed to explain itself.
The existing /no result record|no_review_recorded/ branch deliberately does not
match the incomplete-record message, so nothing catches it by accident.

What changed (motivation -> approach -> change)

Four branches, not the two the issue proposed, because four distinct strings
reach this function. The issue's set missed two of them:

backend source string the issue's set
review_pool.runtime_preflight no kiro-cli executable was found matched (kiro-cli)
review_pool.runtime_preflight the ACP runtime (...) is not importable missed - no kiro-cli, no runtime_unavailable
routes.py::_first_change_error its agent runtime is unavailable on this host missed - renders the sentence, never the enum
review_driver.run_review / _first_change_error ... never completed the review matched

The run-level miss is the one worth naming: _first_change_error maps
runtime_unavailable to prose, so a translator keyed on the enum token never
sees it.

The patterns are also narrower than the issue asked for. A branch on the bare
word kiro-cli would swallow acp/runtime.py's missing-agent-spec message -
Agent spec '...' is not installed: kiro-cli found no '....json' in ... - repair with \kirocrew setup --agent-only --clean`- which reaches this function through the pool's spawn error and already carries the exact repair command. Replacing it with "install kiro-cli" would be worse than today's pass-through, so the branch matchesno kiro-cli executable` and a test pins the agent-spec message as
verbatim.

Each cause gets its own sentence rather than one shared "runtime unavailable",
because the remedies differ: install the CLI / put it on PATH, repair the
install, or nothing actionable named yet (the raw line under the sentence still
carries the specifics). Four new keys in all 13 catalogs, en-XA regenerated.

Tests

website/src/test/CodeReviewSageRunList.test.tsx, five cases, every fixture the
string the backend really emits:

  • the two incomplete-record wordings plus the reason token reach the new sentence
    and do not fall into the "recorded no findings" branch
  • the missing-kiro-cli message renders the sentence naming PATH
  • the not-importable message renders the load-failure sentence and not the
    PATH one (different remedy, so it must not share a key)
  • the run-level runtime is unavailable sentence and the bare enum both resolve
  • the missing-agent-spec message still passes through verbatim, with its repair
    command intact

Mutation-verified, one test killed per mutation: broadening
/no kiro-cli executable/ to /kiro-cli/ (the pattern the issue prescribed)
fails the agent-spec guard; disabling each of the other three branches fails
exactly its own case.

Manual verification

  • node scripts/i18n-check.mjs with I18N_BASE_REF at the PR base: 19/19 PASS
    (4 new English keys, 0 badly shaped, 0 QA findings, 0 values still reading as
    English, key-refs resolve, en-XA matches).
  • vitest run src/i18n/ 660 passing, incl. catalog parity, dead keys, and the
    per-language style suites; vitest run src/test/CodeReviewSageRunList.test.tsx
    40 passing; tsc --noEmit and eslint clean on the touched files.
  • Brand Name, Focus Cue, Harness Parity and scrub-lint gates run locally against
    the base: all pass.
  • The screenshots below are from website/scripts/capture-sage-cause-translator.mjs,
    a new harness that runs the real built SPA with the backend answered from
    fixtures. It asserts the rendered text rather than just saving a PNG: each card
    shows its own sentence, no translated card still contains the backend wording,
    and the detail notice keeps the driver's raw line.

Screenshots / video

Before - every new cause as raw backend English (this same harness on the
pre-fix translator):

Sage review list before: four cards showing lowercase backend error text

After - one sentence per cause. The last card is the missing-agent-spec message,
unchanged on purpose:

Sage review list after: each card explains its cause in a full sentence

The failure notice in the detail pane: sentence, then the driver's own wording

Sage run detail: notice reading "Reviewer wrote a findings record but stopped before completing the review." above the raw driver text

Related Issues

Closes #7242
Refs #7233

Pattern harvest

Rule candidate: review-prompt
Pattern: a frontend translator keyed on a backend enum token that the backend
maps to prose before it leaves the API - the token never reaches the client, so
the branch is dead and the prose renders untranslated. Same shape as matching an
error by a word (kiro-cli) that a second, unrelated error also contains.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable) - N/A, no doc surface changed
  • No secrets, credentials, or internal references in the diff

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of de185f8055c08a5774b752ec84998aa800bc341a — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: PASS

Each new failure sentence names what happened and the remedy, matches sibling cause_* copy, ships translated in all 13 catalogs, and keeps the raw line for specifics.

[UX-REVIEWED] de185f8

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Design-level review of de185f8055c08a5774b752ec84998aa800bc341a — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: CONCERNS

Right fix for these four strings, but the design guarantees the same bug recurs: translation keyed on backend prose the API has already stripped of its enum.

Watch

  • The translator's new branches match English sentences (/no kiro-cli executable/, /is not importable/) that live in review_pool.py/review_driver.py, with no shared contract: the frontend test fixtures are hand-copied strings, so a backend reword — or a fifth preflight cause — silently reverts to untranslated pass-through, exactly the failure fix: discriminate Sage no-result failures and preflight the runtime #7240 caused and this PR repairs. The PR's own description names the root cause ("_first_change_error maps runtime_unavailable to prose, so a translator keyed on the enum token never sees it") but fixes the symptom side only.

Suggestions

  • Follow-up: carry the discriminated reason token (runtime_unavailable, review_record_incomplete, plus per-preflight codes) through the run record to the API and key failureReason on tokens, keeping the prose as the raw line — this retires the whole phrase-matching class, including the agent-spec collision the PR had to test around.

[DESIGN-REVIEWED] de185f8

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of de185f8055c08a5774b752ec84998aa800bc341a — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Everything checks out: the four backend strings exist verbatim where the description says (review_pool.py:110-112, review_driver.py:1282-1284, routes.py:549,558), the translator's prose-substring approach is the file's existing mechanism (four sibling branches at format.ts:245-292), the capture harness follows a 337-script convention with shared helpers, and the committed PNGs are the documented PR-evidence process (temp-screenshots/README.md, which explicitly tells reviewers not to ask for their removal). Each new i18n key has exactly one consumer by design, same as every sibling key.

First-Principles-Verdict: PASS

Untranslated backend English on the one card meant to explain itself is a named, user-visible harm; every item traces to one of the four real strings.

What this change ships

Intent: make Code Review Sage's four new "reviewer never ran / stopped short" failures render as actionable, translated sentences instead of raw backend English — a FIX.

  1. Missing-kiro-cli failure card now says install it or add to PATH — justified
  2. Runtime-not-importable card gets its own "repair the install" sentence — justified (distinct remedy)
  3. Run-level "agent runtime unavailable" (prose and enum token) now translated — justified
  4. Incomplete-record failure gets its own sentence, kept apart from "recorded no findings" — justified
  5. Four new keys land in all 13 catalogs + en-XA — mandated by the i18n invariant
  6. Missing-agent-spec message deliberately still passes through verbatim, test-pinned — justified (it carries its own repair command; a broad /kiro-cli/ match would erase it)
  7. New capture harness capture-sage-cause-translator.mjs — declared; matches the 337-sibling convention and reuses the shared serveDist/stubDashboardApi helpers
  8. Three PNGs under temp-screenshots/sage-cause-translator/ — declared; the documented PR-evidence process (its README forbids asking for removal)

Duplication counted: zero — no existing branch or key covers these strings (grep for each of the four phrases hits only the backend emitters, their tests, and this diff). The narrower-than-issue patterns are grounded in a counted collision (the agent-spec message), not preference.

[FIRST-PRINCIPLES-REVIEWED] de185f8

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed de185f8055c08a5774b752ec84998aa800bc341a — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] de185f8

Verdict parsed from the review's SHA-scoped output markers for commit de185f8055c08a5774b752ec84998aa800bc341a.

False positive or not applicable? A repository writer can comment:
/ai-review override fable de185f8055c08a5774b752ec84998aa800bc341a: <one-sentence reason>

@chenmingwei23

Copy link
Copy Markdown
Contributor Author

Dispositioning the three advisory suggestions on aa0a4493. All four posted
lanes are PASS; no code change follows from these, so the head is unchanged.

UX - append a next step to cause_runtime_unavailable ("Reinstalling Kiro Crew
usually restores it"): declined, the suggested remedy would be wrong about half
the time.
That sentence is reachable only through the reason-level fallback in
routes.py::_first_change_error, whose own comment states the condition: a
preflight-failed record normally carries the specific runtime message in its
error fields, which the key order prefers, so the generic sentence renders
exactly when the backend did NOT keep which of the two preflight answers applied.
Those two have different remedies. runtime_preflight fails either because
kiro_crew.acp.runtime is unimportable - where repairing the install is right -
or because no kiro-cli executable resolved, and kiro-cli is an external binary
found via KIROCREW_KIRO_BIN, the known install dirs, or PATH
(kiro_cli.find_kiro_cli_candidates); it is not shipped by the Python package,
which declares no such file in pyproject.toml or MANIFEST.in. So reinstalling
Kiro Crew does not restore a missing CLI, and printing that as the next step on
the one card that cannot tell the two apart would be a guess presented as
guidance.

The absence of a next step there is the honest content, and the two cases where
the cause IS known both carry their own remedy (cause_no_agent_cli names PATH,
cause_runtime_not_importable names repairing the install). Happy to add a docs
link instead if one exists that covers both halves - I did not find one to point
at.

Design - expose the skipped_reason token alongside the sentence so this
translator can key on tokens: agreed, and filed as #7688.
It is a backend
payload change, out of scope for a frontend-scoped fix, and it is the real end of
the drift class rather than another patch from the outside.

First Principles - "Watch": eight prose-keyed regexes drift silently on a
backend reword, with no test going red: same class, same issue (#7688).
The
hedge in this PR is that each branch also matches the bare reason token, so a
payload that ever carries the token keeps working; that is a mitigation, not the
fix, which is why the follow-up is filed rather than deferred silently.

@chenmingwei23

Copy link
Copy Markdown
Contributor Author

The red Frontend Lint & Type Check on aa0a4493 is main-owned, not from this
diff, and a rebase does not clear it.
Measurements, so this is not an assertion:

Main never went red for it because of the failure mode #7511 already describes -
main's own runs are cancelled before the ratchet gates report, so the drift
surfaces on the next unrelated PR. #7569 would clear it wholesale but is
CONFLICTING across 199 files and still assumes a 609 ceiling.

Not folding a main-side CI fix into this PR unilaterally. The one-line
alternatives are: give ArtifactsPage.tsx the missing dep (or hoist that
expression into a useMemo), or bump the ceiling 603 -> 604. Either belongs in its
own PR, which would unblock every open PR touching website/**, not just this one.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of de185f8055c08a5774b752ec84998aa800bc341a and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] de185f8

False positive or not applicable? A repository writer can comment:
/ai-review override gpt de185f8055c08a5774b752ec84998aa800bc341a: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 1, 2026
@chenmingwei23
chenmingwei23 force-pushed the fix/sage-cause-translator-7242 branch from aa0a449 to fefc45b Compare September 2, 2026 00:14
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 2, 2026
PR #7240 gave the Code Review Sage backend four new failure strings, and the
frontend cause translator had a branch for none of them, so a reviewer that
never started rendered as raw lowercase backend English in the run card.

The four strings are the two answers review_pool.runtime_preflight can give
(no kiro-cli executable, ACP runtime not importable), the driver's per-change
wording for a record written without completing the review, and the run-level
sentence routes.py _first_change_error maps `runtime_unavailable` to. Each now
has its own branch and its own localized sentence in 13 catalogs, keyed on the
specific phrase rather than on the word `kiro-cli`: a missing agent spec also
names kiro-cli and already carries its own repair command, so a broad pattern
would have replaced useful text with wrong guidance. A test pins that case.

The screenshot harness answers the dashboard's boot endpoints through the
shared scripts/lib/stub-dashboard-api.mjs rather than carrying its own copy of
them, which is what that helper exists for.

Fixes #7242
Refs #7233
@chenmingwei23
chenmingwei23 force-pushed the fix/sage-cause-translator-7242 branch from fefc45b to de185f8 Compare September 2, 2026 00:36
@chenmingwei23

Copy link
Copy Markdown
Contributor Author

Design re-raised the token-vs-prose class on de185f805 as advisory CONCERNS. It
is the same finding it and First Principles raised on the previous head, it is
correct, and it is already filed as #7688 - "the run payload drops the failure
reason token, so the frontend must match backend prose" - which proposes exactly
what Design suggests: carry the reason beside the sentence and key failureReason
on the token, keeping the prose as the raw line.

Two things I would add to the record rather than argue:

  • Design's sharper phrasing of the risk is right and worth quoting in the
    follow-up: a FIFTH preflight cause, not just a reword, silently reverts to
    pass-through. The mitigation here is thin by design - each branch also matches
    the bare reason token, so a payload that ever carries one keeps working - and
    that is a hedge, not the fix.
  • "The frontend test fixtures are hand-copied strings" is accurate. There is no
    cross-language fixture seam available today; the backend pins the same sentences
    in test_backend_routes.py::TestFailureStringMapping, so a reword breaks a test
    on the backend side, and the frontend fixtures name that file so the next reader
    can find the pair. Code Review Sage: the run payload drops the failure reason token, so the frontend must match backend prose #7688 is what removes the need for the pairing.

Not folding the payload change into this PR: it is a backend contract change on a
frontend-scoped fix that is otherwise green, and stuffing it in here would put an
unreviewed API change behind a converged review.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 2, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed via parallel subagent audit: diff matches description, CI fully green, no blocking findings, no unresolved threads.

@bolichen97
bolichen97 merged commit df653d1 into main Sep 2, 2026
71 of 78 checks passed
@bolichen97
bolichen97 deleted the fix/sage-cause-translator-7242 branch September 2, 2026 04:35
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 2, 2026
chenmingwei23 added a commit that referenced this pull request Sep 3, 2026
A Sage run's failure cause exists as a stable token in the backend --
`skipped_reason` is one of `no_review_recorded`, `review_record_incomplete`,
`runtime_unavailable`, `review_failed` -- but nothing in the payload the
dashboard receives named it. `routes.py::_first_change_error` mapped the token
to an English sentence before the run was serialized, and each per-change
`progress` entry carried prose while the token sat on the driver-internal
record beside it. So the dashboard's translator had to recognize causes by
their wording, and rewording any backend message silently reverted its card to
untranslated pass-through with no test going red -- the frontend's fixtures are
its own copies of the backend's strings.

That drift is not hypothetical. It has already happened and is on main now:
`_first_change_error` maps `no_review_recorded` to "the reviewer finished but
wrote no findings record", and NONE of the translator's eight prose branches
matches it -- the no-record branch keys on "no result record", the driver's
per-change wording for the same cause. The sibling branch for
`review_record_incomplete` deliberately covers both of ITS wordings, so this is
an asymmetry rather than a decision. That card renders raw backend English
today; with the token it explains the cause.

The token is now carried BESIDE the sentence, never instead of it:

- `_first_change_failure` returns `(sentence, token)` from ONE pass over
  `per_change`. One pass rather than two functions because the sentence is
  often built from a record's `deep_error` while the token is that record's
  `skipped_reason`; an independent scan for the token would return a LATER
  record's token and label this failure with an unrelated cause.
  `_first_change_error` keeps its `-> str` signature as a thin wrapper, so
  every existing caller and its eight assertions across two test files stay
  untouched -- the change is additive, not a signature migration.
- The run record gains `reason` on both error branches. Absent, never blank,
  when no record carried a token, so a reader cannot mistake one for the other.
- Each of the driver's five failure paths puts its token on the `progress`
  entry it already wrote prose to, agreeing with the `skipped_reason` it sets
  on the same path.
- `failureReason` resolves the token through the same per-change-then-run
  precedence it already uses for the prose, and consults it exactly where the
  verbatim pass-through used to be -- which is where a reword lands.

The prose path is PERMANENT, not transitional: `progress` lives inside a
persisted run record, so every run already on disk has no token and never will.
Prose matching is the compatibility path for those runs for good.

Two deliberate narrowings, both with a test pinning them:

The token is consulted AFTER the prose branches, not before. For one cause the
prose is strictly more specific than the token: all three runtime-preflight
messages carry the single token `runtime_unavailable`, so a token-first lookup
would replace "no kiro-cli executable was found" and "the ACP runtime is not
importable" -- one of which carries the exact repair command -- with the
generic sentence. The translator's own comment on those branches already makes
this argument about a bare /kiro-cli/ pattern; keying on the token first would
reintroduce the loss it warns about by another route.

`review_failed` is emitted but is NOT in the translator's token table. The
other three label prose the backend wrote, a closed set of fixed sentences the
token faithfully stands in for. `review_failed` labels whatever the failed
dispatch returned, so its prose is arbitrary and usually far more specific:
the missing-agent-spec message arrives on that path carrying its own
`kirocrew setup --agent-only --clean` repair command. Adding it to the table
reddens the verbatim guard, which is how that was caught.

The screenshot harness grew the reword case, and an assertion that made it
observable: its per-card check finds a card BY its expected sentence, so it
could not see a regression on a cause whose sentence another fixture also
renders -- the `find` returned the healthy sibling. It now also asserts no
cause's raw backend wording appears anywhere in the list. The two committed
frames are regenerated from it; `0-before-*` is left alone, being the
historical pre-fix frame from #7686.

Refs #7688
bolichen97 pushed a commit that referenced this pull request Sep 3, 2026
…8186)

A Sage run's failure cause exists as a stable token in the backend --
`skipped_reason` is one of `no_review_recorded`, `review_record_incomplete`,
`runtime_unavailable`, `review_failed` -- but nothing in the payload the
dashboard receives named it. `routes.py::_first_change_error` mapped the token
to an English sentence before the run was serialized, and each per-change
`progress` entry carried prose while the token sat on the driver-internal
record beside it. So the dashboard's translator had to recognize causes by
their wording, and rewording any backend message silently reverted its card to
untranslated pass-through with no test going red -- the frontend's fixtures are
its own copies of the backend's strings.

That drift is not hypothetical. It has already happened and is on main now:
`_first_change_error` maps `no_review_recorded` to "the reviewer finished but
wrote no findings record", and NONE of the translator's eight prose branches
matches it -- the no-record branch keys on "no result record", the driver's
per-change wording for the same cause. The sibling branch for
`review_record_incomplete` deliberately covers both of ITS wordings, so this is
an asymmetry rather than a decision. That card renders raw backend English
today; with the token it explains the cause.

The token is now carried BESIDE the sentence, never instead of it:

- `_first_change_failure` returns `(sentence, token)` from ONE pass over
  `per_change`. One pass rather than two functions because the sentence is
  often built from a record's `deep_error` while the token is that record's
  `skipped_reason`; an independent scan for the token would return a LATER
  record's token and label this failure with an unrelated cause.
  `_first_change_error` keeps its `-> str` signature as a thin wrapper, so
  every existing caller and its eight assertions across two test files stay
  untouched -- the change is additive, not a signature migration.
- The run record gains `reason` on both error branches. Absent, never blank,
  when no record carried a token, so a reader cannot mistake one for the other.
- Each of the driver's five failure paths puts its token on the `progress`
  entry it already wrote prose to, agreeing with the `skipped_reason` it sets
  on the same path.
- `failureReason` resolves the token through the same per-change-then-run
  precedence it already uses for the prose, and consults it exactly where the
  verbatim pass-through used to be -- which is where a reword lands.

The prose path is PERMANENT, not transitional: `progress` lives inside a
persisted run record, so every run already on disk has no token and never will.
Prose matching is the compatibility path for those runs for good.

Two deliberate narrowings, both with a test pinning them:

The token is consulted AFTER the prose branches, not before. For one cause the
prose is strictly more specific than the token: all three runtime-preflight
messages carry the single token `runtime_unavailable`, so a token-first lookup
would replace "no kiro-cli executable was found" and "the ACP runtime is not
importable" -- one of which carries the exact repair command -- with the
generic sentence. The translator's own comment on those branches already makes
this argument about a bare /kiro-cli/ pattern; keying on the token first would
reintroduce the loss it warns about by another route.

`review_failed` is emitted but is NOT in the translator's token table. The
other three label prose the backend wrote, a closed set of fixed sentences the
token faithfully stands in for. `review_failed` labels whatever the failed
dispatch returned, so its prose is arbitrary and usually far more specific:
the missing-agent-spec message arrives on that path carrying its own
`kirocrew setup --agent-only --clean` repair command. Adding it to the table
reddens the verbatim guard, which is how that was caught.

The screenshot harness grew the reword case, and an assertion that made it
observable: its per-card check finds a card BY its expected sentence, so it
could not see a regression on a cause whose sentence another fixture also
renders -- the `find` returned the healthy sibling. It now also asserts no
cause's raw backend wording appears anywhere in the list. The two committed
frames are regenerated from it; `0-before-*` is left alone, being the
historical pre-fix frame from #7686.

Refs #7688

Co-authored-by: gh-autofix#2887 <chenmingwei23@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Code Review Sage: frontend cause translator misses the new discriminated failure messages

2 participants