The offline write-queue (packages/core/src/queue/write-queue-v0.ts, #33) was a
tested pure module that nothing imported. Approving a change while gitea was
unreachable made gitea:applyChange call the client directly and throw, losing
the write. Now it's wired end to end:
- queue-store.ts persists the queue next to the snapshot store (same
degrade-to-empty-on-corruption discipline; injectable path for tests).
- applyChange extracts the guarded write into applyChangeLive and, on
unreachability (any error that is NOT a GiteaApiError rejection), enqueues the
intent — coalesced by (issue, axis) — instead of throwing, returning
{ ok, queued, pending }. A genuine GiteaApiError still surfaces (a doomed
write must not replay forever).
- reconcile drains the queue once a successful read proves gitea is reachable,
re-reading so the board reflects the replays; replays are idempotent
(label plan.noop, assignee/milestone re-set). boot + stale reconcile report
the pending count so the badge shows immediately offline.
- Renderer: use-backlog threads `pending`; the OfflineBanner shows "N queued";
the chat approve message distinguishes a queued (offline) approval from an
applied one.
Also wires vitest into the desktop workspace (was missing, so the main-process
suite couldn't run via `yarn test`) and fixes a stale Capture copy assertion
left by the earlier posh-copy pass.
Tests: queue-store.test.ts (persist/reload/coalesce/replay-drain, real core fns);
9 main-process + 169 core green; 12 demo e2e green. A one-off GITEA_LIVE smoke
verified an online write lands+reverts and an offline approve queues+drains
against the real repo (not committed, per the repo's no-mutating-test convention).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The app editorialised about its own guarantees all over the place. Removed or
trimmed the clutter:
- chat composer footer 'Chat is the write-path. Destructive changes are proposed,
never assumed.' (removed)
- Settings: dropped the 'no truth is lost' token note, the rebuildable-index /
no-webhooks Sync essay (now one line about offline reads), the 'Reginald never
does the arithmetic' model note, and the 'not configurable, that is the point'
labels justification.
- Onboarding: cut 'I never attempt the arithmetic myself', 'nothing human is
lost', the 'no bot comments / only footprint' bootstrap claim, 'everything
reversible', and 'Nothing more' from the token hint.
- Directives: removed the 'entries are never edited, remembers everything
politely' footer.
- Issue detail: removed the 'lives in pm-state, your repo never sees it' note.
- Calibration: cut 'never tracked'. Capture: cut 'no bot comments, your repo
remains yours'. Connect: cut 'stored encrypted, never leaves it'.
Functional guidance and status copy kept. desktop tsc + build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Voice: rewrote REGINALD_SYSTEM and CAPTURE_SYSTEM (core) to an elevated,
dry butler register, and re-voiced his visible lines — chat greetings, the
approve/dismiss/error replies, the standup closer ('The kettle is on. Yours,
Reginald.'), the onboarding welcome, and the capture prose. Both system prompts
now also instruct him never to use an em dash.
Em-dashes: swept every user-facing string in the renderer free of em-dashes
(punctuation only, comments left untouched) via a per-file pass, plus the core
tool descriptions and the memory focus-slot placeholder ('· ' not '— '). Bare
'—' value placeholders became middots ('·'). No em-dash now renders anywhere
in the app or in Reginald's own output.
core 169 tests green (updated the memory placeholder assertion) · core + desktop
tsc clean · verified visually (posh greeting + standup closer render).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
More static mocks found by poking the live app:
- Settings was ~80% mock: a fake Sync card (webhook endpoint :48731, poll
fallback, 'last reconcile 3.2s · 500 issues'), a fake dual-model 'router'
(gemma-4b/qwen-72b with hardcoded URLs), and an unwired Rituals card. Replaced
with honest/live content: the Sync card now describes the real on-demand
reconcile + stale-cache model; the Model card shows the actual probed model
from model.status(); dropped Rituals and the duplicate 'Forget' button.
- Settings showed 'Not connected' while the app was clearly connected: config:get
returned only the *saved* config, so a .env.local/env connection read as null.
It now falls back to a public view of the resolved connection — Settings and
the rail host label reflect the real christian/commitea connection.
- Capture braindump was pre-filled with a fake example ('auth is flaky…'); now
starts empty with just the placeholder.
Verified live: Settings shows christian/commitea connected + google/gemma-4-26b
reachable; Capture empty; a live Reginald chat turn runs the agent loop.
desktop tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Poking at the live app (a real repo with no open issues) surfaced two empty-state gaps:
- Focus (Morning service) rendered a bare header — no cards, no empty state —
because scheduleFocus returns a FocusView with empty now/next/later rather than
undefined, so the `if (!focus)` guard never fired. Guard on 'no now/next/later'
too → shows the 'Nothing to pour' EmptyState.
- Standup's calm sentinel nag (id 0) still rendered '#0 · steeping 0d · blocks'
chrome and was a click target (would try to open issue #0). Render the calm
case as a plain reassurance pill, no chrome, not clickable.
Both verified against the live christian/commitea repo (all issues closed → no
open work). desktop tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- perf.test.ts: the @200 absolute check flaked under machine load (single-shot vs
a 1000ms bound; nominal ~230ms). Measure best-of-3 (a micro-benchmark's minimum
reflects true compute cost, not load spikes) against a 1500ms catastrophic-
regression guard. The scaling test remains the real O(n²) guard.
- ModelAwayState: remove the hardcoded 'queued: 1 directive' badge (no live queue
count is wired) and the now-unused Badge import.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The app is now fixture-free. All product screens render live data (via the
reconcile bridge + view builders) or an EmptyState — no hardcoded demo data,
metrics, or prose anywhere in the UI.
- Removed the `?? FIXTURE` fallbacks and embedded fake strings (fixed dates,
'reconcile 3.2s', 'ahead of forecast', mock forecast prose) from all 10 product
screens; each now shows an EmptyState when its live data is absent.
- directives-screen: fully live off pmstate.directives() (was seeded from a
fixture with a fake consequence-diff card) — loading + empty states, no seed.
- use-chat: drop the CHAT seed (the model-status effect already sets the greeting).
- data/fixtures.ts → data/view-types.ts: stripped every const design fixture,
kept only the view-model TYPES (the builder↔screen contract). Repointed imports.
Testing fixture (no live gitea), per the user's call:
- New apps/desktop/src/main/demo-snapshot.ts — one deterministic, gitea-shaped
snapshot (issues/milestones/deps/timelines + capacity + directives + collaborators).
Served by the reconcile/boot/collaborators/capacity/directives handlers when
COMMITEA_E2E=1, so e2e renders it through the REAL builders with no token.
- smoke.spec rewritten to assert on the demo snapshot through the live render path;
onboarding/offline-toggle assertions dropped (wizard is live → live-onboarding;
offline is real state now). 12/12 smoke pass.
Verify: core tsc clean · desktop tsc clean · 12/12 smoke green (perf#32 benchmark
flakes under machine load — unrelated, passes in isolation).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The cache is a rebuildable index over gitea, never a source of truth (D4).
Two tests lock that invariant where the durable cache actually lives:
- packages/core: cache-purity-v0.test.ts — file-backed node:sqlite. Build the
SQLite mirror from a gitea snapshot, capture every re-derived field, delete the
.sqlite file, rebuild from the same snapshot, assert byte-identical. Plus a
structural D4 guard: every issues-table column must map to a gitea field, so a
future user-authored column can't silently break rebuild-ability.
- apps/desktop: snapshot-store.test.ts — the shipped durable cache is the JSON
snapshot-store. Delete the file → loadSnapshot returns null (degrades to
no-cache, never throws), which is what forces the next getSnapshot to reconcile
fresh from gitea. Corrupt/partial files are likewise treated as no-cache.
Stands up vitest for the desktop main process (first unit tests there);
electron is mocked, snapshot path is injected.
No native better-sqlite3 shipped: the SQLite mirror has no consumer on any hot
path yet, so wiring it into main (native module + asarUnpack + dmg re-verify)
would add packaging risk for no runtime benefit. The purity invariant is proven
at the seam for both caches; the native driver migration is deferred until
SQLite becomes load-bearing.
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>
Lock in the PLAN.md compute targets so a regression that slips an O(n²) into the
scheduler or forecast fails the suite:
- scheduler + capacity layout + Monte Carlo forecast < 1s @ 200 open issues —
measured 232ms, comfortable headroom.
- scaling stays ~linear (400 issues ≈ 3.9x the 100-issue time; asserts < 8x to
rule out O(n²) while tolerating jitter).
Representative fixture: 200 open issues with varied estimates/priorities/assignees
across 3 capacity lanes + a light acyclic dependency web. Bounds are the real
targets with margin so timing jitter can't flake CI; actuals are logged.
Reconcile-<5s@500 is network-bound (~2N gitea calls) and stays covered by the live
reconcile — this benchmarks the pure compute the app runs each turn. +2 core tests.
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>
The StandupScreen already renders drift + plan + nag from real data (standup-view,
#52), but the agent's query_project standup view was a `notImplemented` stub, so
Reginald couldn't answer standup questions from deterministic data.
Implement `standupView(snap, asOf)`: today's plan (the scheduler's earliest pick
per person, with why — critical path / blocks / order), overnight drift (real
anomalies: issues sitting in review, or steeping past their estimate), and the
single stalest blocker to nag about (+ what it blocks). All deterministic; the
model narrates. Added 'standup' to the query_project tool's view enum.
Acceptance met: standup surfaces schedule drift + at least one stale blocker
(and stays calm — no nag, empty drift — when nothing is steeping). +2 core tests;
typecheck clean.
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>
ganttView ran schedule() — the single-serial-worker layout — while the Monte
Carlo forecast ran scheduleWithCapacity() over the real lanes. So the bars you
saw didn't match what was forecast (serial 1.0/day vs the team's actual lanes).
Switch ganttView to scheduleWithCapacity(open, deps, workers): startDay/endDay
now come from the same lane layout each forecast trial uses, and `who` shows the
lane an issue actually landed on (falling back to assignee, then blank). The
dogfood harness had the same split — updated it to match and to print the lane
per row.
Verified: typecheck clean, 14 fixture e2e green, live-onboarding e2e renders the
real capacity-aware Gantt with no uncaught errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scripts/dogfood-report.ts drives the real @commitea/core engine (schedule +
capacity-aware Monte Carlo forecast + per-milestone runway) against the live
christian/commitea backlog and prints the project report each screen derives.
Read-only; `yarn tsx scripts/dogfood-report.ts`. Adds tsx as a devDependency.
First run surfaced two real gaps the fixtures hid: every open issue was
unassigned (so capacity load-balanced work onto the slow half-time lane and the
standup plan-per-person was empty) and no milestone had a due date (so Runway
couldn't judge on-track/at-risk). Both were fixed as PM actions on the repo via
the write path — all open issues assigned, milestone due dates synthesized from
the forecast — so the app's own numbers are now honest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds shareable desktop packaging so the app can be handed to teammates.
- electron-builder@25 + electron-builder.yml: appId io.stephenmann.commitea,
productName CommiTea, icon from the logo (build/icon.png, 1024²), macOS dmg
targets for arm64 + x64. Ships only out/** (electron-vite already bundles
renderer/preload/main with core + react inlined), npmRebuild off,
electronVersion pinned to 34.5.8 (workspace hoisting defeats auto-detect).
- Unsigned by decision: mac.identity null, hardenedRuntime off. electron-builder
ad-hoc signs so arm64 runs; not notarized — teammates right-click → Open once.
- Scripts: `yarn pack` (--dir sanity build), `yarn dist` (both dmgs). package.json
gains version/author/description (required by electron-builder).
- README.md: dev/build/package commands + the Gatekeeper install step.
Verified: packaged arm64 app boots from a fresh profile (lands on onboarding);
`yarn dist` produced CommiTea-0.1.0-arm64.dmg (98M) and CommiTea-0.1.0.dmg (x64,
102M), each mounting with a drag-to-Applications layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every remaining fixture-only surface now renders reconciled gitea data,
with the fixture kept only as the no-config demo fallback.
- lib/views/project-data.ts: ProjectData — the uniform input (reconciled
backlog + deps + timelines + calibration + workers + today) that AppShell
assembles once and every view builder consumes.
- lib/views/{issue-detail,gantt-view,deps-graph,standup-view,inbox-view}.ts:
pure builders, ProjectData → the fixture-shaped object each screen already
renders. Real signals only; honest degradation where a signal isn't derivable
(buffered p80 vs per-issue Monte Carlo; flat "idle in review"; no fabricated
inbox mentions/outages).
- Screens take an optional `data?` and fall back to the fixture; AppShell wires
the real view whenever the backlog is reconciled. Issue "blocks" chips and the
rail inbox badge now resolve from real data too.
- Dev-only rail surfaces (First run / States / Primitives) gated on
import.meta.env.DEV || demo — shown in dev + e2e, hidden in a packaged app.
Rail host label reflects the connected instance.
- Extended the live onboarding e2e to click Board → Gantt → Deps → Issue
sidecar → Standup → Inbox on real gitea data with a pageerror guard; 14
fixture e2e stay green, typecheck + prod build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The foundation for a shareable team build. Replaces the .env.local-only dev config
with a real, per-teammate connection flow.
main:
- config-store.ts: token encrypted at rest via Electron safeStorage (OS keychain),
config JSON in userData. Token lives only in main; renderer gets everything but.
- resolveConfig: saved config > .env.local (dev) > null; ignored under COMMITEA_E2E.
pm-state repo defaults to `${repo}-pm-state`. resetClients() re-reads on change so
saving config takes effect without a restart. gitea:status gains `demo` (e2e).
- IPC: config:get (no token), config:test (authed read validates token+repo),
config:set (encrypt+save+reset), config:clear. Model bridge reads config.modelUrl
and probes reachability — chat is "configured" only if a model actually answers;
localhost default is dev-only (app.isPackaged gate).
renderer:
- ConnectScreen: real onboarding form (URL/owner/repo/PAT/optional model) → test →
save. AppShell gates on it: demo → shell (fixtures/e2e); configured → shell (real);
else → connect. Settings Connection card is real (repo/url/model/sidecar) with
Reconfigure + Disconnect. Chat cleanly disables with a "no model" state instead of
the scripted canned reply.
Verified: main + desktop typecheck clean, 14 fixture e2e green (demo mode unchanged),
live onboarding e2e: fresh app → connect form → validated PAT → real board (24 done /
10 open). COMMITEA_NO_ENV_LOCAL + COMMITEA_USERDATA are test hooks for the onboarding path.
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>