diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index ff314c97..d38c28c8 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -3524,3 +3524,4 @@ FOLLOW-UP SCOPE (folded in 2026-06-11): 3. TODO — exact toolchain pin for reproducible builds. No FVM .fvmrc / .tool-versions / .flutter-version exists; a fresh clone builds with whatever Flutter the dev has (>= floor). Add an exact pin (FVM .fvmrc or asdf/mise .tool-versions) targeting the current toolchain (Dart 3.12.1 / Flutter 3.44.1).', NULL, '2026-06-11 07:05:54', '2026-06-11 07:05:54', '2026-06-11 07:05:54', NULL, 'f6d9c657c6987f7927bc3ba0bc02b3a4', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBAWHM1SQ1686ZJ8JQCFQ1ZW', 'status', 'backlog', 'ready', NULL, '2026-06-11 07:05:58', '2026-06-11 07:05:58', '2026-06-11 07:05:58', NULL, '1553f134361839180feffa625a88c06d', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBAWHM1SQ1686ZJ8JQCFQ1ZW', 'status', 'ready', 'done', NULL, '2026-06-11 10:12:25', '2026-06-11 10:12:25', '2026-06-11 10:12:25', NULL, '53cfd5cf779b9a8474afe7e74efd02a3', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 8e62733c..690fabc0 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -3259,3 +3259,12 @@ FOLLOW-UP SCOPE (folded in 2026-06-11): 2. TODO — tighten env floors to reality. pubspec.yaml `environment` currently declares Dart >=3.5.0 / Flutter >=3.19.0, but we actually require more (alchemist 0.12 needs Flutter 3.32; the held markdown 7.3.1 needs Dart 3.9). Raise floors to ~Dart >=3.9.0 / Flutter >=3.32.0 — honest minimums. Raising Dart to 3.9 also unblocks the held markdown 7.2.2 -> 7.3.1 bump. 3. TODO — exact toolchain pin for reproducible builds. No FVM .fvmrc / .tool-versions / .flutter-version exists; a fresh clone builds with whatever Flutter the dev has (>= floor). Add an exact pin (FVM .fvmrc or asdf/mise .tool-versions) targeting the current toolchain (Dart 3.12.1 / Flutter 3.44.1).', 'ready', 'medium', NULL, NULL, NULL, '2026-06-11 06:50:21', '2026-06-11 07:05:58', NULL, '352f3ceb8714f14bd12515a9b4542794', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBAWHM1SQ1686ZJ8JQCFQ1ZW', 'task', NULL, 'Evaluate pinned dependency versions + full CVE/advisory check', 'flutter pub outdated reports 16 packages behind latest (3 direct: ffi 2.1.3→2.2.0, jovial_svg 1.1.26→1.1.30, markdown 7.2.2→7.3.1; dev: alchemist 0.12.1→0.14.0, mocktail 1.0.4→1.0.5, test 1.31.0→1.31.1; plus transitive incl. xml 6.6.1→7.0.1 major). Per the prefer-zero-deps + exact-pin + advisory-review guardrail (D-42, CLAUDE.md supply chain), evaluate each pinned/direct dep: review CVEs/advisories (OSV.dev + pub.dev) for the current pin AND the candidate version, then bump the safe ones (artefact + assets/licenses.yaml in the same commit) and document any deliberately-held pins. Transitive deps move with the resolver/Flutter SDK; note but don''t force. Triggered by repeated ''N packages have newer versions'' noise on every build. + +FOLLOW-UP SCOPE (folded in 2026-06-11): + +1. DONE: osv-scanner supply-chain gate added to `make push-check` (ci/osv_scan.sh; fail-closed on any pubspec.lock advisory). Requires osv-scanner on PATH (brew install osv-scanner). + +2. TODO — tighten env floors to reality. pubspec.yaml `environment` currently declares Dart >=3.5.0 / Flutter >=3.19.0, but we actually require more (alchemist 0.12 needs Flutter 3.32; the held markdown 7.3.1 needs Dart 3.9). Raise floors to ~Dart >=3.9.0 / Flutter >=3.32.0 — honest minimums. Raising Dart to 3.9 also unblocks the held markdown 7.2.2 -> 7.3.1 bump. + +3. TODO — exact toolchain pin for reproducible builds. No FVM .fvmrc / .tool-versions / .flutter-version exists; a fresh clone builds with whatever Flutter the dev has (>= floor). Add an exact pin (FVM .fvmrc or asdf/mise .tool-versions) targeting the current toolchain (Dart 3.12.1 / Flutter 3.44.1).', 'done', 'medium', NULL, NULL, NULL, '2026-06-11 06:50:21', '2026-06-11 10:12:25', NULL, '4738f0609647b88e169e6e5788ded217', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb2ec0c..68bed165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,17 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ## [Unreleased] +### Fixed + +- **Ticket/decision sidebars load on first open after a desktop launch.** A + desktop launch starts in HOME (not a git repo), and the daemon booted its + pql/git/files workspace there — so pql ran in HOME, hit a stale + `~/.pql/pql.db`, and the sidebars showed "pql … failed" until the project was + reopened (a manual refresh worked once the workspace had swapped to the repo). + The daemon now boots at the last opened project when the launch directory + isn't itself a repo, so pql targets the real workspace from the first request. + (T-352) + ### Changed - **Minimum toolchain raised to honest values.** `pubspec.yaml` now declares diff --git a/lib/main.dart b/lib/main.dart index 063aab08..d7f3bb49 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -83,6 +83,22 @@ Future main() async { final appDir = await _resolveAppDir(); final themes = await _loadBundledThemes(); + // Resolve the workspace to boot the daemon at. A desktop launch starts in + // HOME (not a git repo), so atCwd would point pql/git/files at HOME — where + // pql hits a stale ~/.pql/pql.db and the sidebars error on first load. Boot + // at the last project instead so the daemon targets the real repo from the + // first request. (T-352) + Directory startupWorkRoot = resolveWorkspaceRoot(Directory.current); + if (!kIsWeb) { + final bootSettings = SettingsStore(appDir: appDir); + await bootSettings.load(); + startupWorkRoot = resolveStartupWorkspace( + cwdRoot: startupWorkRoot, + lastProject: bootSettings.get('app.lastProject'), + isGitRepo: (d) => Directory('${d.path}/.git').existsSync(), + ); + } + // Resolve toolchain + boot daemon inline — same as Linux. // With proper signing (Developer ID), no sandbox or isolate needed. final toolchain = Toolchain(); @@ -297,7 +313,7 @@ Future main() async { daemonBus = events; kernelArrangement = arrangement; kernelPanels = panels; - final workRoot = FilesService.atCwd(events: _BusEventSink(events)).root; + final workRoot = startupWorkRoot; final dispatcher = buildDispatcher(events, toolchain, workRoot, arrangement, panels); // Build the client at the workspace's socket path. The // server is started below (swapIpcServer) which the diff --git a/lib/src/daemon/files_commands.dart b/lib/src/daemon/files_commands.dart index b20bb742..c34a2857 100644 --- a/lib/src/daemon/files_commands.dart +++ b/lib/src/daemon/files_commands.dart @@ -33,7 +33,7 @@ class FilesService { /// Build from the current working directory, walking up to the git /// root if present. Falls back to CWD otherwise. factory FilesService.atCwd({required DaemonEventSink events}) { - final root = _resolveWorkspaceRoot(Directory.current); + final root = resolveWorkspaceRoot(Directory.current); return FilesService(root: root, events: events); } @@ -148,7 +148,7 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) { // --------------------------------------------------------------------------- -Directory _resolveWorkspaceRoot(Directory start) { +Directory resolveWorkspaceRoot(Directory start) { Directory cur = start.absolute; for (var i = 0; i < 64; i++) { final g = Directory('${cur.path}/.git'); @@ -160,6 +160,24 @@ Directory _resolveWorkspaceRoot(Directory start) { return start.absolute; } +/// Pick the workspace root to boot the daemon at. +/// +/// Prefer the launch CWD when it's inside a git repo. Otherwise — desktop +/// launches start in HOME, which isn't a repo — fall back to the last opened +/// project so pql/git/files target the real workspace from the very first +/// request. Booting at HOME instead makes pql run there and hit a stale +/// `~/.pql/pql.db`, so the ticket/decision sidebars error on first load and +/// only recover once the project is (re)opened. Falls back to [cwdRoot] when +/// there's no valid last project. (T-352) +Directory resolveStartupWorkspace({required Directory cwdRoot, required String? lastProject, required bool Function(Directory) isGitRepo}) { + if (isGitRepo(cwdRoot)) return cwdRoot; + if (lastProject != null && lastProject.isNotEmpty) { + final dir = Directory(lastProject); + if (isGitRepo(dir)) return dir; + } + return cwdRoot; +} + /// Build the default IgnoreSet: clide's always-hide list layered under /// the `ignore_files:` chain from `.pql/config.yaml` (per D-4), in /// order, later files winning. clide owns that config key (D-3) and diff --git a/test/daemon/files_commands_test.dart b/test/daemon/files_commands_test.dart index 50adf54b..19ca5f97 100644 --- a/test/daemon/files_commands_test.dart +++ b/test/daemon/files_commands_test.dart @@ -246,4 +246,32 @@ void main() { Directory.current = saved; } }); + + group('resolveStartupWorkspace (T-352)', () { + Directory dir(String p) => Directory(p); + + test('uses the CWD when it is a git repo', () { + final root = resolveStartupWorkspace(cwdRoot: dir('/work/repo'), lastProject: '/home/user/other', isGitRepo: (d) => d.path == '/work/repo'); + expect(root.path, '/work/repo'); + }); + + test('falls back to lastProject when the CWD is not a repo (desktop launch in HOME)', () { + final root = resolveStartupWorkspace( + cwdRoot: dir('/home/user'), + lastProject: '/work/repo', + isGitRepo: (d) => d.path == '/work/repo', // HOME is not a repo + ); + expect(root.path, '/work/repo'); + }); + + test('keeps the CWD when neither it nor lastProject is a repo', () { + final root = resolveStartupWorkspace(cwdRoot: dir('/home/user'), lastProject: '/gone', isGitRepo: (_) => false); + expect(root.path, '/home/user'); + }); + + test('keeps the CWD when lastProject is null/empty', () { + expect(resolveStartupWorkspace(cwdRoot: dir('/home/user'), lastProject: null, isGitRepo: (_) => false).path, '/home/user'); + expect(resolveStartupWorkspace(cwdRoot: dir('/home/user'), lastProject: '', isGitRepo: (_) => false).path, '/home/user'); + }); + }); }