---
type: plan
title: Caddie — vision and roadmap
slug: caddie-vision-and-roadmap
status: active
owner: rian
created: 2026-08-19
related:
  - /srv/apps/caddie/.logs/planning/02-tool-contract.md
  - /srv/apps/caddie/.logs/planning/03-punchlist-plan.md
  - /srv/apps/caddie/.logs/planning/04-interaction-standard.md
  - /srv/.logs/planning/bw-app-standard-2026.md
  - /srv/.logs/planning/bw-auth-external-users-2026.md
summary: >
  Caddie is the client-facing project hub: one place where a client signs in,
  sees where their project stands, does what is needed from them, and looks up
  reference details. Projects are composed from linear stages; anything
  interactive is a sovereign standalone tool app integrated through the Caddie
  Tool Contract. This plan fixes the architecture, the v1 domain model, the
  decision log, and the cross-project roadmap (caddie spine, tool contract,
  Punchlist as the first tool).
---

# Caddie — vision and roadmap

## 1. What caddie is

An app to keep clients in the loop and give them one place to reference details
about a project. Website-development and marketing projects, composed per
project rather than forced through one rigid flow. The canonical website
example: collect initial info + access → design direction → mockups + sign-off
→ build on staging → client review → launch.

Steps in a project may employ **tools** — apps rian builds as standalones on
their own domains (vibe-coded, tested on real work), then registers with
caddie so a project stage can launch them. The client experiences one signed-in
journey; the machinery stays decomposed.

## 2. The strategy: disposable hub, durable tools

Rian's stated approach is "build the plane as I fly it and then figure out how
to rebuild it later." The architecture takes that seriously instead of fighting
it:

- **The contract is the durable artifact.** Tools own their data, run on their
  own subdomains, and talk to the hub only through the Caddie Tool Contract
  (`02-tool-contract.md`): a signed launch handoff + signed status webhooks.
  A rebuilt caddie re-mints tokens and re-registers tools; the tools never
  notice.
- **The hub is deliberately boring.** Caddie stores coordination state only —
  who is waiting on whom, what is next, what was approved, what a tool
  reported. Its tables must stay re-derivable from an export. Every
  interactive or branching need becomes a tool, even a one-field form.
- **Tools accumulate; the hub is replaceable.** As value concentrates in the
  tool library and the contract, the cost of the predicted rebuild shrinks to
  one app.
- **There are TWO shared standards, not one.** The Tool Contract
  (`02-tool-contract.md`) moves a user into a tool and status back out. The
  **Interaction Standard** (`04-interaction-standard.md`) carries the
  conversation — comments, @mentions, screenshot attachments, and one bell
  whose inbox follows the person across the hub and every tool. A caddie app
  is an app that speaks both.

## 3. The pieces, and what already exists

| Piece | Role | State (as of 2026-08-19) |
|---|---|---|
| **caddie** (this app) | The hub: projects, stages, client timeline, reference, tool launching | Fresh BW-app scaffold, v0.1.0, zero product code |
| **Caddie Tool Contract** | How any tool plugs in (launch, identity, provisioning, status, return) | Spec drafted — `02-tool-contract.md` |
| **BW Interaction Standard** | The shared conversation + alert layer every caddie app implements | Spec drafted — `04-interaction-standard.md` |
| **punchlist** (new) | First tool: client onboarding / advanced tasklists | Plan drafted — `03-punchlist-plan.md` |
| **scout** | Design-direction tool (step 2 of the website flow) | Live at v0.4.1, own Postgres, BW auth; retrofit = one session of contract glue |
| **review** | Staging review + per-page sign-off (step 5) | Live but off-stack (Next.js + Supabase). **Decided: rebuild from the ground up as a caddie app** — and its comment/notification system is the model the Interaction Standard is drawn from |
| **tracking-setup** | GA4/GTM/Ads onboarding with client approval loop | Dormant MVP; its concepts (google_account_ctx, approve/reject loop, audit) feed Punchlist rather than being retrofitted |
| **bw-guides / bw-guide-shot** | Client tutorials + authenticated screenshot capture | Active; the screenshot pipeline is how Punchlist help images get made |

Caddie is **not** a client registry: `with` is the system-of-record for
people/orgs (its frozen SATELLITE-CONTRACT). Caddie reserves a nullable
`party_id` per project and stores display names only; formal satellite wiring
waits until `with`'s cutover milestone lands.

## 4. The client experience (website build, in caddie terms)

The client gets a BW invite email (their account + set-password link, landing
on caddie). They sign in and see one project page:

1. **Header**: project name + status chip.
2. **"Needed from you" strip** — present only when the ball is in their court:
   an approve button, or a tool CTA ("Open Punchlist — 3 items").
3. **Timeline** of visible stages: done stages compact with checkmarks, the
   active stage expanded, upcoming stages muted titles.
4. **Reference tab**: agency-maintained sections (links, credentials-free
   details, decisions).

The website roadmap is usually *Onboarding* (punchlist) → *Design direction*
(scout) → *Mockups* (approval) → *Staging* → *Reviewing* (review, later) →
*Launch*. But that is a plan, not a track: Onboarding gets closed out early,
Staging and Reviewing run together, and Reviewing often stays open long after
Launch is done. Several stages are active at once and each closes on its own.
The same tool can also appear more than once — an onboarding punchlist and a
separate launch punchlist are two assignments and therefore two independent
boards. A marketing project composes differently from the same parts. Tool stages launch the tool full-page with silent SSO and
a persistent "Part of {project} on Caddie · back to project" host bar; status
flows back server-to-server so the hub is correct even if the browser never
returns.

Three UX rules that fall out of review: an expanded tool stage ALWAYS carries
a quiet "Open {tool}" link (the loud CTA + count appears in the strip only
when that tool reports something waiting on this person — a client
re-checking their own answers must never find the door missing, and the strip
never says "0 items"). Attaching a tool is not the end of setup: the agency finishes
inside the tool (e.g. instantiates the onboarding set on the board) and only
then activates the stage — a stage is activated when it is client-ready.
Sign-in lands a client with exactly one project straight on it; with more, on
a minimal project list, active first.

**The journey rule (2026-09-02, from the first tool's client use).** A tool's
landing page is the project's journey, not the tool's inventory: title, blurb,
**the stage rail** (done / here / next — click any to read what it collected
or what is coming), **the active stage's work** (the tool's own — its concepts,
its board, its review), **one primary act** and **one round-closer**. "View"
and "choose" are quiet on purpose; the team's tooling folds under it. That is
this page seen from the hub, and it is what "a tool attached to a stage" looks
like from the client's side. Until the contract sends the rail (02 §2), a tool
holds the project's stages in caddie's own shape (position, title, body_md,
status, client_visible, started_at/closed_at, key) and draws the same page.
Stage prose is the team's, per project, written FOR the client — no
provenance, no open questions; planned stages ship with template prose. The
page speaks to the person by name ("Hi Dana — one thing needs you"). A first
visit's guided introduction opens by itself, once, with Skip one press away —
never behind a "shall I show you around?" modal — and it starts and closes on
this page (02 §6).

## 5. Caddie's own v1 (the spine)

Caddie's own features are deliberately few, and they are all about **laying
out the project**: composing it from stages and milestones, pulling tools in
at the right step, tracking where each participant has got to, and rolling
status up. Everything interactive belongs to a tool; everything conversational
belongs to the Interaction Standard. What is left — the layout, the ball, the
reference material — is caddie.

Stack: the BW-app scaffold as-is, plus a Postgres sidecar from day one
(SQLAlchemy 2.0 + Alembic; the auth kit's four tables move in via
`bw_store_sqlalchemy.managed(...)` — the mechanism is PACKS.md's managed mode.
Scout is the precedent for running the kit on Postgres, but it PREDATES that
module and hand-maps a custom store onto its own tables — do not copy scout's
adapter; use managed mode). Product code lives in
app-owned files only; the vendored kit and standard test pack are never
hand-edited.

### 5.1 Tables (SQLAlchemy-flavored summary)

- **projects** — `id` (== kit instance slug, plain text, **no FK to kit
  tables**), `display_name`, `state: active|paused|complete|archived`, plus the
  `with` linkage columns (§6): `client_party_id uuid NULL`,
  `with_project_id uuid NULL`, and `client_name_hint text` — a **cached hint,
  never truth**.
  **Project create is one service path that creates the kit instance AND this
  row together** (archive mirrors it) — otherwise the kit's invite/grant
  screens have no instance to grant on and the M1 gate cannot run.
- **stages** — a named phase of work, NOT a step in a state machine (§5.2):
  `project_id FK`, `position` (the planned roadmap order — display only),
  `title`, `body_md`, `links jsonb`, `status: planned|active|done|skipped`,
  `target_date date NULL` (optional; the roadmap is often pre-scheduled, but
  nothing enforces or nags on it in v1), `requires_approval bool`,
  `client_visible bool`, `source_template_key` (provenance only, no FK),
  `started_at`, `closed_at`. **No `kind` enum and no one-active-stage
  constraint** — a stage composes optional parts (prose, an approval, a tool)
  and any number may be active at once.
- **field_definitions** — the shared data-point dictionary (§5.3):
  `key PK` (stable slug), `label`, `type: text|longtext|url|date|number|select`,
  `options jsonb` (for select), `help text`, `default_client_visible bool`,
  `archived bool`, `merged_into text NULL` (points at the surviving definition
  after a merge), `created_by`, `created_at`.
- **project_fields** — the per-project values: `project_id FK`,
  `definition_key FK`, `value text`, `client_visible bool` (defaults from the
  definition), `position int`, `updated_by`, `updated_at`.
  `UNIQUE(project_id, definition_key)`.
- **stage_templates** — `key PK`, title, body_md, links, `requires_approval`,
  `default_tool_slug`, `default_tool_config jsonb`, `client_visible`.
- **project_templates** + **project_template_stages** — "Website build v1" =
  the ordered stage-template slots that make up a typical roadmap, with
  per-slot `overrides jsonb`.
- **tool_registry** — `slug PK`, `display_name`, `base_url`,
  `secret_ref` (env-var NAME; the secret itself lives in an env file),
  `capabilities jsonb` (e.g. `{"embed": false}`), `contract_version`, `enabled`.
- **tool_assignments** — `stage_id FK unique`, `project_id`, `tool_slug FK`,
  `config jsonb` (frozen at attach, echoed at provision, never migrated),
  `external_ref` (the tool's own id, returned at provision), plus the
  tool-reported block: `reported_status` (contract enum), `reported_summary`
  (one opaque human line), `client_open_count`, `agency_open_count`,
  `report_seq`, `last_report_at`.
  **The same tool may be assigned to several stages of one project** — an
  onboarding punchlist and a separate launch punchlist are two assignments,
  therefore two independent boards. Unique per *stage*, never per tool.
- **reference_sections** — prose companions to the structured fields: title,
  body_md, links, `client_visible` (**defaults FALSE**), `updated_by`. Saving
  runs a secret-pattern lint (warn, never block): this is exactly where a
  staging password gets pasted at 11pm, and it must not reach a client.
- **approvals** — append-only: stage_id, label snapshot, `approved_by` +
  `approved_by_display` (snapshot survives account churn), approved_at, note.
- **activity** — append-only audit + notification seam: project_id, at,
  `actor_type: agency|client|tool|system`, actor, verb (`project.created`,
  `stage.started`, `stage.closed`, `approval.given`, `tool.reported`,
  `field.set`, …), stage_id, payload jsonb, `client_visible`,
  **`notified_at NULL`** (reserved now so a future notifier needs no schema
  change).
- **project_seen** — `(project_id, username) PK`, `last_seen_at` (drives the
  unseen-changes badge).

### 5.2 Stages, status, and who owns the ball

**Stages are concurrent workstreams, not a linear state machine.** A project
has a starting point, some middle, and an end, and usually a pre-planned
roadmap — but the roadmap is a *plan*, not an enforcement:

- **Several stages can be active at once.** Staging and Reviewing routinely
  bleed together; Launch can complete while Reviewing stays open indefinitely.
- **Stages close independently.** Onboarding gets closed out early and stays
  closed while everything after it runs.
- **`position` is the planned order for display**, nothing more. Advancing is
  always a human act; caddie never auto-advances anything.
- A stage is composed, not typed: prose always, plus optionally an approval
  and/or a tool. That is why the `kind` enum is gone — "staging" is a stage
  with a URL field and later a review tool, and it should not have to pick.

**Caddie does not decide who has the ball — the tools do.** Caddie's job is the
framework, the roadmap, and the running commentary: *where are we in this
project*. The granular "you have three things to do" belongs to the tool that
owns the work, arrives over the contract's status report, and caddie merely
surfaces it. There is therefore **no `sync_turn`, no derived ball-in-court, and
no turn state machine in caddie** — that machinery is deleted from the design.

What caddie *does* render:
- **Project state**: the explicit `state` field, plus which stages are
  currently active ("Staging · Reviewing"). That is the answer to "where are
  we."
- **"Needed from you"**: the union of (a) open approvals on active stages and
  (b) whatever attached tools reported as waiting on this person
  (`reported_status = waiting_client` / `client_open_count`). Caddie
  concatenates; it does not compute.
- **Owner dashboard**: one row per project — state, active stages, each
  attached tool's `reported_summary`, oldest `last_report_at` (staleness), and
  the unseen-by-client badge.

### 5.3 Project context — a shared field dictionary

Projects carry context that varies wildly: scope, key dates, the website, the
staging URL, a GA4 property. The failure mode to avoid is **both** extremes: a
rigid "fill in these five things before you can start", and a free-for-all
where four projects grow four differently-named fields that all mean the same
thing.

So context is **sparse per project, but its labels come from one growing
dictionary**:

- A `field_definition` is created the first time a data point turns out to be
  useful — a PM creating "Staging site URL" (type `url`) once makes it
  available to every project afterwards.
- Adding context to a project **always starts by searching the dictionary**,
  showing each match's label, type, and how many projects already use it. The
  usage count is what signals "this is the canonical one."
- **"Create a new field" is the secondary action**, and before it commits it
  shows near-matches ("Did you mean *Staging site URL*, used in 12 projects?").
  Convergence comes from making reuse the easiest path, not from policing.
- **Merging is the release valve.** Duplicates will still happen; an owner can
  merge two definitions — values repoint to the survivor, the loser is archived
  with `merged_into` set, and nothing is lost. Spec'd now because a dictionary
  without a merge tool degrades into exactly the mess it was meant to prevent.
- Types are global to the definition (a staging URL is a URL everywhere), which
  is what makes rendering consistent: URLs become links, dates format and can
  decorate the roadmap.
- **Client visibility is per value**, defaulting from the definition. The
  website and launch date are usually client-visible; internal scope notes are
  not.

**Boundary with `with`** (its frozen contract, §6): a fact about the *client*
that several tools share — a Drive folder, a company address — belongs on the
organization in `with`, read by `party_id`. A fact about *this project* — its
staging URL, its launch date — belongs here. When in doubt: would this outlive
the project? If yes, it is `with`'s.

### 5.4 API surface (all behind the kit's default-deny middleware)

**Client-facing:** `GET /api/projects`, `GET /api/projects/{slug}` (roadmap +
needed-from-you + context + reference), `POST .../stages/{id}/approve`,
`POST .../seen`, `GET .../activity` (client_visible rows).

**Admin:** project/stage/reference/template CRUD; `start|close|skip|reopen` on
any stage independently (never a single cursor); reorder planned stages; tool
registry + attach + refresh-poll; and the dictionary —
`GET /api/admin/field-definitions?q=` (search returning label, type and
**usage count**, the route that makes reuse the default),
`POST /api/admin/field-definitions` (create; response carries near-matches so
the UI can warn before committing a near-duplicate),
`POST /api/admin/field-definitions/{key}/merge` (owner-only: repoint values,
archive the loser, set `merged_into`), and
`PUT|DELETE /api/admin/projects/{slug}/fields/{key}` to set or clear a value.

**Tool-facing** (S2S, HMAC-signed per the contract, its own router — never
session-authed): `POST /api/tool/report`, `GET /api/tool/assignments/{id}`.
Plus `GET /go/{assignment_id}` — the session-authed redirect that mints launch
tokens (contract §3).

### 5.5 Templates — copy semantics

Instantiation deep-copies stage templates into brand-new stage rows at create
time; `{{client_name}}`/`{{project_name}}` substitute at copy time; unknown
tokens pass through literally. No FK back to templates — editing or deleting a
template can never touch a live project, structurally. Tool slots copy a
*suggestion*; the ToolAssignment + provision call happen only when the agency
explicitly attaches.

## 6. Linking to `with` (conventions now, integration later)

`with` is the system-of-record for people, organizations and projects, and its
SATELLITE-CONTRACT is frozen. The integration itself will be a **separate
bridge app** that reads from and reports to `with` — caddie never calls it
directly. Caddie's only job now is to store the right identifiers so that
bridge has something correct to work with:

- **Store `client_party_id`** — a `party_id` is the umbrella UUID for *a person
  OR an organization*. That is exactly why you point at the party and not at an
  organization id: a client may be a company or a lone freelancer, and the
  column must not care.
- **Never store the client's name as truth.** `with` owns names, spelling,
  merges and archival. Caddie keeps `client_name_hint` as a **cache to refresh**
  from the registry, never as the key.
- **Never store a role.** "Client" is a property of the *relationship*, not of
  the party — the same organization is a client in one context and a vendor in
  another. Caddie stores the id and nothing role-shaped.
- **BW username ≠ party_id.** Identity-that-logs-in is the BW Auth username
  (already solved). Entity identity is the party. A human has both; they are
  linked inside `with`, not by caddie.
- **`with_project_id` is optional secondary metadata**, nullable — the client
  link is the party, not the project.

## 7. Tool integration (summary — the contract doc is authoritative)

Adopted after an adversarial three-way design review (iframe embed vs
reverse-proxy mount vs link-out handoff):

- **Handoff (link-out) is the mandatory core.** Caddie renders a tool stage as
  a link to `GET /go/{assignment_id}` (session-authed), which mints a fresh
  120s single-use signed token and 302s to the tool's `/launch`. The tool
  authenticates the user itself via normal BW SSO (silent `prompt=none` first,
  interactive top-level login as natural fallback), auto-provisions a grant
  scoped to exactly the linked instance, wears a slim host bar with a return
  link, and reports status back through signed webhooks.
- **Embed (same-site iframe) is an optional, per-tool, earned mode** — same
  token and reporting machinery, plus a postMessage bus and frame CSP. Only
  for small widget-shaped tools that prove they need it; never a fleet
  requirement (every embed-capable tool pays a doubled testing surface
  forever).
- **Proxy-mount is rejected**: shared-origin XSS blast radius, hub in every
  tool's hot path, base-path taxes — it inverts both of the substrate's best
  free properties (per-subdomain isolation, independent deploys).

## 8. Decision log

- **D1 — Tools are sovereign standalone apps; integration is a contract, not a
  framework.** Standalone-first matches how rian actually builds; the contract
  makes a caddie rebuild cheap. Rejected: tools as caddie modules.
- **D2 — Handoff core, embed optional.** Judge scores 28/23/16 across velocity,
  security, UX, rebuild survivability, ops, adoptability. Punchlist and the
  scout retrofit ship handoff-only.
- **D3 — Identity always comes from the IdP; the hub is never an identity
  authority.** The launch token's `sub` is advisory (authorization hint), never
  authentication. Removes a whole class of delegation vulnerabilities.
- **D4 — The spine has no workflow engine.** Stages are declarative and
  advanced by a human; caddie has no conditions, no triggers, no
  auto-advancement, and no branching. Any interactive need — even a one-field
  form — is a tool.
- **D13 — Stages are concurrent workstreams, not a linear state machine.**
  Many may be active at once, they close independently, and `position` is the
  planned order for display only. (Supersedes the original one-active-stage
  model, which could not express "Reviewing stays open after Launch".)
- **D14 — Caddie does not own the ball; tools do.** Caddie is the framework,
  roadmap and running commentary — *where are we in the project*. The granular
  "what do you need to do" comes from whichever tool owns that work, over the
  contract's status report. No `sync_turn`, no derived turn state in caddie.
- **D15 — Project context is sparse per project, but its labels come from one
  growing dictionary** (§5.3). Never a required-fields gate; never a
  free-for-all either. Reuse is made the easiest path, and a merge tool exists
  from day one because duplicates are inevitable.
- **D5 — Postgres from day one, kit store in managed mode (PACKS.md); no
  caddie FK ever points at a kit table; actor columns are username strings
  with display-name snapshots.** (react.md compliance + rebuild insurance +
  account-churn safety.) Scout is precedent for kit-on-Postgres but uses a
  pre-module custom store — the mechanism to copy is managed mode, not
  scout's adapter.
- **D6 — Caddie is not a client registry.** `with` is the system-of-record;
  caddie stores `client_party_id` (party = person OR organization), keeps the
  client name only as a refreshable hint, and stores no role. Integration
  itself is a separate bridge app, built later — but the conventions land now
  so nothing has to be back-filled from bad keys (§6).
- **D7 — Clients are external BW accounts via kit invites.** "Send them a
  link" = the invite email (account + set-password + lands on the app). A
  returning client who already has an account gets the kit's `notify_added`
  courtesy email ("you've been given access to {app}") when granted a new
  project — otherwise project #2 would arrive silently. No magic links, no
  app-local auth, ever (house rule + tracking-setup's magic links are
  superseded).
- **D8 — One inbox from the start; OUTBOUND delivery deliberately later.**
  Comments, mentions and the bell are standard for every caddie app from M1
  (`04-interaction-standard.md`). What is deferred is delivery *out* to a
  person who has not logged in — and it is explicitly not email-only: Telegram,
  Slack and WhatsApp are wanted eventually, so the seam is channel-agnostic
  with a digest window, not an email endpoint.
- **D9 — The status-report contract is frozen small**: one 6-value enum +
  counts + one opaque summary line + monotonic seq. Anything richer stays
  behind the tool's own URL. (Verified on paper against punchlist, scout, and
  review lifecycles before freezing.)
- **D11 — Comments live in the app that owns the subject; notifications
  aggregate into ONE inbox.** N bells is worse than no bells; local comments
  keep tools sovereign and survive a caddie rebuild. The inbox is its own small
  service (not a caddie feature) precisely because caddie is the app most
  likely to be rebuilt (`04` §2, §6).
- **D12 — Projects are bounded (start → finish) in v1.** The current work is
  several website builds and one 3-month marketing engagement, all with real
  start and end points. Recurring/retainer shapes are a real future need but
  are not designed yet — nothing is built speculatively for them, and the
  linear stage list does not preclude them.
- **D10 — Editing a template must be structurally unable to mutate live
  work.** Caddie enforces it by deep copy at instantiation; punchlist by
  pinning immutable published template versions (published rows are never
  UPDATEd — guarded in code and by a DB trigger). Two mechanisms, one
  invariant.

- **D16 — The shared chrome is a vendored pack, canonical in caddie's
  workspace.** `packs/caddie-ui/` (2026-09-02), stamped byte-for-byte into
  every caddie app by `packs/stamp-caddie-ui.sh` and verified with `--check`;
  an app never re-implements a piece and never hand-edits its copy. Rejected:
  each app styling its own bell and drawer — three apps did exactly that
  within three weeks of the standard naming them as shared.
- **D17 — A client-owned round marker stays in the tool; the report body
  stays frozen.** "Feedback complete" is recorded by the tool and reported as
  `waiting_agency`; presenting again is `waiting_client`. Rejected:
  `client_closed_round_at` in the status report (02 §5) — the six states
  already say whose turn it is, which is all the hub renders (D9, D14).
- **D18 — Subjects are plural, and the work surface itself is one.** Caddie's
  project page is a subject of caddie (a project-level conversation); a
  tool's board is a subject of the tool. Every subject a page has is listed in
  its Discussion, spoken about or not (04 §4.1).
- **D19 — Stages reach a tool as a pushed, read-only mirror**, on provision
  and on every reconcile; caddie seeds a new project's roadmap from its
  templates; a tool used standalone seeds its own in the same shape (02 §2).
  Rejected: a live read of the hub per page view — a page that depends on the
  hub answering breaks the sovereignty principle (02 §0.6).
- **D20 — A guided introduction opens and closes on the project page.**
  Caddie runs "welcome / where we are / next: the tool" and "your preference
  / when you are done"; the tool runs the middle; the hand-off is
  `?tour=1` / `?intro=1` / `?tour=done` (02 §6). No popup gate on either side.

## 9. Security posture

- Launch tokens: HS256 per-tool secret, 120s TTL, single-use `jti` burned
  FIRST on redeem (INSERT under a unique constraint), minted per-request
  behind a session-authed redirect. The token does transit the tool's access
  log and browser history — which is why `/launch` never renders content on
  the token URL (immediate 303), `/go/` sends `Referrer-Policy: no-referrer`,
  and the burn makes a logged token worthless within its 120s window. Return
  URLs are parsed and matched to the caddie host exactly (never startswith).
