chore(plan): close T-438 (web fence done); file T-440 (Playwright e2e follow-up)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 17:57:49 +02:00
co-authored by Claude Opus 4.8
parent 03d053274e
commit ffcf17a136
4 changed files with 312 additions and 0 deletions
+147
View File
@@ -5455,3 +5455,150 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
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 ('06FCQ8HB61N3TWVJ8YSMHH2TJ4', 'status', 'in_progress', 'review', NULL, '2026-06-15 15:30:48', '2026-06-15 15:30:48', '2026-06-15 15:30:48', NULL, 'a7d81444607b6ec3aa8440fcbc9abee3', 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 ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'status', 'backlog', 'in_progress', NULL, '2026-06-15 15:31:15', '2026-06-15 15:31:15', '2026-06-15 15:31:15', NULL, '5fed82be61479c8191ed8d0663507cad', 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 ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'status', 'in_progress', 'in_progress', NULL, '2026-06-15 15:32:16', '2026-06-15 15:32:16', '2026-06-15 15:32:16', NULL, 'c1e48da490b71dea20c50a486600b643', 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 ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'description', '**Symptom (user, recurring).** When clide is launched from a desktop/dock launcher (not a terminal), it doesn''t see the normal interactive-shell `PATH`, so spawned tools and installed components go missing — pql, git, claude-invoked CLIs, etc. Launching from a terminal works (the shell PATH is inherited).
**Root cause.** A GUI/desktop-launched process inherits a minimal `PATH` (roughly `/usr/bin:/bin`) it never sources `~/.bashrc` / `~/.zprofile` / `/etc/profile.d` / brew shellenv, so `~/.local/bin`, `/opt/homebrew/bin`, and any user-customized dirs (nvm, pyenv, cargo, ) are absent. clide builds the environment for everything it spawns from `Platform.environment`, so that impoverished PATH propagates everywhere.
**Why it keeps happening divergent, partial PATH expansion.** There are THREE separate PATH-augmentation implementations, fixed inconsistently:
1. `lib/src/pty/env.dart` `expandedPath` **still macOS-only** (`if (!Platform.isMacOS) return base;`). Used by `lib/src/git/operations.dart:24` (git resolution) on Linux desktop-launch, git gets the raw PATH.
2. `lib/kernel/src/toolchain_paths.dart` `_expandedPath()` augments on Linux too (this is what **T-347** fixed, for pql).
3. `lib/kernel/src/cli_install.dart` `expandedPath(base, {macOS, home})` a third copy.
So T-347 fixed the *toolchain/pql* path on Linux, but the `env.dart` copy (git, PTY env defaults) is still macOS-only, and claude''s spawn (`agent_bootstrap.agentEnvDelta` `Process.start(environment:)` merged over `Platform.environment`) only prepends the clide-CLI dir the rest of PATH stays un-enriched. Net: the same class of breakage recurs per spawn site because there''s no single source of truth.
**Also:** all three use a **hardcoded dir list** (`~/.local/bin`, `/opt/homebrew/bin`, `/usr/local/bin`). That misses arbitrary user customizations (nvm/pyenv/cargo/asdf/custom dirs) the user''s "normal bash PATH" is whatever their login shell actually produces, not a fixed list.
**Proposed fix (two parts).**
1. *Robust resolution:* derive the real login-shell PATH once at startup spawn the user''s `$SHELL -l -i -c ''printf %s "$PATH"''` (or `-l -c` to avoid interactive side-effects), with a short timeout and a graceful fallback to the current hardcoded-merge behavior. Cache it for the process. This is the established approach (VS Code''s `resolveShellEnv`, the `fix-path` pattern) and captures the user''s actual PATH, not a guess.
2. *Consolidation:* collapse the three `expandedPath`/`_expandedPath` copies into ONE source of truth (e.g. in `lib/src/pty/env.dart` or a small `kernel` env service) that every spawn site uses PTY/terminal (`registry.dart:55` currently passes raw `Platform.environment`), git (`operations.dart`), toolchain (`toolchain_paths.dart`), claude (`agent_bootstrap.dart`), and any other `Process.start`. One resolver, applied everywhere.
**Acceptance.** Desktop-launched clide on Linux + macOS resolves the same PATH the user''s login shell has; pql/git/claude and PTY children all find user-installed tools; the three divergent expanders are unified into one; graceful fallback when the shell probe fails or times out; covered by a test for the resolver + the fallback.
**Related:** T-347 (done fixed the Linux toolchain/pql path, but only `toolchain_paths.dart`), T-215 (CLIDE_SOCK/CLIDE_WORKSPACE + clide on the child PATH), T-211/T-212 (clide-on-PATH install), D-59 (bundled git) / D-92 (bundled pql) bundling covers git/pql specifically, but not the general "user''s installed tools" PATH this addresses.', '**Symptom (user, recurring).** When clide is launched from a desktop/dock launcher (not a terminal), it doesn''t see the normal interactive-shell `PATH`, so spawned tools and installed components go missing pql, git, claude-invoked CLIs, etc. Launching from a terminal works (the shell PATH is inherited).
**Root cause.** A GUI/desktop-launched process inherits a minimal `PATH` (roughly `/usr/bin:/bin`) it never sources `~/.bashrc` / `~/.zprofile` / `/etc/profile.d` / brew shellenv, so `~/.local/bin`, `/opt/homebrew/bin`, and any user-customized dirs (nvm, pyenv, cargo, ) are absent. clide builds the environment for everything it spawns from `Platform.environment`, so that impoverished PATH propagates everywhere.
**Why it keeps happening divergent, partial PATH expansion.** There are THREE separate PATH-augmentation implementations, fixed inconsistently:
1. `lib/src/pty/env.dart` `expandedPath` **still macOS-only** (`if (!Platform.isMacOS) return base;`). Used by `lib/src/git/operations.dart:24` (git resolution) on Linux desktop-launch, git gets the raw PATH.
2. `lib/kernel/src/toolchain_paths.dart` `_expandedPath()` augments on Linux too (this is what **T-347** fixed, for pql).
3. `lib/kernel/src/cli_install.dart` `expandedPath(base, {macOS, home})` a third copy.
So T-347 fixed the *toolchain/pql* path on Linux, but the `env.dart` copy (git, PTY env defaults) is still macOS-only, and claude''s spawn (`agent_bootstrap.agentEnvDelta` `Process.start(environment:)` merged over `Platform.environment`) only prepends the clide-CLI dir the rest of PATH stays un-enriched. Net: the same class of breakage recurs per spawn site because there''s no single source of truth.
**Also:** all three use a **hardcoded dir list** (`~/.local/bin`, `/opt/homebrew/bin`, `/usr/local/bin`). That misses arbitrary user customizations (nvm/pyenv/cargo/asdf/custom dirs) the user''s "normal bash PATH" is whatever their login shell actually produces, not a fixed list.
**Proposed fix (two parts).**
1. *Robust resolution:* derive the real login-shell PATH once at startup spawn the user''s `$SHELL -l -i -c ''printf %s "$PATH"''` (or `-l -c` to avoid interactive side-effects), with a short timeout and a graceful fallback to the current hardcoded-merge behavior. Cache it for the process. This is the established approach (VS Code''s `resolveShellEnv`, the `fix-path` pattern) and captures the user''s actual PATH, not a guess.
2. *Consolidation:* collapse the three `expandedPath`/`_expandedPath` copies into ONE source of truth (e.g. in `lib/src/pty/env.dart` or a small `kernel` env service) that every spawn site uses PTY/terminal (`registry.dart:55` currently passes raw `Platform.environment`), git (`operations.dart`), toolchain (`toolchain_paths.dart`), claude (`agent_bootstrap.dart`), and any other `Process.start`. One resolver, applied everywhere.
**Acceptance.** Desktop-launched clide on Linux + macOS resolves the same PATH the user''s login shell has; pql/git/claude and PTY children all find user-installed tools; the three divergent expanders are unified into one; graceful fallback when the shell probe fails or times out; covered by a test for the resolver + the fallback.
**Related:** T-347 (done fixed the Linux toolchain/pql path, but only `toolchain_paths.dart`), T-215 (CLIDE_SOCK/CLIDE_WORKSPACE + clide on the child PATH), T-211/T-212 (clide-on-PATH install), D-59 (bundled git) / D-92 (bundled pql) bundling covers git/pql specifically, but not the general "user''s installed tools" PATH this addresses.
---
**Implemented 2026-06-15.** One shared resolver `lib/src/env/shell_env.dart`:
- `primeLoginShellPath()` probes `$SHELL -l -c` once at startup (sentinel-framed, 4s timeout, graceful fallback to the process PATH on Windows/empty-SHELL/non-zero/timeout/spawn-failure). Primed in `main.dart`''s `!kIsWeb` boot.
- `expandToolPath()` the canonical user/local-dir merge (moved from toolchain_paths, which re-exports it for its tests).
- `resolvedToolPath()` the single call every spawn site uses.
Routed through it: PTY children (`registry.dart` overrides PATH), git (`env.dart` `operations.dart`), the toolchain probe (`toolchain_paths.dart`), hosted claude (`agent_bootstrap.dart`). Deleted the macOS-only `env.dart` expander and the `cli_install.dart` copy three expanders one.
Verified: `flutter analyze` clean; new `shell_env_test.dart` covers the probe + every fallback + the merge; `env_test`/`cli_install_test` updated; `make test` green; `flutter build web --wasm` still green (resolver is web-safe, probe is `!kIsWeb`).
**Why `review`, not `done`:** logic + tests cover Linux and macOS, but the real *desktop/dock-launch* behavior (the actual GUI process inheriting a sparse PATH) can only be confirmed by launching a packaged build from the launcher recommend a quick live confirm (open clide from the dock, check pql/git/a `~/.local/bin` tool resolve). macOS Homebrew path is code-correct but unverified on a Mac.', NULL, '2026-06-15 15:47:48', '2026-06-15 15:47:48', '2026-06-15 15:47:48', NULL, 'feaf3593dabcb0b7d7681043dddddbb3', 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 ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'status', 'in_progress', 'review', NULL, '2026-06-15 15:47:49', '2026-06-15 15:47:49', '2026-06-15 15:47:49', NULL, '9b7ed7d84e12574200900604143e866c', 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 ('06FCQZ47MAN835B215GSSMRV8W', 'description', NULL, 'Follow-up from T-438 (the `dart:ffi` web fence, D-100): with `flutter build web --wasm` compiling again and a compile gate in CI, restore the *full* web-WASM Playwright e2e — the remaining slice of T-438''s acceptance.
**What''s already done (T-438).** The web build compiles; `.github/workflows/test.yml` has a `web-wasm` job that runs `flutter build web --wasm` (the anti-rot compile gate); `make ui-dev` (build + serve) works.
**What this ticket adds.** The actual browser e2e, which needs runner provisioning the compile gate doesn''t:
- `make test-e2e` / `make ui-smoke` run green locally and in CI (build serve `localhost:4280` Playwright smoke teardown).
- A GitHub Actions job: `setup-node`, `npm install` + `npx playwright install --with-deps` in `tools/ui/`, then `make test-e2e`. Add it to `.github/workflows/test.yml` (replacing the compile-only `web-wasm` job, or as a second job that `needs` it).
- Confirm the Playwright driver (D-26) still matches the current web entrypoint after the fence (degraded web build: no terminal/native-git/highlighting the smoke should assert what *does* render, e.g. the shell boots and a pane mounts).
**Acceptance.** `make test-e2e` and `make ui-smoke` pass locally; a CI job runs the Playwright smoke against the wasm build on every push/PR; D-26/D-32 reflect the restored e2e job.
**Refs:** T-438 (compile fence + gate), D-100 / Q-50 (fence decision), D-26 (Playwright driver), D-32 (CI; the withheld e2e job).', NULL, '2026-06-15 15:53:35', '2026-06-15 15:53:35', '2026-06-15 15:53:35', NULL, '14d8eb1f3bc639cb4ce3cddb5179a853', 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 ('06FCQ8HB61N3TWVJ8YSMHH2TJ4', 'description', 'Implements the D-100 fence decision (resolving Q-50): keep the web/WASM "happy accident" build compiling so the e2e/Playwright UI harness (D-26) can run again.
**Problem.** `flutter build web --wasm` cannot compile the tree — 12 modules import `dart:ffi` unconditionally, which the wasm target forbids. This kills `make test-e2e` / `ui-dev` / `ui-smoke` and the GitHub Actions web-WASM e2e job (currently withheld with a pointer to Q-50).
**dart:ffi importers to fence (as of 2026-06-15):**
- Native PTY: `lib/src/pty/ffi/libc.dart`, `lib/src/pty/native_pty.dart`, `lib/src/pty/windows_pty.dart`
- Tree-sitter: `lib/kernel/src/syntax/tree_sitter_ffi.dart`, `lib/kernel/src/syntax/tree_sitter_service.dart`
- Lua (Tier 6): `lib/lua/lua.dart`, `lib/lua/src/host.dart`
- Windows watchdog: `lib/kernel/src/watchdog_windows.dart`
- Consumers / barrels: `lib/clide.dart`, `lib/src/panes/pane.dart`, `lib/builtin/claude/src/agent_bootstrap.dart`, `lib/test_app.dart`
**Approach (per D-100).** Put each native capability behind a conditional-import facade `import ''x_native.dart'' if (dart.library.ffi) ''x_native.dart'' ... else ''x_stub.dart''` (io/ffi real impl; web stub). Web stubs degrade gracefully and never throw at import time: no PTY/terminal, no native git, no tree-sitter highlighting on web the web build is a UI/e2e surface, not a functional desktop replacement. Desktop builds keep the real FFI impls unchanged (no fidelity loss the CLAUDE.md guardrail holds).
**Acceptance.**
- `flutter build web --wasm` compiles the tree.
- `make test-e2e` / `ui-dev` / `ui-smoke` run again.
- A `flutter build web --wasm` compile gate is added to CI so the fence can''t silently rot, and the withheld web-WASM e2e job in `.github/workflows/test.yml` is re-enabled.
- Desktop unit/widget/golden/integration suites stay green; no desktop behavior change.
**Refs:** D-100, Q-50, D-32 (the withheld e2e job), D-26 (Playwright driver), T-384 (the dead-targets bug this finishes).
---
**Implemented 2026-06-15 dart:ffi fence done, web/WASM build compiles.**
`flutter build web --wasm` → `✓ Built build/web`. Desktop `flutter analyze` clean; `make test` (analyze + format + unit + widget + golden) green; UUID/socket-hash determinism unchanged.
**What was done.** Every `dart:ffi` importer now sits behind a `dart.library.ffi` conditional import with a graceful web stub (discriminator is `dart.library.ffi`, NOT `dart.library.io` dart2wasm provides `dart:io`, so only FFI is the blocker):
- PTY: `pty_session.dart` `pty_backend_io.dart` / `pty_backend_web.dart` (stub throws).
- tree-sitter: pure types extracted to `syntax_result.dart`; `tree_sitter_service.dart` is now a facade over `_ffi`/`_stub`; `tree_sitter_boot_io/stub.dart` fences `TreeSitterLib.init()` in main.dart. Web = no highlighting (plain text).
- watchdog: `watchdog_windows_stub.dart` (all-`-1` sampler).
- claude ABI probe: `native_abi_io/stub.dart` (replaces `dart:ffi show Abi`).
- testmode fd-check: `fd_check_io/stub.dart`.
- Two FFI-constructing tree-sitter tests now import `_ffi.dart` directly (the analyzer resolves the conditional facade to the stub branch).
**Second, distinct web-incompat fixed:** the dart2js fallback (built alongside wasm) rejected the 64-bit FNV literals (`0xcbf29ce484222325`) in `session_naming.dart` + `paths.dart` split into 32-bit halves; dropped a no-op `& 0xFFFFFFFFFFFFFFFF` mask. Desktop/wasm values unchanged.
**CI:** added a `web-wasm` job (`flutter build web --wasm`) to `.github/workflows/test.yml` so the fence can''t silently rot (replaces the withheld-job comment).
**Remaining (why this is `review`, not `done`):** the *compile* gate is in CI, but the full web-WASM **Playwright e2e** (`make test-e2e` / `ui-smoke`: setup-node + `playwright install` in `tools/ui` + serve) is not yet wired into CI it needs browser/node provisioning on the runner, separate from the fence. `make ui-dev` (build + serve) works now; the Playwright smoke is the follow-on. Acceptance items 1, 3a (compile gate), 4 met; 2/3b (full Playwright e2e) pending that harness wiring.', 'Implements the D-100 fence decision (resolving Q-50): keep the web/WASM "happy accident" build compiling so the e2e/Playwright UI harness (D-26) can run again.
**Problem.** `flutter build web --wasm` cannot compile the tree — 12 modules import `dart:ffi` unconditionally, which the wasm target forbids. This kills `make test-e2e` / `ui-dev` / `ui-smoke` and the GitHub Actions web-WASM e2e job (currently withheld with a pointer to Q-50).
**dart:ffi importers to fence (as of 2026-06-15):**
- Native PTY: `lib/src/pty/ffi/libc.dart`, `lib/src/pty/native_pty.dart`, `lib/src/pty/windows_pty.dart`
- Tree-sitter: `lib/kernel/src/syntax/tree_sitter_ffi.dart`, `lib/kernel/src/syntax/tree_sitter_service.dart`
- Lua (Tier 6): `lib/lua/lua.dart`, `lib/lua/src/host.dart`
- Windows watchdog: `lib/kernel/src/watchdog_windows.dart`
- Consumers / barrels: `lib/clide.dart`, `lib/src/panes/pane.dart`, `lib/builtin/claude/src/agent_bootstrap.dart`, `lib/test_app.dart`
**Approach (per D-100).** Put each native capability behind a conditional-import facade `import ''x_native.dart'' if (dart.library.ffi) ''x_native.dart'' ... else ''x_stub.dart''` (io/ffi real impl; web stub). Web stubs degrade gracefully and never throw at import time: no PTY/terminal, no native git, no tree-sitter highlighting on web the web build is a UI/e2e surface, not a functional desktop replacement. Desktop builds keep the real FFI impls unchanged (no fidelity loss the CLAUDE.md guardrail holds).
**Acceptance.**
- `flutter build web --wasm` compiles the tree.
- `make test-e2e` / `ui-dev` / `ui-smoke` run again.
- A `flutter build web --wasm` compile gate is added to CI so the fence can''t silently rot, and the withheld web-WASM e2e job in `.github/workflows/test.yml` is re-enabled.
- Desktop unit/widget/golden/integration suites stay green; no desktop behavior change.
**Refs:** D-100, Q-50, D-32 (the withheld e2e job), D-26 (Playwright driver), T-384 (the dead-targets bug this finishes).
---
**Implemented 2026-06-15 dart:ffi fence done, web/WASM build compiles.**
`flutter build web --wasm` → `✓ Built build/web`. Desktop `flutter analyze` clean; `make test` (analyze + format + unit + widget + golden) green; UUID/socket-hash determinism unchanged.
**What was done.** Every `dart:ffi` importer now sits behind a `dart.library.ffi` conditional import with a graceful web stub (discriminator is `dart.library.ffi`, NOT `dart.library.io` dart2wasm provides `dart:io`, so only FFI is the blocker):
- PTY: `pty_session.dart` `pty_backend_io.dart` / `pty_backend_web.dart` (stub throws).
- tree-sitter: pure types extracted to `syntax_result.dart`; `tree_sitter_service.dart` is now a facade over `_ffi`/`_stub`; `tree_sitter_boot_io/stub.dart` fences `TreeSitterLib.init()` in main.dart. Web = no highlighting (plain text).
- watchdog: `watchdog_windows_stub.dart` (all-`-1` sampler).
- claude ABI probe: `native_abi_io/stub.dart` (replaces `dart:ffi show Abi`).
- testmode fd-check: `fd_check_io/stub.dart`.
- Two FFI-constructing tree-sitter tests now import `_ffi.dart` directly (the analyzer resolves the conditional facade to the stub branch).
**Second, distinct web-incompat fixed:** the dart2js fallback (built alongside wasm) rejected the 64-bit FNV literals (`0xcbf29ce484222325`) in `session_naming.dart` + `paths.dart` split into 32-bit halves; dropped a no-op `& 0xFFFFFFFFFFFFFFFF` mask. Desktop/wasm values unchanged.
**CI:** added a `web-wasm` job (`flutter build web --wasm`) to `.github/workflows/test.yml` so the fence can''t silently rot (replaces the withheld-job comment).
**Remaining (why this is `review`, not `done`):** the *compile* gate is in CI, but the full web-WASM **Playwright e2e** (`make test-e2e` / `ui-smoke`: setup-node + `playwright install` in `tools/ui` + serve) is not yet wired into CI it needs browser/node provisioning on the runner, separate from the fence. `make ui-dev` (build + serve) works now; the Playwright smoke is the follow-on. Acceptance items 1, 3a (compile gate), 4 met; 2/3b (full Playwright e2e) pending that harness wiring.
Closed 2026-06-15: the dart:ffi fence + flutter build web --wasm compile gate are landed, verified (ca08c2a), and on origin/main. The remaining acceptance slice — the full web-WASM Playwright e2e harness wired into CI (make test-e2e / ui-smoke + the node/playwright runner job) — is split out to T-440, since it needs runner provisioning separate from the fence itself.', NULL, '2026-06-15 15:53:42', '2026-06-15 15:53:42', '2026-06-15 15:53:42', NULL, '0b238079758d9882647e0a2fcec7cbe7', 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 ('06FCQ8HB61N3TWVJ8YSMHH2TJ4', 'status', 'review', 'done', NULL, '2026-06-15 15:53:47', '2026-06-15 15:53:47', '2026-06-15 15:53:47', NULL, '799f878cdf6c8f6b9256fb852b4c4537', 2) ON CONFLICT(hash) DO NOTHING;
+1
View File
@@ -264,3 +264,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'T-437', '2026-06-15 11:12:36', '2026-06-15 11:12:36', NULL, '74dd08c8c42f556959746ee1a47561e6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQ8HB61N3TWVJ8YSMHH2TJ4', 'T-438', '2026-06-15 14:14:23', '2026-06-15 14:14:23', NULL, 'fa072ae8dd819784440bb44b5fe689d8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'T-439', '2026-06-15 14:55:15', '2026-06-15 14:55:15', NULL, '2427484ebb324d731cb8e099a2ad04ab', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQZ47MAN835B215GSSMRV8W', 'T-440', '2026-06-15 15:53:05', '2026-06-15 15:53:05', NULL, 'f219bb1a70eb1f49e30975ce8c526051', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
+1
View File
@@ -5,3 +5,4 @@ INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, dele
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQ8HB61N3TWVJ8YSMHH2TJ4', 'web', '2026-06-15 14:22:06', '2026-06-15 14:22:06', NULL, '6867c9766259a7f09e81ff515973914e', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'path', '2026-06-15 15:24:11', '2026-06-15 15:24:11', NULL, 'e73b3b85732a2c21be23353a0f7f1493', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'web', '2026-06-15 15:24:11', '2026-06-15 15:24:11', NULL, '8c93337891e74d67d2465ba2c3b901d5', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCQZ47MAN835B215GSSMRV8W', 'web', '2026-06-15 15:53:35', '2026-06-15 15:53:35', NULL, '4cbf5007b12087b88b8acb36f3616bf8', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
+163
View File
@@ -6254,3 +6254,166 @@ So T-347 fixed the *toolchain/pql* path on Linux, but the `env.dart` copy (git,
**Acceptance.** Desktop-launched clide on Linux + macOS resolves the same PATH the user''s login shell has; pql/git/claude and PTY children all find user-installed tools; the three divergent expanders are unified into one; graceful fallback when the shell probe fails or times out; covered by a test for the resolver + the fallback.
**Related:** T-347 (done fixed the Linux toolchain/pql path, but only `toolchain_paths.dart`), T-215 (CLIDE_SOCK/CLIDE_WORKSPACE + clide on the child PATH), T-211/T-212 (clide-on-PATH install), D-59 (bundled git) / D-92 (bundled pql) bundling covers git/pql specifically, but not the general "user''s installed tools" PATH this addresses.', 'in_progress', 'high', NULL, NULL, NULL, '2026-06-15 14:55:15', '2026-06-15 15:32:16', NULL, 'cd231fbbec9b6faa4659286c24893d30', 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 ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'bug', NULL, 'Consolidate PATH resolution: one login-shell-derived PATH for every spawned tool (git/pql/claude/PTY)', '**Symptom (user, recurring).** When clide is launched from a desktop/dock launcher (not a terminal), it doesn''t see the normal interactive-shell `PATH`, so spawned tools and installed components go missing — pql, git, claude-invoked CLIs, etc. Launching from a terminal works (the shell PATH is inherited).
**Root cause.** A GUI/desktop-launched process inherits a minimal `PATH` (roughly `/usr/bin:/bin`) it never sources `~/.bashrc` / `~/.zprofile` / `/etc/profile.d` / brew shellenv, so `~/.local/bin`, `/opt/homebrew/bin`, and any user-customized dirs (nvm, pyenv, cargo, ) are absent. clide builds the environment for everything it spawns from `Platform.environment`, so that impoverished PATH propagates everywhere.
**Why it keeps happening divergent, partial PATH expansion.** There are THREE separate PATH-augmentation implementations, fixed inconsistently:
1. `lib/src/pty/env.dart` `expandedPath` **still macOS-only** (`if (!Platform.isMacOS) return base;`). Used by `lib/src/git/operations.dart:24` (git resolution) on Linux desktop-launch, git gets the raw PATH.
2. `lib/kernel/src/toolchain_paths.dart` `_expandedPath()` augments on Linux too (this is what **T-347** fixed, for pql).
3. `lib/kernel/src/cli_install.dart` `expandedPath(base, {macOS, home})` a third copy.
So T-347 fixed the *toolchain/pql* path on Linux, but the `env.dart` copy (git, PTY env defaults) is still macOS-only, and claude''s spawn (`agent_bootstrap.agentEnvDelta` `Process.start(environment:)` merged over `Platform.environment`) only prepends the clide-CLI dir the rest of PATH stays un-enriched. Net: the same class of breakage recurs per spawn site because there''s no single source of truth.
**Also:** all three use a **hardcoded dir list** (`~/.local/bin`, `/opt/homebrew/bin`, `/usr/local/bin`). That misses arbitrary user customizations (nvm/pyenv/cargo/asdf/custom dirs) the user''s "normal bash PATH" is whatever their login shell actually produces, not a fixed list.
**Proposed fix (two parts).**
1. *Robust resolution:* derive the real login-shell PATH once at startup spawn the user''s `$SHELL -l -i -c ''printf %s "$PATH"''` (or `-l -c` to avoid interactive side-effects), with a short timeout and a graceful fallback to the current hardcoded-merge behavior. Cache it for the process. This is the established approach (VS Code''s `resolveShellEnv`, the `fix-path` pattern) and captures the user''s actual PATH, not a guess.
2. *Consolidation:* collapse the three `expandedPath`/`_expandedPath` copies into ONE source of truth (e.g. in `lib/src/pty/env.dart` or a small `kernel` env service) that every spawn site uses PTY/terminal (`registry.dart:55` currently passes raw `Platform.environment`), git (`operations.dart`), toolchain (`toolchain_paths.dart`), claude (`agent_bootstrap.dart`), and any other `Process.start`. One resolver, applied everywhere.
**Acceptance.** Desktop-launched clide on Linux + macOS resolves the same PATH the user''s login shell has; pql/git/claude and PTY children all find user-installed tools; the three divergent expanders are unified into one; graceful fallback when the shell probe fails or times out; covered by a test for the resolver + the fallback.
**Related:** T-347 (done fixed the Linux toolchain/pql path, but only `toolchain_paths.dart`), T-215 (CLIDE_SOCK/CLIDE_WORKSPACE + clide on the child PATH), T-211/T-212 (clide-on-PATH install), D-59 (bundled git) / D-92 (bundled pql) bundling covers git/pql specifically, but not the general "user''s installed tools" PATH this addresses.
---
**Implemented 2026-06-15.** One shared resolver `lib/src/env/shell_env.dart`:
- `primeLoginShellPath()` probes `$SHELL -l -c` once at startup (sentinel-framed, 4s timeout, graceful fallback to the process PATH on Windows/empty-SHELL/non-zero/timeout/spawn-failure). Primed in `main.dart`''s `!kIsWeb` boot.
- `expandToolPath()` the canonical user/local-dir merge (moved from toolchain_paths, which re-exports it for its tests).
- `resolvedToolPath()` the single call every spawn site uses.
Routed through it: PTY children (`registry.dart` overrides PATH), git (`env.dart` `operations.dart`), the toolchain probe (`toolchain_paths.dart`), hosted claude (`agent_bootstrap.dart`). Deleted the macOS-only `env.dart` expander and the `cli_install.dart` copy three expanders one.
Verified: `flutter analyze` clean; new `shell_env_test.dart` covers the probe + every fallback + the merge; `env_test`/`cli_install_test` updated; `make test` green; `flutter build web --wasm` still green (resolver is web-safe, probe is `!kIsWeb`).
**Why `review`, not `done`:** logic + tests cover Linux and macOS, but the real *desktop/dock-launch* behavior (the actual GUI process inheriting a sparse PATH) can only be confirmed by launching a packaged build from the launcher recommend a quick live confirm (open clide from the dock, check pql/git/a `~/.local/bin` tool resolve). macOS Homebrew path is code-correct but unverified on a Mac.', 'in_progress', 'high', NULL, NULL, NULL, '2026-06-15 14:55:15', '2026-06-15 15:47:48', NULL, 'e21be607f5fd0f3e0a6ece0f2205f7c3', 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 ('06FCQHWQ40AY6SNVRJ86YWA0J8', 'bug', NULL, 'Consolidate PATH resolution: one login-shell-derived PATH for every spawned tool (git/pql/claude/PTY)', '**Symptom (user, recurring).** When clide is launched from a desktop/dock launcher (not a terminal), it doesn''t see the normal interactive-shell `PATH`, so spawned tools and installed components go missing — pql, git, claude-invoked CLIs, etc. Launching from a terminal works (the shell PATH is inherited).
**Root cause.** A GUI/desktop-launched process inherits a minimal `PATH` (roughly `/usr/bin:/bin`) it never sources `~/.bashrc` / `~/.zprofile` / `/etc/profile.d` / brew shellenv, so `~/.local/bin`, `/opt/homebrew/bin`, and any user-customized dirs (nvm, pyenv, cargo, ) are absent. clide builds the environment for everything it spawns from `Platform.environment`, so that impoverished PATH propagates everywhere.
**Why it keeps happening divergent, partial PATH expansion.** There are THREE separate PATH-augmentation implementations, fixed inconsistently:
1. `lib/src/pty/env.dart` `expandedPath` **still macOS-only** (`if (!Platform.isMacOS) return base;`). Used by `lib/src/git/operations.dart:24` (git resolution) on Linux desktop-launch, git gets the raw PATH.
2. `lib/kernel/src/toolchain_paths.dart` `_expandedPath()` augments on Linux too (this is what **T-347** fixed, for pql).
3. `lib/kernel/src/cli_install.dart` `expandedPath(base, {macOS, home})` a third copy.
So T-347 fixed the *toolchain/pql* path on Linux, but the `env.dart` copy (git, PTY env defaults) is still macOS-only, and claude''s spawn (`agent_bootstrap.agentEnvDelta` `Process.start(environment:)` merged over `Platform.environment`) only prepends the clide-CLI dir the rest of PATH stays un-enriched. Net: the same class of breakage recurs per spawn site because there''s no single source of truth.
**Also:** all three use a **hardcoded dir list** (`~/.local/bin`, `/opt/homebrew/bin`, `/usr/local/bin`). That misses arbitrary user customizations (nvm/pyenv/cargo/asdf/custom dirs) the user''s "normal bash PATH" is whatever their login shell actually produces, not a fixed list.
**Proposed fix (two parts).**
1. *Robust resolution:* derive the real login-shell PATH once at startup spawn the user''s `$SHELL -l -i -c ''printf %s "$PATH"''` (or `-l -c` to avoid interactive side-effects), with a short timeout and a graceful fallback to the current hardcoded-merge behavior. Cache it for the process. This is the established approach (VS Code''s `resolveShellEnv`, the `fix-path` pattern) and captures the user''s actual PATH, not a guess.
2. *Consolidation:* collapse the three `expandedPath`/`_expandedPath` copies into ONE source of truth (e.g. in `lib/src/pty/env.dart` or a small `kernel` env service) that every spawn site uses PTY/terminal (`registry.dart:55` currently passes raw `Platform.environment`), git (`operations.dart`), toolchain (`toolchain_paths.dart`), claude (`agent_bootstrap.dart`), and any other `Process.start`. One resolver, applied everywhere.
**Acceptance.** Desktop-launched clide on Linux + macOS resolves the same PATH the user''s login shell has; pql/git/claude and PTY children all find user-installed tools; the three divergent expanders are unified into one; graceful fallback when the shell probe fails or times out; covered by a test for the resolver + the fallback.
**Related:** T-347 (done fixed the Linux toolchain/pql path, but only `toolchain_paths.dart`), T-215 (CLIDE_SOCK/CLIDE_WORKSPACE + clide on the child PATH), T-211/T-212 (clide-on-PATH install), D-59 (bundled git) / D-92 (bundled pql) bundling covers git/pql specifically, but not the general "user''s installed tools" PATH this addresses.
---
**Implemented 2026-06-15.** One shared resolver `lib/src/env/shell_env.dart`:
- `primeLoginShellPath()` probes `$SHELL -l -c` once at startup (sentinel-framed, 4s timeout, graceful fallback to the process PATH on Windows/empty-SHELL/non-zero/timeout/spawn-failure). Primed in `main.dart`''s `!kIsWeb` boot.
- `expandToolPath()` the canonical user/local-dir merge (moved from toolchain_paths, which re-exports it for its tests).
- `resolvedToolPath()` the single call every spawn site uses.
Routed through it: PTY children (`registry.dart` overrides PATH), git (`env.dart` `operations.dart`), the toolchain probe (`toolchain_paths.dart`), hosted claude (`agent_bootstrap.dart`). Deleted the macOS-only `env.dart` expander and the `cli_install.dart` copy three expanders one.
Verified: `flutter analyze` clean; new `shell_env_test.dart` covers the probe + every fallback + the merge; `env_test`/`cli_install_test` updated; `make test` green; `flutter build web --wasm` still green (resolver is web-safe, probe is `!kIsWeb`).
**Why `review`, not `done`:** logic + tests cover Linux and macOS, but the real *desktop/dock-launch* behavior (the actual GUI process inheriting a sparse PATH) can only be confirmed by launching a packaged build from the launcher recommend a quick live confirm (open clide from the dock, check pql/git/a `~/.local/bin` tool resolve). macOS Homebrew path is code-correct but unverified on a Mac.', 'review', 'high', NULL, NULL, NULL, '2026-06-15 14:55:15', '2026-06-15 15:47:49', NULL, '7697fe6f58f4c57b24274f694db93f63', 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 ('06FCQZ47MAN835B215GSSMRV8W', 'task', NULL, 'Wire the web-WASM Playwright e2e back into CI (make test-e2e / ui-smoke)', NULL, 'backlog', 'medium', NULL, NULL, 'D-26', '2026-06-15 15:53:05', '2026-06-15 15:53:05', NULL, 'b6f52c84ffc9a6da071f09294af097fd', 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 ('06FCQZ47MAN835B215GSSMRV8W', 'task', NULL, 'Wire the web-WASM Playwright e2e back into CI (make test-e2e / ui-smoke)', 'Follow-up from T-438 (the `dart:ffi` web fence, D-100): with `flutter build web --wasm` compiling again and a compile gate in CI, restore the *full* web-WASM Playwright e2e — the remaining slice of T-438''s acceptance.
**What''s already done (T-438).** The web build compiles; `.github/workflows/test.yml` has a `web-wasm` job that runs `flutter build web --wasm` (the anti-rot compile gate); `make ui-dev` (build + serve) works.
**What this ticket adds.** The actual browser e2e, which needs runner provisioning the compile gate doesn''t:
- `make test-e2e` / `make ui-smoke` run green locally and in CI (build serve `localhost:4280` Playwright smoke teardown).
- A GitHub Actions job: `setup-node`, `npm install` + `npx playwright install --with-deps` in `tools/ui/`, then `make test-e2e`. Add it to `.github/workflows/test.yml` (replacing the compile-only `web-wasm` job, or as a second job that `needs` it).
- Confirm the Playwright driver (D-26) still matches the current web entrypoint after the fence (degraded web build: no terminal/native-git/highlighting the smoke should assert what *does* render, e.g. the shell boots and a pane mounts).
**Acceptance.** `make test-e2e` and `make ui-smoke` pass locally; a CI job runs the Playwright smoke against the wasm build on every push/PR; D-26/D-32 reflect the restored e2e job.
**Refs:** T-438 (compile fence + gate), D-100 / Q-50 (fence decision), D-26 (Playwright driver), D-32 (CI; the withheld e2e job).', 'backlog', 'medium', NULL, NULL, 'D-26', '2026-06-15 15:53:05', '2026-06-15 15:53:35', NULL, '8a5cf1cc216e86457c8c9f5ececcb489', 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 ('06FCQ8HB61N3TWVJ8YSMHH2TJ4', 'task', NULL, 'Implement D-100: fence dart:ffi behind web stubs so flutter build web --wasm compiles; restore e2e/ui targets', 'Implements the D-100 fence decision (resolving Q-50): keep the web/WASM "happy accident" build compiling so the e2e/Playwright UI harness (D-26) can run again.
**Problem.** `flutter build web --wasm` cannot compile the tree — 12 modules import `dart:ffi` unconditionally, which the wasm target forbids. This kills `make test-e2e` / `ui-dev` / `ui-smoke` and the GitHub Actions web-WASM e2e job (currently withheld with a pointer to Q-50).
**dart:ffi importers to fence (as of 2026-06-15):**
- Native PTY: `lib/src/pty/ffi/libc.dart`, `lib/src/pty/native_pty.dart`, `lib/src/pty/windows_pty.dart`
- Tree-sitter: `lib/kernel/src/syntax/tree_sitter_ffi.dart`, `lib/kernel/src/syntax/tree_sitter_service.dart`
- Lua (Tier 6): `lib/lua/lua.dart`, `lib/lua/src/host.dart`
- Windows watchdog: `lib/kernel/src/watchdog_windows.dart`
- Consumers / barrels: `lib/clide.dart`, `lib/src/panes/pane.dart`, `lib/builtin/claude/src/agent_bootstrap.dart`, `lib/test_app.dart`
**Approach (per D-100).** Put each native capability behind a conditional-import facade `import ''x_native.dart'' if (dart.library.ffi) ''x_native.dart'' ... else ''x_stub.dart''` (io/ffi real impl; web stub). Web stubs degrade gracefully and never throw at import time: no PTY/terminal, no native git, no tree-sitter highlighting on web the web build is a UI/e2e surface, not a functional desktop replacement. Desktop builds keep the real FFI impls unchanged (no fidelity loss the CLAUDE.md guardrail holds).
**Acceptance.**
- `flutter build web --wasm` compiles the tree.
- `make test-e2e` / `ui-dev` / `ui-smoke` run again.
- A `flutter build web --wasm` compile gate is added to CI so the fence can''t silently rot, and the withheld web-WASM e2e job in `.github/workflows/test.yml` is re-enabled.
- Desktop unit/widget/golden/integration suites stay green; no desktop behavior change.
**Refs:** D-100, Q-50, D-32 (the withheld e2e job), D-26 (Playwright driver), T-384 (the dead-targets bug this finishes).
---
**Implemented 2026-06-15 dart:ffi fence done, web/WASM build compiles.**
`flutter build web --wasm` → `✓ Built build/web`. Desktop `flutter analyze` clean; `make test` (analyze + format + unit + widget + golden) green; UUID/socket-hash determinism unchanged.
**What was done.** Every `dart:ffi` importer now sits behind a `dart.library.ffi` conditional import with a graceful web stub (discriminator is `dart.library.ffi`, NOT `dart.library.io` dart2wasm provides `dart:io`, so only FFI is the blocker):
- PTY: `pty_session.dart` `pty_backend_io.dart` / `pty_backend_web.dart` (stub throws).
- tree-sitter: pure types extracted to `syntax_result.dart`; `tree_sitter_service.dart` is now a facade over `_ffi`/`_stub`; `tree_sitter_boot_io/stub.dart` fences `TreeSitterLib.init()` in main.dart. Web = no highlighting (plain text).
- watchdog: `watchdog_windows_stub.dart` (all-`-1` sampler).
- claude ABI probe: `native_abi_io/stub.dart` (replaces `dart:ffi show Abi`).
- testmode fd-check: `fd_check_io/stub.dart`.
- Two FFI-constructing tree-sitter tests now import `_ffi.dart` directly (the analyzer resolves the conditional facade to the stub branch).
**Second, distinct web-incompat fixed:** the dart2js fallback (built alongside wasm) rejected the 64-bit FNV literals (`0xcbf29ce484222325`) in `session_naming.dart` + `paths.dart` split into 32-bit halves; dropped a no-op `& 0xFFFFFFFFFFFFFFFF` mask. Desktop/wasm values unchanged.
**CI:** added a `web-wasm` job (`flutter build web --wasm`) to `.github/workflows/test.yml` so the fence can''t silently rot (replaces the withheld-job comment).
**Remaining (why this is `review`, not `done`):** the *compile* gate is in CI, but the full web-WASM **Playwright e2e** (`make test-e2e` / `ui-smoke`: setup-node + `playwright install` in `tools/ui` + serve) is not yet wired into CI it needs browser/node provisioning on the runner, separate from the fence. `make ui-dev` (build + serve) works now; the Playwright smoke is the follow-on. Acceptance items 1, 3a (compile gate), 4 met; 2/3b (full Playwright e2e) pending that harness wiring.
Closed 2026-06-15: the dart:ffi fence + flutter build web --wasm compile gate are landed, verified (ca08c2a), and on origin/main. The remaining acceptance slice — the full web-WASM Playwright e2e harness wired into CI (make test-e2e / ui-smoke + the node/playwright runner job) — is split out to T-440, since it needs runner provisioning separate from the fence itself.', 'review', 'medium', NULL, NULL, 'D-100', '2026-06-15 14:14:23', '2026-06-15 15:53:42', NULL, 'afa700dde7efb5f6cf7cda07fb50643b', 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 ('06FCQ8HB61N3TWVJ8YSMHH2TJ4', 'task', NULL, 'Implement D-100: fence dart:ffi behind web stubs so flutter build web --wasm compiles; restore e2e/ui targets', 'Implements the D-100 fence decision (resolving Q-50): keep the web/WASM "happy accident" build compiling so the e2e/Playwright UI harness (D-26) can run again.
**Problem.** `flutter build web --wasm` cannot compile the tree — 12 modules import `dart:ffi` unconditionally, which the wasm target forbids. This kills `make test-e2e` / `ui-dev` / `ui-smoke` and the GitHub Actions web-WASM e2e job (currently withheld with a pointer to Q-50).
**dart:ffi importers to fence (as of 2026-06-15):**
- Native PTY: `lib/src/pty/ffi/libc.dart`, `lib/src/pty/native_pty.dart`, `lib/src/pty/windows_pty.dart`
- Tree-sitter: `lib/kernel/src/syntax/tree_sitter_ffi.dart`, `lib/kernel/src/syntax/tree_sitter_service.dart`
- Lua (Tier 6): `lib/lua/lua.dart`, `lib/lua/src/host.dart`
- Windows watchdog: `lib/kernel/src/watchdog_windows.dart`
- Consumers / barrels: `lib/clide.dart`, `lib/src/panes/pane.dart`, `lib/builtin/claude/src/agent_bootstrap.dart`, `lib/test_app.dart`
**Approach (per D-100).** Put each native capability behind a conditional-import facade `import ''x_native.dart'' if (dart.library.ffi) ''x_native.dart'' ... else ''x_stub.dart''` (io/ffi real impl; web stub). Web stubs degrade gracefully and never throw at import time: no PTY/terminal, no native git, no tree-sitter highlighting on web the web build is a UI/e2e surface, not a functional desktop replacement. Desktop builds keep the real FFI impls unchanged (no fidelity loss the CLAUDE.md guardrail holds).
**Acceptance.**
- `flutter build web --wasm` compiles the tree.
- `make test-e2e` / `ui-dev` / `ui-smoke` run again.
- A `flutter build web --wasm` compile gate is added to CI so the fence can''t silently rot, and the withheld web-WASM e2e job in `.github/workflows/test.yml` is re-enabled.
- Desktop unit/widget/golden/integration suites stay green; no desktop behavior change.
**Refs:** D-100, Q-50, D-32 (the withheld e2e job), D-26 (Playwright driver), T-384 (the dead-targets bug this finishes).
---
**Implemented 2026-06-15 dart:ffi fence done, web/WASM build compiles.**
`flutter build web --wasm` → `✓ Built build/web`. Desktop `flutter analyze` clean; `make test` (analyze + format + unit + widget + golden) green; UUID/socket-hash determinism unchanged.
**What was done.** Every `dart:ffi` importer now sits behind a `dart.library.ffi` conditional import with a graceful web stub (discriminator is `dart.library.ffi`, NOT `dart.library.io` dart2wasm provides `dart:io`, so only FFI is the blocker):
- PTY: `pty_session.dart` `pty_backend_io.dart` / `pty_backend_web.dart` (stub throws).
- tree-sitter: pure types extracted to `syntax_result.dart`; `tree_sitter_service.dart` is now a facade over `_ffi`/`_stub`; `tree_sitter_boot_io/stub.dart` fences `TreeSitterLib.init()` in main.dart. Web = no highlighting (plain text).
- watchdog: `watchdog_windows_stub.dart` (all-`-1` sampler).
- claude ABI probe: `native_abi_io/stub.dart` (replaces `dart:ffi show Abi`).
- testmode fd-check: `fd_check_io/stub.dart`.
- Two FFI-constructing tree-sitter tests now import `_ffi.dart` directly (the analyzer resolves the conditional facade to the stub branch).
**Second, distinct web-incompat fixed:** the dart2js fallback (built alongside wasm) rejected the 64-bit FNV literals (`0xcbf29ce484222325`) in `session_naming.dart` + `paths.dart` split into 32-bit halves; dropped a no-op `& 0xFFFFFFFFFFFFFFFF` mask. Desktop/wasm values unchanged.
**CI:** added a `web-wasm` job (`flutter build web --wasm`) to `.github/workflows/test.yml` so the fence can''t silently rot (replaces the withheld-job comment).
**Remaining (why this is `review`, not `done`):** the *compile* gate is in CI, but the full web-WASM **Playwright e2e** (`make test-e2e` / `ui-smoke`: setup-node + `playwright install` in `tools/ui` + serve) is not yet wired into CI it needs browser/node provisioning on the runner, separate from the fence. `make ui-dev` (build + serve) works now; the Playwright smoke is the follow-on. Acceptance items 1, 3a (compile gate), 4 met; 2/3b (full Playwright e2e) pending that harness wiring.
Closed 2026-06-15: the dart:ffi fence + flutter build web --wasm compile gate are landed, verified (ca08c2a), and on origin/main. The remaining acceptance slice — the full web-WASM Playwright e2e harness wired into CI (make test-e2e / ui-smoke + the node/playwright runner job) — is split out to T-440, since it needs runner provisioning separate from the fence itself.', 'done', 'medium', NULL, NULL, 'D-100', '2026-06-15 14:14:23', '2026-06-15 15:53:47', NULL, '6f5194e9f8cfde23da877f4502b5445d', 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);