Skip to content

feat: name the configuration that stopped a review - #30

Merged
colinhallett merged 6 commits into
mainfrom
surface-configuration-failure-reason
Aug 28, 2026
Merged

colinhallett merged 6 commits into
mainfrom
surface-configuration-failure-reason

Conversation

@colinhallett

@colinhallett colinhallett commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

A run that fails before it reviews anything reaches the pull request as one
sentence: "Tessl Code Review did not complete. View the workflow run." A profile
naming a lens file that does not exist and a crashed CLI produce that same
sentence, in the check summary and in the notice comment alike. Telling them
apart means opening the run, expanding the right step, and scrolling past the
artifact upload — for a failure the CLI had already described in one actionable
line.

What changes

The check-run summary and the failure notice now quote the CLI's message after
the status sentence. The status is still what the check asserts about the commit;
the reason is what the maintainer fixes.

What is quoted, and what is not

src/failure-reason.mjs decides, as an allowlist of failure kinds — not stages.
A kind is on it only when its message is composed from a fixed sentence and the
caller's own input: a flag they passed, a path in their profile, a name they
typed. Everything else keeps the existing sentence and the link, unchanged.

The first draft of this allowed whole failure stages, and that was wrong. A stage
whose name suggests configuration still carries messages that describe the
account, the provider, or the review itself, and these two surfaces are as public
as the repository. Keying on kind keeps the decision explicit per message rather
than inferred from where it came from. A kind this revision does not name is
withheld too, so a later CLI cannot widen what is published by adding one.

What is published is collapsed to one line, has its backticks replaced, and is
truncated at 500 characters, so a message that is not a configuration sentence
cannot escape the code span it renders in or fill a comment.

docs/security-model.md records the rule under data minimization, next to the
artifact field allowlist it follows. The comment protocol in
docs/action-contract.md is updated as well: the check summary was documented as
a fixed set of hand-written sentences, and it now has a second half. That half is
deterministic CLI output, not model output, so neither surface gains an
AI-system marker.

Verification

bash scripts/validate-foundation.sh passes, 153 tests. New coverage for what is
quoted and what is withheld, for the notice body, and through finalize end to
end. The failure notice had no test file at all before this, so it has one now.

The Action itself was then run three times from a caller workflow in a private
repository, pinned to this branch, against a pull request of its own:

  1. A profile naming a lens file that is not there. The check summary and the
    notice both carried the CLI's sentence. This is the round that proves the
    action.yml wiring, which no test in this repository can reach.
  2. A profile naming a lens the plugin does not carry. That failure is not on the
    allowlist, and both surfaces showed the status sentence alone. The message
    withheld enumerated the plugin's skill list, which is the shape of thing this
    allowlist exists to keep off a pull request.
  3. A profile that resolves. The review published, the check approved, and the
    notice comment from the first two rounds was cleared.

Both entry scripts were also run against a genuine CLI failure document — one
produced by pointing tessl code review at a profile naming a missing lens, not
a hand-written fixture — with the test stub recording the GitHub requests. The
posted notice body:

<!-- tessl-code-review:failure:v1 -->
Tessl Code Review did not complete. [View the workflow run](https://github.example/run/1).

The Tessl CLI reported: `Invalid Code Review profile "./qa-broken-profile.yml": Lens ref "./review-lenses/gone/SKILL.md" does not resolve to a readable path.`

A run that fails before reviewing anything reached the pull request as one
sentence pointing at the workflow run, whatever went wrong. A profile naming a
lens file that does not exist and a crashed CLI produced the same check summary
and the same notice, so telling them apart meant opening the run, expanding the
step, and scrolling the log — for a failure the CLI had already described in one
actionable sentence.

The CLI reports why it stopped as a stage/kind pair with a message. The check
summary and the failure notice now quote that message after the status sentence,
for the stages that describe the run's own configuration and inputs. A message
from the executor, an unexpected exception, and a provider's response body stay
withheld: those can carry model output or something a provider echoed back, and
the artifact allowlist exists for the same reason. An unrecognised stage is
withheld too, so a later CLI cannot widen what is published by adding one.

What is quoted is reduced to a single line, has its backticks replaced, and is
truncated, so it cannot escape the code span it renders in or fill a comment.
@colinhallett
colinhallett requested a review from a team as a code owner August 28, 2026 11:07
@colinhallett colinhallett self-assigned this Aug 28, 2026

@tessl-code-review tessl-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tessl Code Review

Changes requested (1)

The new failure-message publication path still needs to fail closed for specific, stable safe failure forms rather than accepting every message carrying an allowed stage. It should also neutralize bidirectional formatting controls before publishing a configuration-derived message. The direct-renderer concern is not applicable to the supported Action interface.

Severity Findings
Major 1
Minor 1

Findings

  • Major · Allowed stages publish unrecognized CLI failure messages
    src/failure-reason.mjs:65
  • Minor · Failure-message sanitization retains bidirectional formatting controls
    src/failure-reason.mjs:45
Working with this review

Verify before acting. These findings are AI-generated and not authoritative. Check each one against the code, yourself or with an agent, before acting on it.

Answer each finding on its thread:

  • Addressed: you changed the code. Say what changed.
  • Refuted: the finding is wrong about the code.
  • Declined: the finding is right, and you are not changing it. Say why.

Reviews are incremental. A later round raises a new finding only if it would request changes, and never repeats one you refuted or declined. Ask for a full review to have the whole change read and everything raised again.

Lenses are configurable. Add your own and scope them to paths with globs. Keep the set small so findings stay distinct. See https://docs.tessl.io/tutorials/setting-up-agentic-code-review#route-reviews-with-yaml

Mention @tessl-code-review to run another review.

Comment thread src/failure-reason.mjs Outdated
Comment thread src/failure-reason.mjs
A stage is too coarse to decide what may be published. Within a stage whose
name suggests configuration, a failure message can still describe the account,
the provider, or the review, and the check run and the notice are as public as
the repository.

Keyed on kind instead, as an allowlist of the kinds whose message is composed
from a fixed sentence and the caller's own input. A profile that names a file
which is not there is one of them.
A bidirectional override renders text in an order other than the one it is
written in, and a code span does not stop it. The sanitiser removed C0 controls
and DEL but left the format characters, so a path read from a profile could
display as something other than what it says.
@colinhallett

Copy link
Copy Markdown
Contributor Author

Both findings addressed: 29093b0 replaced the stage allowlist with an explicit allowlist of failure kinds, and 15374b1 strips Unicode format characters from a published message. Replies on both threads.

@tessl-code-review

(from Claude)

@tessl-code-review tessl-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tessl Code Review

Changes requested (1)

The public failure-reason path still trusts an admitted kind across every CLI stage. Because the CLI version can change independently of the Action, this can expose a non-configuration message under a reused kind; publication must remain limited to the known safe failure forms.

Severity Findings
Major 1

Findings

  • Major · An admitted failure kind is published regardless of its stage
    src/failure-reason.mjs:69
    Still applies. Discussion continues on the existing review comment thread.

Earlier findings: 1 still applies · 1 addressed

Mention @tessl-code-review to run another review.

The CLI is the caller's to choose and versions independently of this Action, so
a kind on its own does not pin a message to the failure the allowlist was
written against. Naming the pair means a later CLI that reuses one of these
kinds at another stage publishes nothing until an entry says otherwise.
@colinhallett

Copy link
Copy Markdown
Contributor Author

f7c1dea keys the allowlist on stage and kind together. Reply on the thread.

@tessl-code-review

(from Claude)

@tessl-code-review tessl-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tessl Code Review

Changes requested (1)

The stage/kind allowlist and formatting-control sanitization now satisfy the previously raised publication boundaries. One remaining fail-closed gap allows malformed CLI result fields to match an admitted form and expose its message; that must be corrected before publishing failure reasons.

Severity Findings
Major 1

Findings

  • Major · Non-string failure identifiers can match an admitted stage/kind form
    src/failure-reason.mjs:75

Earlier findings: 2 addressed

Mention @tessl-code-review to run another review.

Comment thread src/failure-reason.mjs Outdated
A one-element array carrying an admitted value stringifies to the admitted key,
so matching the joined form was not on its own evidence that the document said
what it appeared to say. Each field is now checked as a string first.
@colinhallett

Copy link
Copy Markdown
Contributor Author

e3cb220 checks each identifier as a string before joining. Reply on the thread.

@tessl-code-review

(from Claude)

@tessl-code-review tessl-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tessl Code Review

Changes requested (1)

The public failure-reason allowlist can still be impersonated by moving its delimiter into one identifier, allowing an arbitrary CLI message into the check summary and failure notice. The admission check must recognize only the intended stage and kind values as a pair while retaining the scalar-identifier boundary.

Severity Findings
Major 1

Findings

  • Major · A delimiter inside a failure identifier can impersonate an allowlisted pair
    src/failure-reason.mjs:81
    Still applies. Discussion continues on the existing review comment thread.

Earlier findings: 1 still applies · 2 addressed

Mention @tessl-code-review to run another review.

Joining the two into one lookup key made the key forgeable by whatever the
fields contained: a one-element array stringified into it, and so did a
delimiter inside a field. A stage now names the kinds admitted within it, and
the two are compared as separate values, so no combination of contents composes
the appearance of an admitted pair.
@colinhallett

Copy link
Copy Markdown
Contributor Author

Addressed in 20a4fbd.

The delimiter finding from the last round has no thread of its own — the review summary reported it as "still applies" against the thread for the stage/kind finding, which had already been answered as addressed, so there was nowhere to reply. Answering it here.

The claim is right: joining the two identifiers into one lookup key made the key forgeable by whatever the fields contained, first through array coercion and then through a delimiter inside a field. Both are instances of the same flaw, which is the join itself.

There is no join any more. A stage names the kinds admitted within it, and the two are compared as separate values:

if (!SURFACED_FAILURES.get(failure.stage)?.has(failure.kind)) return undefined;

No delimiter exists to place inside a field, and the string checks on stage, kind and message stay. Covered by tests for a forged pair in each position, an admitted kind at another stage, non-string identifiers, and a kind this revision does not name. 157 tests pass.

@tessl-code-review

(from Claude)

@tessl-code-review tessl-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tessl Code Review

Changes approved

The failure-reason publication boundary now requires scalar stage and kind values that match an explicit pair, and it removes Unicode format controls before rendering. The change is sound; no outstanding work remains.

Earlier findings: 3 addressed

Mention @tessl-code-review to run another review.

@colinhallett
colinhallett merged commit 6849753 into main Aug 28, 2026
3 checks passed
@colinhallett
colinhallett deleted the surface-configuration-failure-reason branch August 28, 2026 14:19
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.

1 participant