design(ops-forgejo): issue create dumps full YAML on success, no machine-readable number - causes duplicate files #316

Closed
opened 2026-06-25 06:57:58 +00:00 by coilyco-ops · 3 comments
Member

Symptom

ward ops forgejo issue create dumps the full created-issue object as multi-line
YAML to stdout on success (assets: [], assignee: null, ... the whole record).
The one field a caller actually needs - the new issue number: - is buried in that
block, and there is no clean machine-readable handle (no --json, no terse
#N created line, no trailing url-only output).

Impact

Scripted callers can't reliably tell success from failure or grab the number.
Concretely: while filing ward#313 I grepped the create output for "number"|html_url|error,
the YAML uses number: (bare, no quotes) so the grep matched nothing, the call
read as a failure, I re-filed, and got a duplicate (ward#314, since closed). The
verb succeeded both times - the output shape just made success look like failure.

Fix options (pick one)

  • Add --json / --quiet that emits only the created number (or {owner}/{repo}#N)
    on stdout, machine-parseable, so a dispatcher can capture num=$(... --json | jq .number).
  • Or print a single terse success line (created coilyco-flight-deck/ward#N <url>) to
    stderr and nothing-but-the-number to stdout.
  • Either way, make failure distinguishable by exit code alone so callers don't have to
    grep prose.

This is the same dispatcher-ergonomics theme as ward#315 (explore can't finish a
file-and-dispatch cleanly): the ops verbs are used programmatically by agents, so their
success output needs a stable contract, not human-oriented YAML.

Found while hand-carrying ward#313/#315 from a warded explore diagnosis.

## Symptom `ward ops forgejo issue create` dumps the full created-issue object as multi-line YAML to stdout on success (`assets: []`, `assignee: null`, ... the whole record). The one field a caller actually needs - the new issue `number:` - is buried in that block, and there is no clean machine-readable handle (no `--json`, no terse `#N created` line, no trailing url-only output). ## Impact Scripted callers can't reliably tell success from failure or grab the number. Concretely: while filing ward#313 I grepped the create output for `"number"|html_url|error`, the YAML uses `number:` (bare, no quotes) so the grep matched nothing, the call read as a failure, I re-filed, and got a duplicate (ward#314, since closed). The verb succeeded both times - the output shape just made success look like failure. ## Fix options (pick one) - Add `--json` / `--quiet` that emits only the created number (or `{owner}/{repo}#N`) on stdout, machine-parseable, so a dispatcher can capture `num=$(... --json | jq .number)`. - Or print a single terse success line (`created coilyco-flight-deck/ward#N <url>`) to stderr and nothing-but-the-number to stdout. - Either way, make failure distinguishable by exit code alone so callers don't have to grep prose. This is the same dispatcher-ergonomics theme as ward#315 (explore can't finish a file-and-dispatch cleanly): the ops verbs are used programmatically by agents, so their success output needs a stable contract, not human-oriented YAML. Found while hand-carrying ward#313/#315 from a `warded explore` diagnosis.
Owner

this one => --json / --quiet

this one => --json / --quiet
Author
Member

🔒 Reserved by ward agent --driver claude — container ward-ward-issue-316-claude-a32d8f8b on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-25T07:13:20Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `ward-ward-issue-316-claude-a32d8f8b` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-25T07:13:20Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Owner

Retro from the implementer:

The code change ended up small, but most of the thinking went into where to put it. The specverb engine renders every response through a default-YAML path with no per-call output hook, and cli-guard is pinned upstream, so I couldn't fix this at the source. The seam that worked was grafting an override onto the built issue create leaf, the same trick the lean issue view override already uses.

What fought back: avoiding both infinite recursion (the internal client shells back into issue create) and double-signing the body. I circled a few designs before realizing I didn't need to re-do the POST at all. With --quiet I just force the engine to project number as bare text, capture its own stdout, and reshape it into {owner}/{repo}#N. Once that clicked it felt clean and low-risk: the default and every --output json caller are completely untouched.

Two surprises, neither in the code: the container was missing trufflehog, so I fetched the binary rather than reach for --no-verify, and docs/FEATURES.md was sitting right on its 4000-char cap, so the feature note lives in its own docs/ops-forgejo-quiet.md instead.

Confidence: high on the pure logic (the ref formatter and stdout capture are unit-tested). The one thing no test exercises is the live POST-then-project against a real Forgejo, since that needs the network. Worth a follow-up: a genuine per-call output hook in cli-guard would let both this and the view override fold back into the guardfile, and the sibling dispatcher-ergonomics issue (#315) is still open.

Retro from the implementer: The code change ended up small, but most of the thinking went into *where* to put it. The `specverb` engine renders every response through a default-YAML path with no per-call output hook, and cli-guard is pinned upstream, so I couldn't fix this at the source. The seam that worked was grafting an override onto the built `issue create` leaf, the same trick the lean `issue view` override already uses. What fought back: avoiding both infinite recursion (the internal client shells back into `issue create`) and double-signing the body. I circled a few designs before realizing I didn't need to re-do the POST at all. With `--quiet` I just force the engine to project `number` as bare text, capture its own stdout, and reshape it into `{owner}/{repo}#N`. Once that clicked it felt clean and low-risk: the default and every `--output json` caller are completely untouched. Two surprises, neither in the code: the container was missing `trufflehog`, so I fetched the binary rather than reach for `--no-verify`, and `docs/FEATURES.md` was sitting right on its 4000-char cap, so the feature note lives in its own `docs/ops-forgejo-quiet.md` instead. Confidence: high on the pure logic (the ref formatter and stdout capture are unit-tested). The one thing no test exercises is the live POST-then-project against a real Forgejo, since that needs the network. Worth a follow-up: a genuine per-call output hook in cli-guard would let both this and the view override fold back into the guardfile, and the sibling dispatcher-ergonomics issue (#315) is still open.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/ward#316
No description provided.