docs: add phased implementation plan and architecture decision records

Turns the design doc into an ordered, dependency-correct build plan (phases
0-9) with an exit criterion per phase, and records the twelve architectural
decisions it rests on as ADRs.

Decisions: single Rust binary with routed compose services; Streamable HTTP
only; per-agent bearer tokens with clientInfo as a display hint only; SQLite;
get_rules delivered via session gating; full-document rule delivery; project_id
from day one; enforcement tier scoped to Bridle-mediated actions; stable tool
list on upstream failure; OpenAI-compatible embeddings with model/dim guarding;
pattern RAG gated behind a spike; CLAUDE.md + AGENTS.md as v1 renderer targets.

Deviates from the design doc's original 1-8 ordering by moving the audit log
and multi-project schema into phase 1, building the admin API incrementally
rather than all at the Web Panel phase, downgrading vendor guardrail sourcing
to manual-first, and gating the pattern-example RAG behind a validation spike.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RZerbsHGF9Ka3bKhCjJ9m
This commit is contained in:
2026-08-08 14:33:27 -05:00
parent 7fdde56560
commit 06917d43e2
13 changed files with 556 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
# ADR-0001: Single Rust binary with routed compose services
**Status:** Accepted (2026-08-08)
## Context
The design doc's architecture diagram shows Rules, Memory, the Kanboard wrapper, and the Web Panel
as separate boxes, which reads as four independent deployables communicating over HTTP. Bridle
targets single-host, single-user deployment. Splitting hand-written components across processes at
that scale buys nothing: it costs four ports, four health checks, four failure modes, and turns
what would be function calls into network hops.
## Decision
All hand-written components compile into **one Rust binary** with internal modules: `mcp` (server
and session handling), `rules`, `memory`, `proxy`, `admin`, and the Web Panel's served assets.
Module boundaries stay clean enough that any module could be split into its own service later
without redesign.
Third-party and off-the-shelf services remain **separate Docker Compose containers** — Qdrant,
Kanboard, the embedding server, and the existing Gitea MCP server — with all agent traffic to them
routed through the binary.
## Consequences
- One deployable, one health check, one log stream, one thing to restart.
- No Python sidecar: the memory module is Rust talking to Qdrant and to an OpenAI-compatible
embeddings endpoint over HTTP.
- Compose profiles for bundled-versus-external services are unaffected — see the deployment section
of the design doc.
- If Bridle ever outgrows single-host, the module boundaries are the seams to split along. This is
a deliberate deferral, not an oversight.

View File

@@ -0,0 +1,24 @@
# ADR-0002: Streamable HTTP as the sole MCP transport
**Status:** Accepted (2026-08-08)
## Context
MCP defines stdio and Streamable HTTP transports; an older HTTP+SSE transport is deprecated in
favor of Streamable HTTP. Bridle is a networked gateway intended to be reachable from agents over
WireGuard, which rules out stdio as the primary transport. A local stdio shim bridging to the HTTP
endpoint was considered for clients with weak Streamable HTTP support, as was continuing to serve
the deprecated SSE transport.
## Decision
Serve **Streamable HTTP only**, at `/mcp`. No stdio shim and no deprecated SSE endpoint at v1.
## Consequences
- Matches the per-agent bearer token model (ADR-0003) directly — the token rides the
`Authorization` header.
- Works over WireGuard without extra components.
- One transport to implement, test, and secure.
- If a target agent turns out to have unusable Streamable HTTP support, a `bridle-stdio` bridging
binary is the fallback. It is not in scope until a specific agent demonstrably needs it.

View File

@@ -0,0 +1,32 @@
# ADR-0003: Per-agent bearer tokens for identity and authorization
**Status:** Accepted (2026-08-08)
## Context
The design doc proposed identifying the connecting agent via "MCP handshake client info". The
`clientInfo` object in the MCP `initialize` request is **self-reported by the client** and trivially
forged. Hanging the permissions model off it — including `secrets: {read: false}` — would make that
model decorative rather than enforcing. This matters more once WireGuard or Pangolin place the
endpoint on a network beyond the operator's full control.
Alternatives considered: mTLS client certificates (stronger binding, but meaningful setup friction
per agent and per machine) and full OAuth 2.1 per the MCP authorization spec (standards-correct and
the right answer for public exposure, but an entire authorization server to build and operate for a
single-user homelab).
## Decision
Each agent identity is issued a **Bridle bearer token**. Identity and permissions derive from the
token presented on the connection. `clientInfo` is retained as a **display hint only** — surfaced in
the audit log and Web Panel, never consulted for an authorization decision.
Tokens are issued, rotated, and revoked through the admin API and Web Panel.
## Consequences
- Simple, revocable, and sufficient behind WireGuard.
- Forging `clientInfo` gains an attacker nothing.
- Token issuance/rotation/revocation is phase-1 scope.
- If Bridle is ever exposed to clients the operator does not control, revisit in favor of OAuth 2.1
per the MCP authorization spec.

