Commit Graph

76 Commits

Author SHA1 Message Date
Croissant Le Doux
a04714bfa6 Kill screen fixtures: live/empty screens + one demo snapshot for e2e
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>
2026-07-10 13:21:47 -04:00
Croissant Le Doux
27edbbd6af Live onboarding wizard: real discovery, repo pick, and bootstrap
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>
2026-07-10 12:47:08 -04:00
Croissant Le Doux
cf30cc1d0f Kill design-only mock surfaces + wire real offline indicator
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>
2026-07-10 12:40:37 -04:00
e317c1099d Merge pull request 'Purity / rebuild guarantee test (#30)' (#60) from feat/purity-rebuild-test into main
Reviewed-on: #60
2026-07-09 22:41:07 +00:00
Croissant Le Doux
73643efcbc Purity / rebuild guarantee test (#30)
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>
2026-07-09 18:02:50 -04:00
b07f07c14d Merge pull request 'Offline write-queue: coalesce + replay without duplicating (#33)' (#58) from feat/offline-write-queue into main
Reviewed-on: #58
2026-07-09 21:57:14 +00:00
Croissant Le Doux
ea8f1184e6 Merge main into offline-write-queue: resolve index.ts, fix stale standup assertion
- 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>
2026-07-09 17:56:31 -04:00
c0dd9e70ef Merge pull request 'Performance pass: benchmark the deterministic compute path (#32)' (#59) from feat/perf-pass into main
Reviewed-on: #59
2026-07-09 21:54:14 +00:00
842661c9a9 Merge branch 'main' into feat/perf-pass 2026-07-09 21:54:07 +00:00
717dc7348f Merge pull request 'Memory layers: budgeted hot context (#27)' (#57) from feat/memory-layers into main
Reviewed-on: #57
2026-07-09 21:53:57 +00:00
d1a4c4410c Merge branch 'main' into feat/memory-layers 2026-07-09 21:53:51 +00:00
345b561591 Merge pull request 'query_project: implement the standup view (#28)' (#56) from feat/standup-query-view into main
Reviewed-on: #56
2026-07-09 21:53:46 +00:00
2a6322e99a Merge branch 'main' into feat/standup-query-view 2026-07-09 21:53:33 +00:00
481cacd99a Merge pull request 'SQLite cache bootstrap + single-issue mirror upsert (#3)' (#55) from feat/sqlite-cache into main
Reviewed-on: #55
2026-07-09 21:53:29 +00:00
Croissant Le Doux
f08c4935dc Performance pass: benchmark the deterministic compute path (#32)
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>
2026-07-09 15:41:56 -04:00
Croissant Le Doux
e5ce3fe87a Offline write-queue: coalesce + replay without duplicating (#33)
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>
2026-07-09 15:40:22 -04:00
Croissant Le Doux
b65ee4c8ad Memory layers: budgeted hot context (#27)
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>
2026-07-09 15:37:57 -04:00
Croissant Le Doux
19e83ff8ba query_project: implement the standup view (#28)
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>
2026-07-09 15:33:20 -04:00
Croissant Le Doux
6fdbc302d2 SQLite cache bootstrap + single-issue mirror upsert (#3)
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>
2026-07-09 15:30:22 -04:00
2a6413821a Merge pull request 'calibration: count same-day closes honestly (#34)' (#54) from feat/calibration-honesty into main
Reviewed-on: #54
2026-07-09 19:23:59 +00:00
008435f1c2 Merge branch 'main' into feat/calibration-honesty 2026-07-09 19:23:55 +00:00
354ba9227e Merge pull request 'apply_changes: unified mutation tool — estimate/priority/assign/milestone, in-app + agent (#24)' (#53) from feat/apply-changes-assign-milestone into main
Reviewed-on: #53
2026-07-09 19:23:49 +00:00
2de3212210 Merge branch 'main' into feat/apply-changes-assign-milestone 2026-07-09 19:23:43 +00:00
86e9a49c08 Merge pull request 'Productionize: real data everywhere (Phase B) + shareable unsigned macOS .dmg (Phase C)' (#52) from feat/kill-fixtures into main
Reviewed-on: #52
2026-07-09 19:23:39 +00:00
Croissant Le Doux
89c873b368 calibration: count same-day closes honestly (#34)
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>
2026-07-09 15:18:16 -04:00
Croissant Le Doux
4fc5b902c4 apply_changes UI: assign + milestone in the dialog and Reginald's tool (#24)
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>
2026-07-09 11:54:03 -04:00
Croissant Le Doux
72dcd396f8 apply_changes: unify the mutation tool with assign + milestone (#24)
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>
2026-07-09 11:31:26 -04:00
Croissant Le Doux
3873e652f2 Gantt bars use the capacity-aware lane layout (match the forecast)
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>
2026-07-09 11:23:01 -04:00
Croissant Le Doux
63f0ea1735 Dogfood harness: run CommiTea's engine on its own backlog (#31)
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>
2026-07-09 10:53:28 -04:00
87a0ba4ba1 Merge branch 'main' into feat/kill-fixtures 2026-07-09 14:41:26 +00:00
62a521e7eb Merge pull request 'feat: config & onboarding — each teammate connects their own Gitea (productionize Phase A)' (#51) from feat/config-onboarding into main
Reviewed-on: #51
2026-07-09 14:41:20 +00:00
Croissant Le Doux
19897f7e53 Phase C: package as an unsigned macOS .dmg (electron-builder)
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>
2026-07-09 02:46:37 -04:00
Croissant Le Doux
c120f34a4a Phase B: real data for the last fixture screens
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>
2026-07-09 02:38:52 -04:00
Croissant Le Doux
0dd26b8b05 feat: config & onboarding — each teammate connects their own Gitea (Phase A)
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>
2026-07-09 02:09:53 -04:00
770c253625 Merge pull request 'feat: capacity-aware scheduling (#8) — real focus factors drive every forecast' (#50) from feat/capacity into main
Reviewed-on: #50
2026-07-09 05:00:20 +00:00
Croissant Le Doux
1636d6bada feat: capacity-aware scheduling (#8) — real focus factors drive every forecast
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>
2026-07-09 00:59:03 -04:00
d80e1266ee Merge pull request 'feat: stream Reginald's replies token-by-token' (#49) from feat/streaming-chat into main
Reviewed-on: #49
2026-07-09 04:43:58 +00:00
cf03827cd5 Merge branch 'main' into feat/streaming-chat 2026-07-09 04:43:51 +00:00
69106b603d Merge pull request 'feat: Runway complete — per-milestone forecasts + real milestone drill-in' (#48) from feat/runway-real into main
Reviewed-on: #48
2026-07-09 04:43:46 +00:00
Croissant Le Doux
dbcdcda5e7 feat: stream Reginald's replies token-by-token
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>
2026-07-09 00:22:12 -04:00
Croissant Le Doux
57595852a4 feat: real Milestone drill-in — completes the Runway story
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>
2026-07-09 00:12:11 -04:00
Croissant Le Doux
ae46cb99b3 feat: real Runway — per-milestone Monte Carlo forecasts
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>
2026-07-09 00:00:00 -04:00
403aef9d16 Merge pull request 'perf+persistence: the durable reconcile mirror (cache + disk)' (#47) from infra/reconcile-cache into main
Reviewed-on: #47
2026-07-09 03:53:29 +00:00
Croissant Le Doux
2f6636684e feat: persist the reconcile cache to disk — instant boot + offline reads
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>
2026-07-08 23:43:44 -04:00
Croissant Le Doux
fca36f9075 perf: main-process reconcile cache — stop refetching the repo on every tool call
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>
2026-07-08 23:18:47 -04:00
aab7fd6eeb Merge pull request 'Verify record_directive end-to-end against the pm-state repo' (#46) from p4/directive-verify into main
Reviewed-on: #46
2026-07-09 03:12:50 +00:00
Croissant Le Doux
469c63fb95 test: verify record_directive end-to-end + label the ledger in the activity line
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>
2026-07-08 23:07:21 -04:00
3d8a320a04 Merge pull request 'P4: complete Reginald — capture_work + record_directive' (#45) from p4/capture-work into main
Reviewed-on: #45
2026-07-09 03:01:51 +00:00
Croissant Le Doux
ba9ea43b4c feat: record_directive — the PM's ledger in pm-state (P4, completes Reginald)
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>
2026-07-08 22:39:12 -04:00
Croissant Le Doux
6e8a6a15bc feat: capture_work — braindump → decomposed issues → filed in gitea (P4)
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>
2026-07-08 22:20:00 -04:00