Resolve the five design-session open items from PLAN.md: - decisions.md: soft write-path, poll-only sync (NAT), lognormal cold-start priors, purity test binds the SQLite cache - pm-state.md: sidecar layout + directive/capacity/calibration schemas + lifecycle inference table - agent-tools.md: query_project read tool + three write tools Also gitignore .env.* (protect the gitea PAT) and record the P0 actual: 10 labels, 5 milestones, 34 tracer-bullet issues + 51 dependencies filed on christian/commitea as the first managed project. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
5.9 KiB
Markdown
124 lines
5.9 KiB
Markdown
# pm-state: the sidecar store
|
|
|
|
Machine-derived and PM-authored state that has no home in the work repo. Split
|
|
across two tiers per [decisions.md](./decisions.md) D4:
|
|
|
|
- **`pm-state` gitea repo** — durable, versioned truth. Committed files below.
|
|
- **local SQLite** — rebuildable cache/index. Never the source of truth.
|
|
|
|
## `pm-state` repo layout
|
|
|
|
```
|
|
charter.md durable project charter + hot-memory seed (human + agent authored)
|
|
directives/log.jsonl durable append-only directive ledger (conflict-free merge)
|
|
capacity/members.yaml durable per-person capacity model (human-set)
|
|
calibration/model.json cached fitted calibration; regenerable from actuals, committed for
|
|
reproducibility + offline forecasting
|
|
```
|
|
|
|
`forecasts/`, `focus/`, and inferred lifecycle timestamps are **not** committed
|
|
— they are SQLite-only and recomputed on rebuild (regenerable from the work repo
|
|
+ calibration model).
|
|
|
|
## Directive log — `directives/log.jsonl`
|
|
|
|
Append-only, one JSON object per line. **Merge is concatenation**: order is
|
|
derived from `ts` at read time, so two clients appending never produce a git
|
|
conflict. `id` is the durable key; `seq` is a display ordinal computed on read,
|
|
never stored (avoids the "who owns the next number" contention). v1 is
|
|
single-writer (one PM); this format is already safe for the deferred
|
|
multi-writer case.
|
|
|
|
```jsonc
|
|
{
|
|
"id": "d_01J8...", // crypto.randomUUID at write; durable identity
|
|
"ts": "2026-07-08T14:03:00Z", // ISO 8601 UTC; sole ordering key
|
|
"actor": "christian", // gitea username of the directive-giver
|
|
"kind": "reprioritize", // reprioritize | reestimate | set-deadline | scope | capacity | note
|
|
"target": { "issue": 87 }, // { issue } | { milestone } | { member } | null (project-wide)
|
|
"quote": "bump the auth bug above everything", // verbatim, shown in the ledger
|
|
"params": { "priority": 1 }, // structured effect the scheduler applies
|
|
"rationale": "pilot customer blocked", // why (optional but nagged for)
|
|
"status": "accepted" // proposed | accepted | amended | withdrawn
|
|
}
|
|
```
|
|
|
|
Lifecycle: a directive is recorded as `proposed`, the scheduler re-runs, the
|
|
agent presents the consequence diff, and the PM's response flips it to
|
|
`accepted` / `amended` / `withdrawn`. All four states stay in the ledger
|
|
(append a status-change line; never mutate a prior line).
|
|
|
|
## Capacity — `capacity/members.yaml`
|
|
|
|
Estimate unit is **ideal person-days**. Capacity is expressed in ideal
|
|
person-days available per calendar day.
|
|
|
|
```yaml
|
|
members:
|
|
- gitea: christian
|
|
focusFactor: 0.8 # productive fraction of a working day (0..1)
|
|
projectAllocation: 0.6 # share of focused time on THIS project (0..1)
|
|
workdays: [mon, tue, wed, thu, fri]
|
|
daysOff: [] # ISO dates, e.g. ["2026-07-14"]; PTO calendars deferred
|
|
# other standing slices (compliance 0.2, pilots 0.2) are documentation only —
|
|
# only projectAllocation feeds the scheduler.
|
|
```
|
|
|
|
Derived: `capacityPerWorkday = focusFactor * projectAllocation` (ideal
|
|
person-days per working day). The scheduler spreads this across `workdays`,
|
|
zeroing `daysOff`. Missing member ⇒ excluded from capacity, flagged by the agent.
|
|
|
|
## Calibration — `calibration/model.json`
|
|
|
|
Fitted from closed-issue actuals (estimate label vs inferred elapsed working
|
|
time). Lognormal on `log(actual / estimate)`.
|
|
|
|
```jsonc
|
|
{
|
|
"version": 1,
|
|
"fittedAt": "2026-07-08T00:00:00Z",
|
|
"n": 42, // closed issues with an estimate feeding the fit
|
|
"coldStart": false, // true while n < 20 → scheduler uses code priors instead
|
|
"global": { "mu": 0.166, "sigma": 0.45 }, // lognormal params on log-ratio; median ratio = e^mu ≈ 1.18
|
|
"byBucket": { // per estimate label; falls back to global when its n is thin
|
|
"1d": { "mu": 0.30, "sigma": 0.55, "n": 12 },
|
|
"2d": { "mu": 0.18, "sigma": 0.40, "n": 9 }
|
|
// 3d / 5d / 8d ...
|
|
},
|
|
"byLabel": { "backend": { "biasMu": 0.12, "n": 7 } }, // additive to mu; applied when n ≥ floor
|
|
"byPerson": { "christian": { "biasMu": -0.05, "n": 20 } }
|
|
}
|
|
```
|
|
|
|
Cold-start (`coldStart: true`, or a bucket with `n` below floor): the scheduler
|
|
ignores the file's fitted params for that axis and samples the **code-resident
|
|
lognormal priors** in `@commitea/core` (per D3). The file still records whatever
|
|
partial `n` exists so the UI's calibration teaser can show progress toward 20.
|
|
|
|
## SQLite cache (rebuildable — not committed)
|
|
|
|
Mirror + derived tables, rebuilt from both gitea repos on reconcile:
|
|
|
|
- `issues`, `labels`, `milestones`, `comments`, `issue_events` — verbatim work-repo mirror
|
|
- `lifecycle` — inferred per-issue timestamps (see below), keyed by issue
|
|
- `forecasts` — last Monte Carlo run per milestone/issue (regenerable)
|
|
- `focus` — current Now/Next/Later snapshot (regenerable)
|
|
- `directives` — indexed view of `log.jsonl` for fast querying
|
|
|
|
## Lifecycle inference
|
|
|
|
Timestamps derived from the gitea issue timeline; no manual time tracking. Maps
|
|
onto the board's five columns:
|
|
|
|
| Board column | Enter when | Source event |
|
|
|--------------|--------------------------------------------------------|-------------------------|
|
|
| Diagnosis | issue opened | `opened` |
|
|
| Triage | first label or milestone applied | `label` / `milestone` |
|
|
| Steeping | first branch or commit references the issue | `commit_ref` / branch |
|
|
| In review | a PR referencing the issue is opened | PR `opened` |
|
|
| Done | issue closed (PR merged is the deploy signal within) | `closed` / PR `merged` |
|
|
|
|
Elapsed **working** time between Steeping→Done (minus non-workdays/daysOff) is
|
|
the "actual" that feeds calibration. Re-openings append new segments; the fit
|
|
uses summed working time.
|