---
type: plan
title: Ticket → caddie — running feedback and standards status
slug: ticket-caddie-feedback
status: living
owner: rian
created: 2026-09-02
related:
  - /srv/apps/ticket/.logs/planning/01-plan.md
  - /srv/apps/caddie/.logs/planning/05-building-a-caddie-app.md
  - /srv/apps/caddie/.logs/planning/02-tool-contract.md
  - /srv/apps/caddie/.logs/planning/04-interaction-standard.md
  - /srv/apps/easel/.logs/planning/caddie-ui-standard-proposal.md
summary: >
  Caddie is an evolving framework and ticket is one of its drop-ins. This file
  is the running list of every place ticket's needs stress a caddie standard —
  what the standard says, what ticket needed, what we did, and the proposal
  back to caddie — plus a dated status table of what the standards actually
  provide today. Input for the next caddie session, the way easel's
  caddie-ui proposal was. Append as hit; never fix a gap silently.
---

# Ticket → caddie: feedback log

> **Rule (agents.md):** before adding a feature or making a significant change,
> check it against caddie. It either fits the standards as they stand, or it is
> a deliberate, written case here for making caddie more general. Never a
> silent third thing — a ticket-only convenience that quietly diverges is
> drift, and every later app inherits it.
>
> **Mechanics:** `tools/caddie-watch.sh` at the start of every session tells
> you which standard files changed since the snapshot; read the diff, update
> §1, then `tools/caddie-watch.sh --update`.

## 1. Standards status at last review (2026-09-02)

| Standard | Says | Actually exists | Ticket's stance |
|---|---|---|---|
| Tool Contract ct1 (`02`) | vendored `caddie_tool.py` + `caddieHost.ts` | **neither exists** anywhere under `/srv/apps` or `/srv/system` (lands with caddie M2; caddie is at 0.3.x, M1 code-complete, gate awaiting rian's phone) | seams only: `external_ref`, rollup fn, `caddie_links` shape (plan §7.3) |
| Interaction Standard (`04`) | vendored kit `bw_interaction.py` + React pack | **no vendorable kit.** The extraction lives INSIDE caddie as `main/app/services/interaction.py` (+ `attachments.py`); punchlist and easel each carry a hand-copied version | copy caddie's `services/interaction.py` byte-for-byte behind a thin interface; swap when the kit is stamped |
| `caddie-ui` pack (easel proposal, 2026-09-02) | the two-row shell, one side panel, the Bell, Thread*, Composer, Body, Menu, Timeline, Stages, Prose, PanelSection | **exists in easel** (`main/frontend/src/caddie-ui/`, canonical until caddie adopts); proposal status `proposal`, not yet accepted by caddie | vendor it byte-for-byte from easel; ticket is the second consumer |
| Runbook (`05`) | create steps, stack requirements, conformance bar, learnings | current; §5 learnings is ~45% of the doc and mixes caddie-general rules with app-specific bugs | follow §2–§4; see F8 |
| BW-app standard (`QUICKSTART`, `PACKS.md`, `UI-STANDARD.md`, `VIEW-AS.md`) | the scaffold + conformance pack | current; ticket scaffolded 2026-09-02 at 0.1.0 with `instance_noun = project` | as-is |
| `with` SATELLITE-CONTRACT | `party_id` column, registry API with a bearer service token | built (`with` v0.11.0); token provisioned out-of-band per satellite | columns in M0; registry calls M3+ |
| Inbox service (`04 §6`, "marshal") | one cross-app inbox, its own service | **not built**; every app is local-only | local-only, storing the §5 event shape like caddie does |

## 2. Feedback entries

Format: **what ticket needed · what the standard says · gap · what we did ·
proposal to caddie · status.** Numbered `F<n>`, newest last.

### F1 — Comment authors who are not BW users (external email participants)
- **Needed:** a ticket's conversation is mostly emails from people with no BW
  account. They must be first-class message authors.
- **Standard:** `04 §4` — `comment.author_username text`; `§5` "an app may
  only notify users who are members"; mentions resolve through the directory.
- **Gap:** no notion of a *participant* who is not a member. Easel hit the same
  wall (its scope doc §5: "anonymous is a first-class commenter, never a
  member") and it is still unresolved at the standard level.
- **Did:** namespaced actor ids — `author_username = 'email:<addr>'` — with the
  human details in app-owned `contact` / `message_meta`. Needs no kit schema
  change; the notifier naturally skips them (no username matches); "never
  notify the author of their own action" still holds.
- **Proposal:** the standard names the rule: *actor identity strings are
  namespaced; a bare string is a BW username; `email:` (and later `guest:`)
  denote external participants, who may author and be quoted but never
  receive a bell.* One sentence in `04 §4` settles it for easel and ticket at
  once, and the eventual inbox service can carry the same rule.
- **Status:** open — raise with the caddie session.

### F2 — The Interaction kit is still not vendorable
- **Needed:** threads/comments/attachments/bell from a stamped kit.
- **Standard:** `04 §8`/`§9` — "the vendored kit (`bw_interaction.py` + a
  React pack)". `05 §1`: "being extracted from the punchlist build".
- **Gap:** the extraction landed inside caddie (`services/interaction.py`) on
  2026-08-24; three apps now carry hand copies; ticket would be the fourth.
- **Did:** copy caddie's file byte-for-byte behind a thin interface, exactly
  as easel did; record the source hash in the snapshot so a caddie change is
  noticed.
- **Proposal:** promote `services/interaction.py` + `attachments.py` to a
  stamped pack now (even before the inbox service) — the same vendoring
  mechanism as `bw-admin`/`caddie-ui`, with a README and a hash. The cost of
  four divergent copies is already visible in easel's proposal §1.
- **Status:** open.

### F3 — Inbound email as a first-class interaction channel
- **Needed:** email *in* is ticket's main input — an email becomes a comment
  on a subject, authenticated, parsed, deduped.
- **Standard:** `04 §7` treats delivery as **outbound only** ("channels are
  adapters behind `send(user, channel, items)`"), deliberately unbuilt.
- **Gap:** no shared shape for mail coming back. Yet every caddie app wants
  it eventually: a client replying "done" to a punchlist digest, or replying
  to an easel notification with feedback, is the same mechanism ticket is
  building — reply-to token → subject → comment.
- **Did:** ticket builds it app-locally (plan §4, §6) with the parts a shared
  kit would need kept separable: webhook auth, sender auth, parser, token
  routing, comment creation.
- **Proposal:** amend `04 §7` to "channels are **two-way** adapters"; when the
  inbox service exists, the reply-to token and the inbound webhook belong
  there (`bw_mailin`), so one address serves the suite and each app receives
  a `(subject, author, body, attachments)` event. Ticket's M2/M3 code is the
  extraction source, as punchlist's was for comments.
- **Status:** open — the biggest architectural ask in this file.

### F4 — The six-state enum has no word for an ongoing queue
- **Needed:** a support project is never "done"; it is quiet between tickets.
- **Standard:** `02 §5` — frozen enum; `done` = finished stage; caddie D12
  keeps projects bounded.
- **Gap:** ticket maps "no open tickets" → `done`, which reads as a closed
  stage for a relationship that is open. (Caddie D12 acknowledges retainers
  as a future shape.)
- **Did:** the mapping in plan §7.2; the `summary` line carries the truth
  ("0 open"). Acceptable for now — the enum is not wrong, just coarse.
- **Proposal:** when D12's retainer shape is designed, consider either an
  `idle` state or a documented convention that `done` + `counts` all zero
  means "quiet", so a hub can render it as such.
- **Status:** noted; not blocking.

### F5 — Notification kinds: an AI actor and a "new item" event
- **Needed:** "a new ticket arrived from email" and "the AI queued something
  for your review" are the two events rian most wants in the bell.
- **Standard:** `04 §5` kinds `mention · reply · turn · status · resolved`;
  easel proposes `decision`; `actor` is a username.
- **Gap:** a new ticket is arguably a `turn` (ball in the agency's court) —
  fine. But the `actor` of an AI-filed ticket is nobody; and "needs review"
  is neither a turn nor a status.
- **Did:** emit `turn` for new/queued tickets with `actor = 'ai:triage'`
  (namespaced like F1) and `context_label` = the ticket number + title.
- **Proposal:** fold into F1's namespaced-actor rule (`ai:` prefix), and let
  the bell render a non-human actor without a display name.
- **Status:** open, minor.

### F6 — Three things called "project"
- **Needed:** ticket's instance noun is `project` (rian's wording). But a
  caddie project is a bounded engagement with stages, a `with` project is a
  unit of client work with its own UUID, and most support email is
  *client*-scoped, not project-scoped (SATELLITE-CONTRACT: "most records
  aren't project-scoped").
- **Standard:** caddie `01 §6` stores `client_party_id` + optional
  `with_project_id`; `05 §2` says "name the instance noun to match rian's
  wording".
- **Gap:** no guidance on how a tool's instance relates to a caddie project
  when the tool's natural unit is the client relationship, not the engagement.
- **Did:** ticket `project` has `kind ∈ {engagement, support}` and carries
  both ids (plan D1). A caddie assignment links to one ticket project via
  `external_ref`; a support project can be linked from several caddie
  projects over time.
- **Proposal:** a paragraph in `05 §3` on instance ↔ caddie-project ↔ party
  cardinality, so the next client-scoped tool does not re-decide it.
- **Status:** open.

### F7 — `05 §2` create runbook is missing the vendoring steps
- **Needed:** the shell, the bell, and the interaction embryo on day one.
- **Standard:** `05 §2` lists the gateway/scaffolder commands only; `05 §1`
  says "ship the minimal embryo your app needs".
- **Gap:** a fresh session has to discover that the embryo is caddie's
  `services/interaction.py` and the pack is easel's `caddie-ui/` by reading
  three other apps' docs (this session did).
- **Did:** the steps are in ticket's plan §7.1 and M0.
- **Proposal:** add two explicit lines to `05 §2`: `cp` the pack from its
  canonical home, `cp` the embryo, with the hash file; or better, F2.
- **Status:** open.

### F8 — The runbook's Learnings section is outgrowing the runbook
- **Observation:** `05 §5` is ~100 of 219 lines and mixes rules every caddie
  app needs (managed-store swap, `__all__` fixture trap, `all_instances=True`,
  level names frozen) with app-specific bugs (a ResizeObserver on a null ref,
  CSS transitions in background tabs, mega-menu duplicates).
- **Proposal:** graduate the general rules into `§3`/`§4` (the doc's own
  summary says this is the intent) and move app-specific ones to the app's
  handoff. Ticket will append only caddie-general learnings there and keep
  its own bugs in `handoff.md`.
- **Status:** noted.

### F9 — Staff seeding: `all_instances=True` should be a scaffolder knob
- **Needed:** staff see every project; clients see one.
- **Standard:** `05 §5` (easel): "every app whose staff work across all
  instances must set the flag when seeding".
- **Gap:** every caddie app has this shape (agency + per-instance clients),
  yet each re-implements the seeding by hand.
- **Proposal:** a `bw-app.toml` knob (`staff_levels_default_all_instances =
  true`) or a documented seed helper in PACKS.md. Filed here because caddie
  owns the app-shape; also fits `v8-feedback.md` if the kit owns it.
- **Status:** open.

## 3. Learnings worth sending up to `05 §5` (caddie-general only)

*(none yet — ticket has written no product code. Append here as hit; keep
ticket-only bugs in `handoff.md`.)*
