Files
commitea/docs/pm-state.md
Croissant Le Doux 9920634e74 docs: settle open design items + dogfood backlog
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>
2026-07-08 11:30:23 -04:00

5.9 KiB

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 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.

{
  "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.

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).

{
  "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.