main: boot the daemon at the last project, not HOME (T-352)

Confirmed root cause of the sidebar failure: a desktop launch starts in
HOME, which isn't a git repo, so resolveWorkspaceRoot returns HOME and
the daemon's pql/git/files all target HOME. pql then finds a stale
~/.pql/pql.db (left from earlier HOME-workdir runs) and errors
"pql.db is from an earlier schema" — exactly what the sidebars showed.
A manual refresh worked only because by then the workspace had swapped
to the repo. (Verified directly: `cd $HOME && pql ticket list` reproduces
the schema error against ~/.pql/pql.db.)

Settings confirmed the user is in the picker path (no startupSticky on
any recent), so nothing auto-opens the repo at boot and the daemon sits
on HOME until the project is opened.

Fix: resolve the startup workspace before boot — prefer the launch CWD
when it's a git repo, else fall back to app.lastProject (the repo). The
daemon now points at the real workspace from its first request, so the
sidebars load immediately without depending on swap/refetch timing. The
ProjectOpened refetch and swap serialization from 2.3.x stay in place for
genuine mid-session project switches.

Pure resolveStartupWorkspace() helper is unit-tested; app boot green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 12:32:18 +02:00
co-authored by Claude Opus 4.8
parent 46f84728d2
commit fc4021e98b
6 changed files with 86 additions and 3 deletions
@@ -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;
+9
View File
@@ -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);
+11
View File
@@ -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
+17 -1
View File
@@ -83,6 +83,22 @@ Future<void> 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<String>('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<void> 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
+20 -2
View File
@@ -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
+28
View File
@@ -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');
});
});
}