os-backlog slice 8: Operations board + tracker-routing rubric + ADR-0033 (issue #17)

- ADR-0033 canonizes Planka=state / git-issues=specs, tracker key grammar,
  card-as-pointer and promotion rules (cites route/to-issues/session-start).
- os-context audit rubric: new category 8 tracker-routing (compliance only).
- BoardEnsurer label-color fallback also rescues Planka::BadRequestError
  (Planka rejects unknown colors as HTTP 400, found live) + regression test.
- os-status /os-status:fix step d: invoke /os-backlog:route directly instead
  of pre-asking forgejo-vs-planka (boundary rule uses both; cf. issue #26).
- Records updated per index+leaf convention.

Closes #17.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLeMz48rvG3s9XpAsDxeho
This commit is contained in:
jared 2026-07-13 10:00:36 -04:00
parent 46b0584643
commit 49c8f5eace
9 changed files with 86 additions and 16 deletions

View File

@ -0,0 +1,27 @@
---
id: "0033"
date: 2026-07-13
status: Accepted
supersedes:
superseded-by:
affected-paths: [plugins/os-backlog/skills/route/SKILL.md, plugins/os-backlog/hooks/session_start.py, plugins/os-backlog/bin/os-backlog, .cc-os/config]
affected-components: [os-backlog, to-issues]
---
# 0033 — Tracker routing: Planka is state, git issues are specs
## Context
The backlog pilot plan (vault note `vault-backlog-pilot-plan.md`) originally routed everything to Forgejo issues, later amended informally to "Forgejo = code work in a repo; Planka = everything else." The os-backlog slices then shipped a sharper boundary in practice — encoded in `plugins/os-backlog/skills/route/SKILL.md`, the `/to-issues` destination-tracker step (global skill `~/.agents/skills/to-issues/SKILL.md`, step 5), and the `plugins/os-backlog/hooks/session_start.py` injection note — but no ADR canonizes it, so the rule lives only in skill prose and could drift or be silently edited away. This ADR records already-implemented practice; it does not introduce new behavior.
## Decision
Canonize the tracker-routing boundary, amending the pilot plan's "Forgejo for everything" rule. (1) Boundary rule: Planka = task STATE (backlog/next/doing/waiting/review/done — what's happening now); git issues = durable SPECS (text that must survive and be referenced: features, migrations, architecture-level work, tracer-bullet slices from /to-issues, PRDs); `repo:<path>` is the escape hatch for projects that already track issues in-repo and want to stay there. (2) Each project declares exactly one destination via the `tracker` key in `.cc-os/config`, grammar: `planka:<board>` | `forgejo:<owner>/<repo>` | `github:<owner>/<repo>` | `repo:<path>` (validated by `os-backlog config-write`). (3) Card-as-pointer + issue-chain-as-spec: when a code effort needs both state tracking and a durable spec, the Planka card is a pointer (title + link) into the git issue chain holding the spec — never duplicate spec text into a card description. (4) Promotion rule: when a Planka-tracked effort accretes significant code/design decisions in card comments, that is the signal to promote — open a git issue (or chain) for the durable spec and turn the card into a pointer; the AI flags promotion to the user, never performs it silently. Operational detail lives in the cited sources (route SKILL.md, to-issues step 5, session_start.py note) and is not restated here; column-ownership rules are ADR-0029's.
## Consequences
Easier: one recorded boundary the route skill, /to-issues, and the SessionStart note can all be audited against; changing the boundary now requires a superseding ADR rather than a quiet skill edit; the pilot plan's informal amendment has a durable home. Harder: two tracking surfaces per code project (board + issue tracker) instead of one, mitigated by the pointer pattern and back-links; promotion is a judgment call the AI can only flag, so under-flagging is a live risk (measured via the session-audit rubric).
## Alternatives rejected
1) Keep "Forgejo for everything" (original pilot plan) — rejected: process/coordination/household-style tasks have no repo home and card state (columns, WIP) is exactly what issue trackers do badly. 2) Planka for everything, specs in card descriptions — rejected: card descriptions are not durable, linkable, or commit-referenceable; specs need git-issue permanence. 3) Leave the rule in skill prose only, no ADR — rejected: prose rules drifting out of sync with enforcement is precisely what ADR-0029 fixed for column moves; the routing boundary deserves the same durability.

View File