View File

@@ -0,0 +1,29 @@
# ADR-0004: SQLite as the primary datastore
**Status:** Accepted (2026-08-08)
## Context
The design doc specified no storage backend for the canonical rules document, permissions, audit
log, sessions, secret metadata, or the approval queue. Options considered were SQLite, a bundled
Postgres, and git-backed YAML in a Gitea repository with a database for everything else.
The git-backed option was genuinely attractive in a Gitea shop — free versioning, diffs, and
PR-based review of guardrail changes. It was rejected because Web Panel edits and direct git edits
create two write paths to the same canonical data that must then be reconciled.
## Decision
**SQLite**, one file (`bridle.db`), holding `projects`, `agents`, `tokens`, `permissions`,
`audit_log`, `rules_documents` (JSON plus `schema_version`), `guardrails` with provenance, and
`approval_queue`.
## Consequences
- Zero operational overhead; backup is copying one file.
- Correct default for the single-host, single-user target.
- Rules versioning must be built explicitly (document history in-table) rather than inherited from
git.
- Migration to Postgres remains straightforward if multi-host or a multi-user panel ever
materializes. Keep SQL portable and avoid SQLite-specific constructs where a standard equivalent
exists.

View File

@@ -0,0 +1,36 @@
# ADR-0005: Rules delivered via `get_rules`, enforced by session gating
**Status:** Accepted (2026-08-08)
## Context
Bridle's premise is that the canonical ruleset governs every agent session. But an MCP server can
only *offer* a `get_rules` tool — it cannot make a model call it. Meanwhile Claude Code auto-loads
`CLAUDE.md` from disk and Codex auto-loads `AGENTS.md` from disk, unprompted, every session. A tool
the model may skip is strictly weaker than a file the harness injects.
Three delivery mechanisms were considered:
1. A local sync client (`bridle sync` or a session-start hook) writing rendered files to disk —
reliable across agents today, but adds a client-side component the design doc does not have.
2. The `instructions` field of the MCP `initialize` result — no client install, but whether a given
client injects it into the system prompt varies and would need per-agent empirical testing.
3. The `get_rules` tool alone — matches the design doc, but leaves delivery best-effort.
## Decision
Deliver rules through the **`get_rules(project_id, agent_type)` tool**, and make it reliable by
**gating the session**: any other tool call before `get_rules` has succeeded returns a structured
error naming the required call. Per-session state tracks whether rules have been fetched.
## Consequences
- Delivery becomes effectively guaranteed using only the in-band MCP mechanism — no client-side
component to install or maintain per agent.
- Costs one extra round-trip at the start of each session.
- The error message is part of the product surface and must be unambiguous enough that an agent
recovers on the first try.
- Populating the `initialize` `instructions` field remains available later as belt-and-braces. It
reuses the same renderer, requires no client install, and does not change this architecture.
- Session state now matters: the gate flag must be scoped to the MCP session, and session loss
means re-fetching.

View File

@@ -0,0 +1,26 @@
# ADR-0006: Full-document rule delivery, no dynamic subsetting
**Status:** Accepted (2026-08-08) — carried forward from the original design doc
## Context
An obvious token-saving optimization is to send an agent only the slice of the ruleset relevant to
its current task or file path. This was considered and rejected during design.
## Decision
The rendered rules document is **always sent in full**. No path-based or task-based filtering of
what an agent receives.
How much leash a given model needs to reliably stay on task is a **per-model concern handled by the
renderer** (ADR-0012), not a universal token-saving optimization applied to everyone. Some
models need a fuller, more complete document than others.
## Consequences
- Predictable behavior: an agent's context either contains the whole policy or the session is
gated (ADR-0005).
- Higher token cost per session, accepted deliberately.
- Renderer verbosity becomes the tuning knob for per-model needs.
- Note that this reasoning applies to **rules**, not to **tools**. Tool schemas are a separate
context budget and *are* filtered per agent, driven by the permissions table (see phase 3).

View File

@@ -0,0 +1,23 @@
# ADR-0007: `project_id` in the data model from day one
**Status:** Accepted (2026-08-08)
## Context
The design doc placed multi-project support in the final build phase. But `get_rules(project_id,
agent_type)` already carries a project identifier in its phase-1 signature, and retrofitting
multi-tenancy after five subsystems exist means migrating rules, audit, memory collections,
permissions, and the panel simultaneously.
## Decision
Every table and every tool signature carries **`project_id` from phase 1**. Memory collections are
namespaced per project (`<project>_sessions`). The Web Panel and CLI default to a single project so
the v1 user experience is unchanged.
## Consequences
- Near-zero upfront cost — the identifier was already in the API design.
- Phase 9 becomes exposing a project switcher rather than performing a cross-subsystem migration.
- Every query must be project-scoped from the start; a missing `WHERE project_id = ?` is a
correctness bug, so this belongs in the review checklist.

View File

@@ -0,0 +1,35 @@
# ADR-0008: `enforcement` severity scoped to Bridle-mediated actions
**Status:** Accepted (2026-08-08)
## Context
The design doc defines an `enforcement` guardrail tier where "the gateway itself gates the action
rather than relying on the agent to self-report". As written this implies more than the
architecture can deliver: the gateway can only gate actions that route **through** the gateway. Any
agent with shell access can `git push` directly, `curl` the Kanboard API, or read a token off disk,
and Bridle will never see it.
Adding optional per-agent interceptors (for example Claude Code `PreToolUse` hooks) that consult
Bridle before the agent's own tools run would close the bypass, but is agent-specific, brittle
across vendor updates, and requires a client-side install per agent.
## Decision
Redefine the tier honestly: **`enforcement` means the gateway refuses the action when that action
is Bridle-mediated.** The bypass is documented as a known limit, in the user-facing documentation
and not only here.
Covered: Gitea, Kanboard, secrets, and rules approval accessed through Bridle.
Not covered: the agent's own shell, `curl`, or `git`.
Behavioral enforcement via agent-side hooks is explicitly out of scope for 1.0.
## Consequences
- The tier is accurate rather than aspirational, and ships in phase 1.
- No per-agent brittleness and no client-side component.
- Users must understand that Bridle constrains what flows through it, not what an agent can
physically do. Under-communicating this would be worse than not having the tier.
- Genuine behavioral enforcement, if ever wanted, needs agent-side hooks or network egress control
— a separate effort.

View File

@@ -0,0 +1,30 @@
# ADR-0009: Stable tool list, structured errors on upstream failure
**Status:** Accepted (2026-08-08)
## Context
Bridle proxies several upstreams — the Gitea MCP server, Kanboard, Qdrant — any of which can be
down while the others are healthy. The design doc did not specify what a connected agent sees in
that case. Options: keep the tool list stable and error on call; health-check upstreams and omit
their tools from `tools/list`, notifying via `list_changed`; or refuse to serve at all unless every
upstream is healthy.
Hiding tools avoids the agent calling something dead, but mutates the tool list mid-session, which
some MCP clients cache or otherwise handle badly. Failing the whole session lets one flaky
container block unrelated work.
## Decision
The **tool list stays stable** for the whole session and always reflects the agent's permissions,
not upstream liveness. A call to a downed upstream returns a **clear structured error** naming the
upstream and the failure mode.
## Consequences
- No mid-session tool-list churn, so no dependence on client `list_changed` handling.
- A Qdrant outage does not block Gitea or Kanboard work — failure is isolated per upstream.
- Agents will occasionally call a tool that cannot currently succeed. The error must be specific
enough that the agent reports the real cause rather than inventing a workaround.
- Upstream health is still tracked and surfaced in the Web Panel; it just does not alter the
advertised tool list.

View File

