feat(aosguard): refuse an issue filed with no priority or autonomy label #1318

Merged
coilyco-ops merged 3 commits from aos/1105-require-issue-labels-v2 into main 2026-08-27 05:04:14 +00:00
Owner

Closes #1105.

Gated on #1316, which pins specgen to the umbra release carrying matches and the two shadow fixes. The gate job runs in the :release image, which bakes specgen at ARG SPECGEN_VERSION and only bootstraps the pinned version when none is on PATH, so this cannot go green until that image republishes.

The gap, measured before the change

$ aosguard ops forgejo issue create <owner> <repo> --title probe --body probe --dry-run
body:
    body: probe
    title: probe          <- no labels key, accepted, exit 0

An unlabelled issue is the fail-closed default that reaches no queue, which is coilyco-gaming/sirens-echo#437's subject.

What now happens

issue create becomes an action shadow whose --labels is required and must carry one priority and one autonomy label. Every case below is from the built binary:

(no labels)                 aosguard: missing required flag --labels                       EXIT=1
--labels autonomy/headless  aosguard: --labels: no priority label: add --labels priority/P2 (...)   EXIT=1
--labels priority/P2        aosguard: --labels: no autonomy label: add --labels autonomy/headless (...)  EXIT=1
--labels priority/P9 ...    aosguard: --labels: no priority label: ...                      EXIT=1
--milestone soon ...        aosguard: arg "milestone": value "soon" is not an integer       EXIT=1
(valid)                     action: create-issue                                            EXIT=0

Nothing is created by any refusal. The check runs while the flags bind, before the create request is assembled, so this is not the response-side reporting the acceptance rejects.

Labels are names now, not ids

--labels priority/P2, not --labels 199. The names are identical across every org and repo the coily* gate reaches and the ids are not, so a name needs no per-org table and no lookup - the friction the director measured eleven times in one day on #1105.

CreateIssueOption.labels declares items: {type: integer}, ids only, so the labels are applied by a second call to the labels sub-collection whose IssueLabelsOption declares the id-or-name union. The policy check precedes both calls, so a violating call creates nothing.

The globs enumerate rather than wildcard

The part worth reviewing hardest. An unknown label name is dropped silently with a 200:

$ aosguard ops forgejo issue-label add coilyco-flight-deck agentic-os 1310 \
      --body-file <<< '{"labels":["priority/NOPE"]}'
EXIT=0, existing labels echoed back, nothing applied