@ -39,4 +39,5 @@ One file per decision, `NNNN-kebab-title.md`, created via `/os-adr:create`.
| 0030 | [Read-only work uses Claude Code plan mode; no /readonly shortcut command](0030-read-only-work-uses-claude-code-plan-mode-no-readonly-shortcut-command.md) | Accepted | 2026-07-13 | | 0030 | [Read-only work uses Claude Code plan mode; no /readonly shortcut command](0030-read-only-work-uses-claude-code-plan-mode-no-readonly-shortcut-command.md) | Accepted | 2026-07-13 |
| 0031 | [os-context prompt-composer plugin supersedes single-file os-orchestration](0031-os-context-prompt-composer-plugin-supersedes-single-file-os-orchestration.md) | Accepted | 2026-07-13 | | 0031 | [os-context prompt-composer plugin supersedes single-file os-orchestration](0031-os-context-prompt-composer-plugin-supersedes-single-file-os-orchestration.md) | Accepted | 2026-07-13 |
| 0032 | [AI-maintained knowledge files are indexes with progressive disclosure](0032-ai-maintained-knowledge-files-are-indexes-with-progressive-disclosure.md) | Accepted | 2026-07-13 | | 0032 | [AI-maintained knowledge files are indexes with progressive disclosure](0032-ai-maintained-knowledge-files-are-indexes-with-progressive-disclosure.md) | Accepted | 2026-07-13 |
| 0033 | [Tracker routing: Planka is state, git issues are specs](0033-tracker-routing-planka-is-state-git-issues-are-specs.md) | Proposed | 2026-07-13 |
<!-- adr-index:end --> <!-- adr-index:end -->

View File

