-
-
Notifications
You must be signed in to change notification settings - Fork 0
Releases: mpiton/forgent
v0.1.0
First minor release. Bundles every Sprint 2 deliverable (T-219 → T-224) plus the Sprint 1 follow-ups merged since v0.0.2. See per-task entries below for details. Highlights:
- Kanban project picker (T-215, T-216, T-217, T-218) — multi-project workspace with
ProjectTabBar, persisted DB-backedprojectstable, drag-and-drop kanban columns. - Task creation + edit + delete UX (T-219, T-220) — Radix Dialog wizard (3-step), TaskEditDialog with all status / priority fields, ConfirmDeleteDialog with focus-trap hardening.
- Auto-select first project on hydration (T-221) —
useEffectboot effect picks the first project when none is persisted, with refetch on switch. - i18n EN+FR parity test (T-222) — vitest hard-fails any new key added to one locale and not mirrored in the other.
- Settings KV table + persisted
current_project_id(T-223) — newdomain/settings/bounded context, V002 migration, IPC commandssettings_get/settings_set,App.tsxboot flow restores the user's last selected project across restarts. - Sprint 2 smoke E2E + persistence-after-restart (T-224) —
scripts/smoke-sprint-2.shbootspnpm tauri devtwice and asserts (1) project + task creation through the wizard, (2) drag-to-Code persists to libsql, (3) the same row + DOM selection both restore on cold boot. Shipped alongside the Sprint 1 smoke under a single workflow matrix with per-shard artifacts. - Smoke E2E unblocked on GH Actions (PR #119) —
tauri_plugin_single_instancegated off under--features pilotto remove the D-Bus session bus dependency that was hanging every CI run; workflow now installsdbus-x11and wraps the smoke underdbus-run-session --as belt-and-suspenders.
Fixed
-
src-tauri/src/lib.rs+.github/workflows/smoke-e2e.yml— Sprint 2 smoke E2E (T-224) was hanging on every GH Actions run since the merge of #118: tauri-pilot ping responded buttauri-pilot wait --selectortimed out at 60s, snapshot artifact was 0 bytes, and the daily app log file was never created. Boot-stage instrumentation (debug/smoke-webview-blankbranch, runs 25864797891 / 25865187570 / 25865551513) localised the hang to insidebuilder.run(generate_context!())BEFORE the usersetup()closure runs —tauri_plugin_single_instanceuses zbus on Linux to register a well-known name on the D-Bus session bus and (a) xvfb does not provide a session bus and (b) even withdbus-run-sessionthe plugin races against the bus daemon's name-release bookkeeping when the second consecutivepnpm tauri devlifecycle starts. Fix: gate the plugin out undercfg(not(feature = "pilot"))so the smoke build skips the D-Bus dependency entirely (production unchanged: every shipped Tauri bundle keepssingle_instancebecause--features pilotis OFF). Addeddbus-x11to the workflow apt list and wrapped the smoke underdbus-run-session --as belt-and-suspenders against future plugin additions that rediscover a session-bus dependency. Verified by debug-branch run 25866097897: bothpnpm tauri dev smoke (sprint-1)andpnpm tauri dev smoke (sprint-2)jobs green end-to-end. -
src/App.tsx+src/features/projects/persist-selected-project-id.ts(+ tests) — drivepnpm exec oxlint . --max-warnings=0(CI gate) to 0 warnings on the T-223 surface. (1) Convert multi-line//comment blocks to/* ... */soeslint(capitalized-comments)does not trigger on continuation lines. (2) ExtractparsePersistedIdfromloadPersistedSelectedProjectIdso the entrypoint stays under themax-statements: 10ceiling. (3) ExtractresolveBootSelectionfromApp.tsx's boot effect for the same reason. (4) Replace every magic literal in the test file with named*_ID/*_CALL/*_DELAY_MSconstants (no-magic-numbers). (5) Reorder imports soMultiple-syntax declarations precedeSingle-syntax ones and members are alphabetical (sort-imports). (6) RenameTgeneric toTValue(id-length). (7) Tighten the persist subscriptionuseEffectarrow body so it satisfiesarrow-body-style. CI frontend job now matches localoxlint . --max-warnings=0. -
src/features/projects/persist-selected-project-id.ts(+ tests) — address PR #117 bot review for T-223. (1) Serialise concurrentpersistSelectedProjectIdwrites through a module-scoped promise chain so rapidselect(a)thenselect(b)clicks cannot overwrite the persisted id with a stale value when libsql races on per-call connections (codex finding). (2) SwitchNumber.isInteger→Number.isSafeIntegerinloadPersistedSelectedProjectIdso an i64 id beyondNumber.MAX_SAFE_INTEGERcannot silently round and restore the wrong project (cubic finding). (3) AddafterAll+mockRestoreon theconsole.warnspy so the mock does not leak across vitest worker files (CodeRabbit finding). Test seamresetWriteChainForTestingexposed forbeforeEachisolation. New regression cases:serialises concurrent writes in submission order via the module write chain,keeps subsequent writes flowing when an earlier write rejects,returns undefined for ids beyond Number.MAX_SAFE_INTEGER. 14/14 module tests, 333/333 vitest, 0 oxlint errors. -
src/features/kanban/components/KanbanColumn.test.tsx+src/features/kanban/components/KanbanBoard.test.tsx+src/features/kanban/components/__snapshots__/KanbanColumn.test.tsx.snap+src/features/kanban/components/__snapshots__/KanbanBoard.test.tsx.snap— stabilize relative-time snapshots in kanban tests. Both test files renderTaskCardwhosecreated_at_labelcomputes "X days/min/h ago" againstDate.now(). With hardcodedcreated_at: "2026-05-12T00:00:00Z"in the in-testbuildTask/DEMO_PROJECTbuilders, the snapshot output drifted by one day every 24 hours of wall-clock time — failing the populated-board snapshot and sevencol-<phase>-3-taskssnapshots every morning regardless of code changes. Wirevi.useFakeTimers({ shouldAdvanceTime: true })+vi.setSystemTime(new Date("2026-05-14T00:00:00Z"))into thebeforeEachof both files andvi.useRealTimers()into theafterEach.shouldAdvanceTime: truekeeps async ticks (await,userEvent.setup()v14, Radix transition timers) responsive while the wall-clock value is pinned. Regenerate the eight affected snapshots once at the canonical "2 days ago" / "in 2 days" delta so they stay deterministic regardless of when CI runs. The fix unblocks the T-222 PR (#90) push which was failing thepnpm exec vitest runpre-push hook from the day-1 drift.
Added
- T-224 — Sprint 2 smoke E2E (closes #92). New
scripts/smoke-sprint-2.shbootspnpm tauri dev --features pilottwice; lifecycle 1 drives the full UI flow (ProjectTabBar+→ createsmoke-sprint-2project pointing at$REPO_ROOT→ IPC + sqlite cross-check for the newprojectsrow → Backlog+ Add task→ TaskCreationWizard title"smoke task"→ step-1 → step-2 → step-3 advance → submit → IPC + sqlite cross-check for the newtasksrow withphase='Backlog'→tauri-pilot dragfrom[data-testid="task-card-<id>"]to[data-testid="kanban-column-code"]with an IPCtasks_movefallback armed because@dnd-kitPointerSensor(activationConstraint: { distance: 6 }insrc/features/kanban/hooks/use-kanban-dnd.ts:28) listens topointerdown/move/upwhiletauri-pilot dragdispatches the HTML5drag*family — the AC istasks.phasepersistence, not the input vector →sqlite3 -readonlyprobeSELECT phase FROM tasks WHERE id = '<id>'returns'Code'→ DOM snapshot anchored onkanban-task-<id>confirms card underkanban-column-code-list);shutdown_app()then issueskill -TERM -- "-$DEV_PID"withSIGKILLfallback (process-group discipline copied verbatim fromscripts/smoke-e2e.sh:87-107) and unlinks thetauri-pilotsocket so lifecycle 2 binds cleanly; lifecycle 2 boots a freshpnpm tauri devinstance, waits for the Kanban board to mount, re-probes the sqlite phase (still'Code'— persistence AC), and waits for the samekanban-task-<id>card to reappear insidekanban-column-code-list(proves T-223 persistedcurrent_project_idAND T-221's tasks-list refetch hits the right project). Three PNG screenshots (target/smoke-sprint-2-{pre-drag,post-drag,post-restart}.png) attach to the PR description for visual review. Hardening reuse from PR #66 / T-134: identicalsetsid+ negative-PIDkillprocess-group discipline so unrelatedtarget/debug/forgentornode ...viteprocesses are never reaped (CodeRabbit #3217663820); identicalplugin_socket_dir()helper mirroringtauri-plugin-pilot/src/server/unix.rs:80so the CLI and the plugin never disagree on whether$XDG_RUNTIME_DIRis a private directory (CodeRabbit #3217860886); per-lifecycle freshness gate (LIFECYCLE_LOG_SIZE_BEFORE = $(wc -c <"$LOG_FILE")+tail -c "+N"slice + process-substitutiongrep -q "Forgent ready") so each boot proves it actually mounted the bootstrap layer (CodeRabbit #3217663815 + cubic-dev-ai #3217772727); same redacted*_DISPLAYpaths for every[smoke] ...echo line; samecargo install tauri-pilot-cli --version "=0.5.1" --lockedpin (workflow). DB + WAL/SHM/journal sidecars are wiped at script start (rm -f "$DB_PATH" "$DB_PATH-wal" "$DB_PATH-shm" "$DB_PATH-journal") because Sprint 2 asserts on specific rows (projects.name,tasks.title) — Sprint 1's file-existence + mtime gate is not strong enough. IPC poller (poll_ipc_for_id) walks.. | objectsso the four wrap shapes Sprint 1 normalises against ([],{"result":...},{"data":...},{"ok":...}) are all accepted without per-shape branching. Workflow update (.github/workflows/smoke-e2e.yml): single job restructured into astrategy.matrix.scenarioof two entries (sprint-1→scripts/smoke-e2e.sh,sprint-2→scripts/smoke-sprint-2.sh) withfail-fast: falseso a sprint-2 regression cannot mask a sprint-1 regression; apt install list extends tosqlite3+jq(required bysmoke-sprint-2.sh's sqlite pr...
Assets 8
v0.0.2
f5e3bcc Sprint 1 — Hexagonal foundations. First feature release after the v0.0.1-bootstrap scaffold tag. Closes the Sprint 1 task list (T-101 → T-135, 35 tasks, GH issues #1 → #49 closed). Sprint goal met: pnpm tauri dev boots the app with a Sidebar landmark, libsql migration V001 (projects, tasks, runs, phase_events) runs on startup, IPC tasks_list returns [], architecture test enforces forbidden imports in domain/, smoke E2E gate green on main. v0.1 MVP remains the post-Sprint-10 target per PRD §1.4 / ARCHI §22.
Fixed
-
scripts/smoke-e2e.sh+.github/workflows/smoke-e2e.yml— three hardening fixes from CodeRabbit's PR #66 review (T-134). (1) The dev server is now launched undersetsidso cleanup useskill -- -$DEV_PID(process group leader) instead of the previous broadpkill -f "target/debug/forgent"/pkill -f "node .*vite"fallbacks that would have reaped any matching user process outside the repo (CodeRabbit #3217663820); the negative-PID signal targets every descendant (pnpm, the tauri-cli wrapper,cargo run, the forgent binary and vite) without false positives. (2) DB and log assertions are now freshness-gated againstSMOKE_START_EPOCH(stat -c %Y "$DB_PATH" >= SMOKE_START_EPOCH) andLOG_SIZE_BEFORE(tail -c "+$((LOG_SIZE_BEFORE + 1))" | grep) — a leftovertasks.dbfrom yesterday's green run, or a staleForgent readyline in the daily-rolled log file, no longer satisfies the gate when bootstrap silently regresses (CodeRabbit #3217663815). (3)cargo install tauri-pilot-cliin the CI workflow is pinned to--version "=0.5.1" --lockedto matchtauri-plugin-pilot = "0.5.1"insrc-tauri/Cargo.toml; running unpinned would silently break the smoke gate the moment 0.6 lands an incompatibleipc/wait/screenshotsignature (CodeRabbit #3217663799). Verified locally on 2026年05月11日:[smoke] PASS — Sprint 1 acceptance gate met, including the newmtime ≥ run startandafter run startmarkers in the assertion log. -
src/shared/observability/sentry.ts— droppilot.*permission-denied promise rejections at the Sentry SDK boundary viaSentry.init({ ignoreErrors: [/pilot\./] })(T-134 follow-up, fixes RUST-2). Thetauri-plugin-pilotJS bridge firespilot.__callbackIPCs whose rejection is unhandled whilebuild.rsrewritescapabilities/pilot.jsonbetween dev rebuilds, surfacing asUnhandledRejection: pilot.__callback not allowedin Sentry. The plugin is gated behind thepilotcargo feature and never ships in release bundles, so these events have zero production value — filtering them at SDK init keeps the dashboard signal-to-noise high during smoke-E2E iteration without touching the live capture path for any other unhandled rejection.ignoreErrorswas chosen over abeforeSendfilter because the latter requires anullreturn to drop (forbidden by repo-wide oxlintunicorn/no-null), whereasignoreErrorsmatches on the event title / message via regex and short-circuits before any Sentry transport. -
src/App.tsx+src/shared/i18n/locales/{en,fr}.json— PR #64 review feedback (CodeRabbit + cubic-dev-ai, both inline). The<main aria-label="Main content">shipped in T-130 hardcoded an English screen-reader label while every other landmark in the shell (sidebar.landmark,sidebar.navigation,onboarding.landmark) routes throughreact-i18next— under FR locale the main region therefore announced as "Main content" instead of "Contenu principal", breaking the accessibility contract from CLAUDE.md (i18n is mandatory for end-user UI text). Addedcommon.main_landmarkkey in en ("Main content") and fr ("Contenu principal"), introducedconst translate = useT("common")at the top ofApp(), and boundaria-label={translate("main_landmark")}. Thecommonnamespace was the natural home (shell-level landmark, not feature-scoped) — no new namespace registration needed sincecommonis already inNAMESPACES. Existing tests assert bygetByRole("complementary", { name: "Sidebar" })(sidebar landmark) and don't query the main region by accessible name, so no test changes were needed; the new key inherits coverage from the i18n suite's namespace-resolution check.pnpm exec tsc -bclean.pnpm exec oxlint .0 warnings / 0 errors.pnpm exec oxfmtclean.pnpm exec vitest run106/106 still pass. -
src/features/onboarding/components/WelcomeScreen.tsx+WelcomeScreen.test.tsx— PR #63 review feedback (CodeRabbit Major).next()previously usedif (!isLast) setStepIndex((current) => current + STEP_DELTA), whereisLastcame from the render-snapshot closure. On the penultimate step, two rapid clicks both sawisLast = false, both queuedcurrent + 1updaters, and React composed them to pushstepIndexpastSTEPS.length - 1—STEPS[stepIndex]then resolved toundefinedand the next render crashed ontranslate("step." + undefined + ".title"). Fix introduces a module-levelLAST_INDEX = STEPS.length - STEP_DELTAconstant, switches the guard from the staleisLastclosure to a freshstepIndex < LAST_INDEXread, and clamps the updater viaMath.min(LAST_INDEX, current + STEP_DELTA)so even racing batched updates cannot overshoot. Added regression testclamps stepIndex to the final step when Next is clicked rapidly on the penultimate stepadvancing to step 6 then triple-clicking through the boundary, assertingdata-step="success"andaria-valuenow <= aria-valuemax.pnpm exec vitest run src/features/onboarding18/18 pass.pnpm exec oxlint src/features/onboardingclean.pnpm exec tsc -bclean. -
src-tauri/src/infrastructure/observability/sentry_init.rs+src/shared/observability/sentry.ts+scripts/no-secrets.sh— PR #60 review feedback (CodeRabbit + cubic-dev-ai). Backendsentry_init::initnow eagerly parsesSENTRY_DSNviasentry::types::Dsn::from_strand returnsNone(with aneprintln!diagnostic —tracingis not yet wired) when parsing fails, instead of silently buildingClientOptions { dsn: None }and still returning a guard. Previous behaviour masked telemetry misconfiguration: a malformed DSN would compile, returnSome(guard), and Sentry would no-op without warning. FrontendinitSentrynow.trim()simport.meta.env.VITE_SENTRY_DSNso whitespace-only values are treated as unset rather than initialising Sentry with a blank string.scripts/no-secrets.shfilename allowlist tightened from\.example$(which would have letprod.key.examplethrough) to(^|/)\.env(\.[^/]+)?\.example$, scoping the bypass to the.env.examplefamily only. Addedinit_returns_none_when_dsn_malformedregression test (5/5 sentry_init tests pass).cargo clippy --lib -- -D warningsclean.pnpm exec tsc -b / oxlint / oxfmt / vitest 61clean.
Added
scripts/smoke-e2e.sh+.github/workflows/smoke-e2e.yml+src-tauri/build.rs+src-tauri/Cargo.toml+src-tauri/src/lib.rs+src-tauri/.gitignore— Sprint 1 acceptance gate (T-134, GH issue #48, ARCHI §21 livrable Sprint 1, depends on T-130 / T-120 / T-121 already onmain). The smoke flow bootspnpm tauri dev --features pilot, pollstauri-pilot ping(default 180 s local / 300 s in CI), then asserts the five Sprint 1 acceptance criteria in order: the Sidebar landmark (<aside aria-label>from T-128) is mounted in the WebView viatauri-pilot wait --selector "aside[aria-label]"followed by a snapshot grep that accepts eitherMain navigation(EN),Navigation principale(FR) or the locale-freedata-testid="usage-indicator"footer pill (so the assertion does not break when the WebView falls back to the host locale —i18next-browser-languagedetectorreturns FR on a French Linux session even with EN-only fallback configured),tauri-pilot ipc tasks_list --args '{}'returns the literal[](also accepts envelope-wrapped{"result":[]}/{"data":[]}/{"ok":[]}to stay compatible with any future tauri-pilot output-format change; the v0.5--argsflag replaced the v0.4 positional'{}'form referenced inCLAUDE.md/T-134.md),<repo>/.claude/forgent/tasks.dbexists on disk (libsql sqlite created byAppContainer::build),~/.forgent/logs/forgent.log.$(date -u +%Y-%m-%d)contains theForgent readyline emitted bybootstrap()afterapp.manage, andtauri-pilot screenshot --output target/smoke-screenshot.png(falls back to the positional form for older pilot builds) produces a non-empty PNG. Cleanup trapsEXIT/INT/TERM, givespnpm tauri dev5 s SIGTERM grace then SIGKILLs the top-level wrapper plus orphanedtarget/debug/forgent/node ...vitechildren (the dev-server double-fork was the root cause of port 1420 staying claimed across reruns) and dumps the last 40 lines oftarget/smoke-logs/tauri-dev.logto stderr on failure. The script exportsFORGENT_PROJECT_ROOT="$REPO_ROOT"before launching the dev command:tauri-clicds intosrc-tauri/before invokingcargo run, so without that env varAppConfig::load'scurrent_dir()call resolves to the cargo manifest dir and the DB lands atsrc-tauri/.claude/forgent/tasks.dbinstead of the repo root — failing the acceptance criterion silently.TAURI_PILOT_SOCKET="$XDG_RUNTIME_DIR/tauri-pilot-com.forgent.app.sock"is also exported up-front:tauri-pilotdefaults tocom.pilot.test-appfor the socket name (auto-detection only works when exactly one pilot-enabled binary is on the system), so without the override the CLI talks to a stale socket from another project. Three companion infra pieces make the script actually runnable end-to-end:Cargo.tomlgains an optionaltauri-plugin-pilot = "0.5.1"dependency from crates.io (added viacargo add --optional tauri-plugin-pilot) and a corresponding[features]entrypilot = ["dep:tauri-plugin-pilot"]plus the auto-generated aliastauri-plugin-pilot = ["pilot"](kept for tooling that infers the feature name from the dep name). The flag is off by default — release ...