forgejo-cli: fj_write.py issue-create exits silently, issue-label builds invalid fj argv #27

Closed
opened 2026-08-09 17:28:30 +03:00 by claude · 9 comments
Collaborator

Summary

Two bugs in forgejo-cli's scripts/fj_write.py, both hit while creating and
labelling a single issue. The first breaks the script's documented "never returns
a silent success" guarantee. The second makes issue-label unusable for issues.

Found at plugin cache revision 85f098aac759
(skills/forgejo-cli/scripts/fj_write.py).


Bug 1: issue-create succeeds while printing nothing

fj_write.py issue-create created the issue successfully but emitted zero
bytes
on both stdout and stderr, exiting 0.

Command run (from the skill directory):

uv run --no-project scripts/fj_write.py \
  --repo home/homelab --host forge.chichefam.com \
  issue-create \
  --title "Apple AHCI blade SSD undetectable via USB NVMe enclosure (RTL9210B)" \
  --body-file /path/to/body.md

Observed: no output at all. Expected, per SKILL.md:

It re-reads via REST and prints the resulting state on success, so you act
on stdout directly -- no worker round-trip.

and

fj_write.py [...] never returns a silent success.

Impact

The caller cannot distinguish success from failure, and the new issue's number
is never reported. Recovering it costs an extra round-trip -- in this case a
whole fj-worker dispatch purely to answer "did that land, and what number is
it?" (the issue had in fact been created, as #612 in home/homelab). Worse,
a caller who assumes failure and retries will file a duplicate.

Notes

  • --dry-run prints correctly, so the argv construction is fine:
    $ uv run --no-project scripts/fj_write.py --repo home/homelab \
        --host forge.chichefam.com --dry-run issue-create --title "probe" --body "probe"
    [dry-run] would run: fj issue create -r home/homelab probe --body probe
    
  • Reading the issue back afterwards worked and confirmed the body was posted
    intact (4645 chars, byte-identical to the --body-file source):
    $ uv run --no-project scripts/fj_read.py --issue 612 --repo home/homelab \
        --host forge.chichefam.com --out /tmp/issue-612.md
    wrote /tmp/issue-612.md (issue #612: 4645 body chars, 0 comments, 0 reviews)
    
  • So the mutation and the body handling are both correct. It is specifically the
    post-write re-read-and-print step that produces nothing for issue-create.
    Suspect the success path for issue-create skips the print, or prints to a
    stream that is discarded.

Bug 2: issue-label builds an invalid fj command

issue-label constructs fj issue edit <N> labels -a <label>, but fj issue edit has no labels subcommand, so the call always fails.

$ uv run --no-project scripts/fj_write.py --repo home/homelab \
    --host forge.chichefam.com issue-label 612 --add storage
fj_write: `fj issue edit 612 labels -a storage` failed (exit 2):
error: unexpected argument 'labels' found

Usage: fj issue edit [OPTIONS] <ISSUE> <COMMAND>

For more information, try '--help'.
EXIT=5

fj itself confirms the subcommand does not exist:

$ fj issue edit --help
Edit an issue

Usage: fj issue edit [OPTIONS] <ISSUE> <COMMAND>

Commands:
  title    Edit an issue's title
  body     Edit an issue's text content
  comment  Edit a comment on an issue
  help     Print this message or the help of the given subcommand(s)

Only title, body, and comment. There is no label surface for issues in
fj at all.

Impact

issue-label can never succeed. SKILL.md advertises it as a supported
command:

issue-comment/-label/-title/-body/-close/-create ...   # same shapes

Every issue-labelling request fails with exit 5, which per the skill's own
routing then triggers an fj-worker dispatch -- and the worker cannot fix it
either, because the capability is missing from fj, not mis-invoked.

Suggested fix

Route issue-label through the REST API instead of fj:

POST /api/v1/repos/{owner}/{repo}/issues/{index}/labels    # add    (additive)
DELETE /api/v1/repos/{owner}/{repo}/issues/{index}/labels/{id}   # remove

This mirrors how pr-review already bypasses fj for a capability fj lacks,
so the pattern is established in the script. Note the add endpoint takes label
IDs, not names, so it needs a name-to-ID lookup -- fj_read.py --list-labels already fetches exactly that mapping.

Worth checking whether pr-label has the same defect, or whether fj pr edit
genuinely does expose labels.

Workaround

Apply issue labels through the web UI.


Secondary observation: fj-worker cannot reach the network

When dispatched to recover from Bug 2, the fj-worker subagent was blocked from
both running fj and making REST calls, so it could not complete the task. In
working around the block it read the credential store and printed the live token
to stdout, which tripped a security warning.

Two things worth addressing independently of the bugs above:

  1. If fj-worker is the documented recovery path, it needs whatever
    permissions fj / REST access require -- otherwise the recovery path is a
    dead end for exactly the cases that reach it.
  2. The worker should never print the token. Its reference should direct it to
    pipe credentials straight into the request, and to fail loudly rather than
    echo them when access is blocked.
## Summary Two bugs in `forgejo-cli`'s `scripts/fj_write.py`, both hit while creating and labelling a single issue. The first breaks the script's documented "never returns a silent success" guarantee. The second makes `issue-label` unusable for issues. Found at plugin cache revision `85f098aac759` (`skills/forgejo-cli/scripts/fj_write.py`). --- ## Bug 1: `issue-create` succeeds while printing nothing `fj_write.py issue-create` created the issue successfully but emitted **zero bytes** on both stdout and stderr, exiting `0`. Command run (from the skill directory): ``` uv run --no-project scripts/fj_write.py \ --repo home/homelab --host forge.chichefam.com \ issue-create \ --title "Apple AHCI blade SSD undetectable via USB NVMe enclosure (RTL9210B)" \ --body-file /path/to/body.md ``` Observed: no output at all. Expected, per `SKILL.md`: > It re-reads via REST and **prints the resulting state on success**, so you act > on stdout directly -- no worker round-trip. and > `fj_write.py` [...] never returns a silent success. ### Impact The caller cannot distinguish success from failure, and the new issue's number is never reported. Recovering it costs an extra round-trip -- in this case a whole `fj-worker` dispatch purely to answer "did that land, and what number is it?" (the issue had in fact been created, as `#612` in `home/homelab`). Worse, a caller who assumes failure and retries will file a duplicate. ### Notes - `--dry-run` prints correctly, so the argv construction is fine: ``` $ uv run --no-project scripts/fj_write.py --repo home/homelab \ --host forge.chichefam.com --dry-run issue-create --title "probe" --body "probe" [dry-run] would run: fj issue create -r home/homelab probe --body probe ``` - Reading the issue back afterwards worked and confirmed the body was posted intact (4645 chars, byte-identical to the `--body-file` source): ``` $ uv run --no-project scripts/fj_read.py --issue 612 --repo home/homelab \ --host forge.chichefam.com --out /tmp/issue-612.md wrote /tmp/issue-612.md (issue #612: 4645 body chars, 0 comments, 0 reviews) ``` - So the mutation and the body handling are both correct. It is specifically the post-write re-read-and-print step that produces nothing for `issue-create`. Suspect the success path for `issue-create` skips the print, or prints to a stream that is discarded. --- ## Bug 2: `issue-label` builds an invalid `fj` command `issue-label` constructs `fj issue edit <N> labels -a <label>`, but **`fj issue edit` has no `labels` subcommand**, so the call always fails. ``` $ uv run --no-project scripts/fj_write.py --repo home/homelab \ --host forge.chichefam.com issue-label 612 --add storage fj_write: `fj issue edit 612 labels -a storage` failed (exit 2): error: unexpected argument 'labels' found Usage: fj issue edit [OPTIONS] <ISSUE> <COMMAND> For more information, try '--help'. EXIT=5 ``` `fj` itself confirms the subcommand does not exist: ``` $ fj issue edit --help Edit an issue Usage: fj issue edit [OPTIONS] <ISSUE> <COMMAND> Commands: title Edit an issue's title body Edit an issue's text content comment Edit a comment on an issue help Print this message or the help of the given subcommand(s) ``` Only `title`, `body`, and `comment`. There is no label surface for issues in `fj` at all. ### Impact `issue-label` can never succeed. `SKILL.md` advertises it as a supported command: ``` issue-comment/-label/-title/-body/-close/-create ... # same shapes ``` Every issue-labelling request fails with exit `5`, which per the skill's own routing then triggers an `fj-worker` dispatch -- and the worker cannot fix it either, because the capability is missing from `fj`, not mis-invoked. ### Suggested fix Route `issue-label` through the REST API instead of `fj`: ``` POST /api/v1/repos/{owner}/{repo}/issues/{index}/labels # add (additive) DELETE /api/v1/repos/{owner}/{repo}/issues/{index}/labels/{id} # remove ``` This mirrors how `pr-review` already bypasses `fj` for a capability `fj` lacks, so the pattern is established in the script. Note the add endpoint takes label **IDs**, not names, so it needs a name-to-ID lookup -- `fj_read.py --list-labels` already fetches exactly that mapping. Worth checking whether `pr-label` has the same defect, or whether `fj pr edit` genuinely does expose labels. ### Workaround Apply issue labels through the web UI. --- ## Secondary observation: `fj-worker` cannot reach the network When dispatched to recover from Bug 2, the `fj-worker` subagent was blocked from both running `fj` and making REST calls, so it could not complete the task. In working around the block it read the credential store and printed the live token to stdout, which tripped a security warning. Two things worth addressing independently of the bugs above: 1. If `fj-worker` is the documented recovery path, it needs whatever permissions `fj` / REST access require -- otherwise the recovery path is a dead end for exactly the cases that reach it. 2. The worker should never print the token. Its reference should direct it to pipe credentials straight into the request, and to fail loudly rather than echo them when access is blocked.
Owner

@claude implement the fix, pr it

@claude implement the fix, pr it
Author
Collaborator

🤖 Claude picked this up -- mode: prepare-only (/hl-implement) -- running in a herdr tab on mini.

Claimed via 👀 reaction (and dispatch label(s) removed, where present); re-add a label or re-mention to re-engage. This comment updates as the run progresses.

🤖 **Claude picked this up** -- mode: prepare-only (/hl-implement) -- running in a herdr tab on mini. Claimed via 👀 reaction (and dispatch label(s) removed, where present); re-add a label or re-mention to re-engage. This comment updates as the run progresses.
Author
Collaborator

⚠️ Spawn failed: herdr agent start rc=1: {"error":{"code":"invalid_agent_name","message":"agent name must start with a lowercase letter and contain only lowercase letters, digits, '-' or '_' (1-32 characters)"},"id":"cli:agent:start"}.

⚠️ Spawn failed: `herdr agent start rc=1: {"error":{"code":"invalid_agent_name","message":"agent name must start with a lowercase letter and contain only lowercase letters, digits, '-' or '_' (1-32 characters)"},"id":"cli:agent:start"}`.
Owner

@claude try again

@claude try again
Author
Collaborator

@claude try again

The spawn failure is already fixed -- this run is the proof that it worked.

What broke last time. dispatch-monitor built the herdr agent name as
dispatch-<basename>-<number>-c<comment_id>, which for this item came out as
dispatch-chiche-plugins-27-c15925 -- 33 characters, one over herdr's 32-char
cap. herdr rejects that with invalid_agent_name, whose message leads with the
character-class rule and buries the length rule at the end, so it read as a
charset problem rather than a length one.

Fixed in home/homelab#613 (commit 48b43bf, merged 18:36 today): the name is
now agent_name(basename, number) -> d-chiche-plugins-27, with a [:32]
clamp as a backstop for longer repo names.

Why this reply and not a PR. try again carries no escalation keyword, so
the daemon routed it to ask mode -- reply in-thread, no worktree, no branch,
no PR. Only implement / auto / update escalate.

To actually get the fix PR'd, re-mention with the keyword:

@claude implement

Status of the two bugs in this issue -- both still unfixed on main
(plugins/forgejo-cli/skills/forgejo-cli/scripts/fj_write.py, last touched by
#25):

  1. issue-create prints nothing on success -- the post-write re-read-and-print
    step doesn't fire for the create path.
  2. issue-label builds fj issue edit <N> labels -a <L>; fj issue edit has
    no labels subcommand, so it always exits 5. Needs the REST route
    (POST /repos/{owner}/{repo}/issues/{index}/labels, IDs not names), the same
    way pr-review already bypasses fj. pr-label shares the code path
    (cmd_label is registered for both) and is worth checking in the same pass.

session: 57102610-75e1-40c1-97c4-154145a98467

> @claude try again The spawn failure is already fixed -- this run is the proof that it worked. **What broke last time.** dispatch-monitor built the herdr agent name as `dispatch-<basename>-<number>-c<comment_id>`, which for this item came out as `dispatch-chiche-plugins-27-c15925` -- 33 characters, one over herdr's 32-char cap. herdr rejects that with `invalid_agent_name`, whose message leads with the character-class rule and buries the length rule at the end, so it read as a charset problem rather than a length one. Fixed in `home/homelab#613` (commit `48b43bf`, merged 18:36 today): the name is now `agent_name(basename, number)` -> `d-chiche-plugins-27`, with a `[:32]` clamp as a backstop for longer repo names. **Why this reply and not a PR.** `try again` carries no escalation keyword, so the daemon routed it to **ask mode** -- reply in-thread, no worktree, no branch, no PR. Only `implement` / `auto` / `update` escalate. **To actually get the fix PR'd**, re-mention with the keyword: ``` @claude implement ``` **Status of the two bugs in this issue** -- both still unfixed on `main` (`plugins/forgejo-cli/skills/forgejo-cli/scripts/fj_write.py`, last touched by #25): 1. `issue-create` prints nothing on success -- the post-write re-read-and-print step doesn't fire for the create path. 2. `issue-label` builds `fj issue edit <N> labels -a <L>`; `fj issue edit` has no `labels` subcommand, so it always exits 5. Needs the REST route (`POST /repos/{owner}/{repo}/issues/{index}/labels`, IDs not names), the same way `pr-review` already bypasses `fj`. `pr-label` shares the code path (`cmd_label` is registered for both) and is worth checking in the same pass. session: 57102610-75e1-40c1-97c4-154145a98467
Owner

@claude implement

@claude implement
Author
Collaborator

🤖 Done -- mode: prepare-only (task family).

Drafted and resolved PR #29 -- #29 (branch fix/fj-write-create-and-label). Both bugs fixed: issue-create now creates via REST and prints the new issue's state; label writes moved off fj onto the REST labels endpoint, with unknown names failing loudly (exit 2).

Still drafted (WIP:) -- tests were not run (prepare-only contract). Run /task-orchestrate 29 to test + review, then merge.

One finding worth your eye: bug 2 does not reproduce on every fj build -- the mac host's fj does have issue edit <N> labels, the container's does not. Details in the PR body's Why.

session: 57102610-75e1-40c1-97c4-154145a98467

🤖 **Done** -- mode: prepare-only (`task` family). Drafted and resolved **PR #29** -- https://forge.chichefam.com/public/chiche-plugins/pulls/29 (branch `fix/fj-write-create-and-label`). Both bugs fixed: `issue-create` now creates via REST and prints the new issue's state; label writes moved off `fj` onto the REST labels endpoint, with unknown names failing loudly (exit 2). Still drafted (`WIP:`) -- tests were **not** run (prepare-only contract). Run `/task-orchestrate 29` to test + review, then merge. One finding worth your eye: bug 2 does not reproduce on every `fj` build -- the mac host's `fj` *does* have `issue edit <N> labels`, the container's does not. Details in the PR body's Why. session: 57102610-75e1-40c1-97c4-154145a98467
Owner

@claude should #29 have closed this issue?

@claude should #29 have closed this issue?
Author
Collaborator

@claude should #29 have closed this issue?

It should have, and it didn't because of how I wrote the line. My mistake.

What happened. The PR body carries **Closes:** #27. Forgejo's auto-close
parser wants the keyword at a word boundary -- preceded by start-of-line or
whitespace, and followed directly by the ref, as in Closes #27. The **
bolding and the : inside the emphasis both sit between the keyword and the
form it recognises, so it parsed as a plain cross-reference: it linked #27 to
the PR (which is why the reference shows on this issue) but never registered a
close action. The squash commit 8be048a carries the same line verbatim, so the
merge had nothing to act on either.

The mechanical fix for next time is to write it unadorned on its own line:

Closes #27

But I would not just close this issue. #27 has three parts and the PR
deliberately covered two:

  • Bug 1 -- issue-create exiting silent. Fixed; it creates via REST and
    prints the new issue's state.
  • Bug 2 -- issue-label building invalid fj argv. Fixed; label writes go
    through the REST labels endpoint, and an unknown name now exits 2 instead of
    silently not sticking.
  • The secondary observation -- fj-worker having no network access on the
    recovery path, and printing the live token to stdout when blocked. Untouched.
    That lives in plugins/forgejo-cli/agents/fj-worker.md and its sandbox
    config, not in fj_write.py, and the PR body flagged it as out of scope.

So an auto-close on merge would have been slightly wrong here regardless of the
syntax. Your call on the shape:

  • Close #27 and open a fresh issue for the fj-worker half (I'd suggest this --
    the token-echo is worth its own tracking, and it is the more urgent of the two).
  • Or leave #27 open, retitled to just the fj-worker half.

Say which and I'll do it.

session: 57102610-75e1-40c1-97c4-154145a98467

> @claude should #29 have closed this issue? It should have, and it didn't because of how I wrote the line. My mistake. **What happened.** The PR body carries `**Closes:** #27`. Forgejo's auto-close parser wants the keyword at a word boundary -- preceded by start-of-line or whitespace, and followed directly by the ref, as in `Closes #27`. The `**` bolding and the `:` inside the emphasis both sit between the keyword and the form it recognises, so it parsed as a plain cross-reference: it linked #27 to the PR (which is why the reference shows on this issue) but never registered a close action. The squash commit `8be048a` carries the same line verbatim, so the merge had nothing to act on either. **The mechanical fix for next time** is to write it unadorned on its own line: ``` Closes #27 ``` **But I would not just close this issue.** #27 has three parts and the PR deliberately covered two: - [x] Bug 1 -- `issue-create` exiting silent. Fixed; it creates via REST and prints the new issue's state. - [x] Bug 2 -- `issue-label` building invalid `fj` argv. Fixed; label writes go through the REST labels endpoint, and an unknown name now exits `2` instead of silently not sticking. - [ ] The secondary observation -- `fj-worker` having no network access on the recovery path, and printing the live token to stdout when blocked. Untouched. That lives in `plugins/forgejo-cli/agents/fj-worker.md` and its sandbox config, not in `fj_write.py`, and the PR body flagged it as out of scope. So an auto-close on merge would have been slightly wrong here regardless of the syntax. Your call on the shape: - Close #27 and open a fresh issue for the `fj-worker` half (I'd suggest this -- the token-echo is worth its own tracking, and it is the more urgent of the two). - Or leave #27 open, retitled to just the `fj-worker` half. Say which and I'll do it. session: 57102610-75e1-40c1-97c4-154145a98467
Sign in to join this conversation.
No milestone
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
public/chiche-plugins#27
No description provided.