Skip to content

veche: add --json-out PATH, and say so on stderr when the gate skips the council (the twin is silently ignored) #2139

Closed

Description

@remigiusz-antczak

Problem

Scripts and agents that call veche.py have to read its result from stdout, which breaks in two ways.

  1. The COUNCIL_BLOCKING_WORK alarm goes to stderr just before the record is printed (.claude/skills/idea-critic/scripts/veche.py:391-393), and so does the WARN packet is outside --root line (:182). The alarm fires on every COULD_NOT_DECIDE and on any council slower than --alarm-s (:389). In those runs a caller that redirects veche.py ... > result.json 2>&1 gets a file whose first line is not JSON, and json.load fails at character 0. We ended up writing a small extractor that scans a mixed log for the last {...} that contains a verdict.
  2. The convene gate is silent (:205-208). With --stakes medium --reversibility easy --twin the script returns NOT_CONVENED (exit 4) in zero seconds, prints nothing on stderr, runs no seat, and ignores --twin (cost.calls is 0). The caller learns why only by reading reason in the record. --stakes and --reversibility have no help= text (:363-364); the rule that a call is skipped unless stakes are high or reversibility is hard appears only in references/council.md:11-14.

Minimal check for point 2:

python3 /path/to/perun/.claude/skills/idea-critic/scripts/veche.py --cmd false \
  --claim c --steelman s --stakes medium --reversibility easy --twin > out.json
echo "exit $?"        # 4, with nothing on stderr; out.json has "verdict": "NOT_CONVENED" and "calls": 0

Proposal

  1. --json-out PATH: write the final record there (write a temporary file, then rename) on every outcome, including COULD_NOT_DECIDE and NOT_CONVENED. Stdout keeps printing it as today.
  2. On NOT_CONVENED, print one stderr line, for example NOT_CONVENED: stakes=medium reversibility=easy; no seat ran, --twin ignored (declare --stakes high or --reversibility hard to convene).
  3. Add help= to --stakes and --reversibility that states the gate rule.
  4. Open question for the maintainers: should --twin still run alone on a skipped claim, since it is the cheapest way to get a dissent for a medium-stakes call?

Why not a different pack

This is about the output contract of Perun's own veche.py. The script already builds one JSON record, so the smallest step is to also write it to a file and to say out loud when it skipped the work.

Confidentiality

No secrets, real names, or internal IDs.

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).

    1. --json-out PATH writes the record on every outcome, including COULD_NOT_DECIDE and NOT_CONVENED, through a temp file in the same directory and os.replace, so a reader never sees half a record. Stdout still prints it.
    2. A skipped council prints veche: NOT_CONVENED, no seat called (<reason>) on stderr, followed by ; --twin ignored when --twin was given.
    3. --stakes and --reversibility have help text that states the gate: the council is skipped only when stakes are low or medium and reversibility is easy.
    4. On the open question, behavior is unchanged: a skipped council runs no seat, including the twin, and the stderr line now says so.

    Covered by scripts/test_veche_surface.py, scripts/test_veche_runtime.py, scripts/test_veche_json_out_exit_code.py and scripts/test_veche_json_out_late_write.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 roadmapenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions