Skip to content

veche: a seat reply with a brace in the surrounding prose, or with scores as a second JSON object, is dropped as unparseable #2137

Closed

Description

@remigiusz-antczak

What happened

parse() slices a seat reply from the first { to the last } and calls json.loads once (.claude/skills/idea-critic/scripts/veche.py:100; the evil twin uses the same slice at :331). A reply is therefore rejected when the prose around the answer contains any brace, or when it holds two top-level JSON objects. Real claude -p replies do both: a preamble that quotes {...}, a trailing note, or the answer object followed by a second object that carries scores. A rejected seat loses its vote (:260-262), a rejected critique reply is skipped without a trace (:291-293), and a rejected twin leaves only a dropped note (:336-338). The reply contract (CONTRACT, :34-44) does not mention scores, which --option requests separately in the body (:195), so a seat that follows the contract literally emits the answer first and the scores after it. In a scan of about 690 raw replies from unattended runs, 10 (about 1.5 percent) failed with Extra data; 7 of those 10 were an answer plus a second scores object.

What you expected

Any reply that contains one valid answer object is accepted, wherever the surrounding prose puts braces. A later scores object is merged (or the contract is changed so that one object holds everything), and references/council.md states the reply contract a custom --cmd must meet.

How to reproduce

import importlib.util
spec = importlib.util.spec_from_file_location(
    "veche", "/path/to/perun/.claude/skills/idea-critic/scripts/veche.py")
veche = importlib.util.module_from_spec(spec)
spec.loader.exec_module(veche)

obj = '{"verdict": "REVISE", "claims": []}'
cases = {
    "brace in prose before the object": "Here is the {final} answer:\n" + obj,
    "brace in prose after the object": obj + "\nNote: use {curly} braces.",
    "scores as a second object": obj + '\n{"scores": {"do-nothing": {"impact": 1}}}',
    "plain prose around the object": "Sure.\n" + obj + "\nDone.",
}
for name, text in cases.items():
    try:
        print(name, "->", veche.parse(text, "claims")["verdict"])
    except Exception as e:
        print(name, "->", type(e).__name__, e)

Observed: the first three raise JSONDecodeError (Expecting property name enclosed in double quotes for the first, Extra data for the other two). The last parses.

Proposed fix (smallest)

One helper shared by parse() and the twin: scan with json.JSONDecoder().raw_decode from each {, keep the first object that is a dict with a valid verdict (for the twin: a string dissent), and, when --option is in force and that object has no scores, take scores from a later object. Add scores to CONTRACT when --option is used, and document the seat contract (one JSON object, no tool-call markup) in references/council.md next to the paragraph that says any script reading the prompt on stdin can be a seat (:81-82).

Environment

  • deep-code-review version (from .claude/skills/deep-code-review/VERSION): 1.620.0
  • Host agent (Claude Code / Cursor / Codex / other): Claude Code
  • OS: Linux

Confidentiality

This report contains no secrets, real names, internal IDs, or private hostnames.

Activity

  1. remigiusz-antczak commented on Oct 30, 2026

    @remigiusz-antczak
    CollaboratorAuthor

    Fixed across #2145 (release 1.622.0) and #2147 (release 1.623.0).

    Seat, critique and evil-twin replies are now scanned with json.JSONDecoder().raw_decode from each {, and the first object with a valid verdict (for the twin, a non-empty dissent) is used. Braces in the surrounding prose no longer drop a seat, and with --option the scores map may sit in that object or in any other object of the reply. references/council.md states the reply contract a custom --cmd must meet, as the parser accepts it. Covered by scripts/test_veche_surface.py.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:decisionsDecision quality and roadmapbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions