diff --git a/docs/adr/0033-tracker-routing-planka-is-state-git-issues-are-specs.md b/docs/adr/0033-tracker-routing-planka-is-state-git-issues-are-specs.md new file mode 100644 index 0000000..f013851 --- /dev/null +++ b/docs/adr/0033-tracker-routing-planka-is-state-git-issues-are-specs.md @@ -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:` 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:` | `forgejo:/` | `github:/` | `repo:` (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. diff --git a/docs/adr/README.md b/docs/adr/README.md index 8873ee8..b975643 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -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 | | 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 | +| 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 | diff --git a/docs/implementation-status.md b/docs/implementation-status.md index 7b02158..0b379ac 100644 --- a/docs/implementation-status.md +++ b/docs/implementation-status.md @@ -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); this file distilled to index+PD (issue #25 parts 2–3, ADR-0032). os-backlog slice 7 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 - leaves. + cache via BoardResolver (issue #22); slice 8 — Operations board + tracker-routing rubric + 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); -bulk vault migration; os-adr rollout to pilot projects; os-backlog slice #17 and routing -rollout. +bulk vault migration; os-adr rollout to pilot projects; os-backlog routing rollout (#14); +cross-project filing convention (#27) and wakeup design spike (#28). ## Implemented components (one-liners) diff --git a/docs/implementation-status/os-backlog.md b/docs/implementation-status/os-backlog.md index c46b6eb..9c4ca46 100644 --- a/docs/implementation-status/os-backlog.md +++ b/docs/implementation-status/os-backlog.md @@ -84,6 +84,19 @@ column ownership). 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` 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 - cleared) awaits its human decision gates; #14 routing-skill rollout (onboard cc-os - itself, then one more project) pending. +- **Slice 8 (2026-07-13, issue #17):** Operations board created in the Dev project (board + `1818308120145822783`) with uniform lists/labels; recurring card "Biweekly orchestration + 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. diff --git a/plugins/os-backlog/lib/backlog/board_ensurer.rb b/plugins/os-backlog/lib/backlog/board_ensurer.rb index 9406c61..17a0243 100644 --- a/plugins/os-backlog/lib/backlog/board_ensurer.rb +++ b/plugins/os-backlog/lib/backlog/board_ensurer.rb @@ -140,7 +140,7 @@ module Backlog last_error = nil candidates.each do |color| 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 next end diff --git a/plugins/os-backlog/tests/board_ensurer_test.rb b/plugins/os-backlog/tests/board_ensurer_test.rb index 1bd17e2..a46cb88 100644 --- a/plugins/os-backlog/tests/board_ensurer_test.rb +++ b/plugins/os-backlog/tests/board_ensurer_test.rb @@ -72,6 +72,17 @@ class BoardEnsurerTest < Minitest::Test refute_equal first_choice, p0.color 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 client = FakePlankaClient.new diff --git a/plugins/os-backlog/tests/test_helper.rb b/plugins/os-backlog/tests/test_helper.rb index 652ef46..5393a33 100644 --- a/plugins/os-backlog/tests/test_helper.rb +++ b/plugins/os-backlog/tests/test_helper.rb @@ -11,7 +11,7 @@ module BacklogTestHelpers class FakePlankaClient attr_reader :projects, :boards, :lists, :labels, :cards, :comments - def initialize(reject_colors: []) + def initialize(reject_colors: [], reject_colors_with_400: []) @next_id = 0 @projects_store = [] @boards_store = [] @@ -24,7 +24,8 @@ module BacklogTestHelpers @projects = ProjectsResource.new(self) @boards = BoardsResource.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) @comments = CommentsResource.new(self) end @@ -179,9 +180,10 @@ module BacklogTestHelpers end class LabelsResource - def initialize(client, reject_colors: []) + def initialize(client, reject_colors: [], reject_colors_with_400: []) @client = client @reject_colors = reject_colors + @reject_colors_with_400 = reject_colors_with_400 end def list(board_id) @@ -189,6 +191,11 @@ module BacklogTestHelpers end 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]) raise Planka::ValidationError.new(status: 422, body: { "message" => "color #{attrs[:color]} rejected" }) diff --git a/plugins/os-context/skills/audit-sessions/references/rubric.md b/plugins/os-context/skills/audit-sessions/references/rubric.md index 25ff3d9..6780ce6 100644 --- a/plugins/os-context/skills/audit-sessions/references/rubric.md +++ b/plugins/os-context/skills/audit-sessions/references/rubric.md @@ -30,6 +30,13 @@ severity, and category. one scripted Bash loop. 7. **drift** — session starts disciplined then drifts into long direct runs mid-session, 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) diff --git a/plugins/os-status/skills/fix/SKILL.md b/plugins/os-status/skills/fix/SKILL.md index 8205bae..f5ec746 100644 --- a/plugins/os-status/skills/fix/SKILL.md +++ b/plugins/os-status/skills/fix/SKILL.md @@ -49,11 +49,14 @@ like a `.gitignore` addition or a config stamp. c. **`project-graph-present`** → invoke `/os-vault:onboard-project`. - d. **`tracker-configured`** → **human gate.** Do not guess a tracker. Ask the user - which tracker this project uses, then invoke `/os-backlog:route` (the - os-backlog routing skill) with that answer. If `/os-backlog:route` does not - exist yet in this installation (issue #14 not yet landed), tell the user and - skip — do not fabricate a `.cc-os/config` tracker value yourself. + d. **`tracker-configured`** → invoke `/os-backlog:route` directly and let it run + its own inspect → propose → confirm flow. Do NOT pre-ask the user "forgejo or + planka?" — that framing is wrong: the boundary rule uses BOTH (Planka = state, + git issues = specs; a project routinely keeps its git-issue spec chains AND a + `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 for a project-level fix.** This is an environment/settings.json condition, not