- S2S both directions: HMAC-SHA256 over timestamp + nonce + body; ±300s skew
  window; nonce replay cache; per-assignment monotonic `seq` so ordering never
  matters. Secrets live in 0600/0660 env files (gateway-readable), written by
  the registration CLI, never printed, never in argv.
- Provisioned grants are scoped to exactly one instance and gated on the
  assignment's current participant list. Revocation is pending-until-acked
  (retried with backoff) AND converges independently via the participant diff
  on every reconcile — a revoked client's access outlives revocation by
  minutes at worst, even across total webhook loss. (Kit-level deactivation is
  NOT assumed to propagate into tools instantly under the managed store; it
  rides the same convergent channel.)
- Client-visible surfaces carry no secrets: reference sections are policy-bound
  to links + non-secret details (credentials belong in the tools that need
  them, or nowhere).
- Everything state-changing lands in append-only tables (activity, approvals,
  and each tool's own event log).
- After M2 lands: run `srv-gw security-audit` and review tool-facing routes;
  future embed mode requires `frame-ancestors` CSP on tool embed routes before
  it ships.

## 10. Roadmap (cross-project; each milestone gate is a demo, not a vibe)

Caddie versioning: M1 → 0.2.x–0.3.x (done), standards alignment (the pack,
04 §3–4 complete) → 0.4.0 (2026-09-02), M2 → 0.5.x, the joint gate + hardening
(items 5–6) → 0.6.x.

1. **caddie M1 — bare spine, zero tools.** All §5 tables + routes (minus the
   tool router), client + admin views, "Website build v1" roadmap template
   seeded (tool slots render as prose placeholders), **the project-context
   field dictionary** (§5.3), **plus the Interaction kit in local-only mode** — comments, @mentions, screenshot attachments and the bell
   working on stages (`04` §9). **Gate:** create a project from the template,
   invite a fresh external client, client signs in on a phone, sees timeline +
   reference, taps Approve, and comments on a stage with a screenshot; an
   @mention lights rian's bell with a deep link straight to that comment. Two
   stages run active at once and close independently; a "Staging site URL"
   field is created once, then found and *reused by name* on a second project. *Caddie is already useful for
   every current project at this gate.*
2. **punchlist M1 — engine runs a flow** (standalone; see `03-punchlist-plan.md`).
3. **punchlist M2 — sets, invites, authoring** (standalone gate, no caddie
   hop: fresh external user completes a client step from the invite email;
   one fill of `who_needs_access` spins up 5 flows).
4. **caddie M2 — tool plumbing + the contract made real.** ToolRegistry,
   ToolAssignment, `/go/`, `/api/tool/*`, reconcile + participant convergence,
   the registration script, and the vendorable drop-ins (`caddie_tool.py` +
   its stub tool, `caddieHost.ts`). **Gate:** the contract conformance suite
   green against the drop-in's own stub tool — launch, burn-first 303,
   provisioning refusals, report, participant diff, acked revoke, all
   exercised without a real tool. (Punchlist is deliberately NOT this gate:
   it doesn't speak the contract until its M3.)
5. **punchlist M3 × caddie — the joint integration gate.** Punchlist vendors
   the drop-ins; register, attach to a live project stage. Stand up the
   **inbox service** and repoint both apps (config + backfill). Then the
   **bootstrap dry run**: fresh throwaway email → invite → set-password →
   caddie project page → tool launch → granted board → complete a step —
   zero owner intervention, screens counted (judge risk R2) — caddie's strip
   count drops with the turn flipping via the webhook, and **one bell shows
   items from both apps, from inside either app** (`04` §9).
6. **First real client through the full loop.** Run a real onboarding through
   caddie. Instrument the launch path and **measure silent-SSO success rate +
   wall time on iOS Safari and Android Chrome** (judge risk R1). Harden what
   the measurement says.
7. **scout retrofit** — vendor the drop-ins + the interaction kit, one
   migration, 2–3 `post_status` calls, register. One session.
8. **Delivery** — email adapter + digest window first, then a chat channel
   (`04` §7). Build it once a real client has run through and we know what
   people actually miss.
9. **review, rebuilt as a caddie app** — from the ground up on-stack, its
   comment/sign-off model already distilled into `04`.
10. **Later / as needed:** the `with` bridge app, embed mode for the first tool
    that earns it, recurring/retainer project shapes (D12).

## 11. Open questions for rian

Resolved 2026-08-19: `with` (conventions now, bridge app later — §6) · review
(rebuild as a caddie app — §3) · retainers (future, D12) · notifications (one
inbox from M1, multi-channel delivery later — D8) · "milestones" (stages are
concurrent workstreams, D13) · tracking (caddie shows the stage; tools own the
ball, D14) · project context (a shared field dictionary, D15). Still open:

1. **Target dates on stages are in, but unenforced** — `target_date` is
   optional, shown on the roadmap, and nothing nags or auto-advances on it.
   Decided rather than asked because it is nearly free now and awkward later;
   say the word and it comes out.
2. **A name for the inbox service** (`04` §6) — `marshal.bowden.works` is a
   candidate (the marshal keeps play moving and tells players what's
   happening). Only needed by the joint-gate milestone.

## 12. Sharpest risks and their early tests

- **R1 Silent-SSO invisibility is assumed, not measured** → instrument and
  measure on real phones during M3 (see roadmap #5).
- **R2 The new-client bootstrap chain is untested end-to-end** → the M2 joint
  gate is exactly that dry run (roadmap #4).
- **R3 The status enum could fail a future tool** → mapped punchlist, scout,
  and review onto it on paper before freezing (contract §5); any tool that
  cannot map revises the contract *before* adoption, never after.
