os-backlog: deterministic ticket-filing surface — labels/body-file in the Ruby CLI plus a filer skill or agent #186

Open
opened 2026-07-31 12:34:24 +00:00 by jared · 4 comments
Owner

Problem

Filing the seven gap-analysis tickets (#179–#185) on 2026-07-31 required the main session to hand-roll tea invocations instead of using os-backlog at all. The friction was not one missing flag; it was that the deterministic parts of ticket management have no code surface, so the session had to rediscover them.

What actually happened, in order:

  1. Reached for plugins/os-backlog/bin/issue-create — does not exist. The real entry point is bin/os-backlog issue-create, a subcommand of the dispatcher. Cost: one failed invocation plus a --help read.
  2. issue-create accepts --title/--body/[--priority] only. The tickets needed enhancement + ready-for-agent (six of them) and ready-for-human (one). No flag exists. Per skills/capture/SKILL.md:28, that is deliberate: "issue-create is structurally incapable of applying state labels; there is no flag for one, by design."
  3. So the session fell back to raw tea issues create --labels …, which meant re-deriving by hand: that this repo is forgejo:jared/cc-os (read from .cc-os/config), that tea is the tool and not gh, that the label names are exactly enhancement/ready-for-agent/ready-for-human (verified with tea labels), that tea has no --description-file so bodies must go through -d "$(cat …)", and that large bodies passed as args can hang (known quirk, hence per-call timeout 90).
  4. Verification was also hand-rolled: tea issues <n> does not print labels usefully, so confirming the labels landed took a separate tea issues ls --fields index,title,labels.

Every one of those steps is deterministic. None of them needed a model. The main session spent context and turns on mechanics, and a different session filing the same batch would rediscover all of it from scratch.

The design gap

capture is built for one use case: cheap mid-session capture of work that will not be done now, deliberately with no state labels, so the AI never pre-triages. That design is correct and should not change.

There is a second, distinct use case with no surface at all: filing a batch of already-triaged, fully-specified tickets — the output of a gap analysis, a lint-mining run, a review. These tickets are triage-complete by construction; ready-for-agent is precisely the accurate label, and withholding it makes them invisible to label-driven queues. (Three earlier tickets from the 2026-07-30 batch, #176/#177/#178, are unlabeled for what looks like exactly this reason.)

Right now that use case routes around os-backlog entirely, which loses the tracker abstraction: raw tea hardcodes forgejo, so the same workflow would silently not work in a github: or repo: project.

What to build

1. Extend the Ruby CLI so the mechanics are code. bin/os-backlog issue-create should gain:

  • --label NAME (repeatable) or --labels a,b, validated against the labels that actually exist on the resolved tracker, failing with the available list on a typo rather than silently creating an unlabeled issue.
  • --body-file PATH — reads the body from a file, so bodies never transit an argv and the tea large-body hang stops being something each caller must defend against with timeout.
  • Label round-trip verification in the returned JSON: report the labels the tracker actually attached, not the ones requested. That removes the separate tea issues ls confirmation step.
  • Ideally a --dry-run that prints the resolved tracker, slug, and final command without executing.

Hard constraint carried forward: next is human-curated per ADR-0042 and the AI never applies or removes it. A --label flag must reject next outright at the CLI level, not merely by convention in a skill body. Encoding that in code is strictly stronger than today's "there is no flag" approach, which enforces it only by omission and is why the session bypassed the CLI in the first place.

Whether this belongs on issue-create or on a sibling subcommand (e.g. issue-file, keeping capture's no-state-label guarantee untouched) is an open design question — decide it before implementing, and record it if it changes the capture contract.

2. Add a delegation surface so the main session never handles this. Either a /os-backlog:file skill or a backlog-filer subagent whose whole job is: take a set of title + body-file + labels tuples, resolve the tracker, create each issue, verify labels round-tripped, and report back the numbers and titles. The main session hands over a spec and gets back a list. The AI is present only for surprises — a label that does not exist, a tracker not configured, a partial batch failure — not for the mechanics.

A subagent is likely the better fit for batches: filing N issues is N tool calls of pure bookkeeping that should not consume main-session context. This matches the user's existing preference for handing ticket comment/close/label API sequences to a cheap subagent.

3. Fix the discoverability miss. plugins/os-backlog/CLAUDE.md names bin/os-backlog correctly, but nothing steers a session that reaches for bin/issue-create first. Whatever surface lands should be named in os-backlog's CLAUDE.md and in docs/implementation-status/os-backlog.md.

Acceptance criteria

  • Filing a labeled, triage-complete issue on this repo requires exactly one bin/os-backlog invocation with no raw tea anywhere, and works unchanged against a github: tracker.
  • A body of arbitrary size is passed by file path, never through argv.
  • --label next is rejected by the CLI with a clear message.
  • An invalid label name fails loudly and lists the valid ones.
  • The command's output states which labels the tracker actually attached.
  • The delegation surface (skill or agent) files a multi-issue batch and reports numbers + titles, without the main session composing tracker commands.

Verify

Re-file a two-issue batch through the new surface, then confirm with tea issues ls --fields index,title,labels that both carry their intended labels. Then repeat the --label next case and confirm rejection.

Provenance

Surfaced 2026-07-31 while filing #179–#185, the output of a gap analysis of os-aidd-lint's clean scan on plugins/cc-architect/skills/cc-architect/SKILL.md. Related: plugins/os-backlog/bin/os-backlog (issue-create at ~line 200), plugins/os-backlog/skills/capture/SKILL.md, ADR-0042 (git-issues-only, next is human-only), .cc-os/config. Reported directly by Jared.

Related unlabeled issues from the prior batch that this would have prevented: #176, #177, #178.

## Problem Filing the seven gap-analysis tickets (#179–#185) on 2026-07-31 required the main session to hand-roll `tea` invocations instead of using os-backlog at all. The friction was not one missing flag; it was that the deterministic parts of ticket management have no code surface, so the session had to rediscover them. What actually happened, in order: 1. Reached for `plugins/os-backlog/bin/issue-create` — does not exist. The real entry point is `bin/os-backlog issue-create`, a subcommand of the dispatcher. Cost: one failed invocation plus a `--help` read. 2. `issue-create` accepts `--title/--body/[--priority]` only. The tickets needed `enhancement` + `ready-for-agent` (six of them) and `ready-for-human` (one). No flag exists. Per `skills/capture/SKILL.md:28`, that is deliberate: *"`issue-create` is structurally incapable of applying state labels; there is no flag for one, by design."* 3. So the session fell back to raw `tea issues create --labels …`, which meant re-deriving by hand: that this repo is `forgejo:jared/cc-os` (read from `.cc-os/config`), that `tea` is the tool and not `gh`, that the label names are exactly `enhancement`/`ready-for-agent`/`ready-for-human` (verified with `tea labels`), that `tea` has no `--description-file` so bodies must go through `-d "$(cat …)"`, and that large bodies passed as args can hang (known quirk, hence per-call `timeout 90`). 4. Verification was also hand-rolled: `tea issues <n>` does not print labels usefully, so confirming the labels landed took a separate `tea issues ls --fields index,title,labels`. Every one of those steps is deterministic. None of them needed a model. The main session spent context and turns on mechanics, and a different session filing the same batch would rediscover all of it from scratch. ## The design gap `capture` is built for one use case: cheap mid-session capture of work that will not be done now, deliberately with no state labels, so the AI never pre-triages. That design is correct and should not change. There is a second, distinct use case with no surface at all: **filing a batch of already-triaged, fully-specified tickets** — the output of a gap analysis, a lint-mining run, a review. These tickets are triage-complete by construction; `ready-for-agent` is precisely the accurate label, and withholding it makes them invisible to label-driven queues. (Three earlier tickets from the 2026-07-30 batch, #176/#177/#178, are unlabeled for what looks like exactly this reason.) Right now that use case routes around os-backlog entirely, which loses the tracker abstraction: raw `tea` hardcodes forgejo, so the same workflow would silently not work in a `github:` or `repo:` project. ## What to build **1. Extend the Ruby CLI so the mechanics are code.** `bin/os-backlog issue-create` should gain: - `--label NAME` (repeatable) or `--labels a,b`, validated against the labels that actually exist on the resolved tracker, failing with the available list on a typo rather than silently creating an unlabeled issue. - `--body-file PATH` — reads the body from a file, so bodies never transit an argv and the `tea` large-body hang stops being something each caller must defend against with `timeout`. - Label round-trip verification in the returned JSON: report the labels the tracker actually attached, not the ones requested. That removes the separate `tea issues ls` confirmation step. - Ideally a `--dry-run` that prints the resolved tracker, slug, and final command without executing. **Hard constraint carried forward:** `next` is human-curated per ADR-0042 and the AI never applies or removes it. A `--label` flag must reject `next` outright at the CLI level, not merely by convention in a skill body. Encoding that in code is strictly stronger than today's "there is no flag" approach, which enforces it only by omission and is why the session bypassed the CLI in the first place. Whether this belongs on `issue-create` or on a sibling subcommand (e.g. `issue-file`, keeping `capture`'s no-state-label guarantee untouched) is an open design question — decide it before implementing, and record it if it changes the capture contract. **2. Add a delegation surface so the main session never handles this.** Either a `/os-backlog:file` skill or a `backlog-filer` subagent whose whole job is: take a set of title + body-file + labels tuples, resolve the tracker, create each issue, verify labels round-tripped, and report back the numbers and titles. The main session hands over a spec and gets back a list. The AI is present only for surprises — a label that does not exist, a tracker not configured, a partial batch failure — not for the mechanics. A subagent is likely the better fit for batches: filing N issues is N tool calls of pure bookkeeping that should not consume main-session context. This matches the user's existing preference for handing ticket comment/close/label API sequences to a cheap subagent. **3. Fix the discoverability miss.** `plugins/os-backlog/CLAUDE.md` names `bin/os-backlog` correctly, but nothing steers a session that reaches for `bin/issue-create` first. Whatever surface lands should be named in os-backlog's CLAUDE.md and in `docs/implementation-status/os-backlog.md`. ## Acceptance criteria - Filing a labeled, triage-complete issue on this repo requires exactly one `bin/os-backlog` invocation with no raw `tea` anywhere, and works unchanged against a `github:` tracker. - A body of arbitrary size is passed by file path, never through argv. - `--label next` is rejected by the CLI with a clear message. - An invalid label name fails loudly and lists the valid ones. - The command's output states which labels the tracker actually attached. - The delegation surface (skill or agent) files a multi-issue batch and reports numbers + titles, without the main session composing tracker commands. ## Verify Re-file a two-issue batch through the new surface, then confirm with `tea issues ls --fields index,title,labels` that both carry their intended labels. Then repeat the `--label next` case and confirm rejection. ## Provenance Surfaced 2026-07-31 while filing #179–#185, the output of a gap analysis of os-aidd-lint's clean scan on `plugins/cc-architect/skills/cc-architect/SKILL.md`. Related: `plugins/os-backlog/bin/os-backlog` (`issue-create` at ~line 200), `plugins/os-backlog/skills/capture/SKILL.md`, ADR-0042 (git-issues-only, `next` is human-only), `.cc-os/config`. Reported directly by Jared. Related unlabeled issues from the prior batch that this would have prevented: #176, #177, #178.
Author
Owner

Design amendment from Jared, 2026-07-31: should the filing surface just invoke /triage after creating the issue, so labeling stays owned by the skill built for it?

Half yes. Splitting it:

Do not auto-invoke /triage after every create. ~/.claude/skills/triage/SKILL.md is a maintainer-in-the-loop state machine, not a labeling function. Step 2 recommends a category/state and then waits for direction; step 3 reproduces the bug or checks out the PR; step 4 may run /grilling; step 5 posts an agent-brief comment carrying a mandatory "generated by AI during triage" disclaimer. It also sets disable-model-invocation: true — it is deliberately never model-fired. Calling it programmatically after each create would route around that choice and would either block a capture (whose entire point is not interrupting the session) or force the AI to answer on the maintainer's behalf, which reintroduces the AI pre-triage that capture's no-label design exists to prevent.

There is also a redundancy cost in the batch case: for tickets the session just authored, /triage's redundancy and specification checks re-derive context the author already has.

But the underlying point is right: os-backlog must not own label semantics. Those are already owned by docs/agents/triage-labels.md (five state roles, two category roles, plus the standing rule that waiting/review/next/P0-P3 are os-backlog's and must not be repurposed for triage). Revised design for this ticket:

  • The --label flag validates against the live tracker labels, and its documentation points at docs/agents/triage-labels.md rather than restating the taxonomy. Restating it would be a duplicated-authority defect: two owners, silent drift on the next edit.
  • When the caller does not know the state, the surface applies needs-triage rather than nothing. That is /triage's own documented entry state, so a later /triage run discovers the issue in its "unlabeled / needs-triage" bucket naturally. This closes the real gap behind #176/#177/#178 — issues invisible to label-driven queues — without any skill-to-skill call.
  • /triage stays exactly where it is: the human-invoked path for issues whose category, validity, or specification is genuinely unknown.

Net effect: labeling stays consistent and centrally defined, os-backlog stays deterministic, and the expensive judgment path is entered on purpose rather than on every create.

Design amendment from Jared, 2026-07-31: should the filing surface just invoke `/triage` after creating the issue, so labeling stays owned by the skill built for it? Half yes. Splitting it: **Do not auto-invoke `/triage` after every create.** `~/.claude/skills/triage/SKILL.md` is a maintainer-in-the-loop state machine, not a labeling function. Step 2 recommends a category/state and then *waits for direction*; step 3 reproduces the bug or checks out the PR; step 4 may run `/grilling`; step 5 posts an agent-brief comment carrying a mandatory "generated by AI during triage" disclaimer. It also sets `disable-model-invocation: true` — it is deliberately never model-fired. Calling it programmatically after each create would route around that choice and would either block a `capture` (whose entire point is not interrupting the session) or force the AI to answer on the maintainer's behalf, which reintroduces the AI pre-triage that capture's no-label design exists to prevent. There is also a redundancy cost in the batch case: for tickets the session just authored, `/triage`'s redundancy and specification checks re-derive context the author already has. **But the underlying point is right: os-backlog must not own label semantics.** Those are already owned by `docs/agents/triage-labels.md` (five state roles, two category roles, plus the standing rule that `waiting`/`review`/`next`/`P0`-`P3` are os-backlog's and must not be repurposed for triage). Revised design for this ticket: - The `--label` flag validates against the **live tracker labels**, and its documentation *points at* `docs/agents/triage-labels.md` rather than restating the taxonomy. Restating it would be a duplicated-authority defect: two owners, silent drift on the next edit. - When the caller does not know the state, the surface applies **`needs-triage`** rather than nothing. That is `/triage`'s own documented entry state, so a later `/triage` run discovers the issue in its "unlabeled / needs-triage" bucket naturally. This closes the real gap behind #176/#177/#178 — issues invisible to label-driven queues — without any skill-to-skill call. - `/triage` stays exactly where it is: the human-invoked path for issues whose category, validity, or specification is genuinely unknown. Net effect: labeling stays consistent and centrally defined, os-backlog stays deterministic, and the expensive judgment path is entered on purpose rather than on every create.
Author
Owner

Per spec #250 (os-backlog lifecycle implementation): the shared tracker lib built in #253 implements this deterministic ticket-filing surface — this issue closes when that lib lands, not as separate work.

Per spec #250 (os-backlog lifecycle implementation): the shared tracker lib built in #253 implements this deterministic ticket-filing surface — this issue closes when that lib lands, not as separate work.
Author
Owner

Partial progress check: --label + scope-gate validation shipped (ADR-0115, bin/os-backlog issue-create). But --body-file flag and the delegation surface (filer skill/subagent) are still absent — grep for body-file/filer in plugins/os-backlog/ finds nothing. #253's template validator ships a different slice (tier/section validation), not this ticket's surface. Leaving open.

Partial progress check: --label + scope-gate validation shipped (ADR-0115, bin/os-backlog issue-create). But --body-file flag and the delegation surface (filer skill/subagent) are still absent — grep for body-file/filer in plugins/os-backlog/ finds nothing. #253's template validator ships a different slice (tier/section validation), not this ticket's surface. Leaving open.
Author
Owner

Frozen in the 2026-08-16 backlog reset — see #419 for the expiry procedure. Do not work unless a live run rediscovers this issue.

Frozen in the 2026-08-16 backlog reset — see #419 for the expiry procedure. Do not work unless a live run rediscovers this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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
jared/cc-os#186
No description provided.