CommiTea foundation + P3 UI: gitea client, e2e harness, all product screens #35
1
.env.example
Normal file
1
.env.example
Normal file
@@ -0,0 +1 @@
|
||||
GITEA_TOKEN=
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,8 @@ out/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
|
||||
26
docs/PLAN.md
26
docs/PLAN.md
@@ -87,7 +87,11 @@ directives. Electron app, one window, "tell me what to do" experience.
|
||||
Deliverables per phase; replace TBD with actuals at phase close.
|
||||
|
||||
- **P0 — Scaffold** — repo, Electron shell, gitea API client + token auth,
|
||||
`pm-state` repo bootstrap. Actual: TBD
|
||||
`pm-state` repo bootstrap. Actual (2026-07-08): yarn4 workspaces + electron
|
||||
shell + `@commitea/core` label schema + design system mirrored. **Dogfood
|
||||
backlog filed** on `christian/commitea`: 10 labels (est/p/deadline, exclusive
|
||||
scopes), 5 phase milestones, 34 tracer-bullet issues with 51 native
|
||||
dependencies. `pm-state` repo bootstrap still pending (P1-6 / P5-1).
|
||||
- **P1 — Sync + data model** — read mirror into SQLite cache, webhook
|
||||
listener + reconcile-on-launch, lifecycle inference from event stream,
|
||||
label schema applied. Actual: TBD
|
||||
@@ -113,12 +117,16 @@ vertical slices within phases where possible.)
|
||||
- Concurrent multi-PM directive writing
|
||||
- Cloud LLM as *requirement* (it's a config option, not a dependency)
|
||||
|
||||
## Open items for design session
|
||||
## Open items for design session — RESOLVED 2026-07-08
|
||||
|
||||
- Chat-as-only-write-path: strict or soft (UI edits allowed but agent
|
||||
observes/objects)?
|
||||
- `pm-state` file formats (JSONL event log settled; capacity/calibration
|
||||
schemas TBD)
|
||||
- Tool schema for the fat `query_project` tool
|
||||
- Webhook endpoint mechanics per instance (port allocation, cleanup on quit)
|
||||
- Cold-start estimate distributions (industry priors vs uniform pessimism)
|
||||
Settled in [decisions.md](./decisions.md), [pm-state.md](./pm-state.md),
|
||||
[agent-tools.md](./agent-tools.md):
|
||||
|
||||
- Chat-as-only-write-path → **soft, split by semantics** (decisions.md D1)
|
||||
- `pm-state` file formats → directive/capacity/calibration schemas (pm-state.md)
|
||||
- Fat `query_project` tool schema → one read tool + three write tools
|
||||
(agent-tools.md)
|
||||
- Webhook mechanics → **poll + reconcile only in v1** (NAT), behind a
|
||||
`ChangeSource` interface (decisions.md D2)
|
||||
- Cold-start distributions → **lognormal per-bucket priors** in code
|
||||
(decisions.md D3)
|
||||
|
||||
119
docs/agent-tools.md
Normal file
119
docs/agent-tools.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Agent tools
|
||||
|
||||
Reginald gets **few, fat tools** so a small local model (gemma-4b class) can
|
||||
survive with one thing to reach for. One read tool, three write tools. All I/O
|
||||
is compact JSON; ticket data is fetched through tools, never copied into hot
|
||||
memory ([PLAN.md](./PLAN.md) memory layers).
|
||||
|
||||
Model routing (per PLAN.md): the read tool + prose/standup run on the small
|
||||
local model; `capture_work` decomposition and `record_directive` negotiation
|
||||
route to the big model.
|
||||
|
||||
## `query_project` (read)
|
||||
|
||||
The single read tool. A `view` enum selects the shape; `filters` narrows it. The
|
||||
scheduler's deterministic output backs every forecast field — the model never
|
||||
computes, it reports.
|
||||
|
||||
```jsonc
|
||||
query_project({
|
||||
view: "focus" | "issue" | "milestone" | "board" | "runway"
|
||||
| "calibration" | "directives" | "standup" | "search",
|
||||
filters?: {
|
||||
issueId?: number,
|
||||
milestoneId?: number,
|
||||
state?: "diagnosis" | "triage" | "steeping" | "in_review" | "done",
|
||||
assignee?: string, // gitea username
|
||||
label?: string,
|
||||
query?: string, // free text, for view:"search"
|
||||
limit?: number // default 20, max 100
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
View payloads (compact; forecasts always ranges, never point dates):
|
||||
|
||||
- **focus** — `{ now, next[], later[] }`, each `{ issue, title, rationale }`.
|
||||
- **issue** — intent (title, description, comments, assignee, labels) + derived
|
||||
(lifecycle timeline, per-issue forecast range, dependency ids, provenance).
|
||||
- **milestone** — `{ due, hard, stats, cone: {p10,p50,p80 dates}, issues[] }`.
|
||||
- **board** — issues grouped by the five lifecycle columns.
|
||||
- **runway** — per-milestone `{ due, band: {p50,p80}, slack }` + capacity list.
|
||||
- **calibration** — `{ n, coldStart, globalMultiplier, byLabelBias[], byPersonBias[] }`.
|
||||
- **directives** — pending consequence diff + recent ledger entries.
|
||||
- **standup** — drift report, per-person plan, stale blockers.
|
||||
- **search** — issues matching `query`.
|
||||
|
||||
## `capture_work` (write — big model)
|
||||
|
||||
Braindump → interview → proposed issue set. Returns a **proposal**, never files
|
||||
directly; the Capture screen's review tray edits it before `apply_changes` files
|
||||
it. Decomposition + estimate negotiation is the one place the big model earns its
|
||||
keep.
|
||||
|
||||
```jsonc
|
||||
capture_work({
|
||||
braindump: string,
|
||||
answers?: { question: string, answer: string }[] // interview turns so far
|
||||
})
|
||||
// → { needsMoreInfo?: string[], // follow-up questions; present as chips
|
||||
// proposal?: { issues: [{ title, body, estimate: EstimateLabel,
|
||||
// priority?: PriorityLabel, deps?: number[],
|
||||
// milestone?: number }],
|
||||
// consequence: string } } // one-line schedule impact
|
||||
```
|
||||
|
||||
## `apply_changes` (write — unified mutation)
|
||||
|
||||
Every mutation funnels here: filing captured issues, label/estimate/priority/
|
||||
milestone/dependency edits. Additive ops apply directly; **destructive ops
|
||||
require `approved: true`** (the caller obtains approval via the consequence
|
||||
diff / Dialog first — see [decisions.md](./decisions.md) D1). Batched so one call
|
||||
= one coherent change with one consequence.
|
||||
|
||||
```jsonc
|
||||
apply_changes({
|
||||
ops: [
|
||||
{ op: "create_issue", title, body, labels?, milestone?, deps? },
|
||||
{ op: "set_estimate", issue, estimate: EstimateLabel },
|
||||
{ op: "set_priority", issue, priority: PriorityLabel },
|
||||
{ op: "set_milestone", issue, milestone: number | null },
|
||||
{ op: "set_deadline_hard", milestone: number, hard: boolean },
|
||||
{ op: "add_dep", issue, dependsOn: number },
|
||||
{ op: "remove_dep", issue, dependsOn: number }, // destructive
|
||||
{ op: "close_issue", issue }, // destructive
|
||||
{ op: "remove_label", issue, label } // destructive
|
||||
],
|
||||
approved?: boolean // required iff any op is destructive
|
||||
})
|
||||
// → { applied: number, consequence: string, rejected?: {op, reason}[] }
|
||||
```
|
||||
|
||||
Only touches labels in the CommiTea namespaces (`est/*`, `p/*`, `deadline/hard`)
|
||||
plus native issue fields — never invents labels, comments, or synthetic issues
|
||||
(zero-pollution goal).
|
||||
|
||||
## `record_directive` (write — big model)
|
||||
|
||||
Appends to the directive log ([pm-state.md](./pm-state.md)), triggers a scheduler
|
||||
re-run, and returns the consequence diff for propose-approve. Does **not** mutate
|
||||
gitea itself — a directive is intent; its effects land through `apply_changes`
|
||||
after approval.
|
||||
|
||||
```jsonc
|
||||
record_directive({
|
||||
kind: "reprioritize" | "reestimate" | "set-deadline" | "scope" | "capacity" | "note",
|
||||
quote: string, // verbatim PM words, stored in the ledger
|
||||
target?: { issue?: number, milestone?: number, member?: string },
|
||||
params?: object, // structured effect, e.g. { priority: 1 }
|
||||
rationale?: string
|
||||
})
|
||||
// → { directiveId, consequence: { before, after }[], summary: string }
|
||||
```
|
||||
|
||||
## Not tools
|
||||
|
||||
Reads that are pure UI state (theme, current view, back-stack) never go through
|
||||
tools. The scheduler, Monte Carlo, calibration fit, and lifecycle inference are
|
||||
**code**, invoked by the runtime around these tools — the model requests a view
|
||||
or proposes a change; deterministic code produces every number.
|
||||
73
docs/decisions.md
Normal file
73
docs/decisions.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
123
docs/pm-state.md
Normal file
123
docs/pm-state.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user