So priority/* would accept priority/p2 or priority/P9, let the write through, and apply nothing - the control reporting success over the hazard it exists to prevent. umbra#324 made matches carry alternative globs so the vocabulary is enumerated instead.

The full flag surface is preserved

The first draft dropped six optional flags, because an action arg bound to an unsupplied input failed the whole call. Kai's call was to fix that upstream rather than accept the reduction, so two umbra fixes landed first:

  • umbra#326 - an omitted optional arg is left out of the request instead of failing the call. Safe because build-time validation already rejects a $ref no input declares, so a runtime miss can only be a declared-but-omitted input.
  • umbra#328 - a scalar arg takes the type its field declares, so --milestone 16 reaches the wire as 16 and not "16". Without this, carrying --milestone would have silently sent the wrong type, which is worse than dropping it.

The shadow now carries --body, --milestone, --assignees, --due_date and --ref. Only the deprecated single --assignee and --closed stay off: --assignees supersedes the first, and filing an already-closed issue is not something this surface should make easy.

The two-taxonomy problem dissolved

#1105 asks for the rule to cover coilysiren/inbox's unprefixed labels or be scoped around them. Neither is needed. coilysiren/inbox#392 closed on 2026-08-27, and the live label sets confirm inbox and website now carry priority/P0..P4 and autonomy/*, all exclusive=true. One rule covers every repo this surface writes, and no exemption exists to go stale.

Verification

Proved live end to end before the flags were restored: probe issue #1310 filed through the shadow, both label names applied by the second call, $issue.number threaded, labels confirmed on re-read, then closed. move-issue dry-run checked for regression and is unaffected - a shadow replaces the CLI leaf, not the grant. pre-commit run --all-files passes.

Follow-ups filed

  • #1312 - no docs/ page, because the repo is at its 40-doc budget ceiling.
  • #1317 - move-issue is a second path to an unlabelled issue, and its "deferred (array flow)" note is now stale.
Closes #1105. > **Gated on #1316**, which pins specgen to the umbra release carrying `matches` and the two shadow fixes. The `gate` job runs in the `:release` image, which bakes specgen at `ARG SPECGEN_VERSION` and only bootstraps the pinned version when none is on PATH, so this cannot go green until that image republishes. ## The gap, measured before the change ``` $ aosguard ops forgejo issue create <owner> <repo> --title probe --body probe --dry-run body: body: probe title: probe <- no labels key, accepted, exit 0 ``` An unlabelled issue is the fail-closed default that reaches no queue, which is `coilyco-gaming/sirens-echo#437`'s subject. ## What now happens `issue create` becomes an action shadow whose `--labels` is required and must carry one priority and one autonomy label. Every case below is from the built binary: ``` (no labels) aosguard: missing required flag --labels EXIT=1 --labels autonomy/headless aosguard: --labels: no priority label: add --labels priority/P2 (...) EXIT=1 --labels priority/P2 aosguard: --labels: no autonomy label: add --labels autonomy/headless (...) EXIT=1 --labels priority/P9 ... aosguard: --labels: no priority label: ... EXIT=1 --milestone soon ... aosguard: arg "milestone": value "soon" is not an integer EXIT=1 (valid) action: create-issue EXIT=0 ``` Nothing is created by any refusal. The check runs while the flags bind, before the create request is assembled, so this is **not** the response-side reporting the acceptance rejects. ## Labels are names now, not ids `--labels priority/P2`, not `--labels 199`. The names are identical across every org and repo the `coily*` gate reaches and the ids are not, so a name needs no per-org table and no lookup - the friction the director measured eleven times in one day on #1105. `CreateIssueOption.labels` declares `items: {type: integer}`, ids only, so the labels are applied by a second call to the labels sub-collection whose `IssueLabelsOption` declares the id-or-name union. The policy check precedes both calls, so a violating call creates nothing. ## The globs enumerate rather than wildcard The part worth reviewing hardest. An unknown label **name** is dropped silently with a 200: ``` $ aosguard ops forgejo issue-label add coilyco-flight-deck agentic-os 1310 \ --body-file <<< '{"labels":["priority/NOPE"]}' EXIT=0, existing labels echoed back, nothing applied ``` So `priority/*` would accept `priority/p2` or `priority/P9`, let the write through, and apply nothing - the control reporting success over the hazard it exists to prevent. umbra#324 made `matches` carry alternative globs so the vocabulary is enumerated instead. ## The full flag surface is preserved The first draft dropped six optional flags, because an action arg bound to an unsupplied input failed the whole call. Kai's call was to fix that upstream rather than accept the reduction, so two umbra fixes landed first: * **umbra#326** - an omitted optional arg is left out of the request instead of failing the call. Safe because build-time validation already rejects a `$ref` no `input` declares, so a runtime miss can only be a declared-but-omitted input. * **umbra#328** - a scalar arg takes the type its field declares, so `--milestone 16` reaches the wire as `16` and not `"16"`. Without this, carrying `--milestone` would have silently sent the wrong type, which is worse than dropping it. The shadow now carries `--body`, `--milestone`, `--assignees`, `--due_date` and `--ref`. Only the deprecated single `--assignee` and `--closed` stay off: `--assignees` supersedes the first, and filing an already-closed issue is not something this surface should make easy. ## The two-taxonomy problem dissolved #1105 asks for the rule to cover `coilysiren/inbox`'s unprefixed labels or be scoped around them. Neither is needed. coilysiren/inbox#392 closed on 2026-08-27, and the live label sets confirm `inbox` and `website` now carry `priority/P0..P4` and `autonomy/*`, all `exclusive=true`. One rule covers every repo this surface writes, and **no exemption exists to go stale**. ## Verification Proved live end to end before the flags were restored: probe issue #1310 filed through the shadow, both label names applied by the second call, `$issue.number` threaded, labels confirmed on re-read, then closed. `move-issue` dry-run checked for regression and is unaffected - a shadow replaces the CLI leaf, not the grant. `pre-commit run --all-files` passes. ## Follow-ups filed * #1312 - no `docs/` page, because the repo is at its 40-doc budget ceiling. * #1317 - `move-issue` is a second path to an unlabelled issue, and its "deferred (array flow)" note is now stale.
feat(aosguard): refuse an issue filed with no priority or autonomy label
Some checks failed
ci / aos-eval-tests (pull_request) Successful in 7s
ci / ward-doctor (pull_request) Successful in 8s
ci / aos-cli-tests (pull_request) Successful in 26s
ci / gate (pull_request) Failing after 21s
478f8c2e32
Closes #1105. Needs the specgen 0.173.0 image from #1316.

## The gap, measured before the change

    $ aosguard ops forgejo issue create <owner> <repo> --title probe --body probe --dry-run
    body:
        body: probe
        title: probe          <- no labels key, accepted, exit 0

An unlabelled issue is the fail-closed default that reaches no queue.

## What now happens

`issue create` becomes an action shadow whose --labels is required and must
carry one priority and one autonomy label. The refusal names the missing axis
and exits 1, and nothing is created by that run: the check runs while the flags
bind, before the create request is assembled.

## Labels are names now, not ids

--labels priority/P2, not --labels 199. The names are identical across every org
and repo the coily* gate reaches and the ids are not, so a name needs no per-org
table and no lookup. POST /issues declares ids only, so the labels are applied
by a second call to the labels sub-collection, whose schema declares the
id-or-name union. The policy check precedes both calls.

## The globs enumerate rather than wildcard

The labels endpoint drops an unknown name silently with a 200, so priority/*
would accept priority/p2, let the write through, and apply nothing. Enumerating
the vocabulary refuses a near-miss instead.

## The two-taxonomy problem dissolved

coilysiren/inbox#392 landed, so inbox and website both carry priority/* and
autonomy/*. One rule covers every repo this surface writes and no per-repo
exemption exists to go stale.

## The shadow is narrower than the leaf it replaces

An action argument bound to an input that was not supplied fails the whole call
(umbra ResolveArg), so every input here is required and the leaf's optional
--assignee, --assignees, --closed, --due_date, --milestone and --ref are not
carried. Set a milestone or assignee with `issue edit` after filing. Recorded
in the verb's own describe rather than a docs page, because the repo is at its
40-doc budget ceiling; #1312 carries that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Agent-Role: platform
feat(aosguard): carry the leaf's optional fields through the create-issue shadow
Some checks failed
ci / aos-eval-tests (pull_request) Successful in 9s
ci / ward-doctor (pull_request) Successful in 8s
ci / aos-cli-tests (pull_request) Successful in 33s
ci / gate (pull_request) Failing after 26s
cf72f5781a
Kai's call on #1105: restore the flags rather than accept the reduction.

The first draft dropped six of `issue create`'s optional flags because an action
arg bound to an unsupplied input failed the whole call, so every input had to be
required. umbra#326 fixes that (an omitted optional is left out of the request),
and umbra#328 makes a scalar arg take the type its field declares, so
`--milestone 16` reaches the wire as the number 16 rather than the string "16".

With both, the shadow carries --body, --milestone, --assignees, --due_date and
--ref. Only the deprecated single --assignee and --closed stay off: --assignees
supersedes the first, and filing an already-closed issue is not something this
surface should make easy.

Verified against a binary built from the umbra branch: the minimal call sends
title/body/labels with no placeholders, a supplied --milestone renders unquoted,
`--milestone soon` is refused before the write, and all four label refusals still
name their axis and exit 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Agent-Role: platform
merge main into the create-issue label guard branch
All checks were successful
ci / aos-eval-tests (pull_request) Successful in 9s
ci / ward-doctor (pull_request) Successful in 11s
ci / aos-cli-tests (pull_request) Successful in 40s
ci / gate (pull_request) Successful in 1m12s
dev-base-pr / build (pull_request) Successful in 2m11s
fef5977a6f
coilyco-ops deleted branch aos/1105-require-issue-labels-v2 2026-08-27 05:04:14 +00:00
Sign in to join this conversation.
No reviewers
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
coilyco-flight-deck/agentic-os!1318
No description provided.