@ -62,12 +62,13 @@ entry is in the leaf file named.
skill (ADR-0030); os-doc-hygiene `file_length` scanner signal shipped (issue #25 part 1); skill (ADR-0030); os-doc-hygiene `file_length` scanner signal shipped (issue #25 part 1);
this file distilled to index+PD (issue #25 parts 23, ADR-0032). os-backlog slice 7 this file distilled to index+PD (issue #25 parts 23, ADR-0032). os-backlog slice 7
shipped: SessionStart injection note + /to-issues tracker routing (issue #16); board-id shipped: SessionStart injection note + /to-issues tracker routing (issue #16); board-id
cache via BoardResolver (issue #22). Detail: os-context / os-doc-hygiene / os-backlog cache via BoardResolver (issue #22); slice 8 — Operations board + tracker-routing rubric
leaves. category + ADR-0033 canonizing Planka-state/git-issues-spec (issue #17). Detail:
os-context / os-doc-hygiene / os-backlog leaves.
**Remaining optional items:** additional project onboarding (one at a time, per ADR-0013); **Remaining optional items:** additional project onboarding (one at a time, per ADR-0013);
bulk vault migration; os-adr rollout to pilot projects; os-backlog slice #17 and routing bulk vault migration; os-adr rollout to pilot projects; os-backlog routing rollout (#14);
rollout. cross-project filing convention (#27) and wakeup design spike (#28).
## Implemented components (one-liners) ## Implemented components (one-liners)

View File

@ -84,6 +84,19 @@ column ownership).
returned board name; 404/mismatch/any cache failure degrades to name lookup and rewrites returned board name; 404/mismatch/any cache failure degrades to name lookup and rewrites
the cache; write failures never fail the command). Comment-preserving `Config.set` the cache; write failures never fail the command). Comment-preserving `Config.set`
mirrors os-status's writer. 14 new tests; suite 90 runs / 198 assertions / 0 failures. mirrors os-status's writer. 14 new tests; suite 90 runs / 198 assertions / 0 failures.
- **Outstanding:** slice #17 (ops board + rubric category + routing ADR; blocked-by #16 now - **Slice 8 (2026-07-13, issue #17):** Operations board created in the Dev project (board
cleared) awaits its human decision gates; #14 routing-skill rollout (onboard cc-os `1818308120145822783`) with uniform lists/labels; recurring card "Biweekly orchestration
itself, then one more project) pending. IRL audit (recurring)" (card `1818310715438531668`, P2 + semi, first progress comment
`1818310889929966683`; recurrence contract: comment + move back to Backlog, never Done).
Tracker-routing convention canonized as **ADR-0033** (Planka = state / git issues = specs,
tracker key grammar, pointer + promotion rules; cites route/to-issues/session-start
rather than restating). os-context audit rubric gained category 8 `tracker-routing`
(capture/route/promote compliance only). Bug found live and fixed: label color fallback
only rescued `Planka::ValidationError`, but Planka rejects unknown colors as HTTP 400
`Planka::BadRequestError` — fallback now rescues both (regression test; suite 91 runs /
0 failures). os-status `/os-status:fix` step d reworded: invoke `/os-backlog:route`
directly, never pre-ask "forgejo or planka?" (boundary rule uses both — surfaced by the
servers-repo onboarding, cf. issue #26).
- **Outstanding:** #14 routing-skill rollout (onboard cc-os itself, then one more project);
#26 route-inspect issue-shape classification; #27 cross-project filing index; #28 wakeup
design spike.

View File

@ -140,7 +140,7 @@ module Backlog
last_error = nil last_error = nil
candidates.each do |color| candidates.each do |color|
return @client.labels.create(board.id, name: label_name, color: color, position: 65_536) return @client.labels.create(board.id, name: label_name, color: color, position: 65_536)
rescue Planka::ValidationError => e rescue Planka::ValidationError, Planka::BadRequestError => e
last_error = e last_error = e
next next
end end

View File

@ -72,6 +72,17 @@ class BoardEnsurerTest < Minitest::Test
refute_equal first_choice, p0.color refute_equal first_choice, p0.color
end end
def test_label_color_falls_back_when_server_rejects_with_400
first_choice = Backlog::BoardSpec::LABEL_COLOR_CANDIDATES.fetch("P3").first
client = FakePlankaClient.new(reject_colors_with_400: [first_choice])
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
ensurer.ensure("llf-schema", project_name: "Dev")
p3 = client.labels_store.find { |l| l.name == "P3" }
refute_equal first_choice, p3.color
end
def test_activate_renames_archived_board_back def test_activate_renames_archived_board_back
client = FakePlankaClient.new client = FakePlankaClient.new

View File

@ -11,7 +11,7 @@ module BacklogTestHelpers
class FakePlankaClient class FakePlankaClient
attr_reader :projects, :boards, :lists, :labels, :cards, :comments attr_reader :projects, :boards, :lists, :labels, :cards, :comments
def initialize(reject_colors: []) def initialize(reject_colors: [], reject_colors_with_400: [])
@next_id = 0 @next_id = 0
@projects_store = [] @projects_store = []
@boards_store = [] @boards_store = []
@ -24,7 +24,8 @@ module BacklogTestHelpers
@projects = ProjectsResource.new(self) @projects = ProjectsResource.new(self)
@boards = BoardsResource.new(self) @boards = BoardsResource.new(self)
@lists = ListsResource.new(self) @lists = ListsResource.new(self)
@labels = LabelsResource.new(self, reject_colors: reject_colors) @labels = LabelsResource.new(self, reject_colors: reject_colors,
reject_colors_with_400: reject_colors_with_400)
@cards = CardsResource.new(self) @cards = CardsResource.new(self)
@comments = CommentsResource.new(self) @comments = CommentsResource.new(self)
end end
@ -179,9 +180,10 @@ module BacklogTestHelpers
end end
class LabelsResource class LabelsResource
def initialize(client, reject_colors: []) def initialize(client, reject_colors: [], reject_colors_with_400: [])
@client = client @client = client
@reject_colors = reject_colors @reject_colors = reject_colors
@reject_colors_with_400 = reject_colors_with_400
end end
def list(board_id) def list(board_id)
@ -189,6 +191,11 @@ module BacklogTestHelpers
end end
def create(board_id, attrs) def create(board_id, attrs)
if @reject_colors_with_400.include?(attrs[:color])
# Planka 2.x rejects unknown color names as E_MISSING_OR_INVALID_PARAMS
raise Planka::BadRequestError.new(status: 400,
body: { "message" => "color #{attrs[:color]} rejected" })
end
if @reject_colors.include?(attrs[:color]) if @reject_colors.include?(attrs[:color])
raise Planka::ValidationError.new(status: 422, raise Planka::ValidationError.new(status: 422,
body: { "message" => "color #{attrs[:color]} rejected" }) body: { "message" => "color #{attrs[:color]} rejected" })

View File

@ -30,6 +30,13 @@ severity, and category.
one scripted Bash loop. one scripted Bash loop.
7. **drift** — session starts disciplined then drifts into long direct runs mid-session, 7. **drift** — session starts disciplined then drifts into long direct runs mid-session,
especially after a design/decision settles. especially after a design/decision settles.
8. **tracker-routing** — mid-session follow-up work (a concrete task, bug, or deferred
item not done this session) left as a TODO comment or chat mention instead of captured
to the tracked board; durable spec text routed to a Planka card description (or task
state routed to git issues) against the Planka-state/git-issues-spec boundary
(ADR-0033, `.cc-os/config` tracker key); or a Planka card visibly accreting spec-like
decisions in comments with no promotion flag raised to the user — silent promotion
counts too. Convention compliance only, not whether the routing choice was wise.
## Per-session report format (return exactly this) ## Per-session report format (return exactly this)

View File

@ -49,11 +49,14 @@ like a `.gitignore` addition or a config stamp.
c. **`project-graph-present`** → invoke `/os-vault:onboard-project`. c. **`project-graph-present`** → invoke `/os-vault:onboard-project`.
d. **`tracker-configured`** → **human gate.** Do not guess a tracker. Ask the user d. **`tracker-configured`** → invoke `/os-backlog:route` directly and let it run
which tracker this project uses, then invoke `/os-backlog:route` (the its own inspect → propose → confirm flow. Do NOT pre-ask the user "forgejo or
os-backlog routing skill) with that answer. If `/os-backlog:route` does not planka?" — that framing is wrong: the boundary rule uses BOTH (Planka = state,
exist yet in this installation (issue #14 not yet landed), tell the user and git issues = specs; a project routinely keeps its git-issue spec chains AND a
skip — do not fabricate a `.cc-os/config` tracker value yourself. `planka:` tracker key for new ad hoc state). The route skill owns that
decision conversation and its gates. If `/os-backlog:route` is not installed,
tell the user and skip — do not fabricate a `.cc-os/config` tracker value
yourself.
e. **`subagent-model-env-override`** → **human gate, and typically out of scope e. **`subagent-model-env-override`** → **human gate, and typically out of scope
for a project-level fix.** This is an environment/settings.json condition, not for a project-level fix.** This is an environment/settings.json condition, not