Skip to content

[AI-2935] Report the harness list as JSON - #999

Merged
alexeyzimarev merged 6 commits into
mainfrom
georgepayne/ai-2935-harness-list-json
Sep 18, 2026
Merged

alexeyzimarev merged 6 commits into
mainfrom
georgepayne/ai-2935-harness-list-json

Conversation

@George-Payne

Copy link
Copy Markdown
Member

What

kcap harness list --json emits the detected / wired / dismissed report as one JSON document on stdout and nothing else — the contract kcap import --discover --json already sets. The human table is untouched.

{"harnesses":[{"vendor":"claude","label":"Claude Code","binary_on_path":true,
               "config_found":false,"wired":false,"dismissed":false}]}

Why

First step of AI-2935. A tool setting kcap up for someone has to ask which coding agents to record, and the only honest option list is the one this machine produces. That list already exists — it drives the new-harness nudges — but only as a formatted table, so anything reading it is parsing display text.

Shape, and why

  • Every known harness is listed, present or not. A consumer can then tell "unsupported" from "not installed here" without carrying its own vendor list and going stale the day a vendor is added.
  • The two detection signals stay apart — binary_on_path and config_found — rather than being ORed the way HarnessInventory folds them. Same reasoning as FirstRunHarnessReport: a caller offering someone a choice can say which signal it saw, and one that only wants "is it here" ORs them itself.
  • vendor is the stable key, the id dismiss and reset take. label is display text.
  • --json is refused on dismiss and reset rather than ignored, mirroring the import --discover guard — ignoring it would hand a caller expecting JSON a line of prose on a subcommand that writes.

Testing

Five unit tests on the renderer, in the style of ServiceStatusJsonTests: registry order and completeness, snake_case keys, the two signals staying apart, an absent harness, and a dismissal read back from the ledger. Ran the real command in the dev container for both the list and the refusal.

A tool setting kcap up has to ask which coding agents to record, and the
option list it should offer is the one this machine produces. `kcap harness
list --json` emits that report as one document on stdout and nothing else,
the contract `kcap import --discover --json` set.

Every known harness is listed, present or not, so a consumer can tell
unsupported from not-installed without carrying its own vendor list. The two
detection signals stay apart rather than being ORed, as they are in the
first-run machine report: a caller naming the signal it saw needs both.
`--json` is refused on `dismiss` and `reset` rather than ignored.
@linear-code

linear-code Bot commented Sep 18, 2026

Copy link
Copy Markdown

AI-2935

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add machine-readable JSON output to harness list

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 10-20 Minutes

Grey Divider

AI Description

• Adds JSON output for every known harness while preserving the human-readable table.
• Reports binary, configuration, wiring, and dismissal states as distinct fields.
• Rejects --json on mutating subcommands and documents/tests the output contract.
Diagram

graph TD
  CLI["list --json"] --> Command["Harness command"] --> Registry["Harness registry"] --> Renderer["JSON renderer"] --> Output["stdout JSON"]
  Ledger[("Offer ledger")] --> Renderer
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reuse the first-run machine report
  • ➕ Shares the existing projection of binary, configuration, and wiring signals.
  • ➕ Reduces duplicated harness-state evaluation logic.
  • ➖ Its dictionary-based wire shape lacks labels and stores dismissals separately.
  • ➖ Its already_wired contract differs from the requested wired field.
  • ➖ It would couple a stable CLI output contract to the first-run transport model.

Recommendation: Keep the dedicated harness-list DTO and pure renderer. It matches the command-specific contract, follows the existing source-generated CLI JSON pattern, and avoids coupling this public output to first-run API semantics; a shared internal state projection is only warranted if more consumers duplicate this evaluation.

Files changed (5) +168 / -3

Enhancement (2) +70 / -3
HarnessCommand.csRoute harness list requests to JSON output +16/-3

Route harness list requests to JSON output

• Recognizes '--json' for the list subcommand and emits the new renderer output without changing the human table. Rejects the option for dismiss, reset, help, or unknown subcommands to prevent prose or mutations when JSON is expected.

src/Capacitor.Cli/Commands/HarnessCommand.cs

HarnessListJson.csDefine and render the harness inventory JSON payload +54/-0

Define and render the harness inventory JSON payload

• Introduces source-generated snake_case JSON records and a pure renderer. The renderer lists every registry harness in order with separate binary/configuration signals plus wiring and ledger dismissal state.

src/Capacitor.Cli/Commands/HarnessListJson.cs

Tests (1) +68 / -0
HarnessListJsonTests.csCover the harness JSON renderer contract +68/-0

Cover the harness JSON renderer contract

• Tests registry completeness and ordering, snake_case fields, independent detection signals, absent harnesses, and persisted dismissals.

test/Capacitor.Cli.Tests.Unit/Commands/HarnessListJsonTests.cs

Documentation (2) +30 / -0
README.mdDocument machine-readable harness inventory output +16/-0

Document machine-readable harness inventory output

