The 26b is slow (~30s/call); the chat now shows the answer forming instead of
freezing until it's done. The final prose streams over SSE; tool-calling turns
stay structured (no partial tokens), so streaming kicks in for the narration.
core (@commitea/core):
- chat-client.complete gains an optional onToken — when set, it requests
stream:true and parses the OpenAI SSE stream, emitting content deltas and
assembling streamed tool-call argument fragments into the final result.
- GiteaHttpResponse exposes the optional `body` stream (real fetch has it; stubs
don't). agent-loop threads onToken to each completion.
app:
- model:chat forwards each delta to the renderer (event.sender.send); preload
exposes model.onToken(cb) → unsubscribe. useChat accumulates the live stream
into a growing bubble (with a cursor), replaced by the authoritative final
content when the turn resolves. Unconfigured → scripted reply, unchanged.
Verified: 118 core tests green (2 streaming: SSE content deltas + tool-call
fragment assembly), desktop typecheck clean, 14 fixture e2e green. Live: a real
turn against gemma-4-26b assembles the correct answer via the streaming path
(live-reginald green) — the reply now renders token-by-token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking a milestone on Runway now opens its real detail: scope + done %, a Monte
Carlo cone over the remaining open work, and the milestone's issues grouped by
lifecycle column. Threaded the gitea milestone id through the Runway row → AppShell
→ a milestoneView().
- backlog.ts: milestoneView(id, ...) → { name, due, scope/done, forecast cone +
range, groups by lifecycle column }. Reuses forecast + buildBurnUpData + lifecycle
inference. null for an unknown id → the screen shows the demo fixture.
- RunwayMilestone gains an `id`; runwayView sets it; RunwayScreen.onOpenMilestone(id).
- MilestoneScreen takes optional `data`; renders real header/stats/cone/issue-groups
when present, fixture otherwise.
Verified: desktop typecheck clean, 14 fixture e2e green. Live: clicking "P2 —
Scheduler + Monte Carlo" opens a real detail — 7 issues · est 20d, 0/7 done, cone
"80% Aug 17–26", issues in Triage/In-review from the real event stream (screenshot).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Runway milestone list is now real. Each open gitea milestone's open scope gets
its own Monte Carlo forecast (reusing the P2 engine); the p80 landing range is
shown, and compared to the milestone's due date (on track / at risk) when one
exists. Ranges, never point dates.
- backlog.ts: runwayView(issues, milestones, deps) → RunwayMilestone[] — per
milestone: forecast its open scope, map p50..p90 to a date range, normalize the
RunwayBar band across a shared horizon, tone/ note from due-vs-p80. Milestones
with no open scope (shipped) are omitted; empty → the demo fixture.
- RunwayScreen takes optional `milestones`; AppShell feeds runwayView. The header's
calibration note was already real (#1).
Scope: each milestone forecasts its remaining work *from today* independently —
they aren't scheduled relative to each other yet (so a smaller later phase can
show an earlier date). Cross-milestone sequencing is a refinement. Capacity stays
fixture — true per-person capacity (focus factor, allocation) is #8, config-driven.
Verified: desktop typecheck clean, 14 fixture e2e green. Live: Runway shows the
real P1/P2/P4/P5 milestones with per-milestone forecasts (e.g. "P2 — Scheduler +
Monte Carlo · 80% Aug 14–25 · 32d of work"); the fixture lists Beta/Pilot/v1.0,
so the real names prove it (new assertion + screenshot).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the in-memory cache into a durable mirror. The reconcile snapshot is
written to disk on every successful reconcile; on boot the app shows it instantly
(stale-while-revalidate) instead of a blank board, and if gitea is unreachable,
reads fall back to it (offline). Rebuildable — the durable truth stays in gitea.
- snapshot-store.ts: load/save the snapshot as JSON in app userData (never throws;
corrupt/absent → "no cache"). At this scale (~34 issues, 37KB) the whole snapshot
fits in memory, so a JSON file beats indexed SQL — no query benefit yet, no
native-module (better-sqlite3/electron-rebuild) or WASM dependency. That's the
next step if the mirror ever needs indexed queries over larger data.
- gitea.ts: getSnapshot persists on a fresh pull; bootSnapshot() returns the
persisted snapshot (without seeding the cache — agents still reconcile fresh);
gitea:boot serves it; gitea:reconcile falls back to it on failure (stale:true).
- useBacklog: stale-while-revalidate — boot instantly, then a fresh reconcile
supersedes; a reconcile error keeps the shown snapshot instead of erroring.
Verified: desktop typecheck clean, 14 fixture e2e green. Live: the snapshot
persists (34 issues / 44 deps / 34 timelines / 5 milestones written to disk); a
second launch with gitea unreachable renders the full real board — NOW/NEXT/LATER
+ the Monte Carlo cone — entirely from the cache (new live-persistence e2e).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A full reconcile is ~2N gitea calls (deps + timelines per issue). Every agent
tool call (query_project) was doing a fresh one; the UI reconcile and the agent
didn't share anything. Now a single in-memory snapshot cache backs both.
- gitea.ts: getSnapshot(client, { maxAgeMs }) — reads within the window reuse the
cache; maxAgeMs:0 forces fresh. invalidateSnapshot() drops it. The explicit UI
reconcile forces fresh (and warms the cache); agent tool calls tolerate a 30s
TTL to stay responsive; applyChange + createIssues invalidate so the board and
forecast reflect the write immediately.
- model.ts: query_project reads getSnapshot (30s TTL) instead of reconciling live.
This is the SQLite mirror's cache semantics in memory — rebuildable, the durable
truth stays in gitea (purity split, D4). Persistent SQLite (offline + instant
boot) is a separate slice: Electron 34's Node 20 has no node:sqlite, so it needs
better-sqlite3 + electron-rebuild or sql.js/WASM — deferred as its own decision.
Verified: desktop typecheck clean, 14 fixture e2e green, live Reginald still
answers correctly from the cache (writes invalidate → board stays correct).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pm-state repo now exists, so the last unverified path is proven: Reginald
records a directive to the real ledger.
- chat panel: the "consulted" activity line names record_directive as "the
directive ledger" (matching query_project → the project, propose_change → the labels).
- live-directive e2e (gated): a directive typed in chat → Reginald logs it and
says "recorded", consulting the directive ledger.
Verified live against gemma-4-26b + the commitea-pm-state repo: "freeze scope for
beta, pilots come first" appended to directives/log.jsonl as
{kind: scope, quote: <verbatim>, status: accepted} — logged, never claimed applied.
Reginald is now complete and every tool is verified end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last agent tool. When the PM states standing intent ("pilots come first"),
Reginald logs it verbatim to an append-only JSONL ledger in the pm-state repo —
a directive is intent; its effects still land through propose_change. This
completes Reginald's tool surface: query_project · propose_change · capture_work
· record_directive.
core (@commitea/core):
- directives/record-directive-v0: schema (kind/quote/target/params/rationale +
id/ts/status), serialize/parseDirectiveLog (ts-ordered, seq computed on read,
corrupt lines skipped), appendDirective (concatenation merge), toDirectiveInput.
- RECORD_DIRECTIVE_TOOL + system prompt update ("log standing intent; never claim
a change is applied").
- gitea client: getFile/putFile (contents API, base64-agnostic) for the pm-state repo.
app:
- main: a pm-state client (same token, `commitea-pm-state` repo — the purity
split, D4); appendDirectiveEntry (read→append→write, id/ts stamped here),
readDirectives. model:chat executes record_directive; pmstate:directives reads
the ledger. Degrades cleanly when the pm-state repo is absent.
- Directives screen shows the real ledger when present, the fixture demo otherwise.
Note: the pm-state repo isn't created yet — my token lacks write:user (repo
creation). Create `commitea-pm-state` (private) to activate the live path; all the
code + tests are in place. Override with COMMITEA_PMSTATE_REPO.
Verified: 116 core tests green (8 directive + 2 contents-API added), desktop
typecheck clean, 14 fixture e2e green. Gated live test: the real gemma-4-26b calls
record_directive for "pilots come first" (logs intent, doesn't claim to apply it);
the append/read + POST/PUT contents paths are unit-tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last big agent capability. In the Capture screen, a rough braindump runs real
big-model decomposition into a small, estimated issue set; you review/edit the
labels and approve, and the issues are opened in gitea. This is the one place the
big model earns its keep (docs/agent-tools.md).
core (@commitea/core):
- capture-work: PROPOSE_ISSUES_TOOL + CAPTURE_SYSTEM; captureWork(complete, dump)
forces a single structured decomposition and returns validated issues; parseCaptureArgs
drops blank titles + invalid est/p labels. ProposedIssue / CaptureProposal.
- gitea client: createIssue({title, body?, labelIds?}) → POST /issues, normalized.
app:
- model bridge model:capture runs captureWork on the (loaded) big model.
- gitea bridge gitea:createIssues opens each approved issue with its est/* + p/*
labels (reusing the #41 label-id resolver — zero-pollution, no invented labels).
- Capture screen: when a model is configured, "Brew tickets" runs real capture and
"Approve all" files the set; otherwise the scripted demo interview runs. Fixed a
race — the brew handler re-checks model status at click time so a configured
model never falls into the scripted path before status resolves.
Verified: 108 core tests green (7 capture + createIssue added), desktop typecheck
clean, 14 fixture e2e green. Gated live e2e against gemma-4-26b: the auth braindump
→ 3 real tickets ("Resolve token refresh + session staleness" est/3d p/1, "Fix
webhook double-firing" est/2d p/2, "Write auth setup docs" est/1d p/3), reviewable
and editable; Discard so the test files nothing (createIssue POST is unit-tested).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes "chat is the write-path" (decisions.md D1). Ask Reginald to re-estimate
or reprioritize an issue; it formulates a proposal, you approve it inline, and the
write runs through the same guarded apply_changes engine the Issue screen uses.
The model never writes — it proposes; the app owns approval + execution.
core (@commitea/core):
- propose_change tool declaration + REGINALD_SYSTEM updated ("never claim a change
is applied; you propose, the human approves").
- proposalsFor(args, currentLabels, title): pure — builds the concrete, non-noop
ChangeProposal(s) (change + label diff) for a propose_change request, dropping
invalid/unchanged axes. ChangeProposal / ProposeChangeArgs types.
app:
- model bridge executes propose_change by planning against the issue's current
labels (no write) and returns the proposals with the turn.
- useChat surfaces pending proposals + approve/dismiss; approve calls onApplyChange
(AppShell's guarded handler → PUT + board/forecast refetch), dismiss leaves it.
- ChatPanel renders each proposal as a propose-approve card (diff + Approve/Dismiss).
Verified: 101 core tests green (4 proposalsFor added), desktop typecheck clean,
14 fixture e2e green. Gated live e2e against gemma-4-26b: "Set the estimate on #3
to est/5d" → Reginald proposes "est/2d → est/5d" as an inline card, says it's
*proposed* not done; Dismiss leaves the repo untouched. The approve→write path is
the #41 engine (separately verified change→revert).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Instead of a hardcoded model name (which forces LM Studio to JIT-swap your loaded
model out — and fails when a big model already fills memory), resolve the model
at request time: an explicit env override wins, else ask the server which model
is *loaded* (LM Studio's native /api/v0/models), else the first non-embedding
model, else a default. Reginald now uses whatever you load, no config churn.
- main/model.ts: resolveLoadedModel() drives both model:status and model:chat;
COMMITEA_MODEL_SMALL still overrides.
- useChat exposes the resolved model id; the panel header shows it
(google/gemma-4-26b-a4b-qat → "gemma-4-26b-a4b · local").
- live-reginald e2e: header assertion relaxed to the loaded model; timeouts
raised for a slow big local model (~2 calls/turn + a reconcile).
Verified: 14 fixture e2e green; live e2e drives the app against the loaded
gemma-4-26b — "What now?" → "You should work on #2 … on the critical path,
unblocks #33 and #4" (the real scheduler pick), header shows the live model.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The first write path. Read, forecast, and calibration were all real; now you can
*manage* CommiTea from CommiTea. Estimates/priority are exclusive label axes, so
a change is a label swap — proposed, approved, then written. Nothing is assumed.
core (@commitea/core):
- planIssueChange(current, change): pure diff planner — swaps the est/*|p/* axis,
clears on null, dedups a doubled axis; returns the resulting label set + a
before/after diff + noop flag. describeChange() renders "est/2d → est/5d".
- request() seam extended for writes (method/body, JSON, 204). client gains
listLabels() (name→id) and setIssueLabels() (PUT /issues/{n}/labels).
app:
- main bridge gitea:applyChange — resolves plan.labels → ids (cached, refetch on
miss), PUTs, returns the plan + fresh issue. Token never leaves main.
- preload + global.d.ts expose applyChange; useBacklog returns a refetch so a
write re-reconciles the board + forecast.
- Issue screen: an Adjust button (shown only when configured) opens a
propose-approve Dialog — estimate/priority pickers, live "est/3d → est/8d"
consequence, Apply/Cancel. AppShell wires it, reflects new labels on the open
issue immediately, and refetches.
Verified: 83 core tests green (7 apply-changes + 2 client-write new), desktop
typecheck clean, 14 fixture e2e green. Live spec exercises propose + CANCEL (no
mutation); the real PUT was verified once manually (change #2 est/3d→est/8d→200,
reverted clean). Icon: pencil (no sliders-horizontal in the set).
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>
The last three P3 screens, retiring every placeholder:
- DirectivesScreen — jade consequence diff (propose-approve: Make it so
/ Amend / Withdraw, resolving moves it into the ledger) + append-only
ledger (seq, who/when/why, verbatim quote, status badge).
- SettingsScreen — gitea connection + managed repos, sync switches,
model router, read-only label schema, rituals, appearance radios
(wired to the shared theme), single danger action.
- OnboardingScreen — full-window first run: welcome → connect (test
gate) → repo pick → propose-approve bootstrap; onDone routes into the
app. Shell early-returns it (no rail/chat), matching the design.
Shell routes directives/settings and the firstrun full-window flow.
DIRECTIVES fixture added. typecheck + 14 e2e green (incl. directive
resolve, appearance↔theme sync, onboarding test gate); all three
screenshot-verified.
Closes P3-8. P3 (UI views) complete — all 14 screens live on fixtures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First real P3 screens, replacing the placeholders:
- FocusScreen — Now/Next/Later cards (Now = jade + Start/Defer footer),
each with #id, label tags, steeping badge, agent-font rationale, and
a title link that drives the shell issue drill-in. Milestone card
wraps the burn-up cone.
- StandupScreen — typeset letter: overnight drift (dot + delta), per-
person plan (initials avatars), warn-tint stale-blocker nag; sections
settle in on a 90ms stagger, reduced-motion-safe.
- charts/chart.tsx — BurnUpCone (actual polyline, forecast cone with
dashed 80% bounds, jade today rule) + RunwayBar (for P3-5). Fixed
sample geometry until P2 feeds it.
Fixtures (today/focus/standup) mirrored from data.js. Shell routes
focus/standup to the real screens; the drill-in now starts from a Focus
card. Screenshot helper freezes animations so fade-in screens capture
settled. typecheck + 9 e2e green; light/standup/states verified.
Closes P3-3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reimplements the handoff Shell in React/TS: 208px left rail (nav +
connection dot + Evening service theme switch), max-1120 main column,
330px Reginald chat panel. Routing across top-level views with an
issue drill-in + back-stack of one; data-theme owned by the shell.
- ChatPanel: fixture write-path — echoes a canned reply so layout +
interactions are real; model router wiring lands in P4.
- states.tsx: EmptyState / OfflineBanner / ModelAwayState + the States
specimen gallery, ported from the handoff.
- PlaceholderScreen stands in for not-yet-built views (P3-3+), keeping
navigation live; it also exposes the issue drill-in for now.
- Gallery loses its own theme toggle (shell owns data-theme); reachable
via a Primitives rail entry as a living reference.
Fixtures mirrored from the handoff's data.js. typecheck + 7 e2e green
(nav, theme, offline banner + disabled composer, chat echo, drill-in
back-stack); light/dark/states screenshots verified.
Closes P3-2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verbatim port of the handoff primitives into components/ui/ — Icon,
Button, IconButton, Badge, Tag, Card, Tabs (core); Input, Select,
Checkbox, Radio, Switch (forms); Dialog, Toast, Tooltip (feedback).
Each keeps its injected token-referencing CSS byte-for-byte; the
handoff .d.ts contracts become the exported prop interfaces. Barrel at
components/ui/index.ts.
Adds a PrimitivesGallery (app root for now; real shell is P3-2) that
exercises every primitive with a light/dark toggle. Smoke suite asserts
the gallery, section coverage, theme flip, and dialog open/Escape;
screenshots captured for both themes. typecheck + 5 e2e green.
Closes P3-1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Launches the built app (out/main/index.js) via Playwright's _electron
API — no browser project, no chromium download. Adds a launch fixture
(electronApp/window/app), an AppPage page object with a screenshot
helper for autonomous visual review, and a boot smoke suite (shell
renders, @commitea/core label-parse runs in the renderer, preload API
exposed). Scripts: e2e (build+run), e2e:only, e2e:report. Artifacts
gitignored.
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>
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>