@@ -0,0 +1,29 @@
# ADR-0010: Generic OpenAI-compatible embeddings with model/dimension guarding
**Status:** Accepted (2026-08-08)
## Context
The design doc specified embeddings generated locally via LM Studio on an RTX 3060. Hard-coding
that makes the entire memory subsystem depend on one service and one GPU box being up. Separately,
the choice of embedding model **pins the vector dimensionality** of a Qdrant collection — changing
models silently corrupts a collection or forces a full reindex, and neither should happen by
accident.
## Decision
Target a **generic OpenAI-compatible embeddings endpoint**, configured by `EMBEDDING_URL`,
`EMBEDDING_MODEL`, and `EMBEDDING_DIM`. LM Studio, Ollama, and text-embeddings-inference all
satisfy this.
Record `{model, dim, created_at}` in each Qdrant collection's metadata. On any write, compare the
configured model and dimension against the collection's recorded values; on mismatch, **refuse the
write** and flag that a reindex is required.
## Consequences
- The 3060 is no longer a hard single point of failure; users may point at whatever they run.
- Changing embedding models becomes a loud, explicit reindex rather than silent corruption.
- Slightly more configuration surface than hard-coding LM Studio.
- Bundling a small CPU embedding container as a compose profile default remains available later to
improve the out-of-box experience for new adopters.

View File

@@ -0,0 +1,28 @@
# ADR-0011: Pattern-example RAG gated behind a validation spike
**Status:** Accepted (2026-08-08)
## Context
The pattern-example RAG pipeline — clone, chunk, embed, tag, search, refresh — is the largest
single body of work in the design doc, and it rests on an unvalidated hypothesis: that semantically
retrieved chunks from an example repository measurably improve an agent's output. Naive chunking
of source code is known to produce poor retrieval, so doing it well requires AST-aware chunking via
tree-sitter, which is most of the cost.
## Decision
Insert **phase 5.5, a throwaway spike**, before committing to phase 6. Index one real example repo
with naive chunking, run roughly ten realistic queries, and judge whether the retrieved chunks
would actually have improved an agent's output. The spike code is discarded regardless of outcome.
Phase 6 proceeds only if the spike passes. Its deliverable is a written go/no-go recommendation
with the queries and retrieved chunks as evidence.
## Consequences
- A large speculative build is de-risked for a few days of throwaway work.
- If the spike fails, the `patterns:` section of the canonical document still exists — names,
descriptions, and repository URLs — just without indexed, searchable example code.
- If it passes, the spike's queries become the seed of phase 6's evaluation set.
- Phase 6's size estimate stays honest rather than being quietly discovered mid-build.

View File

@@ -0,0 +1,28 @@
# ADR-0012: `CLAUDE.md` and `AGENTS.md` as the v1 renderer targets
**Status:** Accepted (2026-08-08)
## Context
The design doc names four agent targets: Claude, Codex, Kimi, and DeepSeek. Phase 2 exists to prove
the single-source-of-truth thesis — that one canonical document renders correctly into genuinely
different agent formats. Shipping only one renderer would not prove it, because nothing would force
the canonical schema to stay format-neutral; it would quietly shape itself around the single target.
Shipping all four means four renderers and four golden-test suites before the thesis is validated
even once.
## Decision
Ship **`CLAUDE.md` (Claude conventions) and `AGENTS.md` (Codex)** at 1.0. Kimi and DeepSeek land
post-1.0 as `AGENTS.md` dialects carrying the more explicit, step-by-step phrasing the design doc
calls for on weaker instruction-followers.
## Consequences
- Two formats different enough to keep the canonical schema honestly format-neutral.
- Golden-file tests cover both from phase 2; renderer correctness is that phase's actual
deliverable.
- The renderer must be structured so a dialect (same base format, different phrasing verbosity) is
a cheap addition rather than a fork.
- `agent_profiles` entries for Kimi and DeepSeek may exist in the canonical schema before their
renderers do.

204
docs/implementation-plan.md Normal file
View File

