spec-kitty / spec-kitty/spec-kitty

Bug: `spec-kitty implement --json` allocator contaminates stdout with Rich output and reports result:null (ANSI in error) on a SUCCESSFUL lane allocation

Open
#2,605 1 comment 0 reactions 0 assignees View on GitHub
priority:P1 reliability usability workflow
Dominant language
Python
Stars
1.6k
Forks
165
Avg merge
14h 22m
Merged PRs (30d)
343

Description

**Parent:** #2017 (implement-loop friction / guards-and-contracts umbrella).
**Direct sibling (same loop-friction campaign, allocation-logic facet):** #2570 — that issue tracks the allocator's *serialization/self-dirty* frictions and explicitly does **not** cover the `--json` output-contract facet reported here.
**Contract lineage (all CLOSED; this is a live recurrence on a surface they did not cover):** #1641 (JSON commands must keep stdout machine-clean), #1336 (charter subcommands emit non-JSON despite `--json`), #1891 (`agent action implement` rejected `--json`).

## Summary

`spec-kitty implement WP## --mission --acknowledge-not-bulk-edit --json` does **not** honor the `--json` machine-output contract. Instead of emitting exactly one JSON document on stdout, it interleaves Rich/human-formatted output on stdout, and — worse — when a JSON object *is* emitted it reports the allocation as a **failure on an allocation that actually succeeded**. Any programmatic `--json` consumer (i.e. the exact implement-review orchestration this repo ships) cannot parse the allocator result.

Verified live **2026-07-12 on the current upstream/main tip** while orchestrating mission `loop-friction-quickwins-2-01KXBWA4` ("Implement-Loop Friction Quick-Wins II", coord topology, 6 parallel WP lanes).

## Symptoms

In `--json` mode the following are written to **stdout** (not stderr, not suppressed):

- The **"Bulk Edit Inference Warning"** Rich panel.
- The **"Lane worktree ready"** progress tree.
- The **"CRITICAL: cd to the lane worktree"** banner.
- The lane test-env `export SPEC_KITTY_TEST_DB_NAME=…` block.

And when a JSON object *was* emitted:

- It embedded **ANSI-escaped Rich text inside the `error` field** with `result: null` — **even though the allocation succeeded** (the lane worktree was in fact created).

**Net effect:** `json.load(stdin)` failed outright (mixed Rich text + JSON), or yielded `{"result": null, "error": ""}` on a **success**, for roughly **4 of 6** lane allocations in this run. The only reliable workaround was to ignore `--json` entirely and scrape `git worktree list` to discover which lanes were actually allocated.

## Repro

```bash
# lanes/coord-topology mission, ≥2 WP lanes, mission dir clean
spec-kitty implement WP01 --mission --acknowledge-not-bulk-edit --json \
| python -c "import json,sys; print(json.load(sys.stdin))"
# → JSONDecodeError (Rich panels precede/surround the JSON), OR
# → {'result': None, 'error': '\x1b[...ANSI Rich text...'} — despite the worktree being created
```

## Why this matters (success reads as failure)

`--json` is the machine contract for agents, CI, and external orchestrators — precisely the implement-review loop Spec Kitty ships. A consumer that trusts `--json` here will **silently record failure on a success** and take corrective action (retry, abort, re-allocate) against a worktree that already exists. This is the same class as the closed #1641 / #1336 / #1891 contract issues, now recurring live on the top-level `spec-kitty implement` **allocator** surface — a surface those fixes did not cover (#1891 only made `agent action implement` *accept* the flag; the allocator's stdout contamination + `result:null`-on-success was not addressed).

## Likely surface

`src/specify_cli/cli/commands/implement.py` output path: the human/Rich rendering (bulk-edit warning, lane worktree status tree, cd banner, test-env export block) is not suppressed nor redirected to stderr when `--json` is set, and the successful allocation result is wrapped into an `error` field instead of the `result` field.

## Suggested fix

Honor the machine-output contract on the allocator:

- In `--json` mode, emit **only a single JSON document on stdout**. Route all Rich panels/warnings/banners/export blocks to **stderr**, or suppress them.
- On a **successful** allocation, populate `result` (workspace path, branch/lane, prompt file, base_branch/base_commit) with `error: null` — never `result: null` + an ANSI-wrapped success message in `error`.
- Add a regression test asserting `json.load(stdout)` succeeds and returns a populated `result` for a successful lane allocation, and that stdout contains no ANSI/Rich content in `--json` mode. (Ideally enforce the invariant once at the typer boundary, per #1336 Option B, rather than per-handler.)

## Provenance

Surfaced live while orchestrating mission `loop-friction-quickwins-2-01KXBWA4` (Implement-Loop Friction Quick-Wins II) — a friction-hunting slice under the #2017 umbrella. Distinct facet from its allocation-logic sibling #2570.

Contributor guide

Open the contributing guide

Research direction

Start in src/specify_cli/cli/commands/implement.py and reproduce the documented --json command with the Python json parser. Trace the allocator output path and add a regression test for a successful lane allocation. Done means stdout contains one parseable JSON document with a populated result and no Rich or ANSI content.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli, testing
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.