The 4-step wizard is now fully live — nothing faked.
Backend (core + main):
- GiteaClient.createLabel (POST /labels).
- core/gitea/bootstrap.ts: SCHEMA_LABELS (the full est/p/deadline vocabulary with
colors+descriptions), applySchemaLabels (idempotent — creates only missing),
repoExists + ensurePmStateRepo (token-scoped, org vs personal endpoint). 7 tests.
- config:bootstrap IPC handler + preload bridge + BootstrapResult type.
Wizard:
- Connect step → live config.discover ('connected · N repos visible' is real).
- Repo step → owner dropdown + repo radios from the discovered repos.
- Bootstrap step → real: applies the label schema + ensures the pm-state sidecar,
shows the actual outcome (created vs already-present), then saves config.
- Gate: first run (no config) → wizard; reconnect (existing) → the compact card.
Removed the dead 'firstrun' dev view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1 — cut showcases:
- Delete gallery.tsx (Primitives) and StatesScreen/Specimen from states.tsx
(keep the reusable EmptyState/OfflineBanner/ModelAwayState).
- Delete placeholder-screen.tsx ('built in a later phase' stub).
- app-shell: drop the states/primitives views, the dev-rail block, the
PHASE/TITLE maps, the INBOX_UNREAD=3 fixture fallback, and the now-dead demo state.
P2 — real connectivity:
- Replace the fake 'toggle the connection (demo)' button with a live status dot
derived from the reconcile: green online, red when serving the stale cache
(gitea unreachable), amber while connecting. OfflineBanner + chat offline now
reflect real state, not a manual toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- index.ts: keep both cache (#3) and queue (#33) exports
- agent.test.ts: #28 landed the standup impl but left agent.test.ts asserting
standup is notImplemented (its real test moved to query-project.test.ts);
retarget the unbuilt-view assertion to milestone/runway, which are still stubs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
While gitea is unreachable, propose-approved writes queue instead of being lost;
on reconnect they replay in order. The hard requirement is no duplication.
The mechanism is coalescing by axis. Every write targets one field of one issue
(estimate / priority / assignee / milestone), so `issue:kind` is the axis identity.
Queuing a second write to the same axis supersedes the first (moved to the tail),
so a burst of edits replays as one final write — and replay is idempotent (the
apply path no-ops a change already reflected server-side).
- `enqueueWrite` (coalesce), `pendingWrites`, `coalesceKey`, `affectedIssues`
(what a post-replay reconcile re-reads), and `replayQueue(queue, apply)` which
drains through the same guarded write path and returns the writes that still
failed so they stay queued. Never throws — a failure is data.
Acceptance met: a burst of offline edits + reconnect lands the final state with a
single apply per axis (not one per edit); still-failing writes stay queued. +4
core tests; typecheck green.
Follow-up: persist the queue in main + trigger replay on the reconnect signal
(the offline banner + disabled composer already exist) — the coalesce/replay
core is the tested heart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reginald's context is tiered so the model always sees what matters without ever
copying ticket data into the prompt:
- HOT (this module) — charter + active directives + the focus snapshot, packed
under a hard token budget (2k). Assembled fresh each turn as the prompt seed.
- WARM — the append-only directive/event ledger + digest, summarized on demand.
- COLD — gitea + sidecar via query_project. Ticket bodies/comments/detail live
here and are NEVER inlined; the model fetches them by number when needed.
- `assembleHotContext(inputs, budget=2000)`: focus (tiny, always kept) → most
recent active directives (each while they fit ~⅔) → charter fills the true
remainder, truncated on a line boundary. Measures the fixed tail exactly and
reserves for header/joiner/ellipsis so the total never exceeds budget.
- `estimateTokens` (tokenizer-free ~4 chars/token, slight over-estimate so a real
tokenizer stays under), `activeDirectives` (accepted/amended, most-recent-first).
Acceptance met: hot assembles under the 2k budget even with a ~34k-token charter;
nothing ticket-shaped is inlined (only numbers + titles for focus). +5 core tests;
full core suite + typecheck green.
Follow-up: wire assembleHotContext into the live system prompt in main (needs
charter + directives + focus at chat time) — the assembly + budget is the tested core.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A rebuildable local mirror of the reconciled backlog — an index over gitea's
durable truth, never the source of truth (D4). This lands the core seam:
- `cache/cache-v0.ts`: the 5-table schema (issues/labels/milestones/comments/
issue_events), a `CacheDriver` injected-IO interface (exec/run/get/all), and
pure row<->domain mappers. `upsertIssue` mirrors one issue (+ its milestone),
upsert-by-number so a re-reconcile updates in place; `readIssue` re-derives
`facts` from the stored label set via extractLabelFacts, so the mirror can't
drift from the label semantics. Facts are never stored.
- Resolves#3's open scope: pure mappers + SQL in core (driver-agnostic), native
driver in main. Tests bind node:sqlite (present in the Node 24 test runtime) to
the same CacheDriver seam better-sqlite3 will fill in main.
Acceptance met: upsert one reconciled issue, read back, assert
estimateDays/priority/hardDeadline (+ upsert-in-place, no-milestone, and miss
cases). Core suite green; typecheck clean.
Follow-up (noted in the plan): the main-process better-sqlite3 adapter +
snapshot-store migration — packaging-sensitive (native module), kept out of this
slice so the shippable .dmg stays verified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cold-start surface showed "N/20 closed issues estimated", implying you're
just (20−N) closes away. But calibrationSamples silently drops closed+estimated
issues that closed in 0 working days (same-day closes) — real closes that
structurally can't calibrate. On this repo that's 10 of 24 closes hidden: the
note read 14/20 as if 6 away, when a third of the history will never count.
- core: `calibrationCoverage(issues, timelines, asOf)` → { candidates, usable,
excludedSameDay }, counting the silently-excluded same-day closes. Pure, tested.
- surface it: CalibrationData gains `excludedSameDay`; backlogCalibration returns
the coverage; the Runway note and the Calibration screen now say "… · N same-day
closes can't calibrate" so the thin sample is explained, not just reported.
Verified on christian/commitea: closed=24, usable=14, excludedSameDay=10.
131 core green (incl. new coverage test); core + desktop typecheck; 14 fixture e2e.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surfaces the assign/milestone mutations end-to-end so they're usable in-app and
by the agent — the rest of #24.
Agent path:
- propose_change tool + system prompt now accept `assignee` (login/null) and
`milestone` (id/null). ProposeChangeArgs + proposalsFor extended: a new
ProposalContext (current assignee/milestone + milestones list) lets a proposal
skip no-ops and label the milestone. ChangeProposal gains an always-present
`summary` (plan is now label-only) — chat-panel, use-chat, and the model
executor render `summary`, so non-label proposals display correctly.
Dialog path:
- Client `listCollaborators()` (prepends the repo owner — /collaborators omits
them, so a solo-owner repo still has an assignable person). New
`gitea:collaborators` bridge. The Adjust dialog gains Assignee + Milestone
pickers (current values from the reconciled backlog); pending assign/remilestone
changes flow through the existing apply path.
Tests: +4 core (assign/milestone proposals with no-op skip; collaborators
owner-prepend + no-double-add). 138 core green; core + desktop typecheck clean;
14 fixture e2e green; live-backlog now drives the pickers on real data.
Fixed stale P2 refs in live-backlog (P2 is shipped → correctly off the runway).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The write path handled only the est/* and p/* label axes. Extend it to the two
mutations dogfooding proved were missing — assignee and milestone — behind one
unified change model:
- IssueChange gains `assign` and `remilestone` kinds. `planIssueChange` is now
typed to LabelChange (the label kinds only); `isLabelChange` narrows, and a new
pure `summarizeChange(change, currentLabels?)` gives one confirm line for ANY
kind (label kinds delegate to describeChange; field writes describe directly).
- GiteaClient gains `setIssueAssignees` / `setIssueMilestone` (PATCH /issues/{n},
returning the updated issue; milestone 0 clears).
- The main `gitea:applyChange` handler branches: label swaps resolve ids +
setIssueLabels as before; field writes call the new client methods. Snapshot
invalidated either way so board + forecast catch up. ApplyChangeResult.plan is
now optional (absent for field writes).
- issue-screen uses summarizeChange for its diff preview.
Tests: +5 covering isLabelChange, and summarizeChange for assign/unassign,
milestone set-by-title / by-id / removal, and label delegation. Full core suite
132 green; core + desktop typecheck clean.
Follow-up (rest of #24): surface assignee/milestone pickers in the Adjust dialog
and extend the agent's propose_change tool so Reginald can propose them too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turns the single-serial-worker scheduler into a capacity-aware, multi-lane one.
Configured team members become lanes; an issue runs on its assignee's lane (or the
earliest-free lane), its duration scaled by that lane's throughput
(focusFactor × allocation). Every forecast — Focus cone, Runway, milestone
drill-in — is now capacity-aware.
core (@commitea/core):
- capacity/capacity-v0: CapacityMember + capacityPerWorkday + parseCapacityConfig
(clamps, drops invalid; degrades to []).
- scheduler/scheduler-capacity-v0: scheduleWithCapacity reuses the v0 topo order +
critical path, re-lays work across lanes (layoutOnLanes, resolveLanes, makespan).
Empty workers → the single serial plan verbatim.
- forecast() gains options.workers: each MC trial lays sampled durations across the
lanes and takes the makespan; serial path unchanged. SchedulableIssue gains
assignee; ScheduledItem gains worker.
- 11 new tests (parse/clamp, parallelism halves makespan, speed scaling, assignee
routing, cross-lane deps, forecast makespan shrinks with lanes).
app:
- pm-state capacity/members.json read (readCapacity + pmstate:capacity bridge);
useCapacity hook → workers; forecastBacklog/runwayView/milestoneView pass workers.
- Runway Capacity card shows the real config (person · focus · alloc · pd/day).
Config lives in pm-state (D4); seeded christian(0.8)/stephen(0.6×0.5). Degrades to
the fixture/serial when absent.
Verified: 128 core tests green, desktop typecheck clean, 14 fixture e2e green. Live:
the capacity card is real, and the P2 forecast shifts 32d→37d — honest, since real
focus factors (<1) replace the v0 focus-1.0 assumption.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>