• Adds the 'kcap harness list --json' invocation, example payload, stdout guarantee, stable vendor identifier, and detection-field semantics.

README.md

CHANGES.mdRecord the harness JSON contract and rationale +14/-0

Record the harness JSON contract and rationale

• Explains why all known harnesses are emitted, why detection signals remain separate, and why '--json' is rejected for mutating subcommands.

docs/CHANGES.md

@qodo-code-review

qodo-code-review Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (2) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Informational

1. Four primary types share one file 📘 Rule violation ⚙ Maintainability
Description
HarnessListJson.cs declares HarnessListEntryJson, HarnessListJson, HarnessListJsonContext,
and HarnessListRender as top-level module-visible types even though the file is named for only one
of them. Readers looking up the entry, serializer context, or renderer by the repository's
type-to-file convention must instead discover them in a differently named file, making ownership and
future edits less predictable.
Code

src/Capacitor.Cli/Commands/HarnessListJson.cs[R21-22]

+public sealed record HarnessListEntryJson(
+    string Vendor, string Label, bool BinaryOnPath, bool ConfigFound, bool Wired, bool Dismissed);
Relevance

● Weak

Recent precedent rejected moving related public types into separate files under the one-primary-type
rule.

PR-#817

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 3162234 requires one primary type per file unless a narrow documented exception
applies. The new file defines two payload records, a serializer context, and a renderer as separate
top-level types, while its filename matches only HarnessListJson.

Rule 3162234: One primary type per file, with only narrow documented exceptions
src/Capacitor.Cli/Commands/HarnessListJson.cs[21-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`HarnessListJson.cs` contains four top-level primary types and only one matches the filename; none of the permitted multi-type exceptions applies.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/HarnessListJson.cs[21-37]

## Recommended Fix
Move each top-level type into a matching file: `HarnessListEntryJson.cs`, `HarnessListJson.cs`, `HarnessListJsonContext.cs`, and `HarnessListRender.cs`, preserving their namespace and visibility.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. One test bypasses temp-dir injection 📘 Rule violation ▣ Testability
Description
Keeps_the_two_detection_signals_apart creates and disposes TempDir directly instead of declaring
the test class's required [TempDir] property. When TUnit runs this method, the directory lifecycle
sits outside framework injection and the class mixes a manual fixture with the suite's
injected-fixture convention.
Code

test/Capacitor.Cli.Tests.Unit/Commands/HarnessListJsonTests.cs[40]

+        using var bin = new TempDir();
Relevance

● Weak

Recent precedent rejected replacing direct new TempDir() calls with injected properties in tests.

PR-#972

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2808173 requires test classes to receive temporary directories through a public
required [TempDir] property and prohibits new TempDir() calls. The changed test constructs one
directly at line 40.

Rule 2808173: Use injected [TempDir] public required property in test classes instead of manual fields
test/Capacitor.Cli.Tests.Unit/Commands/HarnessListJsonTests.cs[36-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`HarnessListJsonTests` manually constructs a `TempDir`, bypassing the test framework's injected lifecycle management.

## Fix Focus Areas
- test/Capacitor.Cli.Tests.Unit/Commands/HarnessListJsonTests.cs[8-44]

## Recommended Fix
Add a public required `TempDir` property annotated with `[TempDir]` to the test class, replace the local `bin` instance with that property, and remove manual construction and disposal.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
Review mode: ⚖️ Balanced: This adds a public CLI JSON contract and validation across command dispatch, serialization, detection, and dismissal state, creating meaningful behavioral risk but not enough independent complexity to warrant extended review.

Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

…arness-list-json

# Conflicts:
#	docs/CHANGES.md
@realtonyyoung

Copy link
Copy Markdown
Collaborator

NO FINDINGS. Reviewed the harness-list JSON shape, detection/wiring states, dismissal handling, and the added coverage by inspection. I did not build or run tests.

realtonyyoung
realtonyyoung previously approved these changes Sep 18, 2026

@realtonyyoung realtonyyoung 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.

Approved after code review; no actionable findings. Builds and tests were not run.

alexeyzimarev and others added 4 commits September 18, 2026 14:35
TUnit compares collections without regard to order unless told otherwise, so the
assertion has to ask for matching order to fail on a renderer that sorts its rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The payload records and their serializer context stay together: they are one wire
shape and only make sense read side by side.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator

NO FINDINGS on the updated head. I reviewed the four follow-up commits (ordering assertion, help text, renderer file split, and comment wording); they introduce no new actionable issue. I did not build or run tests.

@realtonyyoung realtonyyoung 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.

Approved the updated head after reviewing the follow-up commits. No actionable findings; no build or tests run.

@alexeyzimarev
alexeyzimarev merged commit 9fec093 into main Sep 18, 2026
8 checks passed
@alexeyzimarev
alexeyzimarev deleted the georgepayne/ai-2935-harness-list-json branch September 18, 2026 12:50
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.

3 participants