diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 855fec05..cc04b9ac 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-04-26T11:16:34Z", + "exported_at": "2026-04-26T12:16:46Z", "decisions": [ { "id": "D-1", @@ -9,7 +9,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-3", @@ -19,7 +19,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-4", @@ -29,7 +29,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-5", @@ -39,7 +39,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-6", @@ -49,7 +49,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-7", @@ -59,7 +59,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-8", @@ -69,7 +69,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-9", @@ -79,7 +79,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-10", @@ -89,7 +89,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-11", @@ -99,7 +99,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-12", @@ -109,7 +109,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-13", @@ -119,7 +119,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-14", @@ -129,7 +129,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-15", @@ -139,7 +139,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-16", @@ -149,7 +149,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-17", @@ -159,7 +159,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-18", @@ -169,7 +169,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-19", @@ -179,7 +179,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-20", @@ -189,7 +189,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-21", @@ -199,7 +199,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-22", @@ -209,7 +209,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-23", @@ -219,7 +219,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-24", @@ -229,7 +229,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-25", @@ -239,7 +239,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-26", @@ -249,7 +249,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-27", @@ -259,7 +259,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-28", @@ -269,7 +269,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-29", @@ -279,7 +279,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-30", @@ -289,7 +289,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-31", @@ -299,7 +299,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-32", @@ -309,7 +309,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-33", @@ -319,7 +319,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-34", @@ -329,7 +329,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-35", @@ -339,7 +339,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-36", @@ -349,7 +349,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-37", @@ -359,7 +359,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-38", @@ -369,7 +369,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-39", @@ -379,7 +379,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-40", @@ -389,7 +389,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-41", @@ -399,7 +399,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-42", @@ -409,7 +409,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-43", @@ -419,7 +419,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-44", @@ -429,7 +429,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-45", @@ -439,7 +439,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-46", @@ -449,7 +449,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-47", @@ -459,7 +459,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-48", @@ -469,7 +469,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-49", @@ -479,7 +479,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-50", @@ -489,7 +489,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-51", @@ -499,7 +499,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-52", @@ -509,7 +509,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-53", @@ -519,7 +519,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-54", @@ -529,7 +529,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-55", @@ -539,7 +539,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-56", @@ -549,7 +549,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-57", @@ -559,7 +559,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "D-58", @@ -569,7 +569,17 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" + }, + { + "id": "D-59", + "type": "confirmed", + "domain": "tooling", + "title": "Bundled git via dugite-native", + "status": "active", + "date": "2026-04-25", + "file_path": "decisions/tooling.md", + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-1", @@ -578,7 +588,7 @@ "title": "Authorisation granularity on the IPC socket", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-2", @@ -587,7 +597,7 @@ "title": "Back-pressure on event streams", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-3", @@ -596,7 +606,7 @@ "title": "Event persistence + audit/undo", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-4", @@ -605,7 +615,7 @@ "title": "`.canvas` schema compatibility with Obsidian", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-5", @@ -614,7 +624,7 @@ "title": "IPC wire-format stability + `schema_version:`", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-6", @@ -623,7 +633,7 @@ "title": "Window chrome — native frame vs frameless custom", "status": "resolved", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-7", @@ -632,7 +642,7 @@ "title": "macOS app bundle signing / notarisation", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-8", @@ -641,7 +651,7 @@ "title": "Extension API shape — widgets, subcommands, both?", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-9", @@ -650,7 +660,7 @@ "title": "Lua runtime vendoring", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-10", @@ -659,7 +669,7 @@ "title": "Extension manifest `schema_version:`", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-11", @@ -668,7 +678,7 @@ "title": "Coverage gates — hard thresholds vs soft reporting", "status": "open", "file_path": "decisions/questions-testing.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-12", @@ -677,7 +687,7 @@ "title": "Screen-reader automation (axe-core via Playwright)", "status": "open", "file_path": "decisions/questions-testing.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-13", @@ -686,7 +696,7 @@ "title": "Web production-mode a11y", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-14", @@ -695,7 +705,7 @@ "title": "i18n plurals / gender / date-format tooling", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-15", @@ -704,7 +714,7 @@ "title": "Editor tab — full LSP vs tree-sitter-only highlight", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-16", @@ -713,7 +723,7 @@ "title": "`tree-sitter-dart` grammar maintenance", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-17", @@ -722,7 +732,7 @@ "title": "Icon set growth", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-18", @@ -731,7 +741,7 @@ "title": "Theme hot-reload in release builds", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-19", @@ -740,7 +750,7 @@ "title": "(withdrawn)", "status": "resolved", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-20", @@ -749,7 +759,7 @@ "title": "Kernel DB service — namespaced SQL access?", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-21", @@ -758,7 +768,7 @@ "title": "Pql absorbs planning vs keeps separate", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-22", @@ -767,7 +777,7 @@ "title": "Ticket persistence strategy", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-23", @@ -776,7 +786,7 @@ "title": "SSH-remote development — run clide against a remote workspace", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-25", @@ -785,7 +795,7 @@ "title": "Body text face — mono everywhere vs Josefin Sans UI + mono code", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-26", @@ -794,7 +804,7 @@ "title": "Small screen layout (\u003c 1000px)", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-27", @@ -803,7 +813,7 @@ "title": "Two-editor split", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-28", @@ -812,7 +822,7 @@ "title": "Terminal strip scope — shell only or logs/errors/tests", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-29", @@ -821,7 +831,7 @@ "title": "Branch picker location", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-30", @@ -830,7 +840,7 @@ "title": "Focus behavior when editor is dirty and viewer is peeked", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "Q-31", @@ -839,7 +849,7 @@ "title": "XWayland fallback for frameless — proper Wayland protocol needed", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-2", @@ -849,7 +859,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-3", @@ -859,7 +869,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-4", @@ -869,7 +879,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-5", @@ -879,7 +889,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-6", @@ -889,7 +899,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-7", @@ -899,7 +909,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-8", @@ -909,7 +919,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-9", @@ -919,7 +929,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-10", @@ -929,7 +939,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-11", @@ -939,7 +949,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" }, { "id": "R-12", @@ -949,7 +959,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:22:06" + "synced_at": "2026-04-26 12:12:18" } ], "decision_refs": [ @@ -1355,6 +1365,18 @@ "ref_type": "references", "note": "**Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml)." }, + { + "source_id": "D-59", + "target_id": "D-31", + "ref_type": "references", + "note": "**Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml)." + }, + { + "source_id": "D-59", + "target_id": "D-42", + "ref_type": "references", + "note": "**Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml)." + }, { "source_id": "D-6", "target_id": "D-1", diff --git a/CHANGELOG.md b/CHANGELOG.md index 176949be..9266f5f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. - Backend isolate — all subprocess and file I/O runs in a dedicated isolate, keeping the merged UI/platform thread on macOS free for rendering. Communicates via SendPort using the existing IPC protocol. + Two-phase boot: resolve toolchain on spawn, initialize services on + project open. Scheduler ticker only runs while a project is active. - Toolchain — centralized binary resolution replacing five ad-hoc mechanisms. Resolves git, pql, tmux, ptyc, shell once at boot via diff --git a/lib/kernel/src/backend.dart b/lib/kernel/src/backend.dart index a8e724e7..cfee46ad 100644 --- a/lib/kernel/src/backend.dart +++ b/lib/kernel/src/backend.dart @@ -1,8 +1,8 @@ /// Manages the backend isolate lifecycle. /// -/// Call [spawn] to start the backend, which resolves the toolchain and -/// boots all daemon services in a separate isolate. Returns when the -/// backend is ready to accept requests. +/// Two-phase boot: +/// 1. [spawn] — starts the isolate, resolves toolchain (binary checks only). +/// 2. [openWorkspace] — initializes services for a specific project root. library; import 'dart:async'; @@ -16,36 +16,44 @@ class Backend { Backend._({ required this.client, required this.toolchain, + required SendPort backendRequestPort, required Isolate isolate, required ReceivePort receivePort, - }) : _isolate = isolate, + }) : _backendRequestPort = backendRequestPort, + _isolate = isolate, _receivePort = receivePort; final IsolateClient client; final Toolchain toolchain; + final SendPort _backendRequestPort; final Isolate _isolate; final ReceivePort _receivePort; - /// Spawn the backend isolate and wait for it to be ready. + Completer? _projectCompleter; + final Map> _validateCompleters = {}; + int _validateId = 0; + + /// Spawn the backend isolate. Returns when the toolchain is resolved. + /// No services are active yet — call [openWorkspace] to activate. static Future spawn({ - required String workspaceRoot, required IsolateClient Function(SendPort backendPort) clientFactory, + String? hintRoot, }) async { final receivePort = ReceivePort(); final completer = Completer(); late final IsolateClient client; late final Isolate isolate; + late final SendPort backendRequestPort; + late final Backend backend; receivePort.listen((message) { if (message is Map) { final type = message['type'] as String?; if (type == 'ready') { - // Backend is booted — extract its request port and toolchain state. - final requestPort = message['requestPort'] as SendPort; - client = clientFactory(requestPort); + backendRequestPort = message['requestPort'] as SendPort; + client = clientFactory(backendRequestPort); - // Apply toolchain state from the backend. final tcData = message['toolchain'] as Map; final toolchain = Toolchain(); toolchain.applyResolved(ResolvedPaths( @@ -57,14 +65,33 @@ class Backend { gitEnv: (tcData['gitEnv'] as Map?)?.cast(), )); - if (!completer.isCompleted) { - completer.complete(Backend._( - client: client, - toolchain: toolchain, - isolate: isolate, - receivePort: receivePort, - )); - } + backend = Backend._( + client: client, + toolchain: toolchain, + backendRequestPort: backendRequestPort, + isolate: isolate, + receivePort: receivePort, + ); + + if (!completer.isCompleted) completer.complete(backend); + } else if (type == 'project.validated') { + final id = message['id'] as String; + final root = message['root'] as String?; + final c = backend._validateCompleters.remove(id); + if (c != null && !c.isCompleted) c.complete(root); + } else if (type == 'project.ready') { + // Update toolchain with project-specific paths. + final tcData = message['toolchain'] as Map; + backend.toolchain.applyResolved(ResolvedPaths( + git: tcData['git'] as String?, + pql: tcData['pql'] as String?, + tmux: tcData['tmux'] as String?, + ptyc: tcData['ptyc'] as String?, + shell: tcData['shell'] as String?, + gitEnv: (tcData['gitEnv'] as Map?)?.cast(), + )); + backend._projectCompleter?.complete(); + backend._projectCompleter = null; } else { // Response or event — forward to the client. client.handleMessage(message); @@ -74,15 +101,37 @@ class Backend { isolate = await Isolate.spawn( backendEntry, - BackendBootMessage( - frontendPort: receivePort.sendPort, - workspaceRoot: workspaceRoot, - ), + BackendBootMessage(frontendPort: receivePort.sendPort, hintRoot: hintRoot), ); return completer.future; } + /// Validate a path as a git repo. Returns the repo root or null. + /// Runs git rev-parse in the backend isolate (no main-thread I/O). + Future validateProject(String path) { + final id = '${_validateId++}'; + final c = Completer(); + _validateCompleters[id] = c; + _backendRequestPort.send({ + 'type': 'project.validate', + 'path': path, + 'id': id, + }); + return c.future; + } + + /// Activate a project. The backend (re)initializes all services + /// for the given root directory. Returns when services are ready. + Future openProject(String path) { + _projectCompleter = Completer(); + _backendRequestPort.send({ + 'type': 'project.open', + 'path': path, + }); + return _projectCompleter!.future; + } + /// Shut down the backend isolate. void dispose() { _isolate.kill(priority: Isolate.beforeNextEvent); diff --git a/lib/kernel/src/backend_entry.dart b/lib/kernel/src/backend_entry.dart index bdb85218..f0b5042c 100644 --- a/lib/kernel/src/backend_entry.dart +++ b/lib/kernel/src/backend_entry.dart @@ -1,8 +1,11 @@ /// Backend isolate entry point. /// -/// Boots the daemon dispatcher, toolchain, and all subprocess services. -/// Communicates with the main isolate via SendPort (responses + events) -/// and ReceivePort (incoming requests). +/// Two-phase boot: +/// 1. Resolve toolchain (find binaries) → report ready. +/// 2. On `project.open` message → initialize services for the project. +/// +/// The dispatcher only registers command handlers after a project is +/// activated. IPC requests arriving before that get an error response. library; import 'dart:io'; @@ -16,80 +19,131 @@ import 'package:clide/src/daemon/git_commands.dart'; import 'package:clide/src/daemon/pane_commands.dart'; import 'package:clide/src/daemon/pql_commands.dart'; import 'package:clide/src/editor/registry.dart' show EditorRegistry; -import 'package:clide/src/daemon/files_commands.dart' show FilesService; import 'package:clide/src/git/client.dart'; import 'package:clide/src/ipc/envelope.dart'; +import 'package:clide/src/ipc/schema_v1.dart'; import 'package:clide/src/panes/event_sink.dart'; import 'package:clide/src/panes/registry.dart'; import 'package:clide/src/pql/client.dart'; /// Message sent from main isolate to bootstrap the backend. class BackendBootMessage { - const BackendBootMessage({ - required this.frontendPort, - required this.workspaceRoot, - }); + const BackendBootMessage({required this.frontendPort, this.hintRoot}); final SendPort frontendPort; - final String workspaceRoot; + /// Optional path hint for initial toolchain resolution (e.g. CLIDE_WORKSPACE). + /// Used to find project-local binaries like dugite before a project opens. + final String? hintRoot; } /// Top-level entry point for the backend isolate. void backendEntry(BackendBootMessage boot) { final frontendPort = boot.frontendPort; - final workspaceRoot = boot.workspaceRoot; - - // Set up the receive port for incoming requests from the frontend. final requestPort = ReceivePort(); - - // Resolve toolchain (file I/O — safe here, not on UI thread). - final toolchain = Toolchain(); - toolchain.applyResolved(resolveToolchainPaths(workspaceRoot)); - - // Boot the dispatcher and all services. - final dispatcher = DaemonDispatcher(); final eventSink = _IsolateEventSink(frontendPort); - final workDir = Directory(workspaceRoot); + final dispatcher = DaemonDispatcher(); + late Toolchain toolchain; - final filesService = FilesService(root: workDir, events: eventSink); - registerFilesCommands(dispatcher, filesService); + // Phase 1: resolve toolchain — just find binaries, don't init services. + // We need a project root for ptyc/dugite paths. Use a sensible + // default; the real project comes from project.open. + final resolveRoot = boot.hintRoot ?? Platform.environment['HOME'] ?? '/tmp'; + toolchain = Toolchain(); + toolchain.applyResolved(resolveToolchainPaths(resolveRoot)); - final editorRegistry = EditorRegistry(events: eventSink, workspaceRoot: workDir); - registerEditorCommands(dispatcher, editorRegistry); - - final gitClient = GitClient(toolchain: toolchain, workDir: workDir); - registerGitCommands(dispatcher, gitClient, eventSink); - - final pql = PqlClient(workDir: workDir, toolchain: toolchain); - registerPqlCommands(dispatcher, pql); - - final paneRegistry = PaneRegistry(events: eventSink); - registerPaneCommands(dispatcher, paneRegistry, toolchain: toolchain); - - // Listen for requests from the frontend. + // Listen for messages from the frontend. requestPort.listen((message) async { - if (message is Map) { + if (message is! Map) return; + final type = message['type'] as String?; + + if (type == 'project.validate') { + // Validate a path as a git repo. Runs git rev-parse in the backend + // isolate (safe from the merged thread). Returns the repo root or null. + final path = message['path'] as String; + final id = message['id'] as String; + try { + final r = await Process.run(toolchain.git, ['rev-parse', '--show-toplevel'], + workingDirectory: path, environment: toolchain.gitEnv); + if (r.exitCode == 0) { + final root = (r.stdout as String).trim(); + frontendPort.send({'type': 'project.validated', 'id': id, 'root': root}); + } else { + frontendPort.send({'type': 'project.validated', 'id': id, 'root': null}); + } + } catch (_) { + frontendPort.send({'type': 'project.validated', 'id': id, 'root': null}); + } + } else if (type == 'project.open') { + // Phase 2: (re)initialize services for the given project. + final projectPath = message['path'] as String; + final workDir = Directory(projectPath); + + // Re-resolve toolchain with the actual project root (finds + // dugite in native/dugite/, ptyc in ptyc/bin/, etc.) + toolchain = Toolchain(); + toolchain.applyResolved(resolveToolchainPaths(projectPath)); + + // Clear existing handlers and re-register with new project. + dispatcher.clear(); + + final filesService = FilesService(root: workDir, events: eventSink); + registerFilesCommands(dispatcher, filesService); + + final editorRegistry = EditorRegistry(events: eventSink, workspaceRoot: workDir); + registerEditorCommands(dispatcher, editorRegistry); + + final gitClient = GitClient(toolchain: toolchain, workDir: workDir); + registerGitCommands(dispatcher, gitClient, eventSink); + + final pql = PqlClient(workDir: workDir, toolchain: toolchain); + registerPqlCommands(dispatcher, pql); + + final paneRegistry = PaneRegistry(events: eventSink); + registerPaneCommands(dispatcher, paneRegistry, toolchain: toolchain); + + // Tell the frontend the project is active. + frontendPort.send({ + 'type': 'project.ready', + 'path': projectPath, + 'toolchain': _serializeToolchain(toolchain), + }); + } else { + // IPC request — dispatch if we have handlers. final req = IpcRequest.fromJson(message); - final resp = await dispatcher.dispatch(req); - frontendPort.send(resp.toJson()); + if (dispatcher.isEmpty) { + frontendPort.send(IpcResponse.err( + id: req.id, + error: IpcError( + code: IpcExitCode.toolError, + kind: IpcErrorKind.toolError, + message: 'No project active', + hint: 'Open a project first', + ), + ).toJson()); + } else { + final resp = await dispatcher.dispatch(req); + frontendPort.send(resp.toJson()); + } } }); - // Tell the frontend we're ready, and give it our request port. + // Send ready with toolchain state and request port. frontendPort.send({ 'type': 'ready', 'requestPort': requestPort.sendPort, - 'toolchain': { - 'git': toolchain.git, - 'pql': toolchain.pql, - 'tmux': toolchain.tmux, - 'ptyc': toolchain.ptyc, - 'shell': toolchain.shell, - 'gitEnv': toolchain.gitEnv, - 'missing': toolchain.missing, - }, + 'toolchain': _serializeToolchain(toolchain), }); } +Map _serializeToolchain(Toolchain tc) => { + 'git': tc.git, + 'pql': tc.pql, + 'tmux': tc.tmux, + 'ptyc': tc.ptyc, + 'shell': tc.shell, + 'gitEnv': tc.gitEnv, + 'missing': tc.missing, + }; + /// Sends IPC events to the frontend via SendPort. class _IsolateEventSink implements DaemonEventSink { _IsolateEventSink(this._port); diff --git a/lib/kernel/src/facade.dart b/lib/kernel/src/facade.dart index a172ff61..d60193a6 100644 --- a/lib/kernel/src/facade.dart +++ b/lib/kernel/src/facade.dart @@ -105,6 +105,8 @@ class KernelServices { DaemonClient? isolateClient, bool autoStartDaemonClient = true, Toolchain? toolchain, + Future Function(String path)? onProjectOpen, + Future Function(String path)? onValidateProject, }) async { final log = Logger(); final events = DaemonBus(); @@ -148,6 +150,8 @@ class KernelServices { events: events, settings: settings, toolchain: tc, + onProjectOpen: onProjectOpen, + onValidateProject: onValidateProject, ); final ipc = isolateClient ?? (daemonClientFactory != null diff --git a/lib/kernel/src/project.dart b/lib/kernel/src/project.dart index 8fb95132..732082f7 100644 --- a/lib/kernel/src/project.dart +++ b/lib/kernel/src/project.dart @@ -49,15 +49,21 @@ class ProjectManager extends ChangeNotifier { required DaemonBus events, required SettingsStore settings, required Toolchain toolchain, + Future Function(String path)? onProjectOpen, + Future Function(String path)? onValidateProject, }) : _log = log, _events = events, _settings = settings, - _toolchain = toolchain; + _toolchain = toolchain, + _onProjectOpen = onProjectOpen, + _onValidateProject = onValidateProject; final Logger _log; final DaemonBus _events; final SettingsStore _settings; final Toolchain _toolchain; + final Future Function(String path)? _onProjectOpen; + final Future Function(String path)? _onValidateProject; Directory? _current; Directory? get current => _current; @@ -81,12 +87,18 @@ class ProjectManager extends ChangeNotifier { } Future open(String path) async { - final root = await resolveWorkspace(path); + final root = await resolveProject(path); if (root == null) { _log.warn('project', 'not a git repo: $path'); return false; } _current = Directory(root); + + // Tell the backend isolate to (re)initialize services for this workspace. + if (_onProjectOpen != null) { + await _onProjectOpen!(root); + } + await _settings.setProjectDir(_current); await _settings.set('app.lastProject', root); @@ -118,7 +130,13 @@ class ProjectManager extends ChangeNotifier { notifyListeners(); } - Future resolveWorkspace(String path) async { + Future resolveProject(String path) async { + // Prefer backend validation (runs in the backend isolate, safe from + // the merged UI thread). Falls back to direct Process.run for tests + // and the CLI binary. + if (_onValidateProject != null) { + return _onValidateProject!(path); + } try { final r = await Process.run(_toolchain.git, ['rev-parse', '--show-toplevel'], workingDirectory: path, environment: _toolchain.gitEnv); if (r.exitCode != 0) return null; diff --git a/lib/kernel/src/scheduler.dart b/lib/kernel/src/scheduler.dart index b3ec5702..44cba683 100644 --- a/lib/kernel/src/scheduler.dart +++ b/lib/kernel/src/scheduler.dart @@ -36,9 +36,27 @@ class SchedulerService { Isolate? _isolate; ReceivePort? _port; StreamSubscription? _sub; + StreamSubscription? _projectSub; + /// Listen for project lifecycle events. The periodic ticker only runs + /// while a project is open — no wasted cycles on the welcome screen. void start() { - if (_isolate != null) return; + _projectSub = _events.on().listen((_) => _startTicker()); + _events.on().listen((_) => _stopTicker()); + } + + /// Start the periodic ticker and fire an immediate first cycle so + /// all panels refresh without waiting for the first interval. + void _startTicker() { + _stopTicker(); + + // Immediate first tick for all tiers. + for (final tier in SchedulerTier.values) { + if (tier == SchedulerTier.midnight) continue; + _events.emit(SchedulerTick(tier: tier)); + } + + // Then start the periodic isolate. _port = ReceivePort(); _sub = _port!.listen((msg) { if (msg is String) { @@ -49,6 +67,15 @@ class SchedulerService { Isolate.spawn(_isolateEntry, _port!.sendPort).then((iso) => _isolate = iso); } + void _stopTicker() { + _sub?.cancel(); + _port?.close(); + _isolate?.kill(priority: Isolate.immediate); + _isolate = null; + _port = null; + _sub = null; + } + static void _isolateEntry(SendPort send) { int lastDay = DateTime.now().day; for (final tier in SchedulerTier.values) { @@ -67,11 +94,7 @@ class SchedulerService { } void dispose() { - _sub?.cancel(); - _port?.close(); - _isolate?.kill(priority: Isolate.immediate); - _isolate = null; - _port = null; - _sub = null; + _projectSub?.cancel(); + _stopTicker(); } } diff --git a/lib/main.dart b/lib/main.dart index 47cb5884..8a6b563d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:clide/app.dart'; import 'package:clide/test_app.dart'; import 'package:clide/builtin/canvas/canvas.dart'; @@ -56,12 +54,11 @@ Future main() async { final themes = await _loadBundledThemes(); // Spawn the backend isolate — all subprocess and file I/O runs there. - // The main isolate stays free for rendering on the merged UI thread. + // Phase 1: resolve toolchain (binary availability only, no workspace). + // Phase 2: openProject() initializes services when a project opens. const workspace = String.fromEnvironment('CLIDE_WORKSPACE'); - final workspaceRoot = workspace.isNotEmpty ? workspace : Directory.current.path; - final backend = kIsWeb ? null : await Backend.spawn( - workspaceRoot: workspaceRoot, + hintRoot: workspace.isNotEmpty ? workspace : null, clientFactory: (backendPort) => IsolateClient( log: Logger(), events: DaemonBus(), @@ -79,6 +76,12 @@ Future main() async { autoStartDaemonClient: false, toolchain: toolchain, isolateClient: backend?.client, + onProjectOpen: backend != null + ? (path) => backend.openProject(path) + : null, + onValidateProject: backend != null + ? (path) => backend.validateProject(path) + : null, ); // Register every built-in. Tier 0 activates only the four that do @@ -118,25 +121,6 @@ Future main() async { await services.extensions.activateAll(); runApp(ClideApp(services: services)); - - // macOS merged thread: let several frames paint, then resolve in an - // isolate, then load the project. Both the delay AND the isolate are - // needed — synchronous file I/O freezes even after frames have painted, - // and isolate spawn freezes if done too early. - if (!kIsWeb) { - const workspace = String.fromEnvironment('CLIDE_WORKSPACE'); - final root = workspace.isNotEmpty ? workspace : Directory.current.path; - toolchain.applyResolved(resolveToolchainPaths(root)); - - await services.project.loadRecents(); - var opened = await services.project.openLast(); - if (!opened) { - opened = await services.project.open(Directory.current.path); - } - if (opened) { - services.panels.activateTab(Slots.workspace, 'claude.primary'); - } - } } /// Resolve the app-settings directory. diff --git a/lib/src/daemon/dispatcher.dart b/lib/src/daemon/dispatcher.dart index fffc1cfc..e6f43680 100644 --- a/lib/src/daemon/dispatcher.dart +++ b/lib/src/daemon/dispatcher.dart @@ -16,6 +16,13 @@ class DaemonDispatcher { _handlers[cmd] = handler; } + /// Remove all registered handlers except ping/version. + void clear() { + _handlers.removeWhere((k, _) => k != 'ping' && k != 'version'); + } + + bool get isEmpty => _handlers.length <= 2; // only ping + version + Future dispatch(IpcRequest req) async { final h = _handlers[req.cmd]; if (h == null) {