@@ -0,0 +1,204 @@
# Agentic Bridle — Phased Implementation Plan
**Status:** approved 2026-08-08 by Colby Whitlock (org lead)
**Source design:** [`Agentic Bridle MCP Plan.md`](../Agentic%20Bridle%20MCP%20Plan.md)
**Decision records:** [`docs/adr/`](adr/)
This document turns the design doc into an ordered, dependency-correct build plan. Every
architectural decision it depends on is recorded as an ADR; this plan is the schedule, the ADRs are
the reasoning.
---
## Locked decisions
| Area | Decision | ADR |
|---|---|---|
| Deployment | One Rust binary (gateway, rules, memory, admin API, panel); Qdrant / Kanboard / Gitea MCP / embeddings as separate compose services, all traffic routed through the binary | [0001](adr/0001-single-binary-with-routed-services.md) |
| Transport | Streamable HTTP only (`/mcp`) | [0002](adr/0002-streamable-http-only.md) |
| Auth | Bridle-issued per-agent bearer tokens; `clientInfo` is a display hint, never a permission source | [0003](adr/0003-per-agent-bearer-tokens.md) |
| Storage | SQLite (`bridle.db`) | [0004](adr/0004-sqlite-datastore.md) |
| Rule delivery | `get_rules` tool, with hard gating — every other tool errors until `get_rules` succeeds in that session | [0005](adr/0005-get-rules-tool-with-session-gating.md) |
| Rule scope | Full rendered document, never a dynamic subset | [0006](adr/0006-full-document-rule-delivery.md) |
| Multi-project | `project_id` in schema and tool signatures from day one; single-project UX at 1.0 | [0007](adr/0007-project-id-from-day-one.md) |
| `enforcement` tier | Scoped to Bridle-mediated actions; bypass documented as a known limit | [0008](adr/0008-enforcement-scoped-to-mediated-actions.md) |
| Degradation | Stable tool list always; downed upstreams return structured errors | [0009](adr/0009-stable-tool-list-on-upstream-failure.md) |
| Embeddings | Generic OpenAI-compatible endpoint; model + dimension recorded in collection metadata, mismatched writes refused | [0010](adr/0010-openai-compatible-embeddings.md) |
| Pattern RAG | Gated behind a throwaway validation spike | [0011](adr/0011-pattern-rag-behind-spike.md) |
| Renderers v1 | `CLAUDE.md` + `AGENTS.md` (Codex); Kimi/DeepSeek dialects post-1.0 | [0012](adr/0012-v1-renderer-targets.md) |
---
## Phase 0 — Foundations
**Size:** small
Cargo workspace, `docs/adr/` populated, compose skeleton (bridle plus profiles for `with-qdrant`,
`with-kanboard`, `with-embeddings`), Gitea CI on pull request, `.env.example`, Docker secrets
wiring.
**Exit criterion:** `docker compose up` yields a health-checked binary that completes an MCP
`initialize` over Streamable HTTP and advertises zero tools.
## Phase 1 — Gateway core
**Size:** large
The security and accountability spine. Everything later assumes it exists.
- Streamable HTTP MCP server; session lifecycle and per-session state, including the
*rules-fetched* flag that phase 2's gate depends on.
- Token auth → agent identity → permissions lookup. Token issuance, rotation, and revocation
through the admin API.
- SQLite schema carrying `project_id` throughout: `projects`, `agents`, `tokens`, `permissions`,
`audit_log`.
- Append-only audit log written from the first request, with source attribution
(user / agent / vendor).
- Docker-secrets loading, structured logging, per-request timeouts.
- Admin API skeleton — the Web Panel's contract starts here so it is not written twice.
**Exit criterion:** two agents connect under distinct authenticated identities with different
permission sets; a forged `clientInfo` changes nothing about what either may do; every call appears
in the audit log correctly attributed.
## Phase 2 — Rules server, renderers, gating
**Size:** large
The core thesis of the project.
- Canonical schema v1 with `schema_version` and JSON Schema validation: guardrails, patterns,
provenance, agent profiles, permissions.
- Storage and full CRUD admin API.
- `get_rules(project_id, agent_type)` plus the gate: any other tool call before it returns a
structured error naming the required call.
- `CLAUDE.md` and `AGENTS.md` renderers under golden-file tests. Renderer correctness is the
deliverable of this phase, so it carries real test coverage.
- Agent-proposal tools (`propose_guardrail`, `add_pattern`) writing to an approval queue. Nothing
auto-applies.
**Exit criterion:** one edit in Bridle demonstrably changes behavior in a live Claude Code session
*and* a live Codex session; a session that skips `get_rules` cannot use any tool.
## Phase 3 — Upstream proxy and Gitea
**Size:** medium
- Generic upstream-MCP client with connection pooling and health tracking.
- Tool namespacing, stable across restarts (see open item 1).
- Per-agent tool filtering driven by the phase-1 permissions table.
- Credential injection per request; upstreams never hold their own tokens.
- Failure isolation per [ADR-0009](adr/0009-stable-tool-list-on-upstream-failure.md).
**Exit criterion:** an agent completes a full Gitea PR workflow through Bridle while holding no
Gitea token itself; killing the Gitea MCP container degrades only Gitea tools.
## Phase 4 — Kanboard wrapper
**Size:** medium
Thin REST-to-MCP translation inside the binary. Validates both the author-our-own-upstream path and
the bundled-versus-external compose story.
**Exit criterion:** task CRUD from an agent against a bundled Kanboard and against an external one,
switched by environment variable alone.
## Phase 5 — Session memory
**Size:** medium
Qdrant integration, pluggable embeddings with model/dimension guarding, `<project>_sessions`
collections, `remember` and `recall` tools, retention and purge.
**Exit criterion:** a decision recorded by Claude Code is recalled by Codex in a later session;
swapping embedding models is detected and refused rather than silently corrupting the collection.
## Phase 5.5 — Pattern RAG spike
**Size:** small, throwaway
Index one real example repo with naive chunking, run roughly ten realistic queries, and judge
whether the retrieved chunks would actually improve an agent's output. This is an explicit go/no-go
gate on phase 6 — the spike code is discarded either way.
**Exit criterion:** a written go/no-go recommendation with the queries and retrieved chunks as
evidence.
## Phase 6 — Pattern-example RAG
**Size:** large — **conditional on phase 5.5 passing**
Repo fetch → tree-sitter AST-aware chunking → tagged upsert; `search_pattern_examples(query,
arch_tag?)`; `refresh_pattern_example(reference_tag)` with clean replacement of prior vectors.
**Exit criterion:** a named pattern's example repo is indexed and returns relevant code for a
natural-language query; a refresh fully replaces prior vectors for that tag.
## Phase 7 — Web Panel
**Size:** large
Served by the same binary. The admin API already exists from phases 16, so this phase is a client
against it: approval queue, rules editor, secrets (write, rotate, revoke — never display), audit
browser, memory browser, integration status.
**Exit criterion:** every phase 16 operation is achievable through the UI with no CLI or database
access.
## Phase 8 — Guardrail sourcing
**Size:** small
Manual paste-a-document flow producing proposed guardrails into the existing approval queue.
Automated vendor-doc fetching only if the manual workflow proves its worth.
**Exit criterion:** a vendor guidance document becomes a reviewable proposal that requires explicit
approval before entering the canonical ruleset.
## Phase 9 — Multi-project, hardening, 1.0
**Size:** medium
Project switcher on the day-one schema, backup and restore, install documentation, packaging,
release.
**Exit criterion:** a second person can stand up Bridle from the README against their own Gitea and
reach a working agent session.
---
## Cross-cutting
- Security review on every phase that touches authentication or secrets.
- Documentation written as features land, not retrofitted.
- One ADR per architectural decision.
- Golden-file and integration tests from phase 2 onward.
---
## Deviations from the design doc's original 18 ordering
- **Audit log and multi-project schema moved to phase 1.** Both are cheap to add upfront and
expensive to retrofit across five subsystems.
- **Web Panel moved after memory,** but its admin API is built incrementally from phase 1 rather
than all at the end.
- **Vendor guardrail sourcing downgraded to manual-first.** Scraping vendor documentation is
fragile; the manual path captures most of the value at a fraction of the cost.
- **Pattern-example RAG gated behind a spike** rather than committed to outright.
---
## Open items
Not blocking phase 0. Sensible defaults noted where they exist.
1. **Tool namespacing convention.** Proposed default: `gitea__create_pr` (double underscore).
Effectively permanent once agents start depending on it.
2. **Audit log integrity.** Plain append-only table, or hash-chained and tamper-evident?
3. **Approval queue notifications.** Panel-only, or out-of-band webhook/email when an agent
proposes a change?
4. **Web Panel stack.** Server-rendered Rust templates versus an embedded SPA. Not blocking until
phase 7.
5. **Kanboard instance and auth model.** API user versus application token.
6. **CI.** Gitea runner availability, and whether PR-gated CI starts at phase 0.
7. **Repository visibility** and whether GPLv3 remains the license.