feat: deterministic scheduler → real Now/Next/Later (P2 thin slice)

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>
This commit is contained in:
Croissant Le Doux
2026-07-08 17:12:22 -04:00
parent 94199639f2
commit 68a93de098
11 changed files with 387 additions and 26 deletions

View File

@@ -7,23 +7,26 @@ const here = dirname(fileURLToPath(import.meta.url))
const MAIN = join(here, '..', 'out', 'main', 'index.js')
// Opt-in only (GITEA_LIVE=1). Launches WITHOUT COMMITEA_E2E so the main process
// reconciles the real repo via .env.local, and asserts real issues render.
// reconciles the real repo via .env.local, and asserts real data renders.
test.describe('live backlog', () => {
test('The pot shows real gitea issues', async () => {
test('The pot + Focus render real gitea data', async () => {
test.skip(!process.env.GITEA_LIVE, 'GITEA_LIVE not set — opt-in live test')
const app = await electron.launch({ args: [MAIN], env: { ...process.env } })
const win = await app.firstWindow()
await win.waitForLoadState('domcontentloaded')
const rail = win.getByRole('navigation', { name: 'Primary' })
await win.getByRole('navigation', { name: 'Primary' }).getByRole('button', { name: 'The pot' }).click()
// our filed dogfood issue #1 (now closed → Done column) — real gitea data
await expect(win.getByText('Gitea read client behind an injected fetch')).toBeVisible({ timeout: 15000 })
// The pot — waiting here also lets the reconcile (issues + deps) complete
await rail.getByRole('button', { name: 'The pot' }).click()
await expect(win.getByText('Gitea read client behind an injected fetch')).toBeVisible({ timeout: 20000 })
await win.screenshot({ path: join(here, '.artifacts', 'screens', 'live-board.png'), fullPage: true, animations: 'disabled' })
// Back to Focus — the deterministic scheduler's real Now/Next/Later. These
// rationale phrases are emitted only by the scheduler, never by the demo fixture.
await rail.getByRole('button', { name: 'Morning service' }).click()
await expect(win.getByText(/on the critical path|unblocks #|waits on #|· ready/).first()).toBeVisible()
await win.screenshot({ path: join(here, '.artifacts', 'screens', 'live-focus.png'), fullPage: true, animations: 'disabled' })
await win.screenshot({
path: join(here, '.artifacts', 'screens', 'live-board.png'),
fullPage: true,
animations: 'disabled',
})
await app.close()
})
})