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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user