The fixture chat panel is now a working agent. Ask Reginald a question and it
consults the real project through a tool loop, then answers in grounded prose.
Read-only v0 — writes still go through the propose-approve controls.
core (@commitea/core/agent):
- chat-client: OpenAI-wire chat completions over an injected fetch (same seam as
gitea). Points at any OpenAI-compatible endpoint (LM Studio/Ollama/OpenAI).
- model-router: small model for prose + the read tool; big model reserved for
later decomposition (pickModel).
- agent-loop: runAgentTurn drives call→tool→result→call until prose (or a step
budget), recording each tool step. Injected complete + execute → fully testable.
- query-project: the single read tool's engine — compact focus/board/calibration/
issue/search views built from scheduler + lifecycle + calibration; unbuilt views
return a notImplemented marker (never fabricated). The model reports, never computes.
- agent-tools: query_project declaration + Reginald's system prompt.
app:
- main model bridge (model:status, model:chat) runs the loop; query_project
reconciles the repo and builds the view. Model traffic stays in main (token/CSP).
gitea.ts refactored to share getGiteaClient + reconcileSnapshot.
- preload + global.d.ts expose the model bridge; useChat drives the panel — real
agent turn when a model is configured, scripted fixture reply otherwise (so
fixture e2e is unchanged). A subtle "consulted the project" activity line.
Model config (env, defaults to LM Studio on :1234): COMMITEA_MODEL_URL /
_SMALL (google/gemma-4-e4b) / _BIG (qwen/qwen3.6-35b-a3b). COMMITEA_E2E=1 keeps
it unconfigured so the panel stays scripted.
Verified: 88 core tests green (14 agent: client parse, loop tool/error/budget,
all views) + a gated live integration test. Desktop typecheck clean, 14 fixture
e2e green. Gated live e2e drives the real app against gitea + gemma-4-e4b: asked
"what now?", Reginald called query_project and answered "focus is on issue #2"
(the real scheduler pick).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the D3 loop. The forecast now learns from the team's own estimate-vs-actual
history (the working time #5 infers from git events) instead of guessing forever.
core (@commitea/core/calibration-v0):
- fitCalibration(samples): lognormal fit on log(actual/estimate) — global +
per-bucket (once a bucket clears the floor) + per-person bias. coldStart until
n >= 20 closed-with-estimate issues.
- calibrationSamples(): pull those samples from the closed backlog via lifecycle
inference (estimate label vs inferred actualWorkingDays).
- toDurationModel(): project the fit to the params forecast consumes.
- forecast() gains options.model: when past cold-start, fitted params drive the
sim (per bucket, global fallback); otherwise the code priors do. Forecast.coldStart
now reflects the model. nearestBucket extracted + exported.
app:
- AppShell fits calibration once from the reconciled backlog, feeds the model into
forecastBacklog (cone), and drives the Calibration screen + Runway header.
- Focus cone footer, Runway note, and Calibration screen now say cold-start (N/20)
vs calibrated (on N closed) from real data; Calibration scatter / bucket bias /
per-person all fitted, degrading honestly on a thin dataset.
Known refinement: same-day closes yield 0 working-day actuals (day-granular) and
are excluded, so a fast-moving repo can sit at n=0 — honest, but a fractional
(hours-based) actual would let those count. Per-person uses gitea login, not
display name, until the person map lands.
Note: also re-lands #10 (Monte Carlo) and #5 (lifecycle) which merged into their
stacked base branches but never propagated to main (stacked-merge trap); this
branch is cut from main and carries all three so main is whole again.
Verified: 74 core tests green (9 calibration + 2 forecast-switch added), desktop
typecheck clean, 14 fixture e2e green, live spec asserts the real cold-start
calibration surface (Runway note + screen badge fitted from actuals).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fill the board's Steeping / In-review columns (and the calibration actuals)
from real gitea timeline events, replacing the three-column-only v0.
core (@commitea/core):
- inferLifecycle(issue, events, asOf): five-column inference — closed → done;
open PR ref → review; commit ref → steeping; any triage signal → triage;
else diagnosis. Earliest event of each kind fixes the stage timestamp.
- Derives actualWorkingDays (work-start → close) — the estimate-vs-actual the
calibration fit (D3) learns from — and steepingDays (first commit → now) for
the board age badge.
- workingDaysBetween(): whole Mon–Fri days in [start, end), day-granular.
- normalizeTimeline() + client.getIssueTimeline(): map gitea's raw timeline
(label/milestone → triage, commit_ref → commit, pull_ref → pull, close,
reopen), drop the rest. Paginated.
app:
- reconcile now fetches every issue's timeline and returns it keyed by number;
threaded through the bridge → useBacklog → board/focus.
- issuesToBoardColumns + scheduleFocus run inferLifecycle: real Steeping/In-review
columns, steeping-age `days` badge, focus-card steeping badge.
Known refinement: gitea's pull_ref fires on any PR mention, so an issue merely
referenced in a PR body can read as In-review; distinguishing closing refs from
mentions needs the PR link's state (later). Re-opening multi-segment actuals
also deferred.
Verified: 63 core tests green (15 lifecycle, incl. workingDaysBetween + the five
transitions), desktop typecheck clean, 14 fixture e2e green, live spec asserts
the board's Done column is populated from real events.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the demo cone on Morning service with a real, seeded Monte Carlo
forecast over the open backlog. The LLM never does this — it's plain,
reproducible code (evidence-based scheduling).
core (@commitea/core/forecast-v0):
- Code-resident lognormal cold-start priors per estimate bucket (D3):
sampled actual = estimate * exp(N(mu, sigma)), mu > 0 (actuals run long),
sigma shrinks as tickets grow. Replaced by the team's empirical fit at
n >= 20 (#5 supplies the actuals).
- forecast(): seeded mulberry32 + Box-Muller over the scheduler's
deterministic order (order is fixed from estimates/deps; only durations
vary, so the cone stretches, never reorders). Returns p50/p80/p95 landing
+ a per-issue burn-up curve (p10/p50/p90). 12 unit tests; reproducible.
renderer:
- lib/dates.ts: working-day -> calendar mapper (skips weekends) + buildBurnUpData.
- BurnUpCone gains a data-driven twin; falls back byte-identical to the
fixture cone when no forecast (demo mode unchanged).
- Focus card shows the real "80% of the open backlog lands by <range>",
real scope count, and names the cold-start priors.
v0 scope (each a later slice): single serial worker (capacity is #8);
cold-start priors only (empirical fit is #5); no historical actual polyline
(needs lifecycle events, #5). Header chrome (reconcile time, ahead/behind
badge) stays fixture until milestone due dates land.
Verified: 51 core tests green, desktop typecheck clean, 14 fixture e2e green,
live spec asserts the real cone renders (25 open issues, "lands by Nov 11-27").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CommiTea now recommends its own next unit of work from the live backlog.
- @commitea/core: `schedule()` — dependency topo-sort with priority +
estimate tie-breaks, single serial capacity, cycle detection, and
critical-path marking; `selectFocus()` takes the top three. Pure,
deterministic; the LLM does none of this. +11 tests (39 in core).
Client gains `getIssueDependencies`.
- main: reconcile also fetches native issue dependencies for the open
scope and returns edges.
- renderer: `scheduleFocus()` maps real issues+deps→Now/Next/Later;
Focus renders scheduler output (fixture fallback when unconfigured).
v0 scope (each a later slice): single serial worker (per-person
capacity #8), point durations (Monte Carlo cone #10), estimate-only
(calibration #5). Verified: 14 e2e green (fixtures) + gated live spec —
the board shows the real 25 open + 9 closed, and Focus picks #2
ChangeSource (critical path) as Now. Screenshots confirmed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The app now displays its own live backlog instead of fixtures. First
end of the sync loop — the tap-root (#1) grows list reads and a read
path through the Electron main process.
- @commitea/core: client gains `listIssues` (paginated, PRs excluded)
and `listMilestones`; a `lifecycle-v0` mapper (closed→done,
labelled/milestoned→triage, bare→diagnosis — steeping/review await
event inference in P1-5). +10 unit tests.
- main: gitea bridge over IPC (token stays in main, never the renderer);
config from env / .env.local; gated off under COMMITEA_E2E so the
committed e2e stays on fixtures. Preload exposes the typed bridge.
- renderer: useBacklog() reconciles once on mount; issuesToBoardColumns
shapes real issues into The pot. Board takes optional real columns +
a loading state, falling back to demo fixtures when unconfigured.
Verified: 14 e2e green (fixture mode) + a gated live spec that launches
against the real repo — the board renders the actual 25 open + 9 closed
issues (screenshot). SQLite mirror + reconcile-on-a-timer + lifecycle
event inference are the next slices (#2/#3/#5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
createGiteaClient(config, fetchImpl).getIssue(index) fetches one issue
and normalizes raw gitea JSON to a typed GiteaIssue with scheduler
facts precomputed via extractLabelFacts. Network is an injected
FetchLike (core has no DOM lib; global fetch is structurally
assignable), so it unit-tests against a stub — no live calls in the
suite. Non-2xx responses throw GiteaApiError carrying status + body.
Closes P1-1. Verified end-to-end against the live repo's issue #9.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>