# Settled design decisions

Resolves the "Open items for design session" in [PLAN.md](./PLAN.md). Dated
2026-07-08. Companion docs: [pm-state.md](./pm-state.md) (sidecar formats),
[agent-tools.md](./agent-tools.md) (tool schemas).

## D1 — Write path is soft, split by semantics

Chat (Reginald) is the write path for **PM-semantic** mutations; gitea-native
content stays directly editable.

- **Through the agent** — estimates (`est/*`), priority (`p/*`),
  `deadline/hard`, milestone assignment, dependency edits, and directives.
  Additive ops act directly; destructive ops go propose-approve (Dialog or the
  consequence diff on the Directives screen).
- **Direct, reconciled** — issue title/description, comments, assignees. Edit
  them in the CommiTea UI ("composer writes to gitea, as you") or in the gitea
  web UI; reconcile absorbs out-of-band edits because **gitea is the source of
  truth** for intent. The agent observes changes on the next reconcile and may
  object in standup, but never blocks them.

Rejected: strict (every write through chat). Hostile to quick edits and fights
the reconcile-from-gitea model — an edit made in gitea's own web UI would be
un-representable.

## D2 — Sync is poll + reconcile, no live webhooks in v1

`gitea.stephenmann.io` is remote and the desktop app sits behind NAT, so the
server cannot POST to a localhost webhook. v1:

- **Reconcile on launch** — full read of the work repo(s) + `pm-state` repo into
  the local SQLite cache.
- **Light poll while running** — `since`/conditional-request poll of issues and
  the issue timeline (target 30–60 s cadence; visible < 2 s is a webhook-era
  goal, relaxed to the poll interval for v1).
- The change-source is an **interface** (`ChangeSource`) with a polling
  implementation; a webhook implementation can plug in later for a LAN /
  self-hosted / tunnelled instance without touching the reconcile core.

Rejected now: reachability-detection hybrid (moving parts, cleanup-on-quit),
outbound tunnel (runtime dependency + public ingress). Both remain future
options behind the same interface.

## D3 — Cold-start forecasts use lognormal per-bucket priors

Before the team has n ≥ 20 closed issues with estimates, Monte Carlo samples a
**lognormal** actual-duration distribution per estimate bucket
(`1d/2d/3d/5d/8d`), with a pessimism-skewed median (actuals run long). The
priors live in code (`@commitea/core`), not in a data file. At n ≥ 20 the
scheduler switches to the team's own empirical fit (see calibration model in
[pm-state.md](./pm-state.md)); `byLabel` / `byPerson` bias terms layer on once
their own sample sizes clear a floor.

Rejected: uniform pessimism multiplier (`actual = est × U[1.3, 2.0]`) — cruder,
dishonest tails, no path to per-bucket calibration.

## D4 — Purity test applies to the SQLite cache, not the pm-state repo

The plan's invariant — *delete the sidecar → resync → no truth lost* — is about
the **local SQLite cache**, which is a rebuildable index over two durable
sources:

- **Work repo(s) in gitea** — human-authored intent (issues, milestones + due
  dates, dependencies, assignees, labels, comments).
- **`pm-state` repo in gitea** — the sidecar's own durable truth that is *not*
  regenerable from the work repo: directive log, capacity config, charter, and
  the (cached-but-committed) calibration model.

Delete SQLite → rebuild from both repos → nothing lost. The `pm-state` repo is
never the thing you delete; it is versioned and backed up in gitea like any
other repo. Regenerable state (issue mirror, inferred lifecycle timestamps,
Monte Carlo forecasts, focus snapshot) lives in SQLite only and is recomputed on
rebuild. See [pm-state.md](./pm-state.md) for the file/table split.
