authorgravatar for 269035359+t3-code[bot]@users.noreply.github.comt3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> 2026-08-20 23:05:53-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-20 23:05:53-04:00
log9167622a4ec1e540938adbb1dec70d255fdeb041
tree6e8c4cfbe2121b21210e59186fdd4b0ba400becf
parent12c497083e6ed2d6f90cc7775b5ecea232926b4a
signature Signed by PGP key B5690EEEBB952194

chore: move implementation plans out of repository (#7665)

Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: Alex Southwell <4596216+saphid@users.noreply.github.com>

39 files changed, 39 insertions(+), 5190 deletions(-)

.gitignore+1
...@@ -25,6 +25,7 @@ __screenshots__/...@@ -25,6 +25,7 @@ __screenshots__/
25squashfs-root/25squashfs-root/
26.vercel26.vercel
27.gstack/27.gstack/
28.plans/
28dist-electron/29dist-electron/
29.electron-runtime/30.electron-runtime/
30.showcase/31.showcase/
.plans/01-shared-model-normalization.md deleted-49
...@@ -1,49 +0,0 @@
1# Plan: Centralize Model Normalization in Contracts
2
3## Summary
4
5Move model alias/default normalization into `packages/contracts` so desktop and renderer use one shared source of truth.
6
7## Motivation
8
9- Removes duplicated logic between:
10 - `apps/desktop/src/codexAppServerManager.ts`
11 - `apps/renderer/src/model-logic.ts`
12- Prevents behavior drift when model aliases/defaults are updated.
13
14## Scope
15
16- Add shared model utilities to contracts.
17- Update desktop and renderer to consume shared utilities.
18- Keep renderer-specific display options in renderer.
19
20## Proposed Changes
21
221. Add `packages/contracts/src/model.ts` with:
23 - Canonical model list
24 - Alias map
25 - `normalizeModelSlug`
26 - `resolveModelSlug`
27 - `DEFAULT_MODEL`
282. Export model utilities from `packages/contracts/src/index.ts`.
293. Update `apps/desktop/src/codexAppServerManager.ts` to replace local alias map/helper.
304. Update `apps/renderer/src/model-logic.ts` to wrap or re-export shared functions.
315. Update tests:
32 - Move/duplicate normalization tests to contracts.
33 - Keep renderer tests focused on renderer-only behavior.
34
35## Risks
36
37- Desktop/renderer may currently rely on slightly different fallback behavior.
38- Import graph must avoid bundling issues for Electron main/preload.
39
40## Validation
41
42- `bun run test`
43- `bun run typecheck`
44- Manual check that model selection and session start still send expected model slug.
45
46## Done Criteria
47
48- No duplicated alias/default map in desktop and renderer.
49- Shared model utilities are contract-tested.
.plans/02-typed-ipc-boundaries.md deleted-44
...@@ -1,44 +0,0 @@
1# Plan: Strengthen Typed IPC Boundaries in Main Process
2
3## Summary
4
5Replace loose payload casting in IPC handlers with strict schema parsing and typed helper wrappers.
6
7## Motivation
8
9- `apps/desktop/src/main.ts` currently uses casts like `payload as Parameters<...>`.
10- Casts can hide contract breakages until runtime.
11
12## Scope
13
14- Desktop main process IPC registration.
15- Optional shared helper for handler registration.
16
17## Proposed Changes
18
191. Add IPC helper utility (e.g. `apps/desktop/src/ipcHelpers.ts`) to:
20 - Parse payload(s) with Zod schemas
21 - Standardize typed handler signatures
222. Refactor provider IPC handlers in `apps/desktop/src/main.ts` to use:
23 - `providerSessionStartInputSchema.parse`
24 - `providerSendTurnInputSchema.parse`
25 - `providerInterruptTurnInputSchema.parse`
26 - `providerStopSessionInputSchema.parse`
273. Apply same pattern to agent/terminal handlers where possible.
284. Add tests for handler parsing failure paths (invalid payloads).
29
30## Risks
31
32- Refactor can subtly change IPC error shape/messages.
33- Helper abstraction should stay simple and not obscure control flow.
34
35## Validation
36
37- `bun run test`
38- `bun run typecheck`
39- Manual invalid payload check from renderer/devtools to confirm fast failure.
40
41## Done Criteria
42
43- No provider handler uses `payload as Parameters<...>`.
44- All IPC entrypoints parse unknown payloads at boundary.
.plans/03-split-codex-app-server-manager.md deleted-48
...@@ -1,48 +0,0 @@
1# Plan: Decompose CodexAppServerManager
2
3## Summary
4
5Split `CodexAppServerManager` into smaller modules with clear responsibilities.
6
7## Motivation
8
9- `apps/desktop/src/codexAppServerManager.ts` is large and mixes:
10 - Process lifecycle
11 - JSON-RPC parsing/routing
12 - Session state transitions
13 - Event emission
14- This increases regression risk and slows changes.
15
16## Scope
17
18- Desktop provider internals only.
19- Keep external behavior/API stable.
20
21## Proposed Changes
22
231. Extract modules:
24 - `codex/processLifecycle.ts`
25 - `codex/jsonrpcRouter.ts`
26 - `codex/sessionState.ts`
27 - `codex/parsing.ts`
282. Keep `CodexAppServerManager` as thin orchestrator/facade.
293. Move pure helpers (`classifyCodexStderrLine`, route parsing) into unit-testable files.
304. Add targeted unit tests for:
31 - Message classification
32 - Request/notification/response routing
33 - Session state transitions
34
35## Risks
36
37- Reordering event handling can change behavior.
38- Must preserve pending request timeout/cancellation semantics.
39
40## Validation
41
42- Existing tests pass.
43- Add module-level tests for parsing and transition logic.
44
45## Done Criteria
46
47- Main manager file materially smaller and orchestration-focused.
48- Core protocol/state logic covered by focused tests.
.plans/04-split-chatview-component.md deleted-47
...@@ -1,47 +0,0 @@
1# Plan: Split ChatView into Smaller UI/Logic Units
2
3## Summary
4
5Refactor `ChatView.tsx` into composable pieces with isolated responsibilities.
6
7## Motivation
8
9- `apps/renderer/src/components/ChatView.tsx` is large and handles:
10 - Session orchestration
11 - Send/interrupt actions
12 - Timeline rendering
13 - Header/status UI
14 - Composer UI
15- Hard to test and maintain as one component.
16
17## Scope
18
19- Renderer component boundaries and hooks.
20- Keep visual behavior unchanged.
21
22## Proposed Changes
23
241. Create hook: `apps/renderer/src/hooks/useChatSession.ts`
25 - `ensureSession`
26 - `sendTurn`
27 - `interruptTurn`
282. Split presentational components:
29 - `components/chat/ThreadHeader.tsx`
30 - `components/chat/MessageTimeline.tsx`
31 - `components/chat/ComposerBar.tsx`
323. Keep `ChatView.tsx` as container wiring store + hook + child components.
334. Add focused tests for hook behavior (error handling, session reuse).
34
35## Risks
36
37- Refactor can break subtle UI interactions (auto-scroll, menu close, keyboard send).
38
39## Validation
40
41- `bun run test`
42- Manual smoke: send, stream, interrupt, model switch.
43
44## Done Criteria
45
46- `ChatView.tsx` significantly reduced and easier to scan.
47- Session logic isolated from rendering.
.plans/05-zod-persisted-state-validation.md deleted-41
...@@ -1,41 +0,0 @@
1# Plan: Move Renderer Persisted-State Validation to Zod
2
3## Summary
4
5Use explicit Zod schemas for localStorage state parsing and migration.
6
7## Motivation
8
9- `apps/renderer/src/store.ts` has large manual sanitize functions.
10- Manual type guards are verbose and easier to get wrong during schema evolution.
11
12## Scope
13
14- Renderer state hydration/persistence path.
15- No backend/protocol changes.
16
17## Proposed Changes
18
191. Add schema module: `apps/renderer/src/persistenceSchema.ts`
20 - Persisted payload versions (`v1`, `v2`)
21 - Thread/message/project schemas
222. Replace `sanitizeProjects/sanitizeThreads/sanitizeMessages` with schema parsing + transforms.
233. Keep migration logic explicit (legacy model migration and key migration).
244. Add tests for:
25 - Invalid payload fallback to initial state
26 - Legacy payload migration
27 - Unknown thread/project references filtered
28
29## Risks
30
31- Overly strict schemas could drop valid historical data unexpectedly.
32
33## Validation
34
35- Unit tests for migration/hydration.
36- Manual reload test with existing localStorage data.
37
38## Done Criteria
39
40- Store hydration logic is schema-driven.
41- Migration behavior is tested and documented.
.plans/06-provider-logstream-lifecycle.md deleted-38
...@@ -1,38 +0,0 @@
1# Plan: Add Provider Log Stream Lifecycle Management
2
3## Summary
4
5Ensure `ProviderManager` logging stream is initialized, rotated/structured, and closed safely.
6
7## Motivation
8
9- `apps/desktop/src/providerManager.ts` opens a write stream in constructor.
10- Stream lifecycle is not explicit on shutdown.
11
12## Scope
13
14- Desktop provider logging behavior.
15- App shutdown integration.
16
17## Proposed Changes
18
191. Add explicit `dispose()` on `ProviderManager`:
20 - Remove event listeners
21 - End/close log stream
222. Call `providerManager.dispose()` from app shutdown path in `apps/desktop/src/main.ts`.
233. Optional: change log format to JSON lines with stable fields.
244. Optional: per-session log files under `.logs/providers/`.
25
26## Risks
27
28- Improper close sequencing may lose final log lines.
29
30## Validation
31
32- Manual run/quit cycle to ensure no open handle warnings.
33- Confirm logs flush on quit and file descriptors are not leaked.
34
35## Done Criteria
36
37- ProviderManager owns complete log stream lifecycle.
38- Shutdown path explicitly disposes provider resources.
.plans/07-ci-quality-gates.md deleted-41
...@@ -1,41 +0,0 @@
1# Plan: Add CI Workflow for Core Quality Gates
2
3## Summary
4
5Add GitHub Actions workflow to run lint/typecheck/test (and optionally smoke-test) on pushes and PRs.
6
7## Motivation
8
9- Repository currently has no CI workflow files.
10- Quality checks are only local/manual.
11
12## Scope
13
14- `.github/workflows/ci.yml`
15- Bun + Turbo setup in CI.
16
17## Proposed Changes
18
191. Add `ci.yml` with jobs:
20 - Setup Bun and Node environment
21 - Install deps
22 - `bun run lint`
23 - `bun run typecheck`
24 - `bun run test`
252. Add separate optional job for `bun run smoke-test` (desktop/Electron).
263. Configure caching for Bun/Turbo as appropriate.
27
28## Risks
29
30- Smoke test may be flaky in headless CI environments.
31- CI runtime can grow if caching is misconfigured.
32
33## Validation
34
35- Verify workflow runs on a branch PR.
36- Ensure failures surface clearly by job name.
37
38## Done Criteria
39
40- CI blocks regressions in lint/typecheck/test.
41- Workflow docs added to README.
.plans/08-precommit-format-and-lint.md deleted-39
...@@ -1,39 +0,0 @@
1# Plan: Add Pre-Commit Formatting/Lint Hooks
2
3## Summary
4
5Introduce pre-commit automation so formatting and basic lint checks happen before commits.
6
7## Motivation
8
9- Current lint failures include formatting-only issues.
10- Shift-left feedback reduces noisy CI failures and cleanup churn.
11
12## Scope
13
14- Root tooling config and package scripts.
15- No runtime code changes.
16
17## Proposed Changes
18
191. Add hook tooling (e.g. Husky + lint-staged or Lefthook).
202. Configure staged-file tasks:
21 - `biome format --write`
22 - `biome check`
233. Add setup docs in README.
244. Keep checks fast to avoid developer friction.
25
26## Risks
27
28- Slow hooks can frustrate contributors and be bypassed.
29- Need to ensure compatibility with Bun workspace setup.
30
31## Validation
32
33- Create sample staged changes and verify hook behavior.
34- Confirm formatting fixes are applied automatically.
35
36## Done Criteria
37
38- Pre-commit hook installed and documented.
39- Formatting-only lint failures drop significantly.
.plans/09-event-state-test-expansion.md deleted-42
...@@ -1,42 +0,0 @@
1# Plan: Expand Event/State Transition Test Coverage
2
3## Summary
4
5Add focused tests for renderer event handling and session evolution logic.
6
7## Motivation
8
9- Core behavior is event-driven and stateful.
10- Existing renderer tests cover only a subset of timeline/model behavior.
11
12## Scope
13
14- `apps/renderer/src/session-logic.test.ts`
15- Optional reducer tests for `apps/renderer/src/store.ts`.
16
17## Proposed Changes
18
191. Add tests for `evolveSession`:
20 - `thread/started`
21 - `turn/started`
22 - `turn/completed` success/failure
23 - error/session closed events
242. Add tests for `applyEventToMessages`:
25 - start/delta/completed flow
26 - out-of-order event cases
27 - turn completion clearing streaming flags
283. Add reducer integration tests for `APPLY_EVENT`.
29
30## Risks
31
32- Tests may be brittle if event payload fixtures are too coupled to implementation details.
33
34## Validation
35
36- `bun run test`
37- Ensure new tests remain deterministic and fast.
38
39## Done Criteria
40
41- High-risk event transitions are covered by unit tests.
42- Regressions in stream assembly/session status are caught quickly.
.plans/10-unify-process-session-abstraction.md deleted-42
...@@ -1,42 +0,0 @@
1# Plan: Unify Process and PTY Session Abstractions in ProcessManager
2
3## Summary
4
5Refactor `ProcessManager` to use a single runtime-session interface for child-process and PTY modes.
6
7## Motivation
8
9- `apps/desktop/src/processManager.ts` maintains parallel maps and branch-heavy logic.
10- New execution backends/providers will multiply complexity.
11
12## Scope
13
14- Desktop process execution internals.
15- Preserve public `ProcessManager` API.
16
17## Proposed Changes
18
191. Introduce internal interface (e.g. `RuntimeSession`):
20 - `write(data)`
21 - `kill()`
22 - lifecycle/output event hooks
232. Implement:
24 - `ChildProcessSession`
25 - `PtySession`
263. Replace dual maps with one `Map<string, RuntimeSession>`.
274. Keep output/exit event contract unchanged.
285. Add tests for both implementations.
29
30## Risks
31
32- PTY behavior differs by platform; abstraction must not hide required differences.
33
34## Validation
35
36- Existing `processManager.test.ts` passes.
37- Add PTY-path tests where feasible.
38
39## Done Criteria
40
41- Manager no longer branches per backend in `write/kill/killAll`.
42- Session backends are independently testable.
.plans/11-effect.md deleted-40
...@@ -1,40 +0,0 @@
1PR 1: Service contracts + error taxonomy
2Add ProviderService, CodexService, CheckpointStore as Context.Tag service defs.
3Add typed Schema.TaggedError hierarchies for all 3 services (cause: Schema.optional(Schema.Defect) on each).
4No behavior change yet, just interfaces and compile-time wiring points.
5PR 2: CheckpointStore Effect adapter
6Wrap current filesystemCheckpointStore behind CheckpointStoreLive (adapter).
7Map all thrown/Promise errors to tagged errors.
8Add service tests proving parity for isGitRepository, capture, restore, diff, prune.
9PR 3: CodexService Effect adapter
10Wrap current CodexAppServerManager behind CodexServiceLive (adapter).
11Convert public API to Effect return types with typed errors.
12Preserve existing EventEmitter internally for now, but expose Effect-friendly subscribe API.
13PR 4: ProviderService Effect adapter
14Wrap current ProviderManager behind ProviderServiceLive (adapter).
15Provider methods become Effect methods with typed errors.
16Route emitted provider events through an Effect PubSub surface.
17PR 5: wsServer migration to Effect services
18Stop instantiating provider/codex classes directly in wsServer.
19Resolve ProviderService (and related services) from one runtime/layer graph.
20Keep WS contract behavior identical.
21PR 6: Native CheckpointStore implementation
22Refactor checkpoint internals from Promise/throws to native Effect.
23Replace ad-hoc locking with Effect concurrency primitive (keyed lock/semaphore/queue).
24Keep adapter tests plus new failure-path tests.
25PR 7: Codex transport/RPC core as native Effect
26Split codex into scoped process layer + RPC request/response layer + session registry.
27Replace timeout/pending maps with Deferred + Effect timeout/finalizer semantics.
28Keep protocol behavior and ordering guarantees.
29PR 8: Codex protocol decoding hardening
30Replace ad-hoc unknown parsing with runtime schema decoding for inbound/outbound protocol shapes.
31Map decode failures to typed tagged errors (with root cause).
32Add regression tests for malformed/partial protocol messages.
33PR 9: Native ProviderService orchestration
34Rebuild provider logic in Effect using CodexService + CheckpointStore dependencies.
35Move event fanout, checkpoint capture/revert orchestration, thread-log routing to Effect state/services.
36Remove throw-based flow entirely from provider path.
37PR 10: Cleanup + deprecation removal
38Remove legacy class implementations/adapters once parity is proven.
39Finalize layer composition and startup graph docs.
40Add architecture notes for service boundaries and error model.
.plans/12-effect-new.md deleted-67
...@@ -1,67 +0,0 @@
1# Effect Migration Plan (From Current State)
2
3Current status summary:
4
5- Service contracts, typed errors, and most checkpoint/persistence services exist.
6- `ProviderServiceLive` is already native orchestration (not a thin adapter).
7- Production server path still uses legacy `ProviderManager`/`FilesystemCheckpointStore`.
8- Checkpoint flow now avoids snapshot re-sync and is write-time driven.
9
10## PR 1: Wire Provider/Checkpoint Effect Stack Into `wsServer`
11
12- Build one runtime layer graph for provider + checkpoint + persistence + orchestration.
13- Resolve `ProviderService` from runtime in `wsServer`.
14- Replace `ProviderManager` method calls in WS handlers with `ProviderService` calls.
15- Forward provider events by subscribing to `ProviderService.subscribeToEvents`.
16- Keep WS method/push payloads identical.
17
18## PR 2: Runtime Composition + Startup Ownership
19
20- Create/centralize `AppLive` composition for server startup.
21- Ensure outer runtime provides Node/platform services once.
22- Ensure migrations run at startup via scoped/layer startup path.
23- Remove ad-hoc service initialization in request-time paths.
24
25## PR 3: Session Lifecycle Hygiene + Checkpoint Invariants
26
27- Add explicit checkpoint session cleanup on `stopSession` / `stopAll`.
28- Remove per-session lock/cwd map leaks.
29- Keep strict invariant model:
30 - root checkpoint created at session initialization before agent modifications
31 - each completed turn captures filesystem checkpoint and persists metadata
32 - no after-the-fact metadata rebuild/sync
33- Add tests for lifecycle cleanup and invariant-failure surfaces.
34
35## PR 4: Provider Event Stream Hardening (Without Extra Service Fragmentation)
36
37- Keep `ProviderService` as the public event surface.
38- Internally move callback fanout to Effect concurrency primitives (`Queue`/`PubSub`) for ordering/backpressure control.
39- Keep API as `subscribeToEvents` unless we explicitly choose stream API later.
40- Add tests for ordering and subscriber isolation under load.
41
42## PR 5: Codex Runtime Split (Scoped Effect Core)
43
44- Extract `CodexAppServerManager` responsibilities into Effect-native layers:
45 - scoped process lifecycle
46 - RPC request/response + pending map via `Deferred`
47 - session registry/state
48- Keep `CodexAdapter` contract stable while swapping internals.
49- Preserve protocol behavior and timeout semantics.
50
51## PR 6: Codex Protocol Decode Hardening
52
53- Replace ad-hoc unknown parsing with runtime schema decode.
54- Map decode failures to typed tagged errors with `cause` retained.
55- Add regression tests for malformed/partial protocol frames.
56
57## PR 7: Remove Legacy Provider Stack
58
59- Remove `ProviderManager` + legacy checkpoint integration from runtime path.
60- Remove `FilesystemCheckpointStore` from active server flow (keep only if explicitly needed for compatibility tooling).
61- Update tests to assert only Effect service path is used.
62
63## PR 8: Final Cleanup + Docs
64
65- Update architecture docs with final layer graph and service boundaries.
66- Document error model and recovery semantics.
67- Trim dead compatibility code and stale plan references.
.plans/13-provider-service-integration-tests.md deleted-123
...@@ -1,123 +0,0 @@
1# ProviderService Integration Test Plan
2
3Goal:
4
5- Validate end-to-end `ProviderService` behavior with real layers:
6 - `ProviderServiceLive`
7 - `CheckpointServiceLive`
8 - `CheckpointStoreLive`
9 - `CheckpointRepositoryLive` (sqlite in-memory)
10 - `ProviderSessionDirectoryLive`
11- Only fake the adapter event source (deterministic Codex-like stream).
12- Avoid mocking checkpointing/persistence orchestration logic.
13
14## Test Harness
15
16Build a deterministic `TestProviderAdapterLive` in `apps/server/src/provider/Layers/TestProviderAdapter.integration.ts`:
17
18- Service contract: `ProviderAdapterShape<ProviderAdapterError>`.
19- Internal state:
20 - session registry (session + cwd + threadId)
21 - thread snapshot store (`threadId`, `turns`)
22 - event subscribers
23- Behavior:
24 - `startSession`: creates session with threadId.
25 - `sendTurn`: appends a deterministic turn snapshot and emits ordered events:
26 - `turn/started`
27 - `item/started` / `item/completed` (tool + approval variants depending on scenario)
28 - `item/agentMessage/delta` chunks
29 - `turn/completed`
30 - optional "mutator" callback per turn to change workspace files before completion.
31 - `readThread`, `rollbackThread`, `stopSession`, `stopAll`.
32
33Use real git-backed temporary workspaces in integration tests:
34
35- initialize repo with baseline commit
36- run provider turn in workspace
37- assert checkpoint diffs against real git refs
38
39## Core Integration Specs
40
411. `startSession` initializes checkpoint root exactly once
42
43- Arrange:
44 - start provider session in git repo.
45- Assert:
46 - `provider_checkpoints` contains root row (turn 0).
47 - checkpoint ref exists in git.
48 - second `startSession` for new session creates a new independent root.
49
502. Turn without filesystem change
51
52- Arrange:
53 - emit normal turn events, no file mutation.
54- Assert:
55 - provider subscribers receive:
56 - `turn/started`
57 - `turn/completed`
58 - synthetic `checkpoint/captured`
59 - `listCheckpoints` returns root + turn 1.
60 - `getCheckpointDiff(0 -> 1)` returns empty/no-op diff.
61
623. Turn with filesystem change
63
64- Arrange:
65 - mutate `README.md` during turn.
66- Assert:
67 - `listCheckpoints` returns root + turn 1.
68 - `getCheckpointDiff(0 -> 1)` contains file path and hunk.
69 - persisted checkpoint metadata includes non-empty `checkpointRef`.
70
714. Multi-turn sequencing and checkpoint monotonicity
72
73- Arrange:
74 - turn 1: no file change
75 - turn 2: file change
76 - turn 3: file change
77- Assert:
78 - turn counts are monotonic and contiguous in DB (0,1,2,3).
79 - latest checkpoint is marked current.
80 - diffs for adjacent turns map to expected filesystem deltas.
81
825. Revert to checkpoint
83
84- Arrange:
85 - execute 3 turns with at least one file-changing turn.
86 - call `revertToCheckpoint(turnCount=1)`.
87- Assert:
88 - workspace content matches turn 1 state.
89 - adapter `rollbackThread` called with `numTurns=2`.
90 - DB rows for turns >1 are removed.
91 - later refs are deleted from git.
92
936. Capture failure surface
94
95- Arrange:
96 - adapter emits `turn/completed`, but file mutation leaves invalid repo state or store capture fails.
97- Assert:
98 - `ProviderService` emits `checkpoint/captureError`.
99 - no partial metadata/ref divergence is left behind.
100
101## WebSocket Coverage (Thin Integration)
102
103Add one ws server integration spec:
104
105- Subscribe to `providers.event`.
106- Run a deterministic provider turn through ws methods.
107- Assert push stream includes:
108 - `turn/started`, tool events, `turn/completed`, `checkpoint/captured`.
109- Assert orchestration projection still updates assistant message and turn diff summary.
110
111## Proposed PR Split
112
113PR A:
114
115- Test adapter harness + shared integration fixtures (repo setup, runtime/layer setup).
116
117PR B:
118
119- Core ProviderService integration specs (cases 1-4).
120
121PR C:
122
123- Revert + failure-path specs (cases 5-6) + ws thin integration spec.
.plans/14-server-authoritative-event-sourcing-cleanup.md deleted-227
...@@ -1,227 +0,0 @@
1# Server-Authoritative Event-Sourcing Cleanup Plan
2
3Goal:
4
5- Move to a cleaner service architecture with:
6 - durable, server-authoritative event sourcing
7 - strict command routing/validation
8 - pluggable provider adapters
9 - explicit separation between transport, domain orchestration, provider runtime, and persistence
10
11## Target Service Graph (ASCII)
12
13```text
14 +---------------------------+
15 | wsServer |
16 | transport |
17 +---------------------------+
18 | orchestration.dispatchCommand
19 v
20 +-------------------------------------------+
21 | OrchestrationCommandRouter |
22 +-------------------------------------------+
23 |
24 v
25 +-------------------------------------------+
26 | OrchestrationCommandHandlers |
27 +-------------------------------------------+
28 |
29 v
30 +-------------------------------------------+
31 | OrchestrationEventStore |
32 +-------------------------------------------+
33 |
34 v
35 +-------------------------------------------+
36 | OrchestrationProjectionService |
37 +-------------------------------------------+
38 | snapshot/replay
39 +---------------------------> wsServer
40
41
42wsServer -- providers.* RPC --> +---------------------------+
43 | ProviderService |
44 +---------------------------+
45 | |
46 v v
47 +-------------------+ +-------------------------+
48 | ProviderSession | | ProviderAdapterRegistry |
49 | Registry (durable)| +-------------------------+
50 +-------------------+ |
51 ^ v
52 | +-------------------------+
53 | | ProviderAdapter(s) |
54 | +-------------------------+
55 | |
56 | runtime events v
57 | +---------------------------+
58 +----------| ProviderRuntimeIngestion |
59 +---------------------------+
60 | | |
61 v v v
62 Router Session Checkpoint
63 Registry Service
64
65 +-------------------------------------------+
66 | CheckpointService |
67 +-------------------------------------------+
68 | | |
69 v v v
70 +--------------------+ +-------------+ +-------------------+
71 | CheckpointCatalog | | Checkpoint | | ProviderAdapter(s)|
72 | (durable) | | Store (git) | | (read/rollback) |
73 +--------------------+ +-------------+ +-------------------+
74 |
75 v
76 +------+
77 |SQLite|
78 +------+
79
80OrchestrationEventStore ------> SQLite
81OrchestrationProjectionService -> SQLite
82ProviderSessionRegistry ------> SQLite
83CheckpointCatalog ------> SQLite
84```
85
86## Commit Series
87
88### Commit 1: Split public vs system orchestration command contracts
89
90- Create separate schemas/types:
91 - `ClientOrchestrationCommandSchema`
92 - `SystemOrchestrationCommandSchema`
93 - `OrchestrationCommandSchema = union(client, system)`
94- Ensure client transport can only submit client commands.
95- Keep system commands for server-internal workflows only.
96- Expected files:
97 - `packages/contracts/src/orchestration.ts`
98 - `apps/server/src/wsServer.ts`
99 - orchestration/service tests
100- Tests:
101 - reject system-only command via WS dispatch path
102 - preserve internal dispatch functionality for system commands
103
104### Commit 2: Introduce `OrchestrationCommandRouter` + handler boundary
105
106- Add dedicated router service to validate, authorize, and route commands.
107- Move command-to-event mapping out of `orchestration/Layer.ts` into handlers.
108- Add aggregate-level invariant checks before append (thread exists, project exists, etc.).
109- Expected files:
110 - `apps/server/src/orchestration/Services/CommandRouter.ts` (new)
111 - `apps/server/src/orchestration/Layers/CommandRouter.ts` (new)
112 - `apps/server/src/orchestration/Layer.ts`
113 - `apps/server/src/orchestration/reducer.ts` (only if needed for event payload changes)
114- Tests:
115 - router validation and invariant failures
116 - handler happy-path tests per command type
117
118### Commit 3: Harden event store for idempotency + optimistic append metadata
119
120- Add DB-level idempotency guard for `command_id` (`UNIQUE` where non-null).
121- Extend append API to support idempotent replays and deterministic return of prior event on duplicate `commandId`.
122- Add optional aggregate version metadata for future optimistic concurrency.
123- Expected files:
124 - `apps/server/src/persistence/Migrations/00x_*.ts` (new migration)
125 - `apps/server/src/persistence/Services/OrchestrationEvents.ts`
126 - `apps/server/src/persistence/Layers/OrchestrationEvents.ts`
127- Tests:
128 - duplicate command ID append returns same event/sequence (or explicit idempotent behavior)
129 - concurrent append behavior stays ordered and deterministic
130
131### Commit 4: Extract provider-runtime -> orchestration bridge from `wsServer`
132
133- Create `ProviderRuntimeIngestionService` that:
134 - subscribes to `ProviderService.streamEvents`
135 - translates runtime events into orchestration commands
136 - dispatches through router/engine
137- Remove provider-to-orchestration state mutation logic from `wsServer`.
138- Expected files:
139 - `apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts` (new)
140 - `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (new)
141 - `apps/server/src/wsServer.ts`
142- Tests:
143 - ingestion service mapping tests (turn started/completed, message delta/completed, runtime error)
144 - ws integration confirms same external push behavior
145
146### Commit 5: Make session directory durable (`ProviderSessionRegistry`)
147
148- Replace in-memory-only `ProviderSessionDirectoryLive` with persistence-backed registry.
149- Keep in-memory cache optional, but source of truth must be persistent.
150- Add startup reconciliation to prune dead sessions / keep known thread mapping.
151- Expected files:
152 - `apps/server/src/provider/Services/ProviderSessionDirectory.ts` (or new SessionRegistry service)
153 - `apps/server/src/provider/Layers/ProviderSessionDirectory.ts`
154 - `apps/server/src/persistence/Migrations/00x_*.ts` (new table/indexes)
155 - provider persistence tests
156- Tests:
157 - survives server restart with correct mapping
158 - stale session cleanup semantics
159
160### Commit 6: Re-key checkpoint metadata from session to thread identity
161
162- Change checkpoint catalog primary identity from `provider_session_id` to durable `thread_id`.
163- Keep `session_id` as nullable metadata only.
164- Update checkpoint flows (`initialize`, `capture`, `list`, `diff`, `revert`) to use thread identity.
165- Expected files:
166 - `apps/server/src/persistence/Migrations/00x_*.ts` (checkpoint schema migration)
167 - `apps/server/src/persistence/Services/Checkpoints.ts`
168 - `apps/server/src/persistence/Layers/Checkpoints.ts`
169 - `apps/server/src/checkpointing/Layers/CheckpointService.ts`
170- Tests:
171 - resume/new session over same thread sees same checkpoint history
172 - revert/diff still work after session churn
173
174### Commit 7: Add durable projection persistence for orchestration read models
175
176- Introduce projection tables/snapshots persisted in DB to avoid full replay dependency.
177- Keep event stream as source of truth; projection rebuild stays deterministic.
178- `getSnapshot` reads from projection store (memory cache optional).
179- Expected files:
180 - `apps/server/src/persistence/Migrations/00x_*.ts` (projection tables)
181 - `apps/server/src/orchestration/*` projection service/layer
182 - `apps/server/src/wsServer.ts` (snapshot/replay path wiring)
183- Tests:
184 - cold boot snapshot load without replaying full history in process
185 - projection rebuild from events yields same result as previous reducer semantics
186
187### Commit 8: Narrow `ProviderService` responsibilities
188
189- Keep `ProviderService` focused on provider RPC/session lifecycle + unified runtime stream.
190- Move checkpoint-capture side effects out of provider event worker into dedicated ingestion/checkpoint pipeline service.
191- Preserve adapter pluggability and provider-neutral contracts.
192- Expected files:
193 - `apps/server/src/provider/Layers/ProviderService.ts`
194 - new orchestration/checkpoint runtime coordinator service(s)
195- Tests:
196 - provider service routing stays intact
197 - checkpoint capture still triggered by turn completion through new coordinator
198
199### Commit 9: Look over schemas (contracts and events)
200
201- Scan for unused schemas.
202- Use effect/Schema everywhere
203- Analyze which we need
204 - RPC Input/Output (both for routeRequest and command handler)
205 - Event payloads
206 - Persistence entities
207
208### Commit 10: Remove dead legacy path and finalize docs
209
210- Remove unused legacy manager/store path from active architecture:
211 - `providerManager.ts`
212 - `filesystemCheckpointStore.ts` (if no longer needed by tests/tools)
213- Look over effect services for unused methods, errors, etc
214- Update architecture docs with final service boundaries and boot/runtime graph.
215- Expected files:
216 - legacy files + references
217 - `AGENTS.md`/docs as needed
218 - `.plans` docs linkage
219- Tests:
220 - full server integration suite passes on Effect-only path
221 - no regressions in WS protocol behavior
222
223## Risk Controls
224
225- Keep WS method names and payload contracts stable throughout.
226- Gate each commit with targeted integration tests before moving forward.
227- Avoid broad event-type churn in one step; migrate schemas incrementally with clear compatibility windows.
.plans/15-effect-server.md deleted-11
...@@ -1,11 +0,0 @@
1Rewrite `createServer` and `index.ts` to be Effect native.
2
3Maybe use `effect/unstable/Socket` for the web socket server
4
5- https://github.com/Effect-TS/effect-smol/blob/main/packages/effect/src/unstable/socket/SocketServer.ts
6- https://github.com/Effect-TS/effect-smol/blob/main/packages/platform-node/test/NodeSocket.test.ts
7
8- Migrate remaining runtime code to Effect
9 - `gitManager` -> `src/git`
10 - `terminalManager` -> `src/terminal` (Manager + PTY)
11 - ...
.plans/16-pr89-review-remediation-phases.md deleted-165
...@@ -1,165 +0,0 @@
1# PR #89 Review Remediation Plan (Phased)
2
3## How To Use These Files
4
5- Working checklist with updateable status per item (single source of truth): `.plans/16c-pr89-remediation-checklist.md`
6- This file (`16-pr89-review-remediation-phases.md`): phase strategy and grouping.
7
8## Scope
9
10- Source: GitHub review comments on PR #89 (`Add server-side orchestration engine with event sourcing`).
11- Triage baseline used here:
12 - Total threads: 185
13 - Outdated: 94 (excluded)
14 - Active unresolved: 85
15 - Invalid/false-positive: 3 (excluded)
16 - Duplicate reposts: collapsed
17 - Unique actionable findings after filtering: 58
18 - Post-rewrite validity audit: 5 additional stale items marked invalid, leaving 53 actionable (`34 valid` + `19 partially-valid`)
19
20## Phase 0: Canonical Triage Lock
21
22- Create a single tracking checklist for the 53 currently actionable findings.
23- Map every duplicate thread to its canonical item.
24- Mark invalid/false-positive items with explicit rationale.
25
26Exit criteria:
27
28- Every open thread is mapped to one canonical fix item or marked invalid.
29
30## Phase 1: Runtime Survival and Critical Event Wiring
31
32Related bug groups solved together:
33
34- Worker loop/fiber fatal error handling in orchestration reactors.
35- WebSocket message error boundaries and unhandled rejection guards.
36- Close invalid `providers.event` review findings as documented architecture mismatch (no code change expected).
37
38Primary files:
39
40- `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`
41- `apps/server/src/orchestration/Layers/CheckpointReactor.ts`
42- `apps/server/src/wsServer.ts`
43
44Exit criteria:
45
46- A single event-processing failure cannot permanently stop ingestion/reactor loops.
47- WS message handling cannot produce unhandled promise rejections.
48- Invalid provider-event-channel review findings are closed with architecture rationale.
49
50## Phase 2: State Consistency and Ordering
51
52Related bug groups solved together:
53
54- Fire-and-forget revert completion causing consistency windows.
55- Non-atomic append/projection paths and retry behavior.
56- Race-sensitive thread/event association issues.
57
58Primary files:
59
60- `apps/server/src/orchestration/Layers/CheckpointReactor.ts`
61- `apps/server/src/orchestration/Layers/OrchestrationEngine.ts`
62- `apps/server/src/orchestration/Layers/ProjectionPipeline.ts`
63- `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`
64
65Exit criteria:
66
67- Revert flow is deterministically reflected in read model updates.
68- Append/project failure mode is explicit and safe under retry.
69- No cross-thread misassociation under concurrent runtime events.
70
71## Phase 3: Checkpointing Correctness Bundle
72
73Related bug groups solved together:
74
75- Checkpoint input normalization consistency.
76- Snapshot/projector coverage mismatches.
77- Checkpoint ref/workspace CWD utility duplication.
78- Checkpoint diff/error handling behavior gaps.
79
80Primary files:
81
82- `apps/server/src/checkpointing/Layers/CheckpointStore.ts`
83- `apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts`
84- `apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts`
85- `apps/server/src/orchestration/Layers/CheckpointReactor.ts`
86- `apps/server/src/wsServer.ts`
87
88Exit criteria:
89
90- Checkpoint capture/restore/revert paths use one normalization policy.
91- Required projectors are actually represented in snapshot reads.
92- Shared checkpoint/ref/CWD helpers are centralized.
93
94## Phase 4: Memory and Lifecycle Hygiene
95
96Related bug groups solved together:
97
98- Unbounded in-memory dedup sets/maps.
99- Missing cleanup/lifecycle protections in long-lived effects/resources.
100
101Primary files:
102
103- `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts`
104- `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`
105- `apps/server/src/config.ts`
106
107Exit criteria:
108
109- Long-running server memory does not grow unbounded from dedup bookkeeping.
110- Resource cleanup paths are registered for interruption/shutdown.
111
112## Phase 5: Transport, Parsing, and Platform Edge Cases
113
114Related bug groups solved together:
115
116- UTF-8 chunk boundary decode correctness.
117- Markdown/file-link parsing edge cases.
118- Shell/OS-specific PATH parsing behavior.
119- Git rename parsing and small keybinding edge cases.
120
121Primary files:
122
123- `apps/server/src/wsServer.ts`
124- `apps/server/src/git/Layers/CodexTextGeneration.ts`
125- `apps/web/src/markdown-links.ts`
126- `apps/server/src/os-jank.ts`
127- `apps/server/src/git/Layers/GitCore.ts`
128- `apps/server/src/keybindings.ts`
129
130Exit criteria:
131
132- Edge-case parsers are robust across valid but non-trivial inputs.
133- Platform-dependent command behavior has safe fallbacks.
134
135## Phase 6: Build and Maintainability Cleanup
136
137Related bug groups solved together:
138
139- Build script/runtime assumption cleanup.
140- Redundant error-union declarations and utility/type duplication.
141- Non-functional cleanup comments/docs markers.
142
143Primary files:
144
145- `apps/server/package.json`
146- `apps/server/src/checkpointing/Errors.ts`
147- Shared utility locations introduced during earlier phases
148- `AGENTS.md` (if cleanup is still pending)
149
150Exit criteria:
151
152- Build path is explicit and environment-safe.
153- Redundant types/utilities are removed in favor of single sources of truth.
154
155## Phase 7: Verification and Closeout
156
157- Add backend tests for all behavioral fixes (integration-focused; external services may be layered/mocked, core business logic not mocked out).
158- Run lint and backend tests for all touched packages.
159- Resolve threads with fix references per canonical checklist item.
160
161Exit criteria:
162
163- Lint passes.
164- Backend tests pass.
165- All actionable review threads are resolved or explicitly justified.
.plans/16c-pr89-remediation-checklist.md deleted-478
...@@ -1,478 +0,0 @@
1# PR #89 Remediation Checklist (Consolidated)
2
3_Last updated: 2026-02-26_
4
5This is the working checklist for remediation execution.
6
7Status values:
8
9- `TODO`: Not started
10- `IN_PROGRESS`: Currently being worked
11- `BLOCKED`: Waiting on decision/dependency
12- `DONE`: Implemented and verified
13- `CLOSED_INVALID`: Stale/invalid review finding
14
15Counts: active `51` (`valid=33`, `partially-valid=18`), closed-invalid `6`
16
17## Active Checklist
18
19### Phase 1
20
21- [x] `C002` A dispatch error in `processEvent` will terminate the `Effect.forever` loop, permanently halting event ingestion. Consider adding error recovery (e.g., `Effect.catchAll` with logging) around `processEvent` so failures don't kill the fiber.
22 - Status: `DONE`
23 - Verdict: `valid`
24 - Severity: `High`
25 - Area: `Runtime resilience and failure handling`
26 - File: `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:333`
27 - Threads: PRRT_kwDORLtfbc5wj4cH, PRRT_kwDORLtfbc5wnWwF, PRRT_kwDORLtfbc5wyTaP, PRRT_kwDORLtfbc5wzliw, PRRT_kwDORLtfbc5w0_g3, PRRT_kwDORLtfbc5w1HGT (+5 duplicate thread(s))
28 - Audit note: Ingestion worker loop can terminate on unhandled processEvent failure.
29
30- [x] `C003` Consider attaching a no-op error listener before `socket.write` (e.g., `socket.on('error', () => {})`) to prevent an unhandled `EPIPE`/`ECONNRESET` from crashing the process if the client disconnects mid-handshake.
31 - Status: `DONE`
32 - Verdict: `valid`
33 - Severity: `High`
34 - Area: `WebSocket robustness`
35 - File: `apps/server/src/wsServer.ts:75`
36 - Threads: PRRT_kwDORLtfbc5v-cf4
37 - Audit note: Upgrade reject writes then destroys socket without defensive error listener.
38
39- [x] `C012` Forked revert dispatch risks read model inconsistency
40 - Status: `DONE`
41 - Verdict: `valid`
42 - Severity: `Medium`
43 - Area: `Runtime resilience and failure handling`
44 - File: `apps/server/src/orchestration/Layers/CheckpointReactor.ts:542`
45 - Threads: PRRT_kwDORLtfbc5whszW, PRRT_kwDORLtfbc5wyTaS, PRRT_kwDORLtfbc5wzli0, PRRT_kwDORLtfbc5w0_g4, PRRT_kwDORLtfbc5w1HGX (+4 duplicate thread(s))
46 - Audit note: Revert completion dispatch remains forked; state consistency window remains.
47
48- [ ] `C019` ProviderRuntimeIngestion processes events for wrong thread on race
49 - Status: `TODO`
50 - Verdict: `partially-valid`
51 - Severity: `Medium`
52 - Area: `Runtime resilience and failure handling`
53 - File: `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:178`
54 - Threads: PRRT_kwDORLtfbc5wkPaL
55 - Audit note: SessionId-only routing can misassociate events under races/rebinds.
56
57- [x] `C020` On `message.completed`, the message ID is added to the set and `thread.message.assistant.complete` is dispatched. On `turn.completed`, the same set is iterated and `thread.message.assistant.complete` is dispatched again for each ID—including already-completed ones. Consider removing message IDs from the set after dispatching on `message.completed`, or filtering out already-completed IDs before the `turn.completed` loop.
58 - Status: `DONE`
59 - Verdict: `partially-valid`
60 - Severity: `Medium`
61 - Area: `Runtime resilience and failure handling`
62 - File: `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:266`
63 - Threads: PRRT_kwDORLtfbc5w1GPr
64 - Audit note: Duplicate complete dispatch exists; downstream impact often idempotent.
65
66- [x] `C026` Consider adding `.catch(() => {})` after `Effect.runPromise(handleMessage(ws, raw))` to prevent unhandled rejections from crashing the server if `encodeResponse` or setup logic fails.
67 - Status: `DONE`
68 - Verdict: `valid`
69 - Severity: `Medium`
70 - Area: `WebSocket robustness`
71 - File: `apps/server/src/wsServer.ts:545`
72 - Threads: PRRT_kwDORLtfbc5wj4cE
73 - Audit note: runPromise result still not caught; rejection can surface unhandled.
74
75- [x] `C027` WS message handler can cause unhandled promise rejection
76 - Status: `DONE`
77 - Verdict: `valid`
78 - Severity: `Medium`
79 - Area: `Runtime resilience and failure handling`
80 - File: `apps/server/src/wsServer.ts:545`
81 - Threads: PRRT_kwDORLtfbc5wyTaW, PRRT_kwDORLtfbc5wzli3 (+1 duplicate thread(s))
82 - Audit note: Same unhandled rejection path remains in WS message handler.
83
84- [x] `C042` Duplicated `resolveThreadWorkspaceCwd` across three files
85 - Status: `DONE`
86 - Verdict: `partially-valid`
87 - Severity: `Low`
88 - Area: `Runtime resilience and failure handling`
89 - File: `apps/server/src/orchestration/Layers/CheckpointReactor.ts:62`
90 - Threads: PRRT_kwDORLtfbc5wzli2
91 - Audit note: Duplication exists but one instance is variant logic, so impact is moderate.
92
93- [x] `C043` Duplicated workspace CWD resolution logic across reactor modules
94 - Status: `DONE`
95 - Verdict: `valid`
96 - Severity: `Low`
97 - Area: `Runtime resilience and failure handling`
98 - File: `apps/server/src/orchestration/Layers/CheckpointReactor.ts:62`
99 - Threads: PRRT_kwDORLtfbc5wnWwM, PRRT_kwDORLtfbc5w1C3-, PRRT_kwDORLtfbc5w1HGZ (+2 duplicate thread(s))
100 - Audit note: Workspace CWD resolution duplication still present across modules.
101
102- [x] `C044` Checkpoint reactor swallows diff errors silently for `turn.completed`
103 - Status: `DONE`
104 - Verdict: `partially-valid`
105 - Severity: `Low`
106 - Area: `Runtime resilience and failure handling`
107 - File: `apps/server/src/orchestration/Layers/CheckpointReactor.ts:274`
108 - Threads: PRRT_kwDORLtfbc5wkPaO
109 - Audit note: Errors are swallowed to empty diff with warning; not fully silent but still lossy.
110
111- [x] `C045` `truncateDetail` slices to `limit - 1` then appends `"..."` (3 chars), producing strings of length `limit + 2`. Consider slicing to `limit - 3` instead. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
112 - Status: `DONE`
113 - Verdict: `valid`
114 - Severity: `Low`
115 - Area: `Runtime resilience and failure handling`
116 - File: `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:29`
117 - Threads: PRRT_kwDORLtfbc5wzp4R
118 - Audit note: truncateDetail still overshoots limit.
119
120- [x] `C046` `latestMessageIdByTurnKey` is written to but never read, and `clearAssistantMessageIdsForTurn` doesn't clear its entries—only `clearTurnStateForSession` does. Consider removing this map entirely if unused, or clearing it alongside `turnMessageIdsByTurnKey` in `clearAssistantMessageIdsForTurn`. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
121 - Status: `DONE`
122 - Verdict: `valid`
123 - Severity: `Low`
124 - Area: `Runtime resilience and failure handling`
125 - File: `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:133`
126 - Threads: PRRT_kwDORLtfbc5wxvIQ
127 - Audit note: latestMessageIdByTurnKey still unused/unpruned in per-turn clear path.
128
129- [x] `C053` Consider using `socket.end(response)` instead of `socket.write(response)` + `socket.destroy()` to ensure the HTTP error response is fully flushed before closing the connection.
130 - Status: `DONE`
131 - Verdict: `valid`
132 - Severity: `Low`
133 - Area: `WebSocket robustness`
134 - File: `apps/server/src/wsServer.ts:83`
135 - Threads: PRRT_kwDORLtfbc5v-WPD
136 - Audit note: Still uses write+destroy rather than end() for rejection response.
137
138- [ ] `C054` When array chunks contain a multi-byte UTF-8 character split across boundaries, decoding each chunk separately produces replacement characters. Consider using `Buffer.concat()` on all chunks before calling `.toString("utf8")`. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
139 - Status: `TODO`
140 - Verdict: `valid`
141 - Severity: `Low`
142 - Area: `WebSocket robustness`
143 - File: `apps/server/src/wsServer.ts:104`
144 - Threads: PRRT_kwDORLtfbc5whtrR
145 - Audit note: Array chunk UTF-8 decode remains vulnerable to split multibyte corruption.
146
147- [x] `C059` Suggestion: don’t spread `params` into `body`; it can override `_tag` and mishandle non-object values. Keep `_tag` separate and nest `params` under a single key (e.g., `data`), or validate `params` is a plain object.
148 - Status: `DONE`
149 - Verdict: `partially-valid`
150 - Severity: `Low`
151 - Area: `WebSocket robustness`
152 - File: `apps/web/src/wsTransport.ts:59`
153 - Threads: PRRT_kwDORLtfbc5whtrN
154 - Audit note: Transport \_tag override risk exists but current callsites are constrained.
155
156### Phase 2
157
158- [x] `C001` Non-atomic event appending can corrupt state on retry. If an error occurs mid-loop (lines 96-102) after some events are persisted but before the receipt is written, the command appears to fail. A retry generates new UUIDs via `crypto.randomUUID()` in the decider, appending duplicate events. Consider wrapping the loop in a transaction or using deterministic event IDs derived from `commandId`. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
159 - Status: `DONE`
160 - Verdict: `valid`
161 - Severity: `High`
162 - Area: `Event ordering and state consistency`
163 - File: `apps/server/src/orchestration/Layers/OrchestrationEngine.ts:96`
164 - Threads: PRRT_kwDORLtfbc5wzp4T
165 - Audit note: Append/project/receipt are non-atomic; retry can duplicate events.
166
167- [x] `C013` If `projectionPipeline.projectEvent` fails after `eventStore.append` succeeds, the event is persisted but `readModel` isn't updated, causing desync. Consider updating the in-memory `readModel` immediately after append (before the external projection), so local state stays consistent regardless of downstream failures.
168 - Status: `DONE`
169 - Verdict: `valid`
170 - Severity: `Medium`
171 - Area: `Event ordering and state consistency`
172 - File: `apps/server/src/orchestration/Layers/OrchestrationEngine.ts:99`
173 - Threads: PRRT_kwDORLtfbc5whtrM
174 - Audit note: Persisted event can outpace in-memory projection on mid-flight failure.
175
176- [x] `C015` The gap-filling fallback logic can retain messages from turns that are about to be deleted, causing foreign key violations. Consider removing the fallback logic entirely, or filtering `fallbackUserMessages` and `fallbackAssistantMessages` to only include messages whose `turnId` is in `retainedTurnIds`. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
177 - Status: `DONE`
178 - Verdict: `partially-valid`
179 - Severity: `Medium`
180 - Area: `Event ordering and state consistency`
181 - File: `apps/server/src/orchestration/Layers/ProjectionPipeline.ts:99`
182 - Threads: PRRT_kwDORLtfbc5whxJO
183 - Audit note: Message fallback retention issue is real, but prior FK-violation claim is overstated.
184
185- [x] `C016` The in-memory `pendingTurnStartByThreadId` map isn't restored during bootstrap. If the service restarts after processing `thread.turn-start-requested` but before `thread.session-set`, the `userMessageId` and `startedAt` will be lost since bootstrap resumes _after_ the committed sequence. Consider persisting this pending state or processing these two events atomically. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
186 - Status: `DONE`
187 - Verdict: `valid`
188 - Severity: `Medium`
189 - Area: `Event ordering and state consistency`
190 - File: `apps/server/src/orchestration/Layers/ProjectionPipeline.ts:490`
191 - Threads: PRRT_kwDORLtfbc5wxvH8
192 - Audit note: Pending turn-start map is in-memory only and not rebuilt on bootstrap.
193
194### Phase 3
195
196- [x] `C008` Inconsistent input normalization across CheckpointStore methods
197 - Status: `DONE`
198 - Verdict: `partially-valid`
199 - Severity: `Medium`
200 - Area: `Checkpointing correctness`
201 - File: `apps/server/src/checkpointing/Layers/CheckpointStore.ts:94`
202 - Threads: PRRT*kwDORLtfbc5widJw, PRRT_kwDORLtfbc5wnWv*, PRRT_kwDORLtfbc5w0_g7, PRRT_kwDORLtfbc5w1C36 (+3 duplicate thread(s))
203 - Audit note: Edge schema strategy is in place across contracts/consumers (trim/normalize via schemas and decode at boundaries); CheckpointStore remains an internal repository boundary.
204
205- [x] `C017` `REQUIRED_SNAPSHOT_PROJECTORS` includes `pending-approvals` and `thread-turns`, but `getSnapshot` doesn't query their data. If these projectors lag behind, the returned `snapshotSequence` will be lower than what the included data actually reflects, causing clients to replay already-applied events. Consider filtering `REQUIRED_SNAPSHOT_PROJECTORS` to only include projectors whose data is actually fetched in the snapshot. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
206 - Status: `DONE`
207 - Verdict: `partially-valid`
208 - Severity: `Medium`
209 - Area: `Checkpointing correctness`
210 - File: `apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts:71`
211 - Threads: PRRT_kwDORLtfbc5wiLhQ
212 - Audit note: Snapshot sequence can under-report due to extra projectors, but replay impact is lower now.
213
214- [x] `C033` Three error classes defined but never instantiated anywhere
215 - Status: `DONE`
216 - Verdict: `partially-valid`
217 - Severity: `Low`
218 - Area: `Checkpointing correctness`
219 - File: `apps/server/src/checkpointing/Errors.ts:51`
220 - Threads: PRRT_kwDORLtfbc5wlYgo
221 - Audit note: Original claim overstated; some errors used, others appear unused.
222
223- [x] `C034` Redundant `CheckpointInvariantError` in `CheckpointServiceError` union type
224 - Status: `DONE`
225 - Verdict: `valid`
226 - Severity: `Low`
227 - Area: `Checkpointing correctness`
228 - File: `apps/server/src/checkpointing/Errors.ts:79`
229 - Threads: PRRT_kwDORLtfbc5wj5fn
230 - Audit note: CheckpointInvariantError remains redundantly included in service union.
231
232- [x] `C035` Redundant error type in CheckpointServiceError union definition
233 - Status: `DONE`
234 - Verdict: `valid`
235 - Severity: `Low`
236 - Area: `Checkpointing correctness`
237 - File: `apps/server/src/checkpointing/Errors.ts:79`
238 - Threads: PRRT_kwDORLtfbc5wlYgs, PRRT_kwDORLtfbc5wxsO6, PRRT_kwDORLtfbc5w1C4B (+2 duplicate thread(s))
239 - Audit note: Same as C034.
240
241### Phase 4
242
243- [ ] `C018` Unbounded memory growth in turn start deduplication set
244 - Status: `TODO`
245 - Verdict: `valid`
246 - Severity: `Medium`
247 - Area: `Memory/resource growth`
248 - File: `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:84`
249 - Threads: PRRT_kwDORLtfbc5whszQ, PRRT_kwDORLtfbc5wl2A8, PRRT_kwDORLtfbc5wyTaT, PRRT_kwDORLtfbc5wzliz, PRRT_kwDORLtfbc5w0_g-, PRRT_kwDORLtfbc5w1HGW (+5 duplicate thread(s))
250 - Audit note: handledTurnStartKeys still grows without pruning.
251
252### Phase 5
253
254- [ ] `C009` Git's braced rename syntax (e.g., `src/{old => new}/file.ts`) isn't handled correctly. The current slice after `=>` produces invalid paths like `new}/file.ts`. Consider expanding the braces to construct the full destination path. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
255 - Status: `TODO`
256 - Verdict: `valid`
257 - Severity: `Medium`
258 - Area: `Edge-case parsing/platform behavior`
259 - File: `apps/server/src/git/Layers/GitCore.ts:41`
260 - Threads: PRRT_kwDORLtfbc5w1CxT
261 - Audit note: Braced rename parsing still breaks paths like src/{old => new}/file.ts.
262
263- [ ] `C010` `loadCustomKeybindingsConfig` fails when the config file doesn't exist, which is expected for new users. Consider catching `ENOENT` and returning an empty array instead. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
264 - Status: `TODO`
265 - Verdict: `valid`
266 - Severity: `Medium`
267 - Area: `Edge-case parsing/platform behavior`
268 - File: `apps/server/src/keybindings.ts:418`
269 - Threads: PRRT_kwDORLtfbc5wxvIJ
270 - Audit note: ENOENT for missing keybindings config still not handled as empty/default.
271
272- [ ] `C022` Fish shell outputs `$PATH` as space-separated, not colon-separated. Consider checking if the shell is fish and using `string join : $PATH` instead, or validating the result contains colons before assigning.
273 - Status: `TODO`
274 - Verdict: `valid`
275 - Severity: `Medium`
276 - Area: `Edge-case parsing/platform behavior`
277 - File: `apps/server/src/os-jank.ts:10`
278 - Threads: PRRT_kwDORLtfbc5wkRZM
279 - Audit note: fish PATH formatting risk still exists in os-jank path recovery.
280
281- [ ] `C023` Using `-il` flags causes the shell to source profile scripts that may print banners or other text, polluting the captured `PATH`. Consider using `-lc` (login only, non-interactive) to reduce unwanted output.
282 - Status: `TODO`
283 - Verdict: `valid`
284 - Severity: `Medium`
285 - Area: `Edge-case parsing/platform behavior`
286 - File: `apps/server/src/os-jank.ts:10`
287 - Threads: PRRT_kwDORLtfbc5wj4cM
288 - Audit note: -ilc shell invocation can pollute captured PATH output.
289
290- [x] `C029` `parseFileUrlHref` already decodes the path (line 46), but `safeDecode` is called again here, corrupting filenames containing `%` sequences. Consider skipping the decode when `fileUrlTarget` is non-null.
291 - Status: `DONE`
292 - Verdict: `valid`
293 - Severity: `Medium`
294 - Area: `Edge-case parsing/platform behavior`
295 - File: `apps/web/src/markdown-links.ts:105`
296 - Threads: PRRT_kwDORLtfbc5wnVsU
297 - Audit note: file URL decoding still double-decodes in one path.
298
299- [x] `C030` `EXTERNAL_SCHEME_PATTERN` matches `script.ts:10` as a scheme because `.ts:` looks like `scheme:`. Consider requiring `://` after the colon, or checking that what follows the colon is not just digits. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
300 - Status: `DONE`
301 - Verdict: `valid`
302 - Severity: `Medium`
303 - Area: `Edge-case parsing/platform behavior`
304 - File: `apps/web/src/markdown-links.ts:111`
305 - Threads: PRRT_kwDORLtfbc5wnVsK
306 - Audit note: Scheme regex still misclassifies script.ts:10 as external scheme.
307
308- [ ] `C038` Multi-byte UTF-8 characters split across chunks will be corrupted when decoding each chunk separately. Consider accumulating all chunks first, then decoding once, or use `TextDecoder` with `stream: true`. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
309 - Status: `TODO`
310 - Verdict: `valid`
311 - Severity: `Low`
312 - Area: `Edge-case parsing/platform behavior`
313 - File: `apps/server/src/git/Layers/CodexTextGeneration.ts:136`
314 - Threads: PRRT_kwDORLtfbc5w1GPo
315 - Audit note: Chunk-by-chunk UTF-8 decode can still corrupt split multibyte characters.
316
317- [x] `C039` The `+` key can be parsed (via trailing `+` handling) but cannot be encoded because `shortcut.key.includes("+")` returns true for the literal `+` key. Consider checking `shortcut.key === "+"` separately and encoding it as `"space"` style (e.g., a special token), or adjusting the condition to allow the single `+` character. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
318 - Status: `DONE`
319 - Verdict: `partially-valid`
320 - Severity: `Low`
321 - Area: `Edge-case parsing/platform behavior`
322 - File: `apps/server/src/keybindings.ts:352`
323 - Threads: PRRT_kwDORLtfbc5wxvIB
324 - Audit note: Parser/encoder mismatch remains, but encoder path currently low-use.
325
326- [x] `C040` `upsertKeybindingRule` has a race condition: concurrent calls read the same file state, then the last write overwrites earlier changes. Consider wrapping the read-modify-write sequence with `Effect.Semaphore` to serialize access. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
327 - Status: `DONE`
328 - Verdict: `valid`
329 - Severity: `Low`
330 - Area: `Edge-case parsing/platform behavior`
331 - File: `apps/server/src/keybindings.ts:488`
332 - Threads: PRRT_kwDORLtfbc5wxvIA
333 - Audit note: upsertKeybindingRule read-modify-write remains race-prone.
334
335### Phase 6
336
337- [ ] `C028` Branch sync dispatches both server and stale local update
338 - Status: `TODO`
339 - Verdict: `partially-valid`
340 - Severity: `Medium`
341 - Area: `Other`
342 - File: `apps/web/src/components/BranchToolbar.tsx:102`
343 - Threads: PRRT_kwDORLtfbc5v-XCu
344 - Audit note: Optimistic local+server dual update is intentional but can temporarily diverge.
345
346- [x] `C037` `Effect.callback` should return a cleanup function to close the server(s) on fiber interruption. Without it, the `Net.Server` handles keep the process alive and leak the port if the effect is cancelled. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
347 - Status: `DONE`
348 - Verdict: `partially-valid`
349 - Severity: `Low`
350 - Area: `Other`
351 - File: `apps/server/src/config.ts:41`
352 - Threads: PRRT_kwDORLtfbc5wj4cO
353 - Audit note: Callback cleanup missing, but practical exposure is low in one-shot startup path.
354
355- [ ] `C047` `SqlSchema.findOneOption` can produce both SQL errors and decode errors, but `mapError` wraps all as `PersistenceSqlError`. Consider distinguishing `ParseError` from SQL errors and mapping decode failures to `PersistenceDecodeError` instead. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
356 - Status: `TODO`
357 - Verdict: `valid`
358 - Severity: `Low`
359 - Area: `Other`
360 - File: `apps/server/src/persistence/Layers/OrchestrationCommandReceipts.ts:75`
361 - Threads: PRRT_kwDORLtfbc5wiaR-
362 - Audit note: Decode and SQL errors still collapsed into one persistence error kind.
363
364- [x] `C049` `JSON.stringify(cause)` returns `undefined` for `undefined`, functions, or symbols, violating the `string` return type. Consider coercing the result to a string (e.g., `String(JSON.stringify(cause))`) or adding a fallback.
365 - Status: `DONE`
366 - Verdict: `valid`
367 - Severity: `Low`
368 - Area: `Other`
369 - File: `apps/server/src/provider/Layers/ProviderService.ts:59`
370 - Threads: PRRT_kwDORLtfbc5wnVsI
371 - Audit note: JSON.stringify(cause) may return undefined despite string expectations.
372
373- [ ] `C050` The read-modify-write pattern (`getBySessionId` → merge → `upsert`) is susceptible to lost updates under concurrent writes. Consider wrapping in a transaction or adding optimistic concurrency control (e.g., version field) if concurrent session updates are expected. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
374 - Status: `TODO`
375 - Verdict: `valid`
376 - Severity: `Low`
377 - Area: `Other`
378 - File: `apps/server/src/provider/Layers/ProviderSessionDirectory.ts:94`
379 - Threads: PRRT_kwDORLtfbc5wiLhY
380 - Audit note: ProviderSessionDirectory upsert remains read-merge-write without concurrency control.
381
382- [x] `C051` Using `??` for `providerThreadId` and `adapterKey` makes it impossible to clear these fields by passing `null`, since `null ?? existing` evaluates to `existing`. Consider using explicit `undefined` checks (like `resumeCursor` does) if clearing should be supported. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
383 - Status: `DONE`
384 - Verdict: `partially-valid`
385 - Severity: `Low`
386 - Area: `Other`
387 - File: `apps/server/src/provider/Layers/ProviderSessionDirectory.ts:119`
388 - Threads: PRRT_kwDORLtfbc5wxvH9
389 - Audit note: Null-clearing issue is real for providerThreadId; adapterKey part overstated.
390
391- [ ] `C052` Race condition: `processHandle` may be `null` when `data` callback fires, since it's assigned after `Bun.spawn` returns. Consider initializing `BunPtyProcess` first, then passing it to the callback to avoid losing initial output. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
392 - Status: `TODO`
393 - Verdict: `valid`
394 - Severity: `Low`
395 - Area: `Other`
396 - File: `apps/server/src/terminal/Layers/BunPTY.ts:97`
397 - Threads: PRRT_kwDORLtfbc5w1CxE
398 - Audit note: Data callback may race before processHandle assignment.
399
400- [ ] `C056` When `onOpenChange` is provided without `open`, the internal `_open` state never updates because `setOpenProp` takes precedence. Consider calling `_setOpen` when `openProp === undefined`, regardless of whether `setOpenProp` exists.
401 - Status: `TODO`
402 - Verdict: `partially-valid`
403 - Severity: `Low`
404 - Area: `Other`
405 - File: `apps/web/src/components/ui/sidebar.tsx:114`
406 - Threads: PRRT_kwDORLtfbc5wxvIq
407 - Audit note: Bug pattern exists, but current callsites mostly avoid triggering it.
408
409- [ ] `C057` The `resizable` object is recreated on every render, causing `SidebarRail`'s `useEffect` to repeatedly read localStorage and update the DOM. Consider memoizing the object with `useMemo`. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
410 - Status: `TODO`
411 - Verdict: `valid`
412 - Severity: `Low`
413 - Area: `Other`
414 - File: `apps/web/src/routes/_chat.$threadId.tsx:105`
415 - Threads: PRRT_kwDORLtfbc5wyWz4
416 - Audit note: Resizable object recreation still retriggers effect/storage reads.
417
418- [ ] `C058` When `localStorage.getItem()` returns `null`, `Number(null)` evaluates to `0`, which passes `Number.isFinite(0)`. This causes the sidebar to clamp to `minWidth` on first load, overriding the `DIFF_INLINE_DEFAULT_WIDTH` CSS clamp. Consider checking for `null` or empty string before parsing, e.g. guard with `storedWidth === null || storedWidth === ''`. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
419 - Status: `TODO`
420 - Verdict: `valid`
421 - Severity: `Low`
422 - Area: `Other`
423 - File: `apps/web/src/routes/_chat.$threadId.tsx:122`
424 - Threads: PRRT_kwDORLtfbc5wnVsX
425 - Audit note: Number(null) -> 0 path still forces min width on initial load.
426
427- [ ] `C060` `defaultModel` should be `Schema.optional(Schema.NullOr(Schema.String))` to allow clearing the value. Currently there's no way to reset it to `null` since omitting means "no change" in patch semantics. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary>
428 - Status: `TODO`
429 - Verdict: `valid`
430 - Severity: `Low`
431 - Area: `Other`
432 - File: `packages/contracts/src/orchestration.ts:253`
433 - Threads: PRRT_kwDORLtfbc5whxJC
434 - Audit note: Schema still cannot express null clear for defaultModel patch.
435
436## Closed Invalid Items
437
438- [x] `C014` Engine error handler catches all errors including non-invariant ones
439 - Status: `CLOSED_INVALID`
440 - Severity: `Medium`
441 - File: `apps/server/src/orchestration/Layers/OrchestrationEngine.ts:144`
442 - Threads: PRRT_kwDORLtfbc5wkPaJ
443 - Rationale: Broad catch is intentional for worker liveness; transactional dispatch path prevents the claimed non-invariant idempotency break in current design.
444
445- [x] `C021` Shared mutable default metadata object causes stale eventId
446 - Status: `CLOSED_INVALID`
447 - Severity: `Medium`
448 - File: `apps/server/src/orchestration/decider.ts:27`
449 - Threads: PRRT_kwDORLtfbc5wkPaA
450 - Rationale: Stale-eventId claim no longer applies; eventId is regenerated per event.
451
452- [x] `C025` Duplicated checkpoint ref computation across two files
453 - Status: `CLOSED_INVALID`
454 - Severity: `Medium`
455 - File: `apps/server/src/wsServer.ts:128`
456 - Threads: PRRT_kwDORLtfbc5wvwag
457 - Rationale: No longer duplicated; checkpoint ref helper now centralized.
458
459- [x] `C031` Revert uses wrong turn count from positional inference
460 - Status: `CLOSED_INVALID`
461 - Severity: `Medium`
462 - File: `apps/web/src/session-logic.ts:127`
463 - Threads: PRRT_kwDORLtfbc5v9SCp
464 - Rationale: Revert now uses explicit checkpointTurnCount first; positional fallback is non-primary.
465
466- [x] `C036` Duplicate `checkpointRefForThreadTurn` function in two production files
467 - Status: `CLOSED_INVALID`
468 - Severity: `Low`
469 - File: `apps/server/src/checkpointing/Layers/CheckpointStore.ts:284`
470 - Threads: PRRT_kwDORLtfbc5wiqFX
471 - Rationale: No longer duplicated; single production source via Refs.ts.
472
473- [x] `C055` Duplicate `checkpointRefForThreadTurn` function across files
474 - Status: `CLOSED_INVALID`
475 - Severity: `Low`
476 - File: `apps/server/src/wsServer.ts:128`
477 - Threads: PRRT_kwDORLtfbc5wkPaG
478 - Rationale: No longer duplicated; helper is centralized.
.plans/17-claude-agent.md deleted-441
...@@ -1,441 +0,0 @@
1# Plan: Claude Code Integration (Orchestration Architecture)
2
3## Why this plan was rewritten
4
5The previous plan targeted a pre-orchestration architecture (`ProviderManager`, provider-native WS event methods, and direct provider UI wiring). The current app now routes everything through:
6
71. `orchestration.dispatchCommand` (client intent)
82. `OrchestrationEngine` (decide + persist + publish domain events)
93. `ProviderCommandReactor` (domain intent -> `ProviderService`)
104. `ProviderService` (adapter routing + canonical runtime stream)
115. `ProviderRuntimeIngestion` (provider runtime -> internal orchestration commands)
126. `orchestration.domainEvent` (single push channel consumed by web)
13
14Claude integration must plug into this path instead of reintroducing legacy provider-specific flows.
15
16---
17
18## Current constraints to design around (post-Stage 1)
19
201. Provider runtime ingestion expects canonical `ProviderRuntimeEvent` shapes, not provider-native payloads.
212. Start input now uses typed `providerOptions` and generic `resumeCursor`; top-level provider-specific fields were removed.
223. `resumeCursor` is intentionally opaque outside adapters and must never be synthesized from `providerThreadId`.
234. `ProviderService` still requires adapter `startSession()` to return a `ProviderSession` with `threadId`.
245. Checkpoint revert currently calls `providerService.rollbackConversation()`, so Claude adapter needs a rollback strategy compatible with current reactor behavior.
256. Web currently marks Claude as unavailable (`"Claude Code (soon)"`) and model picker is Codex-only.
26
27---
28
29## Architecture target
30
31Add Claude as a first-class provider adapter that emits canonical runtime events and works with existing orchestration reactors without adding new WS channels or bypass paths.
32
33Key decisions:
34
351. Keep orchestration provider-agnostic; adapt Claude inside adapter/layer boundaries.
362. Use the existing canonical runtime stream (`ProviderRuntimeEvent`) as the only ingestion contract.
373. Keep provider session routing in `ProviderService` and `ProviderSessionDirectory`.
384. Add explicit provider selection to turn-start intent so first turn can start Claude session intentionally.
39
40---
41
42## Phase 1: Contracts and command shape updates
43
44### 1.1 Provider-aware model contract
45
46Update `packages/contracts/src/model.ts` so model resolution can be provider-aware instead of Codex-only.
47
48Expected outcomes:
49
501. Introduce provider-scoped model lists (Codex + Claude).
512. Add helpers that resolve model by provider.
523. Preserve backwards compatibility for existing Codex defaults.
53
54### 1.2 Turn-start provider intent
55
56Update `packages/contracts/src/orchestration.ts`:
57
581. Add optional `provider: ProviderKind` to `ThreadTurnStartCommand`.
592. Carry provider through `ThreadTurnStartRequestedPayload`.
603. Keep existing command valid when provider is omitted.
61
62This removes the implicit “Codex unless session already exists” behavior as the only path.
63
64### 1.3 Provider session start input for Claude runtime knobs (completed)
65
66Update `packages/contracts/src/provider.ts`:
67
681. Move provider-specific start fields into typed `providerOptions`:
69 - `providerOptions.codex`
70 - `providerOptions.claudeCode`
712. Keep `resumeCursor` as the single cross-provider resume input in `ProviderSessionStartInput`.
723. Deprecate/remove `resumeThreadId` from the generic start contract.
734. Treat `resumeCursor` as adapter-owned opaque state.
74
75### 1.4 Contract tests (completed)
76
77Update/add tests in `packages/contracts/src/*.test.ts` for:
78
791. New command payload shape.
802. Provider-aware model resolution behavior.
813. Breaking-change expectations for removed top-level provider fields.
82
83---
84
85## Phase 2: Claude adapter implementation
86
87### 2.1 Add adapter service + layer
88
89Create:
90
911. `apps/server/src/provider/Services/ClaudeAdapter.ts`
922. `apps/server/src/provider/Layers/ClaudeAdapter.ts`
93
94Adapter must implement `ProviderAdapterShape<ProviderAdapterError>`.
95
96### 2.1.a SDK dependency and baseline config
97
98Add server dependency:
99
1001. `@anthropic-ai/claude-agent-sdk`
101
102Baseline adapter options to support from day one:
103
1041. `cwd`
1052. `model`
1063. `pathToClaudeCodeExecutable` (from `providerOptions.claudeCode.binaryPath`)
1074. `permissionMode` (from `providerOptions.claudeCode.permissionMode`)
1085. `maxThinkingTokens` (from `providerOptions.claudeCode.maxThinkingTokens`)
1096. `resume`
1107. `resumeSessionAt`
1118. `includePartialMessages`
1129. `canUseTool`
11310. `hooks`
11411. `env` and `additionalDirectories` (if needed for sandbox/workspace parity)
115
116### 2.2 Claude runtime bridge
117
118Implement a Claude runtime bridge (either directly in adapter layer or via dedicated manager file) that wraps Agent SDK query lifecycle.
119
120Required capabilities:
121
1221. Long-lived session context per adapter session.
1232. Multi-turn input queue.
1243. Interrupt support.
1254. Approval request/response bridge.
1265. Resume support via opaque `resumeCursor` (parsed inside Claude adapter only).
127
128#### 2.2.a Agent SDK details to preserve
129
130The adapter should explicitly rely on these SDK capabilities:
131
1321. `query()` returns an async iterable message stream and control methods (`interrupt`, `setModel`, `setPermissionMode`, `setMaxThinkingTokens`, account/status helpers).
1332. Multi-turn input is supported via async-iterable prompt input.
1343. Tool approval decisions are provided via `canUseTool`.
1354. Resume support uses `resume` and optional `resumeSessionAt`, both derived by parsing adapter-owned `resumeCursor`.
1365. Hooks can be used for lifecycle signals (`Stop`, `PostToolUse`, etc.) when we need adapter-originated checkpoint/runtime events.
137
138#### 2.2.b Effect-native session lifecycle skeleton
139
140```ts
141import { query } from "@anthropic-ai/claude-agent-sdk";
142import { Effect } from "effect";
143
144const acquireSession = (input: ProviderSessionStartInput) =>
145 Effect.acquireRelease(
146 Effect.tryPromise({
147 try: async () => {
148 const claudeOptions = input.providerOptions?.claudeCode;
149 const resumeState = readClaudeResumeState(input.resumeCursor);
150 const abortController = new AbortController();
151 const result = query({
152 prompt: makePromptAsyncIterable(),
153 options: {
154 cwd: input.cwd,
155 model: input.model,
156 permissionMode: claudeOptions?.permissionMode,
157 maxThinkingTokens: claudeOptions?.maxThinkingTokens,
158 pathToClaudeCodeExecutable: claudeOptions?.binaryPath,
159 resume: resumeState?.threadId,
160 resumeSessionAt: resumeState?.sessionAt,
161 signal: abortController.signal,
162 includePartialMessages: true,
163 canUseTool: makeCanUseTool(),
164 hooks: makeClaudeHooks(),
165 },
166 });
167 return { abortController, result };
168 },
169 catch: (cause) =>
170 new ProviderAdapterProcessError({
171 provider: "claudeCode",
172 sessionId: "pending",
173 detail: "Failed to start Claude runtime session.",
174 cause,
175 }),
176 }),
177 ({ abortController }) => Effect.sync(() => abortController.abort()),
178 );
179```
180
181#### 2.2.c AsyncIterable -> Effect Stream integration
182
183Preferred when available in the pinned Effect version:
184
185```ts
186const sdkMessageStream = Stream.fromAsyncIterable(
187 session.result,
188 (cause) =>
189 new ProviderAdapterProcessError({
190 provider: "claudeCode",
191 sessionId,
192 detail: "Claude runtime stream failed.",
193 cause,
194 }),
195);
196```
197
198Portable fallback (already aligned with current server patterns):
199
200```ts
201const sdkMessageStream = Stream.async<ClaudeSdkMessage, ProviderAdapterProcessError>((emit) => {
202 let cancelled = false;
203 void (async () => {
204 try {
205 for await (const message of session.result) {
206 if (cancelled) break;
207 emit.single(message);
208 }
209 emit.end();
210 } catch (cause) {
211 emit.fail(
212 new ProviderAdapterProcessError({
213 provider: "claudeCode",
214 sessionId,
215 detail: "Claude runtime stream failed.",
216 cause,
217 }),
218 );
219 }
220 })();
221 return Effect.sync(() => {
222 cancelled = true;
223 });
224});
225```
226
227### 2.3 Canonical event mapping
228
229Claude adapter must translate Agent SDK output into canonical `ProviderRuntimeEvent`.
230
231Initial mapping target:
232
2331. assistant text deltas -> `content.delta`
2342. final assistant text -> `item.completed` and/or `turn.completed`
2353. approval requests -> `request.opened`
2364. approval results -> `request.resolved`
2375. system lifecycle -> `session.*`, `thread.*`, `turn.*`
2386. errors -> `runtime.error`
2397. plan/proposed-plan content when derivable
240
241Implementation note:
242
2431. Keep raw Claude message on `raw` for debugging.
2442. Prefer canonical item/request kinds over provider-native enums.
2453. If Claude emits extra event kinds we do not model yet, map them to `tool.summary`, `runtime.warning`, or `unknown`-compatible payloads instead of dropping silently.
246
247### 2.4 Resume cursor strategy
248
249Define Claude-owned opaque resume state, e.g.:
250
251```ts
252interface ClaudeResumeCursor {
253 readonly version: 1;
254 readonly threadId?: string;
255 readonly sessionAt?: string;
256}
257```
258
259Rules:
260
2611. Serialize only adapter-owned state into `resumeCursor`.
2622. Parse/validate only inside Claude adapter.
2633. Store updated cursor when Claude runtime yields enough data to resume safely.
2644. Never overload orchestration thread id as Claude thread id.
265
266### 2.5 Interrupt and stop semantics
267
268Map orchestration stop/interrupt expectations onto SDK controls:
269
2701. `interruptTurn()` -> active query interrupt.
2712. `stopSession()` -> close session resources and prevent future sends.
2723. `rollbackThread()` -> see Phase 4.
273
274---
275
276## Phase 3: Provider service and composition
277
278### 3.1 Register Claude adapter
279
280Update provider registry layer to include Claude:
281
2821. add `claudeCode` -> `ClaudeAdapter`
2832. ensure `ProviderService.listProviderStatuses()` reports Claude availability
284
285### 3.2 Persist provider binding
286
287Current `ProviderSessionDirectory` already stores provider/thread binding and opaque `resumeCursor`.
288
289Required validation:
290
2911. Claude bindings survive restart.
2922. resume cursor remains opaque and round-trips untouched.
2933. stopAll + restart can recover Claude sessions when possible.
294
295### 3.3 Provider start routing
296
297Update `ProviderCommandReactor` / orchestration flow:
298
2991. If a thread turn start requests `provider: "claudeCode"`, start Claude if no active session exists.
3002. If a thread already has Claude session binding, reuse it.
3013. If provider switches between Codex and Claude, explicitly stop/rebind before next send.
302
303---
304
305## Phase 4: Checkpoint and revert strategy
306
307Claude does not necessarily expose the same conversation rewind primitive as Codex app-server. Current architecture expects `providerService.rollbackConversation()`.
308
309Pick one explicit strategy:
310
311### Option A: provider-native rewind
312
313If SDK/runtime supports safe rewind:
314
3151. implement in Claude adapter
3162. keep `CheckpointReactor` unchanged
317
318### Option B: session restart + state truncation shim
319
320If no native rewind exists:
321
3221. Claude adapter returns successful rollback by:
323 - stopping current Claude session
324 - clearing/rewriting stored Claude resume cursor to last safe resumable point
325 - forcing next turn to recreate session from persisted orchestration state
3262. Document that rollback is “conversation reset to checkpoint boundary”, not provider-native turn deletion.
327
328Whichever option is chosen:
329
3301. behavior must be deterministic
3312. checkpoint revert tests must pass under orchestration expectations
3323. user-visible activity log should explain failures clearly when provider rollback is impossible
333
334---
335
336## Phase 5: Web integration
337
338### 5.1 Provider picker and model picker
339
340Update web state/UI:
341
3421. allow choosing Claude as thread provider before first turn
3432. show Claude model list from provider-aware model helpers
3443. preserve existing Codex default behavior when provider omitted
345
346Likely touch points:
347
3481. `apps/web/src/store.ts`
3492. `apps/web/src/components/ChatView.tsx`
3503. `apps/web/src/types.ts`
3514. `packages/shared/src/model.ts`
352
353### 5.2 Settings for Claude executable/options
354
355Add app settings if needed for:
356
3571. Claude binary path
3582. default permission mode
3593. default max thinking tokens
360
361Do not hardcode provider-specific config into generic session state if it belongs in app settings or typed `providerOptions`.
362
363### 5.3 Session rendering
364
365No new WS channel should be needed. Claude should appear through existing:
366
3671. thread messages
3682. activities/worklog
3693. approvals
3704. session state
3715. checkpoints/diffs
372
373---
374
375## Phase 6: Testing strategy
376
377### 6.1 Contract tests
378
379Cover:
380
3811. provider-aware model schemas
3822. provider field on turn-start command
3833. provider-specific start options schema
384
385### 6.2 Adapter layer tests
386
387Add `ClaudeAdapter.test.ts` covering:
388
3891. session start
3902. event mapping
3913. approval bridge
3924. resume cursor parse/serialize
3935. interrupt behavior
3946. rollback behavior or explicit unsupported error path
395
396Use SDK-facing layer tests/mocks only at the boundary. Do not mock orchestration business logic in higher-level tests.
397
398### 6.3 Provider service integration tests
399
400Extend provider integration coverage so Claude is exercised through `ProviderService`:
401
4021. start Claude session
4032. send turn
4043. receive canonical runtime events
4054. restart/recover using persisted binding
406
407### 6.4 Orchestration integration tests
408
409Add/extend integration tests around:
410
4111. first-turn provider selection
4122. Claude approval requests routed through orchestration
4133. Claude runtime ingestion -> messages/activities/session updates
4144. checkpoint revert behavior under Claude
4155. stopAll/restart recovery
416
417These should validate real orchestration flows, not just adapter behavior.
418
419---
420
421## Phase 7: Rollout order
422
423Recommended implementation order:
424
4251. contracts/provider-aware models
4262. provider field on turn-start
4273. Claude adapter skeleton + start/send/stream
4284. canonical event mapping
4295. provider registry/service wiring
4306. orchestration recovery + checkpoint strategy
4317. web provider/model picker
4328. full integration tests
433
434---
435
436## Non-goals
437
4381. Reintroducing provider-specific WS methods/channels.
4392. Storing provider-native thread ids as orchestration ids.
4403. Bypassing orchestration engine for Claude-specific UI flows.
4414. Encoding Claude resume semantics outside adapter-owned `resumeCursor`.
.plans/17-provider-neutral-runtime-determinism.md deleted-109
...@@ -1,109 +0,0 @@
1# Plan: Provider-Neutral Runtime Determinism and Flake Elimination
2
3## Summary
4Replace timing-sensitive websocket and orchestration behavior with explicit typed runtime boundaries, ordered push delivery, and server-owned completion receipts. The cutover is broad and single-shot: no compatibility shim, no mixed old/new transport. The design must reduce flakes without baking Codex-specific lifecycle semantics into generic runtime code.
5
6## Implementation Status
7
8All 7 sections are implemented. CI passes (format, lint, typecheck, test, browser test, build). One deferred item remains: the shared `WsTestClient` helper from section 7 — tests use direct transport subscription and receipt-based waits instead.
9
10### New files
11
12| File | Purpose |
13|------|---------|
14| `packages/shared/src/DrainableWorker.ts` | Queue-based Effect worker with deterministic `drain` signal |
15| `packages/shared/src/schemaJson.ts` | Two-phase JSON→Schema decode helpers (`decodeJsonResult`, `formatSchemaError`) |
16| `apps/server/src/wsServer/pushBus.ts` | `ServerPushBus` — ordered typed push pipeline with auto-incrementing sequence |
17| `apps/server/src/wsServer/readiness.ts` | `ServerReadiness` — Deferred-based barriers for startup sequencing |
18| `apps/server/src/orchestration/Services/RuntimeReceiptBus.ts` | Receipt schema union: checkpoint captured, diff finalized, turn quiesced |
19| `apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts` | PubSub-backed receipt bus implementation |
20| `apps/server/src/watchFileWithStatPolling.ts` | Stat-polling file watcher for containers where `fs.watch` is unreliable |
21| `apps/server/vitest.config.ts` | Server-specific test config (timeout bumps) |
22| `apps/server/src/wsServer/pushBus.test.ts` | Push bus serialization and welcome-gating tests |
23| `packages/shared/src/DrainableWorker.test.ts` | Drainable worker enqueue/drain lifecycle tests |
24
25### Key modifications
26
27| File | Change |
28|------|--------|
29| `packages/contracts/src/ws.ts` | Channel-indexed `WsPushPayloadByChannel` map, `WsPush` union schema, `WsPushSequence` |
30| `apps/server/src/wsServer.ts` | Integrated `ServerPushBus` and `ServerReadiness`; welcome gated on readiness |
31| `apps/server/src/keybindings.ts` | Explicit runtime with `start`/`ready`/`snapshot`; dual `fs.watch` + stat-polling watcher |
32| `apps/web/src/wsTransport.ts` | Connection state machine (`connecting`→`open`→`reconnecting`→`closed`→`disposed`); two-phase decode at boundary; cached latest push by channel |
33| `apps/web/src/wsNativeApi.ts` | Removed decode logic; delegates to pre-validated transport messages |
34| `apps/server/src/orchestration/Layers/CheckpointReactor.ts` | Uses `DrainableWorker`; publishes completion receipts |
35| `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts` | Uses `DrainableWorker` for command processing |
36| `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` | Uses `DrainableWorker` for event ingestion |
37| `apps/server/integration/OrchestrationEngineHarness.integration.ts` | Receipt-based waits replace polling loops |
38
39## Key Changes
40### 1. Strengthen the generic boundaries, not the Codex boundary — DONE
41- `ProviderRuntimeEvent` remains the canonical provider event contract; `ProviderService` remains the only cross-provider facade.
42- Raw Codex payloads and event ordering stay isolated in `CodexAdapter.ts` and `codexAppServerManager.ts`.
43- `ProviderKind` was not expanded. The runtime stays provider-neutral by contract.
44
45### 2. Replace loose websocket envelopes with channel-indexed typed pushes — DONE
46- `packages/contracts/src/ws.ts` now derives push messages from a `WsPushPayloadByChannel` channel-to-schema map. `WsPush` is a union schema replacing `channel: string` + `data: unknown`.
47- Every server push carries `sequence: number`, auto-incremented in `ServerPushBus`.
48- `packages/shared/src/schemaJson.ts` provides structured decode diagnostics via `formatSchemaError`.
49- `packages/contracts/src/ws.test.ts` covers typed push envelope validation and channel/payload mismatch rejection.
50
51### 3. Introduce explicit server readiness and a single push pipeline — DONE
52- `apps/server/src/wsServer/pushBus.ts`: `ServerPushBus` with `publishAll` (broadcast) and `publishClient` (targeted) methods, backed by one ordered path. All pushes flow through it.
53- `apps/server/src/wsServer/readiness.ts`: `ServerReadiness` with Deferred-based barriers for HTTP listening, push bus, keybindings, terminal subscriptions, and orchestration subscriptions.
54- `server.welcome` is emitted only after connection-scoped and server-scoped readiness is complete.
55- `wsServer.ts` no longer publishes directly from ad hoc background streams.
56
57### 4. Turn background watchers into explicit runtimes — DONE
58- `apps/server/src/keybindings.ts` refactored as explicit `KeybindingsShape` service with `start`, `ready`, `snapshot` semantics.
59- Initial config load, cache warmup, and dual watcher attachment (`fs.watch` + `watchFileWithStatPolling`) complete before `ready` resolves.
60- `watchFileWithStatPolling.ts` is the thin adapter for environments where `fs.watch` is unreliable.
61
62### 5. Replace polling-based orchestration waiting with receipts — DONE
63- `RuntimeReceiptBus` service defines three receipt types: `CheckpointBaselineCapturedReceipt`, `CheckpointDiffFinalizedReceipt` (with `status: "ready"|"missing"|"error"`), and `TurnProcessingQuiescedReceipt`.
64- `CheckpointReactor`, `ProviderCommandReactor`, and `ProviderRuntimeIngestion` use `DrainableWorker` and publish receipts on completion.
65- Integration harness and checkpoint tests await receipts instead of polling snapshots and git refs.
66
67### 6. Centralize client transport state and decoding — DONE
68- `apps/web/src/wsTransport.ts` implements an explicit connection state machine: `connecting`, `open`, `reconnecting`, `closed`, `disposed`.
69- Two-phase decode (JSON parse → Schema validate) happens at the transport boundary. `wsNativeApi.ts` receives pre-validated messages.
70- Cached latest welcome/config modeled as explicit `latestPushByChannel` state.
71
72### 7. Replace ad hoc test helpers with semantic test clients — MOSTLY DONE
73- `DrainableWorker` replaces timing-sensitive `Effect.sleep` with deterministic `drain()` across reactor tests.
74- Orchestration harness waits on receipts/barriers instead of `waitForThread`, `waitForGitRef`, and retry loops.
75- Behavioral assertions moved to deterministic unit-style harnesses; narrow integration tests kept for real filesystem/socket behavior.
76- **Deferred:** Shared `WsTestClient` helper (connect, awaitSemanticWelcome, awaitTypedPush, trackSequence, matchRpcResponseById). Tests use direct transport subscription instead.
77
78## Provider-Coupling Guardrails
79- No generic runtime API may depend on Codex-native event names, thread IDs, or request payload shapes.
80- No readiness barrier may be defined as "Codex has emitted X." Readiness is owned by the server runtime, not by provider event order.
81- No websocket channel payload may contain raw provider-native payloads unless the channel is explicitly debug/internal.
82- Any provider-specific divergence must be exposed through provider capabilities from `ProviderService.getCapabilities()`, not `if provider === "codex"` branches in shared runtime code.
83- Generic tests must use canonical `ProviderRuntimeEvent` fixtures. Codex-specific ordering and translation tests stay in adapter/app-server suites only.
84- Keep UI/provider-specific knobs such as Codex-only options scoped to provider UX code. Do not pull them into generic transport or orchestration state.
85
86## Test Plan
87- Contracts:
88 - schema tests for typed push envelopes and structured decode diagnostics
89 - ordering tests for `sequence`
90- Server:
91 - readiness tests proving `server.welcome` cannot precede runtime readiness
92 - push bus tests proving terminal/config/orchestration pushes are serialized and typed
93 - keybindings runtime tests with fake watch source plus one real watcher integration test
94- Orchestration:
95 - receipt tests proving checkpoint refs and projections are complete before completion signals resolve
96 - replacement of polling-based checkpoint/integration waits with receipt-based waits
97- Web:
98 - transport tests for invalid JSON, invalid envelope, invalid payload, reconnect queue flushing, cached semantic state
99- Validation gate:
100 - `bun run lint`
101 - `bun run typecheck`
102 - `mise exec -- bun run test`
103 - repeated full-suite run after cutover to confirm flake removal
104
105## Assumptions and Defaults
106- This remains a single-provider product during the cutover, but the runtime contracts must stay provider-neutral.
107- No backward-compatibility layer is required for old websocket push envelopes.
108- The goal is deterministic runtime behavior first; reducing retries and sleeps in tests is a consequence, not the primary mechanism.
109- If a completion signal cannot be expressed provider-neutrally, it does not belong in the shared runtime layer and must stay adapter-local.
.plans/18-server-auth-model.md deleted-823
...@@ -1,823 +0,0 @@
1# Server Auth Model Plan
2
3## Purpose
4
5Define the long-term server auth architecture for T3 Code before first-class remote environments ship.
6
7This plan is deliberately broader than the current WebSocket token check and narrower than a complete remote collaboration system. The goal is to make the server secure by default, keep local desktop UX frictionless, and leave clean integration points for future remote access methods.
8
9This document is written in terms of Effect-native services and layers because auth needs to be a core runtime concern, not route-local glue code.
10
11## Primary goals
12
13- Make auth server-wide, not WebSocket-only.
14- Make insecure exposure hard to do accidentally.
15- Preserve zero-login local desktop UX for desktop-managed environments.
16- Support browser-native pairing and session auth.
17- Leave room for native/mobile credentials later without rewriting the server boundary.
18- Keep auth separate from transport and launch method.
19
20## Non-goals
21
22- Full multi-user authorization and RBAC.
23- OAuth / SSO / enterprise identity.
24- Passkeys or biometric UX in v1.
25- Syncing auth state across environments.
26- Designing the full remote environment product in this document.
27
28## Core decisions
29
30### 1. Auth is a server concern
31
32Every privileged surface of the T3 server must go through the same auth policy engine:
33
34- HTTP routes
35- WebSocket upgrades
36- RPC methods reached through WebSocket
37
38The current split where [`/ws`](../apps/server/src/ws.ts) checks `authToken` but routes in [`http.ts`](../apps/server/src/http.ts) do not is not sufficient for a remote-capable product.
39
40### 2. Pairing and session are different things
41
42The system should distinguish:
43
44- bootstrap credentials
45- session credentials
46
47Bootstrap credentials are short-lived and high-trust. They allow a client to become authenticated.
48
49Session credentials are the durable credentials used after pairing.
50
51Bootstrap should never become the long-lived request credential.
52
53### 3. Auth and transport are separate
54
55Auth must not be defined by how the client reached the server.
56
57Examples:
58
59- local desktop-managed server
60- LAN `ws://`
61- public `wss://`
62- tunneled `wss://`
63- SSH-forwarded `ws://127.0.0.1:<port>`
64
65All of these should feed into the same auth model.
66
67### 4. Exposure level changes defaults
68
69The more exposed an environment is, the narrower the safe default should be.
70
71Safe default expectations:
72
73- local desktop-managed: auto-pair allowed
74- loopback browser access: explicit bootstrap allowed
75- non-loopback bind: auth required
76- tunnel/public endpoint: auth required, explicit enablement required
77
78### 5. Browser and native clients may use different session credentials
79
80The auth model should support more than one session credential type even if only one ships first.
81
82Examples:
83
84- browser session cookie
85- native bearer/device token
86
87This should be represented in the model now, even if browser cookies are the first implementation.
88
89## Target auth domain
90
91### Route classes
92
93Every route or transport entrypoint should be classified as one of:
94
951. `public`
962. `bootstrap`
973. `authenticated`
98
99#### `public`
100
101Unauthenticated by definition.
102
103Should be extremely small. Examples:
104
105- static shell needed to render the pairing/login UI
106- favicon/assets required for the pairing screen
107- a minimal server health/version endpoint if needed
108
109#### `bootstrap`
110
111Used only to exchange a bootstrap credential for a session.
112
113Examples:
114
115- Initial bootstrap envelope over file descriptor at startup
116- `POST /api/auth/bootstrap`
117- `GET /api/auth/session` if unauthenticated checks are part of bootstrap UX
118
119#### `authenticated`
120
121Everything that reveals machine state or mutates it.
122
123Examples:
124
125- WebSocket upgrade
126- orchestration snapshot and events
127- terminal open/write/close
128- project search and file writes
129- git routes
130- attachments
131- project favicon lookup
132- server settings
133
134The default stance should be: if it touches the machine, it is authenticated.
135
136## Credential model
137
138### Bootstrap credentials
139
140Initial credential types to model:
141
142- `desktop-bootstrap`
143- `one-time-token`
144
145Possible future credential types:
146
147- `device-code`
148- `passkey-assertion`
149- `external-identity`
150
151#### `desktop-bootstrap`
152
153Used when the desktop shell manages the server and should be the only default pairing method for desktop-local environments.
154
155Properties:
156
157- launcher-provided
158- short-lived
159- one-time or bounded-use
160- never shown to the user as a reusable password
161
162#### `one-time-token`
163
164Used for explicit browser/mobile pairing flows.
165
166Properties:
167
168- short TTL
169- one-time use
170- safe to embed in a pairing URL fragment
171- exchanged for a session credential
172
173### Session credentials
174
175Initial credential types to model:
176
177- `browser-session-cookie`
178- `bearer-session-token`
179
180#### `browser-session-cookie`
181
182Primary browser credential.
183
184Properties:
185
186- signed
187- `HttpOnly`
188- bounded lifetime
189- revocable by server key rotation or session invalidation
190
191#### `bearer-session-token`
192
193Reserved for native/mobile or non-browser clients.
194
195Properties:
196
197- opaque token, not a bootstrap secret
198- long enough lifetime to survive reconnects
199- stored in secure client storage when available
200
201## Auth policy model
202
203Auth behavior should be driven by an explicit environment auth policy, not route-local heuristics.
204
205### Policy examples
206
207#### `DesktopManagedLocalPolicy`
208
209Default for desktop-managed local server.
210
211Allowed bootstrap methods:
212
213- `desktop-bootstrap`
214
215Allowed session methods:
216
217- `browser-session-cookie`
218
219Disabled by default:
220
221- `one-time-token`
222- `bearer-session-token`
223- password login
224- public pairing
225
226#### `LoopbackBrowserPolicy`
227
228Used for browser access on localhost without desktop-managed bootstrap.
229
230Allowed bootstrap methods:
231
232- `one-time-token`
233
234Allowed session methods:
235
236- `browser-session-cookie`
237
238#### `RemoteReachablePolicy`
239
240Used when binding non-loopback or using an explicit remote/tunnel workflow.
241
242Allowed bootstrap methods:
243
244- `one-time-token`
245- possibly `desktop-bootstrap` when a desktop shell is brokering access
246
247Allowed session methods:
248
249- `browser-session-cookie`
250- `bearer-session-token`
251
252#### `UnsafeNoAuthPolicy`
253
254Should exist only as an explicit escape hatch.
255
256Requirements:
257
258- explicit opt-in flag
259- loud startup warnings
260- never defaulted automatically
261
262## Effect-native service model
263
264### `ServerAuth`
265
266The main auth facade used by HTTP routes and WebSocket upgrade handling.
267
268Responsibilities:
269
270- classify requests
271- authenticate requests
272- authorize bootstrap attempts
273- create sessions from bootstrap credentials
274- enforce policy by environment mode
275
276Sketch:
277
278```ts
279export interface ServerAuthShape {
280 readonly getCapabilities: Effect.Effect<AuthCapabilities>;
281 readonly authenticateHttpRequest: (
282 request: HttpServerRequest.HttpServerRequest,
283 routeClass: RouteAuthClass,
284 ) => Effect.Effect<AuthContext, AuthError>;
285 readonly authenticateWebSocketUpgrade: (
286 request: HttpServerRequest.HttpServerRequest,
287 ) => Effect.Effect<AuthContext, AuthError>;
288 readonly exchangeBootstrapCredential: (
289 input: BootstrapExchangeInput,
290 ) => Effect.Effect<SessionGrant, AuthError>;
291}
292
293export class ServerAuth extends ServiceMap.Service<ServerAuth, ServerAuthShape>()(
294 "t3/ServerAuth",
295) {}
296```
297
298### `BootstrapCredentialService`
299
300Owns issuance, storage, validation, and consumption of bootstrap credentials.
301
302Responsibilities:
303
304- issue desktop bootstrap grants
305- issue one-time pairing tokens
306- validate TTL and single-use semantics
307- consume bootstrap grants atomically
308
309Sketch:
310
311```ts
312export interface BootstrapCredentialServiceShape {
313 readonly issueDesktopBootstrap: (
314 input: IssueDesktopBootstrapInput,
315 ) => Effect.Effect<BootstrapCredential>;
316 readonly issueOneTimeToken: (
317 input: IssueOneTimeTokenInput,
318 ) => Effect.Effect<BootstrapCredential>;
319 readonly consume: (
320 presented: PresentedBootstrapCredential,
321 ) => Effect.Effect<ConsumedBootstrapCredential, BootstrapCredentialError>;
322}
323```
324
325### `SessionCredentialService`
326
327Owns creation and validation of authenticated sessions.
328
329Responsibilities:
330
331- mint cookie sessions
332- mint bearer sessions
333- validate active session credentials
334- revoke sessions if needed later
335
336Sketch:
337
338```ts
339export interface SessionCredentialServiceShape {
340 readonly createBrowserSession: (
341 input: CreateSessionFromBootstrapInput,
342 ) => Effect.Effect<BrowserSessionGrant, SessionCredentialError>;
343 readonly createBearerSession: (
344 input: CreateSessionFromBootstrapInput,
345 ) => Effect.Effect<BearerSessionGrant, SessionCredentialError>;
346 readonly authenticateCookie: (
347 request: HttpServerRequest.HttpServerRequest,
348 ) => Effect.Effect<AuthContext, SessionCredentialError>;
349 readonly authenticateBearer: (
350 request: HttpServerRequest.HttpServerRequest,
351 ) => Effect.Effect<AuthContext, SessionCredentialError>;
352}
353```
354
355### `ServerAuthPolicy`
356
357Pure policy/config service that decides which credential types are allowed.
358
359Responsibilities:
360
361- map runtime mode and bind/exposure settings to allowed auth methods
362- answer whether a route can be public
363- answer whether remote exposure requires auth
364
365This should stay mostly pure and cheap to test.
366
367### `ServerSecretStore`
368
369Owns long-lived server signing keys and secrets.
370
371Responsibilities:
372
373- get or create signing key
374- rotate signing key
375- abstract secure OS-backed storage vs filesystem fallback
376
377Important:
378
379- prefer platform secure storage when available
380- support hardened filesystem fallback for headless/server-only environments
381
382### `BrowserSessionCookieCodec`
383
384Focused utility service for cookie encode/decode/signing behavior.
385
386This should not own policy. It should only own the cookie format.
387
388### `AuthRouteGuards`
389
390Thin helper layer used by routes to enforce auth consistently.
391
392Responsibilities:
393
394- require auth for HTTP route handlers
395- classify route auth mode
396- convert auth failures into `401` / `403`
397
398This prevents every route from re-implementing the same pattern.
399
400Integrates with `HttpRouter.middleware` to enforce auth consistently.
401
402## Suggested layer graph
403
404```text
405ServerSecretStore
406 ├─> BootstrapCredentialService
407 ├─> BrowserSessionCookieCodec
408 └─> SessionCredentialService
409
410ServerAuthPolicy
411 ├─> BootstrapCredentialService
412 ├─> SessionCredentialService
413 └─> ServerAuth
414
415ServerAuth
416 └─> AuthRouteGuards
417```
418
419Layer naming should follow existing repo style:
420
421- `ServerSecretStoreLive`
422- `BootstrapCredentialServiceLive`
423- `SessionCredentialServiceLive`
424- `ServerAuthPolicyLive`
425- `ServerAuthLive`
426- `AuthRouteGuardsLive`
427
428## High-level implementation examples
429
430### Example: WebSocket upgrade auth
431
432Current state:
433
434- `authToken` query param is checked in [`ws.ts`](../apps/server/src/ws.ts)
435
436Target shape:
437
438```ts
439const websocketUpgradeAuth = HttpMiddleware.make((httpApp) =>
440 Effect.gen(function* () {
441 const request = yield* HttpServerRequest.HttpServerRequest;
442 const serverAuth = yield* ServerAuth;
443 yield* serverAuth.authenticateWebSocketUpgrade(request);
444 return yield* httpApp;
445 }),
446);
447```
448
449Then the `/ws` route becomes:
450
451```ts
452export const websocketRpcRouteLayer = HttpRouter.add(
453 "GET",
454 "/ws",
455 rpcWebSocketHttpEffect.pipe(
456 websocketUpgradeAuth,
457 Effect.catchTag("AuthError", (error) => toUnauthorizedResponse(error)),
458 ),
459);
460```
461
462This keeps the route itself declarative and makes auth compose like normal HTTP middleware.
463
464### Example: authenticated HTTP route
465
466For routes like attachments or project favicon:
467
468```ts
469const authenticatedRoute = (routeClass: RouteAuthClass) =>
470 HttpMiddleware.make((httpApp) =>
471 Effect.gen(function* () {
472 const request = yield* HttpServerRequest.HttpServerRequest;
473 const serverAuth = yield* ServerAuth;
474 yield* serverAuth.authenticateHttpRequest(request, routeClass);
475 return yield* httpApp;
476 }),
477 );
478```
479
480Then:
481
482```ts
483export const attachmentsRouteLayer = HttpRouter.add(
484 "GET",
485 `${ATTACHMENTS_ROUTE_PREFIX}/*`,
486 serveAttachment.pipe(
487 authenticatedRoute(RouteAuthClass.Authenticated),
488 Effect.catchTag("AuthError", (error) => toUnauthorizedResponse(error)),
489 ),
490);
491```
492
493### Example: desktop bootstrap exchange
494
495The desktop shell launches the local server and gets a short-lived bootstrap grant through a trusted side channel.
496
497That grant is then exchanged for a browser cookie session when the renderer loads.
498
499Sketch:
500
501```ts
502const pairDesktopRenderer = Effect.gen(function* () {
503 const bootstrapService = yield* BootstrapCredentialService;
504 const credential = yield* bootstrapService.issueDesktopBootstrap({
505 audience: "desktop-renderer",
506 ttlMs: 30_000,
507 });
508 return credential;
509});
510```
511
512The renderer then calls a bootstrap endpoint and receives a cookie session. The bootstrap credential is consumed and becomes invalid.
513
514### Example: one-time pairing URL
515
516For browser-driven pairing:
517
518```ts
519const createPairingToken = Effect.gen(function* () {
520 const bootstrapService = yield* BootstrapCredentialService;
521 return yield* bootstrapService.issueOneTimeToken({
522 ttlMs: 5 * 60_000,
523 audience: "browser",
524 });
525});
526```
527
528The server can emit a pairing URL where the token lives in the URL fragment so it is not automatically sent to the server before the client explicitly exchanges it.
529
530## Sequence diagrams
531
532These flows are meant to anchor the auth model in concrete user journeys.
533
534The important invariant across all of them is:
535
536- access method is not the auth method
537- launch method is not the auth method
538- bootstrap credential is not the session credential
539
540### Normal desktop user
541
542This is the default desktop-managed local flow.
543
544The desktop shell is trusted to bootstrap the local renderer, but the renderer should still exchange that one-time bootstrap grant for a normal browser session cookie.
545
546```text
547Participants:
548 DesktopMain = Electron main
549 SecretStore = secure local secret backend
550 T3Server = local backend child process
551 Frontend = desktop renderer
552
553DesktopMain -> SecretStore : getOrCreate("server-signing-key")
554SecretStore --> DesktopMain : signing key available
555
556DesktopMain -> T3Server : spawn server (--bootstrap-fd ...)
557DesktopMain -> T3Server : send desktop bootstrap envelope
558note over T3Server : policy = DesktopManagedLocalPolicy
559note over T3Server : allowed pairing = desktop-bootstrap only
560
561Frontend -> DesktopMain : request local bootstrap grant
562DesktopMain --> Frontend : short-lived desktop bootstrap grant
563
564Frontend -> T3Server : POST /api/auth/bootstrap
565T3Server -> T3Server : validate desktop bootstrap grant
566T3Server -> T3Server : create browser session
567T3Server --> Frontend : Set-Cookie: session=...
568
569Frontend -> T3Server : GET /ws + authenticated cookie
570T3Server -> T3Server : validate cookie session
571T3Server --> Frontend : websocket accepted
572```
573
574### `npx t3` user
575
576This is the standalone local server flow.
577
578There is no trusted desktop shell here, so pairing should be explicit.
579
580```text
581Participants:
582 UserShell = npx t3 launcher
583 T3Server = standalone local server
584 Browser = browser tab
585
586UserShell -> T3Server : start server
587T3Server -> T3Server : getOrCreate("server-signing-key")
588note over T3Server : policy = LoopbackBrowserPolicy
589
590UserShell -> T3Server : issue one-time pairing token
591T3Server --> UserShell : pairing URL or pairing token
592
593UserShell --> Browser : open /pair?token=...
594
595Browser -> T3Server : GET /pair?token=...
596T3Server -> T3Server : validate one-time token
597T3Server -> T3Server : create browser session
598T3Server --> Browser : Set-Cookie: session=...
599T3Server --> Browser : redirect to app
600
601Browser -> T3Server : GET /ws + authenticated cookie
602T3Server --> Browser : websocket accepted
603```
604
605### Phone user with tunneled host
606
607This is the explicit remote access flow for a browser on another device.
608
609The tunnel only provides reachability. It must not imply trust.
610
611Recommended UX:
612
613- desktop shows a QR code
614- desktop also shows a copyable pairing URL
615- if the phone opens the host URL without a valid token, the server should render a login or pairing screen rather than granting access
616
617```text
618Participants:
619 DesktopUser = user at the host machine
620 DesktopMain = desktop app
621 Tunnel = tunnel provider
622 T3Server = T3 server
623 PhoneBrowser = mobile browser
624
625DesktopUser -> DesktopMain : enable remote access via tunnel
626DesktopMain -> T3Server : switch policy to RemoteReachablePolicy
627DesktopMain -> Tunnel : publish local T3 endpoint
628Tunnel --> DesktopMain : public https/wss URL
629
630DesktopMain -> T3Server : issue one-time pairing token
631T3Server --> DesktopMain : pairing token
632DesktopMain -> DesktopUser : show QR code / shareable URL
633
634DesktopUser -> PhoneBrowser : scan QR / open URL
635PhoneBrowser -> Tunnel : GET https://public-host/pair?token=...
636Tunnel -> T3Server : forward request
637T3Server -> T3Server : validate one-time token
638T3Server -> T3Server : create mobile browser session
639T3Server --> PhoneBrowser : Set-Cookie: session=...
640T3Server --> PhoneBrowser : redirect to app
641
642PhoneBrowser -> Tunnel : GET /ws + authenticated cookie
643Tunnel -> T3Server : forward websocket upgrade
644T3Server --> PhoneBrowser : websocket accepted
645```
646
647### Phone user with private network
648
649This is operationally similar to the tunnel flow, but the access endpoint is on a private network such as Tailscale.
650
651The auth flow should stay the same.
652
653```text
654Participants:
655 DesktopUser = user at the host machine
656 T3Server = T3 server
657 PrivateNet = tailscale / private LAN
658 PhoneBrowser = mobile browser
659
660DesktopUser -> T3Server : enable private-network access
661T3Server -> T3Server : switch policy to RemoteReachablePolicy
662DesktopUser -> T3Server : issue one-time pairing token
663T3Server --> DesktopUser : pairing URL / QR
664
665DesktopUser -> PhoneBrowser : open private-network URL
666PhoneBrowser -> PrivateNet : GET http(s)://private-host/pair?token=...
667PrivateNet -> T3Server : route request
668T3Server -> T3Server : validate one-time token
669T3Server -> T3Server : create mobile browser session
670T3Server --> PhoneBrowser : Set-Cookie: session=...
671T3Server --> PhoneBrowser : redirect to app
672
673PhoneBrowser -> PrivateNet : GET /ws + authenticated cookie
674PrivateNet -> T3Server : websocket upgrade
675T3Server --> PhoneBrowser : websocket accepted
676```
677
678### Desktop user adding new SSH hosts
679
680SSH should be treated as launch and reachability plumbing, not as the long-term auth model.
681
682The desktop app uses SSH to start or reach the remote server, then the renderer pairs against that server using the same bootstrap/session split as every other environment.
683
684```text
685Participants:
686 DesktopUser = local desktop user
687 DesktopMain = desktop app
688 SSH = ssh transport/session
689 RemoteHost = remote machine
690 RemoteT3 = remote T3 server
691 Frontend = desktop renderer
692
693DesktopUser -> DesktopMain : add SSH host
694DesktopMain -> SSH : connect to remote host
695SSH -> RemoteHost : probe environment / verify t3 availability
696DesktopMain -> SSH : run remote launch command
697SSH -> RemoteHost : t3 remote launch --json
698RemoteHost -> RemoteT3 : start or reuse server
699RemoteT3 --> RemoteHost : port + environment metadata
700RemoteHost --> SSH : launch result JSON
701SSH --> DesktopMain : remote server details
702
703DesktopMain -> SSH : establish local port forward
704SSH --> DesktopMain : localhost:FORWARDED_PORT ready
705
706note over RemoteT3 : policy = RemoteReachablePolicy
707note over DesktopMain,RemoteT3 : desktop may use a trusted bootstrap flow here
708
709Frontend -> DesktopMain : request bootstrap for selected environment
710DesktopMain --> Frontend : short-lived bootstrap grant
711
712Frontend -> RemoteT3 : POST /api/auth/bootstrap via forwarded port
713RemoteT3 -> RemoteT3 : validate bootstrap grant
714RemoteT3 -> RemoteT3 : create browser session
715RemoteT3 --> Frontend : Set-Cookie: session=...
716
717Frontend -> RemoteT3 : GET /ws + authenticated cookie
718RemoteT3 --> Frontend : websocket accepted
719```
720
721## Storage decisions
722
723### Server secrets
724
725Use a `ServerSecretStore` abstraction.
726
727Preferred order (use a layer for each, resolve on startup):
728
7291. OS secure storage if available
7302. hardened filesystem fallback if not
731
732The filesystem fallback should store only opaque signing material with strict file permissions. It should not store user passwords or reusable third-party credentials.
733
734### Client credentials
735
736Client-side credential persistence should prefer secure storage when available:
737
738- desktop: OS keychain / secure store
739- mobile: platform secure storage
740- browser: cookie session for browser auth
741
742This concern should stay in the client shell/runtime layer, not the server auth layer.
743
744## What to build now
745
746These are the parts worth building before remote environments ship:
747
7481. `ServerAuth` service boundary.
7492. route classification and route guards.
7503. `ServerSecretStore` abstraction.
7514. bootstrap vs session credential split.
7525. browser session cookie codec as one session method.
7536. explicit auth capabilities/config surfaced in contracts.
754
755Even if only one pairing flow is used initially, these seams will keep future remote and mobile work contained.
756
757## What to add as part of first remote-capable auth
758
7591. Browser pairing flow using one-time bootstrap token and cookie session.
7602. Desktop-managed auto-bootstrap for the local desktop-managed environment.
7613. Auth-required defaults for any non-loopback or explicitly published server.
7624. Explicit environment auth policy selection instead of scattered `if (host !== localhost)` checks.
763
764## What to defer
765
766- passkeys / WebAuthn
767- iCloud Keychain / Face ID-specific UX
768- multi-user permissions
769- collaboration roles
770- OAuth / SSO
771- polished session management UI
772- complex device approval flows
773
774These can all sit on top of the same bootstrap/session/service split.
775
776## Relationship to future remote environments
777
778Remote access is one reason this auth model matters, but the auth model should not be remote-shaped.
779
780Keep the design focused on:
781
782- one T3 server
783- one auth policy
784- multiple credential types
785- multiple future access methods
786
787That keeps the server auth model stable even as access methods expand later.
788
789## Recommended implementation order
790
791### Phase 1
792
793- Introduce route auth classes.
794- Add `ServerAuth` and `AuthRouteGuards`.
795- Move existing `authToken` check behind `ServerAuth`.
796- Require auth for all privileged HTTP routes as well as WebSocket.
797
798### Phase 2
799
800- Add `ServerSecretStore` service with platform-specific layer implementations.
801 - `layerOSXKeychain`, `layer
802- Add bootstrap/session split.
803- Add browser session cookie support.
804- Add one-time bootstrap exchange endpoint.
805
806### Phase 3
807
808- Add desktop bootstrap flow on top of the same services.
809- Make desktop-managed local environments default to bootstrap-only pairing.
810- Surface auth capabilities in shared contracts and renderer bootstrap.
811
812### Phase 4
813
814- Add non-browser bearer session support if mobile/native needs it.
815- Add richer policy modes for remote-reachable environments.
816
817## Acceptance criteria
818
819- No privileged HTTP or WebSocket path bypasses auth policy.
820- Local desktop-managed flows still avoid a visible login screen.
821- Non-loopback or published environments require explicit authenticated pairing by default.
822- Bootstrap and session credentials are distinct in code and in behavior.
823- Auth logic is centralized in Effect services/layers rather than route-local branching.
.plans/19-remote-endpoints-hosted-static.md deleted-349
...@@ -1,349 +0,0 @@
1# Remote Endpoints and Hosted Static App Plan
2
3## Purpose
4
5Make remote access feel first-class while keeping the free DIY path open.
6
7The immediate product goal is:
8
9- users can expose a backend through LAN, their own Tailscale, MagicDNS, a manual HTTPS endpoint, or later T3 Tunnel
10- users can generate a hosted pairing link for `app.t3.codes`
11- the hosted app can pair, persist, reconnect, and operate against saved environments without requiring a backend at the hosted app origin
12- all transports reuse the same backend auth, WebSocket runtime, saved environment registry, and pairing UX
13
14This plan intentionally leaves the paid T3 cloud tunnel fabric out of scope. It defines the OSS foundation that T3 Tunnel should later plug into.
15
16## Current State
17
18Already present or in progress:
19
20- Server auth distinguishes bootstrap credentials from session credentials.
21- One-time pairing credentials can be exchanged for browser sessions or bearer sessions.
22- Saved remote environments store `httpBaseUrl`, `wsBaseUrl`, and a bearer token.
23- Remote environment WebSocket connections use a short-lived WebSocket token.
24- Pairing URLs can carry tokens in the URL fragment.
25- Hosted `/pair?host=...#token=...` can add a saved environment.
26- Hosted static startup can avoid assuming the page origin is the backend.
27
28Main gaps:
29
30- Reachability is represented ad hoc as `endpointUrl`, manual host input, or saved environment URLs.
31- Desktop exposure, hosted pairing, manual remote environments, and future tunnels do not share one endpoint model.
32- Tailscale/MagicDNS endpoints are not detected or surfaced.
33- Hosted-static empty/offline states are still thin.
34- Browser compatibility is not explicitly modeled, especially HTTPS hosted app to HTTP backend mixed-content failure.
35
36## Core Decision: Add `AdvertisedEndpoint`
37
38Add a new first-class contract instead of extending the environment descriptor.
39
40### Why not extend `ExecutionEnvironmentDescriptor`
41
42`ExecutionEnvironmentDescriptor` answers: "What environment is this?"
43
44Examples:
45
46- environment id
47- label
48- platform
49- server version
50- capabilities
51
52`AdvertisedEndpoint` answers: "How can a client reach this environment right now?"
53
54Examples:
55
56- loopback URL
57- LAN URL
58- Tailscale IP URL
59- MagicDNS/Serve URL
60- manual URL
61- future T3 Tunnel URL
62- browser compatibility and exposure level
63
64Those are different lifecycles. One environment can have many endpoints, endpoints can appear/disappear as network interfaces change, and the same descriptor is returned regardless of which endpoint the client used. Extending the descriptor would blur environment identity with transport reachability and make saved environments harder to reason about.
65
66### Target Contract
67
68Add a schema in `packages/contracts`, likely `remoteAccess.ts`:
69
70```ts
71type AdvertisedEndpointProvider =
72 | "loopback"
73 | "lan"
74 | "tailscale-ip"
75 | "tailscale-magicdns"
76 | "manual"
77 | "t3-tunnel";
78
79type AdvertisedEndpointVisibility = "local" | "private-network" | "tailnet" | "public";
80
81type AdvertisedEndpointCompatibility = {
82 hostedHttpsApp: "compatible" | "mixed-content-blocked" | "untrusted-certificate" | "unknown";
83 desktopApp: "compatible" | "unknown";
84};
85
86type AdvertisedEndpoint = {
87 id: string;
88 provider: AdvertisedEndpointProvider;
89 label: string;
90 httpBaseUrl: string;
91 wsBaseUrl: string;
92 visibility: AdvertisedEndpointVisibility;
93 compatibility: AdvertisedEndpointCompatibility;
94 source: "server" | "desktop" | "user";
95 status: "available" | "unavailable" | "unknown";
96 isDefault?: boolean;
97};
98```
99
100Keep the contract schema-only. All classification logic belongs in `packages/shared`, `apps/server`, `apps/desktop`, or `apps/web`.
101
102## HTTP/WS and HTTPS/WSS Readiness
103
104The codebase is partially ready, but the UX and compatibility model are not explicit enough.
105
106What is ready:
107
108- Remote target parsing already derives `ws://` from `http://` and `wss://` from `https://`.
109- Saved environments store both HTTP and WebSocket base URLs.
110- Remote auth uses bearer tokens instead of cookies, so cross-origin hosted clients are viable.
111- WebSocket connections can use a dynamically issued `wsToken`.
112- Server CORS support exists for browser remote auth endpoints.
113
114What is not solved by code alone:
115
116- `https://app.t3.codes` cannot reliably call `http://...` or `ws://...` endpoints because browsers block mixed content.
117- `wss://100.x.y.z:3773` needs a certificate the browser trusts. A raw Tailscale IP does not solve certificate trust.
118- LAN `http://192.168.x.y:3773` is usable from another desktop/native context but not from the hosted HTTPS app.
119- The UI needs to explain why an endpoint is copyable for desktop pairing but not hosted-app compatible.
120
121Policy:
122
123- Support both HTTP/WS and HTTPS/WSS at the runtime layer.
124- Mark endpoint compatibility at the product layer.
125- Generate `app.t3.codes` links only from endpoints that are likely hosted-browser compatible, or show a warning with an explicit fallback.
126
127## Architecture
128
129### Endpoint Sources
130
131Endpoint records can come from several providers:
132
1331. **Server runtime**
134 - headless bind host and port
135 - server-known explicit advertised host config
136
1372. **Desktop shell**
138 - loopback backend URL
139 - LAN exposure state
140 - network interface discovery
141 - Tailscale CLI/status discovery
142
1433. **User configuration**
144 - manually added hostnames
145 - preferred endpoint labels
146 - hidden/disabled endpoints
147
1484. **Future cloud provider**
149 - T3 Tunnel endpoint
150 - billing/account status
151 - tunnel lifecycle state
152
153### Endpoint Registry
154
155Create a central runtime registry:
156
157- `packages/contracts/src/remoteAccess.ts`
158- `packages/shared/src/remoteAccess.ts` for URL normalization and compatibility classification
159- `apps/server/src/remoteAccess/*` for server/headless endpoints
160- `apps/desktop/src/remoteAccess/*` for desktop-discovered endpoints
161- `apps/web/src/environments/endpoints/*` for client-side display and pairing selection
162
163The web app should consume endpoint records and not care whether they came from LAN, Tailscale, or a future tunnel.
164
165### Pairing Link Generation
166
167Move hosted pairing link generation to endpoint-driven input:
168
169```ts
170buildHostedPairingUrl({
171 endpoint: AdvertisedEndpoint,
172 token,
173});
174```
175
176Generated URL:
177
178```text
179https://app.t3.codes/pair?host=<encoded endpoint httpBaseUrl>#token=<one-time token>
180```
181
182Use fragment tokens by default. Continue accepting `?token=` for compatibility.
183
184## Phase 1: Endpoint Abstraction
185
186### Goals
187
188- Centralize URL normalization, protocol derivation, and compatibility checks.
189- Replace ad hoc desktop `endpointUrl` pairing logic with endpoint selection.
190- Preserve all current remote behavior.
191
192### Tasks
193
1941. Add `AdvertisedEndpoint` schemas to `packages/contracts`.
1952. Add shared helpers:
196 - normalize HTTP base URL
197 - derive WebSocket base URL
198 - classify loopback/private/LAN/Tailscale/public host
199 - classify hosted HTTPS compatibility
2003. Add server endpoint discovery:
201 - loopback endpoint
202 - configured non-loopback endpoint
203 - explicit advertised host override
2044. Add desktop endpoint discovery:
205 - local loopback
206 - LAN exposure endpoint
207 - endpoint status labels
2085. Add WebSocket/API method or existing config field for endpoint snapshots.
2096. Refactor settings connections UI:
210 - render endpoint rows
211 - endpoint picker for pairing link copy
212 - show compatibility warnings
2137. Refactor hosted link builder to accept endpoint records.
2148. Add tests for URL normalization and compatibility classification.
215
216### Acceptance Criteria
217
218- Existing LAN/network access UI still works.
219- Pairing links are generated from endpoint records.
220- Loopback endpoints never produce hosted pairing links silently.
221- HTTP private-network endpoints are marked incompatible with `app.t3.codes`.
222- No remote environment runtime changes are required for existing saved environments.
223
224## Phase 2: BYO Tailscale/MagicDNS
225
226### Goals
227
228- Detect free DIY Tailscale reachability.
229- Surface Tailscale endpoints as normal advertised endpoints.
230- Keep users in control of their own tailnet.
231
232### Tasks
233
2341. Detect Tailscale IPs from network interfaces:
235 - IPv4 `100.64.0.0/10`
236 - mark as `provider: "tailscale-ip"`
2372. Add optional desktop-side `tailscale status --json` discovery:
238 - MagicDNS hostname
239 - Tailscale Serve/Funnel HTTPS endpoint if discoverable
240 - graceful failure if CLI is missing
2413. Add manual Tailscale endpoint override:
242 - hostname
243 - label
244 - preferred/default flag
2454. Show Tailscale endpoint rows in settings:
246 - raw IP HTTP endpoint: desktop-compatible, hosted-app likely blocked
247 - HTTPS MagicDNS/Serve endpoint: hosted-compatible if URL is HTTPS
2485. Generate pairing links using selected Tailscale endpoint.
2496. Document DIY setup:
250 - local desktop-to-desktop over Tailscale
251 - hosted app requirements
252 - why HTTPS matters
253
254### Acceptance Criteria
255
256- A machine on Tailscale shows a Tailscale endpoint without paid features.
257- Users can copy a Tailscale-hosted pairing link when the endpoint is HTTPS-compatible.
258- Users can still copy token-only/manual values when endpoint compatibility is unknown.
259- Tailscale is optional and never required for regular LAN/loopback use.
260
261## Phase 3: Hosted Static App Completion
262
263### Goals
264
265- `app.t3.codes` works as a real client shell.
266- It can pair, persist, reconnect, and clearly explain offline/incompatible states.
267
268### Tasks
269
2701. Finish hosted-static root behavior:
271 - no primary backend required
272 - saved environment hydration before initial routing decisions
273 - first saved environment selected as active
2742. Add hosted empty state:
275 - no saved environments
276 - paste pairing URL
277 - add host + token
2783. Add offline saved environment UI:
279 - last connected
280 - reconnect
281 - remove
282 - copy/add alternate endpoint
2834. Audit primary-backend assumptions:
284 - command palette
285 - settings pages
286 - server config atom defaults
287 - keybindings
288 - provider/model lists
289 - update/desktop-only affordances
2905. Add route tests for:
291 - hosted `/pair?host=...#token=...`
292 - hosted root with no saved environments
293 - hosted root with saved environment
294 - primary backend unavailable but saved environment present
2956. Add deployment hardening:
296 - SPA fallback
297 - strict CSP
298 - no third-party scripts
299 - no query token logging
300 - disable or hide source maps in production if needed
3017. Add browser error messages:
302 - mixed content
303 - unreachable backend
304 - CORS failure
305 - certificate failure
306
307### Acceptance Criteria
308
309- `app.t3.codes` can pair a reachable HTTPS backend and reconnect after reload.
310- A saved environment can be used without any backend at `app.t3.codes`.
311- Offline machines show a useful state instead of a generic boot error.
312- HTTP endpoints are still supported in desktop/native/local contexts.
313- Hosted HTTPS app only promises compatibility for HTTPS/WSS endpoints.
314
315## Phase 4: Future T3 Tunnel Provider
316
317Not part of the current implementation, but the endpoint abstraction should make it straightforward.
318
319Future tunnel provider responsibilities:
320
321- create endpoint with `provider: "t3-tunnel"`
322- surface tunnel status
323- provide stable HTTPS URL
324- use existing backend pairing/session auth
325- never bypass server auth
326
327The tunnel fabric can later be Pipenet-derived, Tailscale-derived, or another reverse tunnel implementation. The rest of T3 Code should only see an `AdvertisedEndpoint`.
328
329## Security Checklist
330
331- Pairing tokens are short-lived and one-time.
332- Generated hosted pairing links put tokens in the fragment.
333- The backend remains the authorization boundary.
334- Endpoint discovery never disables backend auth.
335- Hosted app does not silently downgrade to HTTP.
336- Tunnel/public endpoints require explicit user action.
337- Client sessions remain revocable.
338- Endpoint URLs and request logs must avoid recording pairing tokens.
339- Future cloud tunnel must authenticate tunnel creation and tunnel data connections separately from backend pairing.
340
341## Verification
342
343Each implementation PR should run:
344
345- `bun fmt`
346- `bun lint`
347- `bun typecheck`
348- focused tests for changed backend/web behavior
349- backend tests for any server-side endpoint discovery or auth changes using `bun run test`, never `bun test`
.plans/19-version-control-phase-1-vcs-driver-foundation.md deleted-216
...@@ -1,216 +0,0 @@
1# Version Control Phase 1: VCS Driver Foundation
2
3## Goal
4
5Introduce a provider-neutral VCS layer and rewrite the local Git implementation as an Effect-native driver. This phase should preserve user-visible behavior while replacing the Git-first service boundary with an abstraction that can support Git, Jujutsu, and later Sapling or another viable VCS.
6
7The existing `GitCore` implementation is a behavior reference and source of regression tests, not the target architecture. New code should follow the newer package style used by `effect-acp` and `effect-codex-app-server`: typed service tags, schema-backed tagged errors, scoped process usage, explicit decode boundaries, and no Promise-based process helper as the core execution primitive.
8
9## Scope
10
11- Add VCS-domain contracts in `packages/contracts/src/vcs.ts`.
12- Add shared runtime parsing helpers in `packages/shared/src/vcs/*` only when they are useful to both server and web.
13- Add server services under `apps/server/src/vcs`:
14 - `Services/VcsDriver.ts`
15 - `Services/VcsRepositoryResolver.ts`
16 - `Services/VcsProcess.ts`
17 - `Layers/GitVcsDriver.ts`
18 - `errors.ts`
19- Migrate server callers from Git-specific terms where the operation is actually VCS-generic.
20- Update active consumers to the new VCS APIs in the same phase; do not add backwards-compatible export shims.
21- Leave source-control hosting providers out of this phase except for remote metadata needed to describe repository status.
22
23## Non-Goals
24
25- No GitLab, Azure DevOps, or GitHub provider rewrite yet.
26- No Jujutsu driver yet, but every interface must be designed so a Jujutsu driver does not have to pretend to be Git.
27- No T3 Review implementation yet.
28- No broad UI redesign.
29
30## Driver Model
31
32Use provider-neutral nouns in new APIs:
33
34- `VcsDriver`: local repository mechanics.
35- `RepositoryIdentity`: detected VCS kind, root path, common metadata path when available, remotes.
36- `WorkingCopyStatus`: dirty state, changed files, aggregate insertions/deletions, current branch/bookmark/change name.
37- `ChangeSet`: a committed or pending unit of change, not necessarily a Git commit.
38- `RefName`: branch, bookmark, tag, or provider-specific ref.
39
40The initial driver capabilities should be explicit:
41
42```ts
43export interface VcsDriverCapabilities {
44 readonly kind: "git" | "jj" | "sapling" | "unknown";
45 readonly supportsWorktrees: boolean;
46 readonly supportsBookmarks: boolean;
47 readonly supportsAtomicSnapshot: boolean;
48 readonly supportsPushDefaultRemote: boolean;
49}
50```
51
52Do not model Jujutsu as `GitCoreShape extends ...`. The Git driver can expose Git-specific implementation details internally, but the public VCS layer should describe operations by intent:
53
54- `detectRepository(cwd)`
55- `status(cwd, options)`
56- `listRefs(cwd, query/pagination)`
57- `checkoutRef(cwd, ref)`
58- `createRef(cwd, ref, from?)`
59- `createWorkspace(cwd, ref, path?)`
60- `removeWorkspace(path)`
61- `prepareChangeContext(cwd, filePaths?)`
62- `createChange(cwd, message, options)`
63- `push(cwd, target?)`
64- `rangeContext(cwd, base, head)`
65- `listWorkspaceFiles(cwd, options)`
66
67## Effect Process Layer
68
69Create a small reusable `VcsProcess` service instead of using `runProcess`.
70
71Requirements:
72
73- Implement with `ChildProcess` and `ChildProcessSpawner` from `effect/unstable/process`.
74- Support scoped acquisition/release for long-running commands and interruption.
75- Support bounded stdout/stderr collection with truncation markers.
76 - DO not eagerly consume full stdout/stderr, return stream apis and expose helpers for consumers so we don't consume streams to memory unnecessarily...
77- Support stdin.
78- Support timeout through Effect scheduling/interruption, not ad-hoc timers.
79- Stream output lines to progress callbacks as Effects.
80- Return a typed `ProcessOutput` value for successful execution.
81- Fail with typed errors, not generic thrown exceptions.
82
83Errors should be schema-backed tagged classes, for example:
84
85- `VcsProcessSpawnError`
86- `VcsProcessExitError`
87- `VcsProcessTimeoutError`
88- `VcsOutputDecodeError`
89- `VcsRepositoryDetectionError`
90- `VcsUnsupportedOperationError`
91
92Every error should carry operation name, command display string, cwd when applicable, exit code when applicable, stderr/stdout tails when useful, and original cause where available. Override `message` for user readable messages that provides meaning and hints where appropriate. Errors are schema backed so the full error details will be persisted and serialized properly when stored to DB/Logfiles.
93
94## Git Driver Rewrite
95
96Rewrite Git support against `VcsProcess`.
97
98Carry forward current behavior from:
99
100- `apps/server/src/git/Layers/GitCore.ts`
101- `apps/server/src/git/Layers/GitCore.test.ts`
102- current Git status/branch/worktree contracts
103
104But split the implementation into smaller modules:
105
106- command execution and hardening config
107- repository detection
108- status parsing
109- branch/ref parsing
110- worktree operations
111- commit/range context generation
112- push/pull operations
113
114Keep parsing deterministic. Prefer Git porcelain formats, null-separated output, and schema decoding for JSON-like command output. Avoid regex parsing where Git gives a structured format.
115
116## Freshness and Local Caching
117
118Define freshness rules in the VCS layer before adding more providers. Local VCS status is cheap enough to refresh often; network-backed status is not.
119
120Treat these as live/local:
121
122- repository detection for the active cwd
123- working copy dirty state
124- staged/unstaged/untracked file summaries
125- current branch/bookmark/change name
126- local branch/bookmark lists
127- local worktree/workspace lists
128
129These may run on user-visible polling, but should still be debounced and coalesced per repository root. Prefer filesystem-triggered invalidation where available, with a short fallback poll interval. Concurrent requests for the same repository/status shape should share one in-flight Effect.
130
131Treat these as cached or explicit-refresh only:
132
133- remote tracking branch refreshes
134- ahead/behind counts that require network fetches
135- default branch discovery from a remote provider
136- remote branch lists beyond locally known refs
137
138The VCS driver should expose freshness metadata with status results:
139
140```ts
141export interface VcsFreshness {
142 readonly source: "live-local" | "cached-local" | "cached-remote" | "explicit-remote";
143 readonly observedAt: string;
144 readonly expiresAt?: string;
145}
146```
147
148Remote refreshes should be opt-in per operation, for example `refresh: "local-only" | "allow-cached-remote" | "force-remote"`. The default for background status should be `local-only`.
149
150Use Effect `Cache` for repository identity and expensive local metadata:
151
152- key by resolved repository root plus VCS kind
153- invalidate on cwd/root changes and workspace mutation operations
154- use short TTLs for local status caches when filesystem events are unavailable
155- never hide command failures behind stale values unless the caller explicitly accepts stale data
156
157## Cutover Policy
158
159Prefer direct migration and deletion over compatibility wrappers.
160
161Rules:
162
163- Update consumers to call `VcsDriver`/`VcsRepositoryResolver` directly as soon as the new API exists.
164- Delete migrated `GitCore` service methods and tests in the same PR that moves their consumers.
165- Do not keep backwards-compatible export shims, barrel aliases, or old service names for convenience.
166- Transitional modules are allowed only when a caller group is too complex or risky to migrate in the same PR.
167- Every transitional module must have a narrow owner, a removal checklist, and a test proving it delegates to the new implementation.
168- No new feature work may depend on transitional modules.
169
170Expected transitional candidates:
171
172- The highest-level `GitManager` orchestration can be migrated in slices if doing the full Commit + PR flow in one PR is too risky.
173- WebSocket payload compatibility can remain only where changing it would require a coordinated UI/server protocol migration. Internal server code should still use the new VCS contracts.
174
175## Tests
176
177Add integration-style tests with real temporary Git repositories for the new Git driver:
178
179- non-repository detection
180- status for clean/dirty/untracked/staged states
181- branch/ref list with pagination
182- checkout/create branch
183- worktree create/remove
184- commit context generation with file filters
185- commit creation with hook progress events
186- push behavior against a local bare remote
187- status polling does not perform remote network refresh by default
188- concurrent duplicate status requests are coalesced
189- bounded output/truncation
190- timeout/interruption
191- typed error shape for command failure and missing executable
192
193Move or duplicate only the tests needed to prove behavior, then delete the old service tests in the same migration slice.
194
195## Migration Steps
196
1971. Add `vcs` contracts and tagged errors.
1982. Add `VcsProcess` and unit tests around process execution semantics.
1993. Add `VcsDriver` and `VcsRepositoryResolver` service contracts.
2004. Implement `GitVcsDriver` with real Git command integration tests.
2015. Move `GitStatusBroadcaster` and branch/worktree flows to the VCS service directly.
2026. Move commit/range/push callers to the VCS service directly.
2037. Delete migrated `GitCore` internals and tests as each caller group moves.
2048. Add a transitional adapter only for any remaining `GitManager` path that is explicitly too complex to cut over safely in one PR.
2059. Remove every transitional adapter before starting Phase 2 unless the adapter is documented as blocking on the provider cutover.
206
207## Acceptance Criteria
208
209- Current Git branch/status/worktree/commit behavior remains intact.
210- New Git implementation does not depend on `processRunner.ts`.
211- New errors are typed and inspectable by tests.
212- VCS interfaces contain no GitHub/GitLab/Azure concepts.
213- Active consumers use the new VCS APIs directly; any remaining transitional module has a written removal checklist and no compatibility export shim.
214- Background status refresh is local-only by default and cannot hit provider rate limits.
215- Jujutsu can be added by implementing a real driver instead of conforming to Git command semantics.
216- `bun fmt`, `bun lint`, and `bun typecheck` pass.
.plans/20-version-control-phase-2-source-control-provider-foundation.md deleted-268
...@@ -1,268 +0,0 @@
1# Version Control Phase 2: Source Control Provider Foundation
2
3## Goal
4
5Introduce a pluggable source-control provider layer and rewrite GitHub support as an Effect-native provider. This phase should preserve the existing GitHub Commit + PR flow while making GitLab and Azure DevOps additive drivers rather than branches inside GitHub-oriented code.
6
7The existing `GitHubCli` service and GitHub-specific `GitManager` paths are behavior references. The new provider layer should use detailed tagged errors, schema decode boundaries, `effect/unstable/process`, capability flags, and provider-neutral change-request types.
8
9## Scope
10
11- Add provider-domain contracts in `packages/contracts/src/sourceControl.ts`.
12- Add provider URL/reference parsing helpers in `packages/shared/src/sourceControl/*`.
13- Add server services under `apps/server/src/sourceControl`:
14 - `Services/SourceControlProvider.ts`
15 - `Services/SourceControlProviderRegistry.ts`
16 - `Services/SourceControlProcess.ts`
17 - `Layers/GitHubSourceControlProvider.ts`
18 - `errors.ts`
19- Migrate PR creation, PR lookup, default-branch lookup, clone URL lookup, and PR checkout through the provider layer.
20- Update active consumers to the provider APIs directly; do not add backwards-compatible `GitHubCli` export shims.
21- Keep GitHub as the only production provider at the end of this phase, but make GitLab and Azure implementation paths obvious and bounded.
22
23## Non-Goals
24
25- No GitLab implementation in this phase, except fixtures/contracts that prove the abstraction can represent merge requests.
26- No Azure DevOps implementation in this phase, except URL/reference parser test cases if cheap.
27- No in-app review UI yet.
28- No hard dependency on one CLI forever. The first GitHub driver may use `gh`, but the interface should support REST/GraphQL implementations later.
29
30## Provider Model
31
32Use provider-neutral names:
33
34- `SourceControlProvider`: hosted repository and change-request mechanics.
35- `ChangeRequest`: GitHub pull request, GitLab merge request, Azure pull request.
36- `ChangeRequestThread`: review or discussion thread.
37- `ChangeRequestComment`: top-level or inline comment.
38- `ProviderRepository`: owner/project/repo identity plus clone URLs.
39
40Core provider operations:
41
42- `detectRemote(remoteUrl)`
43- `checkAuth(cwd)`
44- `getRepository(cwd | remoteUrl)`
45- `getDefaultTargetRef(repository)`
46- `listChangeRequests(repository, filters)`
47- `getChangeRequest(repository, reference)`
48- `createChangeRequest(repository, input)`
49- `checkoutChangeRequest(cwd, changeRequest, options)`
50- `getCloneUrls(repository)`
51
52Review-facing operations should be designed now, even if unimplemented:
53
54- `listReviewThreads(changeRequest)`
55- `createReviewComment(changeRequest, input)`
56- `replyToReviewThread(thread, input)`
57- `resolveReviewThread(thread)`
58- `submitReview(changeRequest, input)`
59
60Each operation should be guarded by capabilities:
61
62```ts
63export interface SourceControlProviderCapabilities {
64 readonly kind: "github" | "gitlab" | "azure-devops" | "unknown";
65 readonly supportsCreateChangeRequest: boolean;
66 readonly supportsCheckoutChangeRequest: boolean;
67 readonly supportsReviewThreads: boolean;
68 readonly supportsInlineComments: boolean;
69 readonly supportsDraftChangeRequests: boolean;
70}
71```
72
73## Provider Registry
74
75Add a registry that resolves a provider from repository remotes and explicit user input.
76
77Rules:
78
79- Detection should be pure where possible and testable without spawning CLIs.
80- Remote URL parsing belongs in `packages/shared`, not server-only provider layers.
81- Unknown providers should return explicit unsupported-operation errors, not silently fall back to GitHub.
82- Provider selection should be stable per operation and logged with enough context to debug bad remote detection.
83
84The registry should support multiple provider implementations at runtime, not a single dispatcher file with inline provider branches.
85
86## Rate Limits and Provider Caching
87
88Design the provider layer around a strict freshness budget. Provider API and CLI calls must not be part of frequent background polling unless the operation is explicitly marked safe and cached.
89
90Default behavior:
91
92- Pure URL/remote parsing is always live because it is local.
93- Provider detection from local remotes is live-local.
94- Authentication checks are cached.
95- Repository metadata is cached.
96- Default branch metadata is cached.
97- Change-request lists are cached and refreshed on explicit user actions or coarse intervals.
98- Full review threads, comments, file diffs, and timeline data are fetched only when the user opens the relevant review surface or explicitly refreshes it.
99- Create/update operations invalidate affected cache keys immediately after success.
100
101The provider API should make freshness explicit:
102
103```ts
104export interface SourceControlFreshness {
105 readonly source: "live-local" | "cached-provider" | "live-provider";
106 readonly observedAt: string;
107 readonly expiresAt?: string;
108 readonly stale?: boolean;
109}
110
111export type ProviderRefreshPolicy =
112 | "cache-first"
113 | "stale-while-revalidate"
114 | "force-refresh"
115 | "local-only";
116```
117
118Every read operation that can touch a provider should accept a refresh policy. Background UI reads should default to `cache-first` or `stale-while-revalidate`; direct user actions like pressing refresh can use `force-refresh`.
119
120Use Effect `Cache` for provider data:
121
122- auth status: key by provider kind, hostname, workspace identity, and account if known; TTL around minutes, not seconds
123- repository metadata/default branch: key by provider repository stable ID or normalized remote URL; TTL around tens of minutes
124- change-request summary lists: key by provider repository, state/filter, source ref, target ref; short TTL with stale-while-revalidate
125- individual change-request summaries: key by provider repository and provider CR ID; short TTL, invalidated after create/update/comment operations
126- review threads/comments/diffs: key by provider CR ID and head SHA/version when available; fetch on demand for T3 Review
127
128Provider drivers should surface rate-limit signals when available:
129
130- remaining quota
131- reset time
132- retry-after duration
133- whether the limit is primary, secondary/abuse, or unknown
134
135Rate-limit errors should be typed, retryable when the provider gives a reset/retry time, and visible enough for the UI to avoid repeatedly retrying a blocked operation.
136
137Avoid rate-limit footguns:
138
139- no provider calls from render loops or fast status polling
140- no listing all PRs/MRs across all repos to infer one branch state
141- no silent GitHub fallback for unknown providers
142- no unbounded cache cardinality for branch names or free-form search queries
143- no per-thread duplicate provider refresh when multiple views observe the same repository
144
145## GitHub Provider Rewrite
146
147Rewrite GitHub support as `GitHubSourceControlProvider`.
148
149Carry forward behavior from:
150
151- `apps/server/src/git/Layers/GitHubCli.ts`
152- `apps/server/src/git/Layers/GitHubCli.test.ts`
153- `apps/server/src/git/githubPullRequests.ts`
154- GitHub-specific `GitManager` PR paths
155
156Implementation requirements:
157
158- Use `SourceControlProcess` built on `effect/unstable/process`, not `runProcess`.
159- Decode `gh api` and `gh pr --json` responses with Effect Schema.
160- Use typed errors for auth failure, missing CLI, command failure, output decode failure, unsupported reference, and provider mismatch.
161- Keep stdout/stderr bounded.
162- Avoid global mutable auth caches unless they are Effect `Cache` values with explicit keys, TTLs, and invalidation behavior.
163- Parse provider rate-limit headers or CLI/API error payloads when available and map them to typed rate-limit errors.
164- Keep GitHub nouns inside the GitHub driver; convert to `ChangeRequest` at the provider boundary.
165
166## GitManager Cutover
167
168Refactor `GitManager` so it coordinates three independent services:
169
170- `VcsDriver` for local repository mechanics.
171- `SourceControlProviderRegistry` for hosted provider selection.
172- `TextGeneration` for message/body generation.
173
174`GitManager` should stop depending directly on GitHub services. User-visible step labels should be provider-neutral unless the selected provider is known and the label is intentionally provider-specific.
175
176The Commit + PR flow should become:
177
1781. Resolve VCS repository and local status.
1792. Resolve source-control provider from remotes.
1803. Generate commit content through the existing text generation service.
1814. Create local change through `VcsDriver`.
1825. Push through `VcsDriver` or a narrow provider push helper only if the VCS requires provider-specific target syntax.
1836. Generate change-request title/body.
1847. Create the change request through `SourceControlProvider`.
185
186## Cutover Policy
187
188This phase should aggressively remove old GitHub-specific internals.
189
190Rules:
191
192- Move each active consumer directly to `SourceControlProviderRegistry` or a concrete provider test layer.
193- Delete migrated `GitHubCli` methods, tests, and GitHub-specific helper exports in the same PR that moves their final consumer.
194- Do not add compatibility export shims from `apps/server/src/git` to `apps/server/src/sourceControl`.
195- Transitional modules are allowed only for a bounded `GitManager` slice that cannot move safely with the rest of the provider cutover.
196- Every transitional module must have an owner comment, a removal checklist, and no public exports consumed by new code.
197- Provider-neutral web parsing should replace GitHub-only parsing directly; do not keep parallel parser stacks unless a route still requires both during a single PR.
198
199## GitLab and Azure Readiness
200
201Use the triaged references as implementation inputs, not merge targets:
202
203- GitLab PR #592 is useful for `glab mr` command mapping and JSON normalization.
204- Azure issue #1138 defines a good first Azure slice: remote/URL detection and change-request thread setup for same-repo URLs.
205
206The abstraction should let Phase 3 add:
207
208- `GitLabSourceControlProvider` using `glab`.
209- `AzureDevOpsSourceControlProvider` using `az repos pr` or REST APIs.
210
211No provider should need to edit GitHub code to join the registry.
212
213## T3 Review Design Constraint
214
215Do not optimize only for creation/checkout. The provider layer must be able to support a future in-app review surface.
216
217That means contracts should include stable IDs and enough metadata for:
218
219- file-level diffs
220- inline review threads
221- resolved/unresolved state
222- top-level discussion comments
223- pending review submission
224- provider URL back-links
225
226Provider-specific fields can live in a metadata bag, but core review behavior should not require the UI to know whether the backing service is GitHub, GitLab, or Azure DevOps.
227
228## Tests
229
230Add tests at three levels:
231
232- Pure parser tests for GitHub, GitLab, and Azure remote URLs and change-request references.
233- Provider unit tests with fake `SourceControlProcess` output and schema decode failures.
234- Integration-style GitHub CLI tests only where they can run hermetically or be skipped without hiding unit coverage.
235
236Required cases:
237
238- GitHub PR URL, number, and branch-ish references.
239- GitLab MR URL/reference parsing.
240- Azure DevOps PR URL parsing for same-repo URLs.
241- unknown provider returns unsupported-operation errors.
242- missing CLI and auth failures produce distinct typed errors.
243- invalid CLI JSON fails at decode boundary with useful context.
244
245## Migration Steps
246
2471. Add `sourceControl` contracts and provider-neutral schemas.
2482. Add shared remote/reference parser helpers and tests.
2493. Add `SourceControlProcess` and provider errors.
2504. Add provider registry with GitHub-only registration.
2515. Implement `GitHubSourceControlProvider` from scratch against the new process layer.
2526. Cut GitHub PR operations in `GitManager` over to the provider registry.
2537. Replace web PR-reference parsing with provider-neutral parser output while keeping current GitHub UX.
2548. Add provider cache metrics and tests for cache hit, stale refresh, invalidation, and rate-limit error mapping.
2559. Delete the migrated `GitHubCli` implementation, tests, and GitHub-specific helper exports unless an explicit transitional checklist remains.
256
257## Acceptance Criteria
258
259- Existing GitHub Commit + PR and PR checkout flows still work.
260- `GitManager` no longer imports or depends on `GitHubCli`.
261- Active consumers use source-control provider APIs directly; any remaining transitional module has a written removal checklist and no compatibility export shim.
262- Source-control contracts can represent GitHub PRs, GitLab MRs, and Azure DevOps PRs.
263- Unknown/unsupported providers fail explicitly and visibly.
264- GitHub command execution does not depend on `processRunner.ts`.
265- Background provider reads are cached/coalesced and do not consume provider API quota on every status refresh.
266- Rate-limit responses become typed errors with retry/reset metadata where available.
267- The provider API includes the review operations needed by future T3 Review work, even if they are capability-gated.
268- `bun fmt`, `bun lint`, and `bun typecheck` pass.
.plans/README.md deleted-14
...@@ -1,14 +0,0 @@
1# Maintainability Plans
2
31. `01-shared-model-normalization.md`
42. `02-typed-ipc-boundaries.md`
53. `03-split-codex-app-server-manager.md`
64. `04-split-chatview-component.md`
75. `05-zod-persisted-state-validation.md`
86. `06-provider-logstream-lifecycle.md`
97. `07-ci-quality-gates.md`
108. `08-precommit-format-and-lint.md`
119. `09-event-state-test-expansion.md`
1210. `10-unify-process-session-abstraction.md`
1319. `19-version-control-phase-1-vcs-driver-foundation.md`
1420. `20-version-control-phase-2-source-control-provider-foundation.md`
.plans/branch-environment-picker-in-chatview-input.md deleted-74
...@@ -1,74 +0,0 @@
1# Branch/Environment Picker in ChatView Input
2
3## Summary
4
5Add a secondary toolbar below the ChatView input area (similar to Codex UI) that lets users select the target branch and environment mode (Local vs New worktree) before sending their first message.
6
7## UX
8
9- A toolbar appears **below** the input form (always visible when it's a git repo)
10- Two controls:
11 1. **Environment mode** (left side): toggles between "Local" and "New worktree" — **locked after first message** (no longer clickable, just shows current mode as label)
12 2. **Branch picker** (right side): dropdown showing local branches — **always changeable**, even after messages are sent
13- If not a git repo, the toolbar is hidden entirely (thread uses project cwd as-is)
14
15## Changes
16
17### 0. Install `@tanstack/react-query` in `apps/renderer`
18
19Add dependency + wrap app in `QueryClientProvider`.
20
21### 1. `apps/renderer/src/store.ts` — MODIFY
22
23Add a new action to the reducer:
24
25```ts
26| { type: "SET_THREAD_BRANCH"; threadId: string; branch: string | null; worktreePath: string | null }
27```
28
29Reducer case updates `branch` and `worktreePath` on the thread.
30
31### 2. `apps/renderer/src/components/ChatView.tsx` — MODIFY
32
33**Fetch branches** via `useQuery`:
34
35```ts
36const branchQuery = useQuery({
37 queryKey: ["git-branches", activeProject?.cwd],
38 queryFn: () => api.git.listBranches({ cwd: activeProject!.cwd }),
39 enabled: !!activeProject,
40});
41```
42
43**Local state:**
44
45- `envMode: "local" | "worktree"` — environment mode (local component state)
46
47**UI:** Below the `<form>`, render a toolbar bar (hidden if `!branchQuery.data?.isRepo`):
48
49- Left side: env mode button ("Local" / "New worktree") — disabled after first message (locked in)
50- Right side: branch dropdown from `branchQuery.data.branches`
51- Both styled like existing model picker (small text, chevron, dropdown menus)
52
53**Behavior:**
54
55- Branch picker is always active — changing branch dispatches `SET_THREAD_BRANCH` immediately
56- Env mode is only clickable when `activeThread.messages.length === 0`. After first message, it becomes a static label showing the locked-in mode
57- On first send (`onSend`): if `envMode === "worktree"` and a branch is selected, call `api.git.createWorktree` before starting the session, then dispatch `SET_THREAD_BRANCH` with the worktreePath
58- `ensureSession` already uses `activeThread.worktreePath ?? activeProject.cwd`
59
60### Files to modify
61
621. `apps/renderer/package.json` — add `@tanstack/react-query`
632. `apps/renderer/src/main.tsx` (or App entry) — wrap in `QueryClientProvider`
643. `apps/renderer/src/store.ts` — add `SET_THREAD_BRANCH` action
654. `apps/renderer/src/components/ChatView.tsx` — branch/env picker UI with `useQuery`
66
67## Verification
68
691. `turbo build` — compiles
702. Create a new thread → branch bar appears below input with "Local" + current branch
713. Change branch in dropdown → branch updates on thread
724. Toggle "New worktree" → send message → worktree created, session uses worktree cwd
735. After first message: env mode label locks to "Worktree" (not clickable), branch picker still works
746. Non-git project → no branch bar shown
.plans/effect-atom.md deleted-89
...@@ -1,89 +0,0 @@
1# Replace React Query With AtomRpc + Atom State
2
3## Summary
4- Use `effect/unstable/reactivity/AtomRpc` over the existing `WsRpcGroup`; stop wrapping RPC in promises via [wsRpcClient.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsRpcClient.ts) and [wsNativeApi.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsNativeApi.ts).
5- Keep Zustand for orchestration read model and UI state.
6- Keep a narrow `desktopBridge` adapter for dialogs, menus, external links, theme, and updater APIs.
7- Do not introduce Suspense in this migration. Atom-backed hooks should keep returning `data`, `error`, `isLoading|isPending`, `refresh`, and `mutateAsync`-style surfaces so component churn stays low.
8
9## Target Architecture
10- Extract the websocket `RpcClient.Protocol` layer from [wsTransport.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsTransport.ts) into `rpc/protocol.ts`.
11- Define one `AtomRpc.Service` for `WsRpcGroup` in `rpc/client.ts`.
12- Add `rpc/invalidation.ts` with explicit scoped invalidation keys: `git:${cwd}`, `project:${cwd}`, `checkpoint:${threadId}`, `server-config`.
13- Add `platform/desktopBridge.ts` as the only browser/desktop facade.
14- Remove from web by the end: [wsNativeApi.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsNativeApi.ts), [nativeApi.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/nativeApi.ts), [wsNativeApiState.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsNativeApiState.ts), [wsNativeApiAtoms.tsx](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsNativeApiAtoms.tsx), [wsRpcClient.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsRpcClient.ts), and all `*ReactQuery.ts` modules.
15
16## Phase 1: Infrastructure First
171. Extract the shared websocket RPC protocol layer from [wsTransport.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsTransport.ts) without changing behavior.
182. Build the AtomRpc client on top of that layer.
193. Add one temporary `runRpc` helper for imperative handlers that still want `Promise` ergonomics; it must call the AtomRpc service directly and must not reintroduce a facade object.
204. Replace manual registry wiring with one app-level registry provider based on `@effect/atom-react`.
215. Land this as a no-behavior-change PR.
22
23## Phase 2: Replace `wsNativeApi`-Owned Push State
241. Migrate welcome/config/provider/settings state first, because it is already atom-shaped and is the lowest-risk way to delete `wsNativeApi` responsibilities.
252. Replace [wsNativeApiState.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsNativeApiState.ts) with `rpc/serverState.ts`, updated directly from `subscribeServerLifecycle` and `subscribeServerConfig`.
263. Keep the current hook names for one PR: `useServerConfig`, `useServerSettings`, `useServerProviders`, `useServerKeybindings`, `useServerWelcomeSubscription`, `useServerConfigUpdatedSubscription`.
274. Move bootstrap side effects out of [wsNativeApiAtoms.tsx](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsNativeApiAtoms.tsx) into a new root bootstrap component mounted from [__root.tsx](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/routes/__root.tsx).
285. Delete the `server.getConfig()` fallback logic from [wsNativeApi.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsNativeApi.ts); snapshot fetch now lives beside the stream atoms.
29
30## Phase 3: Replace React Query Domain By Domain
311. Replace [gitReactQuery.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/lib/gitReactQuery.ts) first.
322. Add `rpc/gitAtoms.ts` and `rpc/useGit.ts` with `useGitStatus`, `useGitBranches`, `useResolvePullRequest`, and `useGitMutation`.
333. Mutation settlement must invalidate scoped keys, not a global cache. `checkout`, `pull`, `init`, `createWorktree`, `removeWorktree`, `preparePullRequestThread`, and stacked actions invalidate `git:${cwd}`. Worktree create/remove also invalidates `project:${cwd}`.
344. Replace [projectReactQuery.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/lib/projectReactQuery.ts) second. `useProjectSearchEntries` must preserve current “keep previous results while loading” behavior.
355. Replace [providerReactQuery.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/lib/providerReactQuery.ts) third. Preserve current checkpoint error normalization and retry/backoff semantics inside the atom effect. Invalidate by `checkpoint:${threadId}`.
366. Defer the desktop updater until the last phase.
37
38## Phase 4: Move Root Invalidation Off `queryClient`
391. In [__root.tsx](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/routes/__root.tsx), remove `QueryClient` usage and replace the throttled `invalidateQueries` block with throttled invalidation helpers.
402. Keep Zustand orchestration/event application unchanged.
413. Map current effects exactly:
42- git or checkpoint-affecting orchestration events touch `checkpoint:${threadId}`
43- file creation/deletion/restoration touches `project:${cwd}`
44- config-affecting server events touch `server-config`
45
46## Phase 5: Remove Imperative `NativeApi` Usage
471. Create narrow modules instead of a replacement mega-facade:
48- `rpc/orchestrationActions.ts`
49- `rpc/terminalActions.ts`
50- `rpc/gitActions.ts`
51- `rpc/projectActions.ts`
52- `platform/desktopBridge.ts`
532. Migrate direct [nativeApi.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/nativeApi.ts) callers by domain, not file-by-file: git-heavy components first, then orchestration/thread actions, then shell/dialog helpers.
543. After the last caller is gone, delete [nativeApi.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/nativeApi.ts) and the `window.nativeApi` fallback entirely.
554. In the final cleanup PR, remove `NativeApi` from [ipc.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/packages/contracts/src/ipc.ts) if nothing outside web still needs it.
56
57## Phase 6: Remove React Query Completely
581. Delete `@tanstack/react-query` from `apps/web/package.json`.
592. Remove `QueryClientProvider` and router context from [router.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/router.ts) and [__root.tsx](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/routes/__root.tsx).
603. Replace [desktopUpdateReactQuery.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/lib/desktopUpdateReactQuery.ts) with a writable atom plus `desktopBridge.onUpdateState`.
614. Delete the old query-option tests.
62
63## Public Interfaces And Types
64- Preserve the current server-state hook names during the transition.
65- Add permanent domain hooks: `useGitStatus`, `useGitBranches`, `useResolvePullRequest`, `useProjectSearchEntries`, `useCheckpointDiff`, `useDesktopUpdateState`.
66- Do not expose raw AtomRpc clients to components.
67- Do not add Suspense as part of this migration.
68- Final boundary is direct RPC for server features plus `desktopBridge` for local desktop features.
69
70## Test Plan
71- Add unit tests for `rpc/serverState.ts`: snapshot bootstrapping, stream replay, provider/settings updates.
72- Add unit tests for git/project/checkpoint hooks: loading, error mapping, retry behavior, invalidation, keep-previous-result behavior.
73- Update the browser harness in [wsRpcHarness.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/test/wsRpcHarness.ts) to assert direct RPC + atom behavior instead of `__resetNativeApiForTests`.
74- Replace [wsNativeApi.test.ts](/Users/julius/.t3/worktrees/codething-mvp/effect-http-router/apps/web/src/wsNativeApi.test.ts), `gitReactQuery.test.ts`, `providerReactQuery.test.ts`, and `desktopUpdateReactQuery.test.ts` with equivalent atom-backed coverage.
75- Acceptance scenarios:
76- welcome still bootstraps snapshot and navigation
77- keybindings toast still responds to config stream updates
78- git status/branches refresh after checkout/pull/worktree actions
79- PR resolve dialog keeps cached result while typing
80- `@` path search refreshes after file mutations and orchestration events
81- diff panel refreshes when checkpoints arrive
82- desktop updater still reflects push events and button actions
83
84## Assumptions And Defaults
85- Zustand stays in scope; only `react-query` is being removed.
86- `desktopBridge` remains the only non-RPC boundary.
87- The migration lands as 5-6 small PRs, each green independently.
88- Invalidations are explicit and scoped; do not recreate a global cache client abstraction.
89- Orchestration recovery/order logic stays as-is; only the data-fetching and mutation layer changes.
.plans/git-flows-integration-tests.md deleted-99
...@@ -1,99 +0,0 @@
1# Git Flows Integration Tests
2
3## Overview
4
5Real integration tests that run actual git commands against temporary repos. No mocking.
6
7## Step 1: Extract git functions into `apps/desktop/src/git.ts`
8
9The git functions (`listGitBranches`, `createGitWorktree`, `removeGitWorktree`, `createGitBranch`, `checkoutGitBranch`, `initGitRepo`) and their helper `runTerminalCommand` are currently private in `main.ts`. Extract them into a new `apps/desktop/src/git.ts` module with named exports.
10
11`main.ts` will import and re-use them — no behavior change, just moving code.
12
13**Files modified:**
14
15- `apps/desktop/src/git.ts` — new file with all git functions exported
16- `apps/desktop/src/main.ts` — import from `./git` instead of defining inline
17
18## Step 2: Create `apps/desktop/src/git.test.ts`
19
20Integration tests using real temp git repos. Each test group creates a fresh temp directory with `git init`, makes commits, creates branches as needed, and cleans up after.
21
22### Setup/teardown pattern
23
24```ts
25import { mkdtemp, rm, writeFile } from "node:fs/promises";
26import { tmpdir } from "node:os";
27import path from "node:path";
28import { afterEach, beforeEach, describe, expect, it } from "vitest";
29import {
30 listGitBranches,
31 createGitBranch,
32 checkoutGitBranch,
33 createGitWorktree,
34 removeGitWorktree,
35 initGitRepo,
36} from "./git";
37
38// Helper: run a raw git command in a dir (for test setup, not under test)
39// Helper: create an initial commit (git needs at least one commit for branches)
40```
41
42### Test groups
43
44**1. initGitRepo**
45
46- Creates a valid git repo in a temp dir
47- listGitBranches reports `isRepo: true` after init
48
49**2. listGitBranches**
50
51- Returns `isRepo: false` for non-git directory
52- Returns the current branch with `current: true`
53- Sorts current branch first
54- Lists multiple branches after creating them
55- `isDefault` is false when no remote (no origin/HEAD)
56
57**3. checkoutGitBranch**
58
59- Checks out an existing branch (current flag moves)
60- Throws when branch doesn't exist
61- Throws when checkout would overwrite uncommitted changes (dirty working tree)
62
63**4. createGitBranch**
64
65- Creates a new branch (appears in listGitBranches)
66- Throws when branch already exists
67
68**5. createGitWorktree + removeGitWorktree**
69
70- Creates a worktree directory at the expected path
71- Worktree has the correct branch checked out
72- Throws when branch is already checked out in another worktree
73- removeGitWorktree cleans up the worktree
74
75**6. Full flow: local branch checkout**
76
77- init → commit → create branch → checkout → verify current
78
79**7. Full flow: worktree creation from selected branch**
80
81- init → commit → create branch → create worktree → verify worktree dir exists and has correct branch
82
83**8. Full flow: thread switching simulation**
84
85- init → commit → create branch-a, branch-b → checkout a → checkout b → checkout a → verify current matches
86
87**9. Full flow: checkout conflict**
88
89- init → commit → create branch → modify file (unstaged) → checkout other branch → expect error
90
91## Verification
92
93```bash
94# Run the git integration tests
95cd apps/desktop && bun run test
96
97# Or just the git test file
98npx vitest run apps/desktop/src/git.test.ts
99```
.plans/git-flows-test-plan.md deleted-103
...@@ -1,103 +0,0 @@
1# Git Flows Test Plan
2
3## Overview
4
5Add tests for git branch/worktree flows. Two files:
6
71. **Extend** `apps/renderer/src/store.test.ts` — reducer tests for `SET_THREAD_BRANCH`
82. **Create** `apps/renderer/src/git-flows.test.ts` — flow logic tests
9
10All tests are pure Vitest unit tests (no React rendering). They test the reducer directly and simulate handler logic via sequential reducer dispatches + mocked API calls.
11
12## File 1: `apps/renderer/src/store.test.ts` (extend)
13
14Add `describe("SET_THREAD_BRANCH reducer")` with 6 tests:
15
16- Sets branch + worktreePath atomically
17- Clears both to null
18- Updates branch while preserving worktreePath
19- Does not affect other threads (multi-thread state)
20- No-op for nonexistent thread id
21- Does not mutate messages, error, or session fields
22
23Uses existing `makeThread`, `makeState` factories.
24
25## File 2: `apps/renderer/src/git-flows.test.ts` (new)
26
27### Factories
28
29- `makeThread()`, `makeState()`, `makeSession()` — same pattern as store.test.ts
30- `makeBranch()` — creates `GitBranch` objects
31- `makeMessage()` — creates `ChatMessage` objects
32- `makeGitApi()` — returns `{ checkout, createWorktree, createBranch, listBranches }` with `vi.fn()` mocks
33
34### Test groups (~30 tests total)
35
36**1. Local branch checkout flow** (2 tests)
37
38- Successful checkout → SET_THREAD_BRANCH updates branch
39- Checkout failure → SET_ERROR, branch unchanged
40
41**2. Thread branch conflict on send** (3 tests)
42
43- Two threads maintain independent branch state after SET_ACTIVE_THREAD
44- Branch state preserved through multiple thread switches + updates
45- Checkout failure on thread switch sets error only on target thread
46
47**3. Worktree creation on send** (5 tests)
48
49- First message in worktree mode → createWorktree → SET_THREAD_BRANCH with worktreePath
50- No worktree when messages already exist
51- No worktree in local envMode
52- No worktree when worktreePath already set
53- createWorktree failure → SET_ERROR, send aborted, no messages pushed
54
55**4. Env mode locking** (4 tests)
56
57- envLocked=false when no messages
58- envLocked=true with messages
59- Transitions false→true after PUSH_USER_MESSAGE
60- Remains true after SET_ERROR and UPDATE_SESSION
61
62**5. Auto-fill current branch** (3 tests)
63
64- Dispatches SET_THREAD_BRANCH when thread has no branch and current branch exists
65- Does not overwrite existing branch
66- No-op when no branch is marked current
67
68**6. Default branch detection** (2 tests)
69
70- isDefault flag on branch objects
71- current and isDefault can be on different branches
72
73**7. Branch creation + checkout** (3 tests)
74
75- Successful create + checkout updates branch
76- createBranch failure → error, branch unchanged
77- checkout failure after successful create → error, branch unchanged
78
79**8. Session CWD resolution** (3 tests)
80
81- Uses worktreePath when available
82- cwdOverride takes precedence over worktreePath
83- Falls back to project cwd when no worktree
84
85**9. Error handling patterns** (4 tests)
86
87- SET_ERROR sets error on correct thread
88- SET_ERROR with null clears error
89- Error on one thread doesn't affect others
90- Error cleared before successful branch operations
91
92## Verification
93
94```bash
95# Run all renderer tests
96cd apps/renderer && bun run test
97
98# Run just the new test file
99npx vitest run apps/renderer/src/git-flows.test.ts
100
101# Run just the store tests
102npx vitest run apps/renderer/src/store.test.ts
103```
.plans/git-integration-branch-picker-worktrees.md deleted-115
...@@ -1,115 +0,0 @@
1# Git Integration: Branch Picker + Worktrees
2
3## Summary
4
5Add git integration to let users start new threads from a specific branch, optionally creating a git worktree for isolated agent work.
6
7## UX Flow
8
9- **Left click** "+ New thread" → immediately creates a thread (current behavior, unchanged)
10- **Right click** "+ New thread" → opens a context menu with git options:
11 - List of local branches → clicking one creates a thread on that branch (uses project cwd)
12 - Each branch has a "worktree" sub-option → creates a worktree, then creates thread with worktree as cwd
13- When thread has a worktree, the agent session uses the worktree path as its cwd
14- If git fails (not a repo), context menu shows "Not a git repository" disabled item
15
16## Changes
17
18### 1. `packages/contracts/src/git.ts` — CREATE
19
20New Zod schemas and types:
21
22- `gitListBranchesInputSchema` — `{ cwd: string }`
23- `gitCreateWorktreeInputSchema` — `{ cwd: string, branch: string, path?: string }`
24- `gitRemoveWorktreeInputSchema` — `{ cwd: string, path: string }`
25- `gitBranchSchema` — `{ name: string, current: boolean }`
26- Result types for each
27
28### 2. `packages/contracts/src/ipc.ts` — MODIFY
29
30- Add 3 IPC channels: `git:list-branches`, `git:create-worktree`, `git:remove-worktree`
31- Add `git` namespace to `NativeApi` with `listBranches`, `createWorktree`, `removeWorktree`
32
33### 3. `packages/contracts/src/index.ts` — MODIFY
34
35- Add `export * from "./git"`
36
37### 4. `apps/desktop/src/main.ts` — MODIFY
38
39Add 3 IPC handlers + helper functions:
40
41- `listGitBranches()` — runs `git branch --no-color`, parses output into `{ name, current }[]`
42- `createGitWorktree()` — runs `git worktree add <path> <branch>`, defaults path to `../{repo}-worktrees/{branch}`
43- `removeGitWorktree()` — runs `git worktree remove <path>`
44
45Reuses existing `runTerminalCommand()`.
46
47### 5. `apps/desktop/src/preload.ts` — MODIFY
48
49Add `git` namespace with 3 `ipcRenderer.invoke` calls.
50
51### 6. `apps/renderer/src/types.ts` — MODIFY
52
53Add to `Thread`:
54
55```
56branch: string | null
57worktreePath: string | null
58```
59
60### 7. `apps/renderer/src/persistenceSchema.ts` — MODIFY
61
62- Add optional `branch`/`worktreePath` to persisted thread schema (`.nullable().optional()` for backwards compat)
63- Add V3 schema, update union
64- Update `hydrateThread` to default new fields to `null`
65- Update `toPersistedState` to serialize new fields
66
67### 8. `apps/renderer/src/store.ts` — MODIFY
68
69- Update persisted state key to v3, keep v2 as legacy fallback
70
71### 9. `apps/renderer/src/components/Sidebar.tsx` — MODIFY (main UI work)
72
73- Keep existing left-click `handleNewThread` unchanged (immediate thread creation)
74- Add `onContextMenu` handler to "+ New thread" buttons (both global and per-project)
75- On right-click: fetch branches via `api.git.listBranches`, show a custom context menu
76- Context menu items: branch names, each with a nested option to create with worktree
77- Clicking a branch → creates thread with `branch` set, title = branch name
78- Clicking "with worktree" → calls `api.git.createWorktree` first, then creates thread with `worktreePath`
79- Show branch badge on thread list items
80- If not a git repo, show "Not a git repository" as disabled menu item
81
82Context menu component: a positioned `<div>` with `position: fixed` anchored to the click position, dismissed on click-outside or Escape. Follows the existing dropdown pattern from ChatView's model picker.
83
84### 10. `apps/renderer/src/components/ChatView.tsx` — MODIFY
85
86- Line 157: use `activeThread.worktreePath ?? activeProject.cwd` as session cwd
87- Show branch/worktree badge in header bar
88
89## Implementation Order
90
911. `packages/contracts/src/git.ts` (new schemas)
922. `packages/contracts/src/ipc.ts` + `index.ts` (wire up channels)
933. `apps/desktop/src/main.ts` (git command handlers)
944. `apps/desktop/src/preload.ts` (bridge methods)
955. `apps/renderer/src/types.ts` (Thread type update)
966. `apps/renderer/src/persistenceSchema.ts` + `store.ts` (persistence migration)
977. `apps/renderer/src/components/Sidebar.tsx` (branch picker UI)
988. `apps/renderer/src/components/ChatView.tsx` (worktree cwd + badge)
99
100## Edge Cases
101
102- **Not a git repo**: `git branch` fails → context menu shows "Not a git repository" disabled item
103- **Branch has slashes**: `feature/foo` → worktree dir becomes `feature-foo`
104- **Worktree exists**: git error surfaces to user via inline error message in context menu
105- **No persistence breakage**: `.nullable().optional()` fields parse fine with old data
106
107## Verification
108
1091. `turbo build` — confirm contracts/desktop/renderer all compile
1102. Launch app, add a project pointing to a git repo
1113. Click "+ New thread" → verify branch list loads
1124. Select a branch, click Start → thread created with branch in title
1135. Enable worktree checkbox, pick branch, Start → verify worktree directory created on disk
1146. Send a message in worktree thread → verify agent runs in worktree cwd
1157. Add a non-git project → verify graceful error, can still create thread
.plans/spec-1-1-cutover-plan.md deleted-252
...@@ -1,252 +0,0 @@
1# Spec 1:1 Cutover Plan
2
3Goal: Align the orchestration model to `SPEC.md` 1:1 and remove legacy persistence/application cruft.
4
5Execution mode for this plan:
6
7- Hard cutover only. Existing DB and migration history are disposable.
8- Intermediate steps are allowed to break runtime, tests, typecheck, and lint.
9- We optimize for small, reviewable work units, not continuous app operability.
10- Only the final gate requires everything to run cleanly.
11
12## 1. Freeze SPEC contract as source of truth
13
14Work units:
15
16- Create `.plans/spec-contract-matrix.md` with one row per requirement in `SPEC.md` sections `7.1`-`7.4`.
17- Add exact SQL-level requirements per row: table, column, type, nullability, PK/unique, index, and invariants.
18- Add app-level requirements per row: writer path, reader path, and owning module.
19- Mark each row with status labels: `required`, `implemented`, `to-replace`, `delete`.
20- Identify any ambiguous spec lines and record a concrete interpretation in the matrix.
21
22Deliverables:
23
24- Complete matrix file with no unclassified rows.
25- Single source checklist used by all later steps.
26
27Breakage allowed:
28
29- No code changes required yet.
30
31Exit criteria:
32
33- Every requirement in `7.1`-`7.4` has exactly one matrix row.
34
35## 2. Hard cutover migrations (replace current migration set)
36
37Work units:
38
39- Delete the current legacy migration files and rewrite migration loader ordering.
40- Create `001_orchestration_events.ts` with full envelope columns and required event indexes.
41- Create `002_orchestration_command_receipts.ts` with PK + lookup indexes.
42- Create `003_checkpoint_diff_blobs.ts` with uniqueness on `(thread_id, from_turn_count, to_turn_count)`.
43- Create `004_provider_session_runtime.ts` with PK and runtime lookup indexes.
44- Create `005_projections.ts` with all projection tables:
45 - `projection_projects`
46 - `projection_threads`
47 - `projection_thread_messages`
48 - `projection_thread_activities`
49 - `projection_thread_sessions`
50 - `projection_thread_turns`
51 - `projection_checkpoints`
52 - `projection_pending_approvals`
53 - `projection_state`
54- Add all required indexes/constraints in `005_projections.ts`.
55- Ensure old tables (`projects`, `provider_checkpoints`, `provider_sessions`) are not recreated.
56
57Deliverables:
58
59- New 5-file migration chain.
60- Updated migration loader references only new migrations.
61
62Breakage allowed:
63
64- Repositories/services can be temporarily broken due to removed old tables.
65
66Exit criteria:
67
68- Fresh DB initializes with only canonical tables plus migration bookkeeping.
69
70## 3. Align persistence row/request schemas to DB 1:1
71
72Work units:
73
74- Define row schemas for each canonical table (contracts or persistence layer module).
75- Define request schemas for every insert/update/query operation touching canonical tables.
76- Remove or deprecate row/request schemas tied to deleted legacy tables.
77- Normalize enum and null semantics to match contracts exactly.
78- Ensure SQL aliases map 1:1 to schema field names (no implicit shape transforms).
79
80Deliverables:
81
82- Canonical row/request schemas committed.
83- Zero references to legacy row schemas in active code paths.
84
85Breakage allowed:
86
87- Runtime can still fail while query layers are being rewired.
88
89Exit criteria:
90
91- Every canonical table used in code has a typed row schema and typed request schema.
92
93## 4. Rewrite event store for full persisted envelope
94
95Work units:
96
97- Refactor append path to write full envelope fields:
98 - `event_id`, `aggregate_kind`, `stream_id`, `stream_version`, `event_type`, `occurred_at`, `command_id`, `causation_event_id`, `correlation_id`, `actor_kind`, `payload_json`, `metadata_json`
99- Implement stream version assignment/checking per aggregate stream.
100- Refactor read/replay path to decode payload and metadata from JSON and return `OrchestrationEvent` consistently.
101- Remove assumptions from old minimal schema (`aggregate_id`, missing metadata/actor).
102- Add explicit SQL ordering guarantees for replay (`ORDER BY sequence ASC`).
103
104Deliverables:
105
106- Event store append/replay fully aligned with canonical envelope.
107
108Breakage allowed:
109
110- Command dispatch flow can be partially broken until receipts/projectors are updated.
111
112Exit criteria:
113
114- Event store no longer depends on legacy event table shape.
115
116## 5. Add command receipt idempotency
117
118Work units:
119
120- Introduce persistence access layer for `orchestration_command_receipts`.
121- In command dispatch flow, check existing receipt by `commandId` before append.
122- On first execution, persist accepted receipt with `resultSequence`.
123- On domain rejection, persist rejected receipt with error payload.
124- On duplicate command, return prior result from receipt without re-appending event.
125- Ensure receipt write and event append ordering is deterministic.
126
127Deliverables:
128
129- Dispatch path with idempotency behavior wired through receipts.
130
131Breakage allowed:
132
133- Snapshot/read model may still be inconsistent until projectors are fully wired.
134
135Exit criteria:
136
137- Duplicate command IDs no longer create duplicate events.
138
139## 6. Build DB-backed projection pipeline
140
141Work units:
142
143- Create projector runner that consumes events and applies table-specific projections.
144- Implement projector handlers for each projection table.
145- For each handler, update target row(s) and `projection_state.last_applied_sequence` in the same transaction.
146- Define projector names used in `projection_state` and make them stable constants.
147- Add replay bootstrap from event store to bring projections up to latest sequence on startup.
148- Add safe resume logic from projector `last_applied_sequence`.
149
150Deliverables:
151
152- Persistent projector pipeline writing all `projection_*` tables.
153
154Breakage allowed:
155
156- Web/API layer may still read old in-memory model until step 7.
157
158Exit criteria:
159
160- Events drive projection rows in DB; projection state advances transactionally.
161
162## 7. Move RPC reads to projections and diff blobs
163
164Work units:
165
166- Implement snapshot query service reading only projection tables.
167- Build thread hydration from projection rows: messages, activities, checkpoints, session.
168- Compute `snapshotSequence` as the minimum required projector sequence from `projection_state`.
169- Implement `getTurnDiff` query backed by `checkpoint_diff_blobs` only.
170- Remove or bypass in-memory snapshot construction for RPC responses.
171- Validate replay handoff contract: snapshot sequence -> replay from `fromSequenceExclusive`.
172
173Deliverables:
174
175- `orchestration.getSnapshot` and `orchestration.getTurnDiff` served from DB projections/blob store.
176
177Breakage allowed:
178
179- Provider runtime persistence may still be partially legacy until step 8.
180
181Exit criteria:
182
183- No orchestration read RPC depends on legacy tables or in-memory-only state.
184
185## 8. Migrate provider runtime persistence to canonical table
186
187Work units:
188
189- Create repository/service for `provider_session_runtime`.
190- Update adapter/session manager to persist runtime/resume cursor in new table.
191- Ensure domain-visible session state still flows through orchestration events to `projection_thread_sessions`.
192- Remove writes to legacy provider session tables.
193- Verify restart/resume path reads runtime state from canonical table only.
194
195Deliverables:
196
197- Provider runtime state entirely backed by `provider_session_runtime`.
198
199Breakage allowed:
200
201- Some legacy interfaces may still exist but should be disconnected.
202
203Exit criteria:
204
205- Runtime restore no longer reads/writes legacy provider session persistence.
206
207## 9. Remove old cruft aggressively
208
209Work units:
210
211- Delete legacy repositories/services that map to removed tables.
212- Remove dead migration imports and obsolete persistence service interfaces.
213- Remove compatibility code paths that translate legacy row shapes.
214- Remove unused contracts/types linked to deprecated persistence model.
215- Update internal docs/comments to reference canonical projection/event model only.
216
217Deliverables:
218
219- Legacy persistence and translation layers removed from active codebase.
220
221Breakage allowed:
222
223- Temporary compile failures acceptable while deletion/refactor is in progress.
224
225Exit criteria:
226
227- No production code path references deleted legacy tables/services.
228
229## 10. Final verification gate (first point where green is required)
230
231Work units:
232
233- Add migration tests that assert canonical tables, columns, constraints, and indexes.
234- Add event store tests for envelope persistence, metadata, actor kind, and replay.
235- Add receipt idempotency tests for accept/reject/duplicate paths.
236- Add projector tests for transactional row updates + `projection_state` updates.
237- Add snapshot tests verifying projection-sourced output and `snapshotSequence` semantics.
238- Add turn diff tests verifying `checkpoint_diff_blobs` source of truth.
239- Add provider runtime tests for persist + restart + resume behavior.
240- Run project lint/typecheck/tests and fix failures.
241
242Deliverables:
243
244- Green checks with canonical schema + persistence model in place.
245
246Breakage allowed:
247
248- None at end of step.
249
250Exit criteria:
251
252- SPEC `7.1`-`7.4` requirements satisfied and validated by tests.
.plans/spec-contract-matrix.md deleted-433
...@@ -1,433 +0,0 @@
1# SPEC Contract Matrix (Sections 7.1-7.4)
2
3Status legend:
4
5- `required`: requirement acknowledged, no current implementation claim yet.
6- `implemented`: requirement currently satisfied in code + schema.
7- `to-replace`: partial/misaligned implementation exists and must be replaced.
8- `delete`: current path actively conflicts with SPEC and should be removed.
9
10## 7.1 Write-Side Persisted Tables
11
12### W1
13
14- Spec ref: `7.1.1 orchestration_events`
15- Requirement: append-only event store with canonical envelope columns.
16- SQL contract:
17 - `sequence INTEGER PRIMARY KEY` (global monotonic)
18 - `event_id TEXT UNIQUE NOT NULL`
19 - `aggregate_kind TEXT NOT NULL CHECK IN ('project','thread')`
20 - `stream_id TEXT NOT NULL`
21 - `stream_version INTEGER NOT NULL`
22 - `event_type TEXT NOT NULL`
23 - `occurred_at TEXT NOT NULL`
24 - `command_id TEXT NULL`
25 - `causation_event_id TEXT NULL`
26 - `correlation_id TEXT NULL`
27 - `actor_kind TEXT NOT NULL CHECK IN ('client','server','provider')`
28 - `payload_json TEXT NOT NULL`
29 - `metadata_json TEXT NOT NULL`
30- Current writer path: `apps/server/src/persistence/Layers/OrchestrationEventStore.ts`
31- Current reader path: `apps/server/src/persistence/Layers/OrchestrationEventStore.ts`
32- Owner module: `apps/server/src/persistence` (event store + migrations)
33- Status: `to-replace`
34- Notes: current migration/table lacks `stream_id`, `stream_version`, `causation_event_id`, `correlation_id`, `actor_kind`, `metadata_json`.
35
36### W2
37
38- Spec ref: `7.1.2 orchestration_command_receipts`
39- Requirement: command idempotency + ack replay receipts table.
40- SQL contract:
41 - `command_id TEXT PRIMARY KEY`
42 - `aggregate_kind TEXT NOT NULL CHECK IN ('project','thread')`
43 - `aggregate_id TEXT NOT NULL`
44 - `accepted_at TEXT NOT NULL`
45 - `result_sequence INTEGER NOT NULL`
46 - `status TEXT NOT NULL CHECK IN ('accepted','rejected')`
47 - `error TEXT NULL`
48- Current writer path: none
49- Current reader path: none
50- Owner module: `apps/server/src/orchestration` dispatch boundary + `apps/server/src/persistence`
51- Status: `to-replace`
52- Notes: missing table and missing idempotency flow.
53
54### W3
55
56- Spec ref: `7.1.3 checkpoint_diff_blobs`
57- Requirement: store large plaintext diffs separate from checkpoint summaries.
58- SQL contract:
59 - `thread_id TEXT NOT NULL`
60 - `from_turn_count INTEGER NOT NULL`
61 - `to_turn_count INTEGER NOT NULL`
62 - `diff TEXT NOT NULL`
63 - `created_at TEXT NOT NULL`
64 - `UNIQUE(thread_id, from_turn_count, to_turn_count)`
65- Current writer path: none
66- Current reader path: none
67- Owner module: `apps/server/src/persistence` + turn diff query service
68- Status: `to-replace`
69- Notes: no canonical diff blob table yet.
70
71### W4
72
73- Spec ref: `7.1.4 provider_session_runtime`
74- Requirement: server-internal provider runtime/resume state.
75- SQL contract:
76 - `provider_session_id TEXT PRIMARY KEY`
77 - `thread_id TEXT NOT NULL`
78 - `provider_name TEXT NOT NULL`
79 - `adapter_key TEXT NOT NULL`
80 - `provider_thread_id TEXT NULL`
81 - `status TEXT NOT NULL CHECK IN ('starting','running','stopped','error')`
82 - `last_seen_at TEXT NOT NULL`
83 - `resume_cursor_json TEXT NULL`
84 - `runtime_payload_json TEXT NULL`
85- Current writer path: legacy provider session persistence (`apps/server/src/persistence/Layers/ProviderSessions.ts`)
86- Current reader path: legacy provider session persistence (`apps/server/src/persistence/Layers/ProviderSessions.ts`)
87- Owner module: provider runtime manager + persistence runtime repository
88- Status: `to-replace`
89- Notes: existing `provider_sessions` schema is incompatible and too small.
90
91## 7.2 Canonical Persisted Event Schema
92
93### E1
94
95- Spec ref: `7.2 OrchestrationPersistedEventSchema`
96- Requirement: full typed persisted event envelope in shared contracts.
97- SQL contract: envelope fields in W1 must map 1:1 to contracts schema.
98- Current writer path: contracts defined in `packages/contracts/src/orchestration.ts`
99- Current reader path: used by persistence decode boundaries (partial)
100- Owner module: `packages/contracts`
101- Status: `implemented`
102- Notes: contract schema exists; DB + store mapping still incomplete.
103
104### E2
105
106- Spec ref: `7.2 Rules/payload discriminated by eventType`
107- Requirement: `payload` validation keyed by `eventType`.
108- SQL contract: `event_type` drives payload decode schema; invalid combinations rejected.
109- Current writer path: `packages/contracts/src/orchestration.ts`
110- Current reader path: `apps/server/src/persistence/Layers/OrchestrationEventStore.ts` decode path
111- Owner module: contracts + event store
112- Status: `to-replace`
113- Notes: decode is present but DB does not persist full envelope columns.
114
115### E3
116
117- Spec ref: `7.2 Rules/provider ids scope`
118- Requirement: provider ids live in metadata/provider payload, not as thread identity replacement.
119- SQL contract: provider fields persisted inside `metadata_json`; `stream_id` remains project/thread id.
120- Current writer path: `apps/server/src/orchestration/decider.ts` (metadata mostly empty)
121- Current reader path: projector/event consumers
122- Owner module: decider + provider ingestion + event store
123- Status: `to-replace`
124- Notes: metadata plumbing is incomplete in persistence path.
125
126### E4
127
128- Spec ref: `7.2 Rules/streamVersion concurrency guard`
129- Requirement: stream version monotonic per aggregate stream; enforced on write.
130- SQL contract: `stream_version INTEGER NOT NULL` + uniqueness/invariant enforcement per stream.
131- Current writer path: none
132- Current reader path: none
133- Owner module: event store append logic + DB constraints
134- Status: `to-replace`
135- Notes: no stream version assignment/checking today.
136
137## 7.3 Required Projected Tables (Read Models)
138
139### P1
140
141- Spec ref: `7.3.1 projection_projects`
142- Requirement: persisted project projection table.
143- SQL contract:
144 - `project_id TEXT PRIMARY KEY`
145 - `title TEXT NOT NULL`
146 - `workspace_root TEXT NOT NULL`
147 - `default_model TEXT NULL`
148 - `created_at TEXT NOT NULL`
149 - `updated_at TEXT NOT NULL`
150 - `deleted_at TEXT NULL`
151- Current writer path: none (in-memory projector only)
152- Current reader path: none (snapshot not DB-projected)
153- Owner module: projector pipeline + snapshot query
154- Status: `to-replace`
155- Notes: legacy `projects` table is separate concept and should be removed from orchestration model.
156
157### P2
158
159- Spec ref: `7.3.2 projection_threads`
160- Requirement: persisted thread projection table.
161- SQL contract:
162 - `thread_id TEXT PRIMARY KEY`
163 - `project_id TEXT NOT NULL`
164 - `title TEXT NOT NULL`
165 - `model TEXT NOT NULL`
166 - `branch TEXT NULL`
167 - `worktree_path TEXT NULL`
168 - `latest_turn_id TEXT NULL`
169 - `created_at TEXT NOT NULL`
170 - `updated_at TEXT NOT NULL`
171 - `deleted_at TEXT NULL`
172- Current writer path: none (in-memory projector only)
173- Current reader path: none (snapshot not DB-projected)
174- Owner module: projector pipeline + snapshot query
175- Status: `to-replace`
176- Notes: missing table and projector writes.
177
178### P3
179
180- Spec ref: `7.3.3 projection_thread_messages`
181- Requirement: persisted thread message projection table.
182- SQL contract:
183 - `message_id TEXT PRIMARY KEY`
184 - `thread_id TEXT NOT NULL`
185 - `turn_id TEXT NULL`
186 - `role TEXT NOT NULL CHECK IN ('user','assistant','system')`
187 - `text TEXT NOT NULL`
188 - `is_streaming INTEGER/BOOLEAN NOT NULL`
189 - `created_at TEXT NOT NULL`
190 - `updated_at TEXT NOT NULL`
191- Current writer path: none (in-memory projector only)
192- Current reader path: none (snapshot not DB-projected)
193- Owner module: projector pipeline + snapshot query
194- Status: `to-replace`
195- Notes: missing table and message projection writes.
196
197### P4
198
199- Spec ref: `7.3.4 projection_thread_activities`
200- Requirement: persisted thread activity projection table.
201- SQL contract:
202 - `activity_id TEXT PRIMARY KEY`
203 - `thread_id TEXT NOT NULL`
204 - `turn_id TEXT NULL`
205 - `tone TEXT NOT NULL CHECK IN ('info','tool','approval','error')`
206 - `kind TEXT NOT NULL`
207 - `summary TEXT NOT NULL`
208 - `payload_json TEXT NOT NULL`
209 - `created_at TEXT NOT NULL`
210- Current writer path: none (in-memory projector only)
211- Current reader path: none (snapshot not DB-projected)
212- Owner module: projector pipeline + snapshot query
213- Status: `to-replace`
214- Notes: no canonical activity projection persistence.
215
216### P5
217
218- Spec ref: `7.3.5 projection_thread_sessions`
219- Requirement: persisted thread session projection table.
220- SQL contract:
221 - `thread_id TEXT PRIMARY KEY`
222 - `status TEXT NOT NULL CHECK IN ('idle','starting','running','ready','interrupted','stopped','error')`
223 - `provider_name TEXT NULL`
224 - `provider_session_id TEXT NULL`
225 - `provider_thread_id TEXT NULL`
226 - `active_turn_id TEXT NULL`
227 - `last_error TEXT NULL`
228 - `updated_at TEXT NOT NULL`
229- Current writer path: none (in-memory projector only)
230- Current reader path: none (snapshot not DB-projected)
231- Owner module: projector pipeline + snapshot query
232- Status: `to-replace`
233- Notes: current provider session table is not this domain projection.
234
235### P6
236
237- Spec ref: `7.3.6 projection_thread_turns`
238- Requirement: persisted thread turn projection table.
239- SQL contract:
240 - `turn_id TEXT PRIMARY KEY`
241 - `thread_id TEXT NOT NULL`
242 - `turn_count INTEGER NOT NULL`
243 - `status TEXT NOT NULL CHECK IN ('running','completed','interrupted','error')`
244 - `user_message_id TEXT NULL`
245 - `assistant_message_id TEXT NULL`
246 - `started_at TEXT NOT NULL`
247 - `completed_at TEXT NULL`
248- Current writer path: none
249- Current reader path: none
250- Owner module: projector pipeline + session/turn query helpers
251- Status: `to-replace`
252- Notes: missing table and projection logic.
253
254### P7
255
256- Spec ref: `7.3.7 projection_checkpoints`
257- Requirement: persisted checkpoint summary projection table.
258- SQL contract:
259 - `thread_id TEXT NOT NULL`
260 - `turn_id TEXT NOT NULL`
261 - `checkpoint_turn_count INTEGER NOT NULL`
262 - `checkpoint_ref TEXT NOT NULL`
263 - `status TEXT NOT NULL CHECK IN ('ready','missing','error')`
264 - `files_json TEXT NOT NULL`
265 - `assistant_message_id TEXT NULL`
266 - `completed_at TEXT NOT NULL`
267 - `UNIQUE(thread_id, checkpoint_turn_count)`
268- Current writer path: legacy `provider_checkpoints` writes in `apps/server/src/persistence/Layers/Checkpoints.ts`
269- Current reader path: legacy checkpoint repository
270- Owner module: projector pipeline + checkpoint query layer
271- Status: `to-replace`
272- Notes: current table semantics do not match canonical checkpoint projection schema.
273
274### P8
275
276- Spec ref: `7.3.8 projection_pending_approvals`
277- Requirement: persisted pending-approval projection table.
278- SQL contract:
279 - `request_id TEXT PRIMARY KEY`
280 - `thread_id TEXT NOT NULL`
281 - `turn_id TEXT NULL`
282 - `status TEXT NOT NULL CHECK IN ('pending','resolved')`
283 - `decision TEXT NULL CHECK IN ('accept','acceptForSession','decline','cancel')`
284 - `created_at TEXT NOT NULL`
285 - `resolved_at TEXT NULL`
286- Current writer path: none
287- Current reader path: none
288- Owner module: projector pipeline + approval query layer
289- Status: `to-replace`
290- Notes: missing table and projection logic.
291
292### P9
293
294- Spec ref: `7.3.9 projection_state`
295- Requirement: projector progress tracking table.
296- SQL contract:
297 - `projector TEXT PRIMARY KEY`
298 - `last_applied_sequence INTEGER NOT NULL`
299 - `updated_at TEXT NOT NULL`
300- Current writer path: none
301- Current reader path: none
302- Owner module: projector runner/checkpointing
303- Status: `to-replace`
304- Notes: missing table and projector bookkeeping.
305
306### P10
307
308- Spec ref: `7.3 Projection consistency rules`
309- Requirement: projector row updates and `projection_state` update must be atomic per event.
310- SQL contract: per-projector transaction boundary covering both projection write and state update.
311- Current writer path: none (in-memory projector has no SQL transaction)
312- Current reader path: none
313- Owner module: projector runner
314- Status: `to-replace`
315- Notes: requires transactional projection executor.
316
317### P11
318
319- Spec ref: `7.3 Optional debug field`
320- Requirement: `lastEventSequence` on projection rows is optional and not required for correctness.
321- SQL contract: optional; not required in baseline schema.
322- Current writer path: none
323- Current reader path: none
324- Owner module: projector runner
325- Status: `required`
326- Notes: interpretation: exclude from first cutover unless debugging requires it.
327
328## 7.4 Snapshot and RPC Requirements
329
330### R1
331
332- Spec ref: `7.4.1`
333- Requirement: `orchestration.getSnapshot` fully served from projection tables and returns `snapshotSequence`.
334- SQL contract: snapshot query joins/reads only `projection_*` + `projection_state`.
335- Current writer path: in-memory model built in `apps/server/src/orchestration/projector.ts`
336- Current reader path: `apps/server/src/orchestration/Layers/OrchestrationEngine.ts#getReadModel`
337- Owner module: snapshot query service + ws RPC handler
338- Status: `delete`
339- Notes: current in-memory read model path must be removed for SPEC compliance.
340
341### R2
342
343- Spec ref: `7.4.2`
344- Requirement: snapshot `projects[]` source is `projection_projects`.
345- SQL contract: `projects` collection assembled from `projection_projects` rows.
346- Current writer path: none
347- Current reader path: in-memory thread/project arrays
348- Owner module: snapshot query service
349- Status: `to-replace`
350- Notes: no DB project projection reader exists yet.
351
352### R3
353
354- Spec ref: `7.4.3`
355- Requirement: thread snapshot `checkpoints[]` source is `projection_checkpoints` with required fields.
356- SQL contract: fields `turnId`, `completedAt`, `status`, `files[]`, `checkpointRef`, optional `assistantMessageId`, `checkpointTurnCount`.
357- Current writer path: legacy checkpoint repo data model
358- Current reader path: in-memory checkpoints from orchestration events
359- Owner module: snapshot query service + checkpoint projector
360- Status: `to-replace`
361- Notes: canonical projection table and reader not implemented.
362
363### R4
364
365- Spec ref: `7.4.4`
366- Requirement: no `listCheckpoints` orchestration RPC; list in snapshot + full diff via `getTurnDiff` from diff blobs.
367- SQL contract: `getTurnDiff` reads `checkpoint_diff_blobs` only.
368- Current writer path: none for diff blobs
369- Current reader path: `orchestration.getTurnDiff` schema exists, data backing incomplete
370- Owner module: ws RPC handler + diff query service
371- Status: `to-replace`
372- Notes: current checkpoint repository is not canonical source.
373
374### R5
375
376- Spec ref: `7.4.5`
377- Requirement: client acts on `ThreadId`; server resolves provider session via `projection_thread_sessions`.
378- SQL contract: session lookup by `thread_id` from projection table.
379- Current writer path: mixed provider/session handling paths
380- Current reader path: legacy provider session persistence lookups
381- Owner module: provider dispatch/session resolution
382- Status: `to-replace`
383- Notes: remove provider-session-as-routing-key behavior.
384
385### R6
386
387- Spec ref: `7.4.6`
388- Requirement: `snapshotSequence` derived from `projection_state` minimum over dependent projectors.
389- SQL contract: `MIN(last_applied_sequence)` across required projector keys.
390- Current writer path: none
391- Current reader path: currently from in-memory event projection sequence
392- Owner module: snapshot query service
393- Status: `to-replace`
394- Notes: must move from in-memory sequence to DB projection-state semantics.
395
396### R7
397
398- Spec ref: `7.4.7`
399- Requirement: snapshot/replay handoff has no gap (`getSnapshot` -> subscribe from snapshot sequence).
400- SQL contract: read consistency strategy guaranteeing no missing events between snapshot visibility and replay start.
401- Current writer path: event stream via `OrchestrationEventStore.readFromSequence`
402- Current reader path: ws replay flow in `apps/server/src/wsServer.ts`
403- Owner module: ws RPC + event stream handoff layer
404- Status: `to-replace`
405- Notes: interpretation requires explicit consistency boundary (transaction, sequence fence, or equivalent).
406
407## Ambiguous/Interpretation Decisions (tracked upfront)
408
409### A1
410
411- Topic: `orchestration_events.stream_id` vs event runtime `aggregateId` naming.
412- Decision: persist canonical DB column name `stream_id`; map to runtime `aggregateId` where needed in decider/projector code.
413
414### A2
415
416- Topic: JSON column typing in SQLite for `payload`, `metadata`, projection payload/files, runtime cursor/payload.
417- Decision: store as `TEXT` JSON with strict encode/decode schemas at boundaries.
418
419### A3
420
421- Topic: `snapshotSequence` dependency set for min-sequence computation.
422- Decision: include all projectors used to construct snapshot payload (`projects`, `threads`, `messages`, `activities`, `sessions`, `turns`, `checkpoints`, `pending_approvals`).
423
424### A4
425
426- Topic: no-gap handoff mechanism in `7.4.7`.
427- Decision: implement explicit sequence fence semantics at snapshot time; replay starts from fence `fromSequenceExclusive`.
428
429## Checklist Completeness Statement
430
431- Coverage scope: `SPEC.md` sections `7.1`, `7.2`, `7.3`, `7.4`.
432- Requirement rows present: `W1-W4`, `E1-E4`, `P1-P11`, `R1-R7`.
433- Unclassified rows: `0`.
.plans/t3-connect-remote-setup.html deleted-257
...@@ -1,257 +0,0 @@
1<!doctype html>
2<html lang="en">
3<head>
4<meta charset="utf-8">
5<meta name="viewport" content="width=device-width, initial-scale=1">
6<title>Plan: seamless `npx t3 connect` for remote boxes</title>
7<style>
8 :root {
9 --bg: #000;
10 --fg: #fff;
11 --muted: #a3a3a3;
12 --surface: #121212;
13 --surface2: #1a1a1a;
14 --border: #2a2a2a;
15 --accent: #7c9cff;
16 --green: #4ade80;
17 --amber: #fbbf24;
18 --red: #f87171;
19 }
20 * { box-sizing: border-box; }
21 body {
22 margin: 0;
23 background: var(--bg);
24 color: var(--fg);
25 font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
26 }
27 main { max-width: 880px; margin: 0 auto; padding: 48px 24px 96px; }
28 h1 { font-size: 30px; line-height: 1.25; margin: 0 0 8px; }
29 h2 { font-size: 21px; margin: 48px 0 12px; padding-top: 24px; border-top: 1px solid var(--border); }
30 h3 { font-size: 16px; margin: 24px 0 8px; }
31 p, li { color: #e5e5e5; }
32 .sub { color: var(--muted); margin: 0 0 24px; }
33 code, pre {
34 font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
35 font-size: 13.5px;
36 }
37 code { background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; }
38 pre {
39 background: var(--surface);
40 border: 1px solid var(--border);
41 border-radius: 8px;
42 padding: 16px;
43 overflow-x: auto;
44 line-height: 1.5;
45 }
46 pre code { background: none; border: none; padding: 0; }
47 .card {
48 background: var(--surface);
49 border: 1px solid var(--border);
50 border-radius: 10px;
51 padding: 18px 20px;
52 margin: 16px 0;
53 }
54 .card h3 { margin-top: 0; }
55 table { border-collapse: collapse; width: 100%; margin: 16px 0; font-size: 14.5px; }
56 th, td { text-align: left; padding: 8px 12px; border: 1px solid var(--border); vertical-align: top; }
57 th { background: var(--surface2); font-weight: 600; }
58 td code { white-space: nowrap; }
59 .pill {
60 display: inline-block; font-size: 12px; font-weight: 600; border-radius: 999px;
61 padding: 2px 10px; margin-right: 6px; vertical-align: 2px;
62 }
63 .pill.core { background: #1e2a4a; color: var(--accent); }
64 .pill.new { background: #14301e; color: var(--green); }
65 .pill.later { background: #33270e; color: var(--amber); }
66 .ok { color: var(--green); }
67 .warn { color: var(--amber); }
68 .no { color: var(--red); }
69 ul { padding-left: 22px; }
70 li { margin: 4px 0; }
71 .terminal {
72 background: #0a0a0a; border: 1px solid var(--border); border-radius: 8px;
73 padding: 16px; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
74 font-size: 13.5px; line-height: 1.65; color: #d4d4d4; overflow-x: auto;
75 }
76 .terminal .dim { color: #737373; }
77 .terminal .g { color: var(--green); }
78 .terminal .b { color: var(--accent); }
79 figure { margin: 24px 0; }
80 figcaption { color: var(--muted); font-size: 13px; margin-top: 8px; text-align: center; }
81 svg text { font-family: -apple-system, "Segoe UI", sans-serif; }
82 a { color: var(--accent); }
83 .kv { color: var(--muted); font-size: 13.5px; }
84</style>
85</head>
86<body>
87<main>
88
89<h1>Seamless <code>npx t3 connect</code> for remote boxes</h1>
90<p class="sub">Design principle: <strong>the smallest diff that ships the UX.</strong> No relay/infra changes, no new backend surface, no new auth primitives &mdash; every step reuses code that already exists. One PR, built as four phases with clear commit boundaries &mdash; each phase compiles, passes tests, and leaves the product working, so the PR reviews commit-by-commit. (Phase 4, web-triggered update, is an optional follow-up PR.)</p>
91
92<div class="terminal">
93$ npx t3 connect<br><br>
94To set up T3 Connect, open this URL and sign in:<br>
95&nbsp;&nbsp;<span class="b">https://app.t3.codes/connect#B64URL_STATE_AND_CHALLENGE</span><br><br>
96Enter your authentication code: <span class="dim">[code]</span><br><br>
97<span class="g">Connected as theo@t3.gg!</span><br><br>
98Run T3 Code in the background whenever this machine boots? (y/n): y<br><br>
99<span class="g">T3 Code is set up and ready to go.</span>
100</div>
101
102<h2>Why this is a small change</h2>
103<p>The entire <code>t3 connect</code> data plane already works: Clerk PKCE token exchange, encrypted secret store, cloudflared relay-client install, relay environment linking, DPoP tokens. The <em>only</em> broken piece on an SSH box is the redirect: <code>CliTokenManager.login()</code> hardcodes a loopback callback (<code>http://127.0.0.1:34338/callback</code>) that requires a browser on the same machine.</p>
104<p>We swap that one leg for a hosted out-of-band authorization page and keep everything else. Because PKCE's <code>code_verifier</code> never leaves the box, the displayed one-time code is useless to anyone who sees it &mdash; no new token-minting or storage is needed anywhere.</p>
105
106<div class="card">
107<h3>Reused as-is (zero changes)</h3>
108<ul>
109 <li><code>exchangeToken()</code> PKCE exchange &mdash; <code>apps/server/src/cloud/CliTokenManager.ts:147</code></li>
110 <li>Token persistence in <code>ServerSecretStore</code> (<code>cloud-cli-oauth-token</code>)</li>
111 <li><code>acquireRelayClientForLink()</code> cloudflared install + progress &mdash; <code>cli/connect.ts:146</code></li>
112 <li><code>CliState.setCliDesiredCloudLink()</code> + server-side provisioning on start</li>
113 <li>All relay endpoints (<code>infra/relay</code>) and contracts &mdash; <strong>untouched</strong></li>
114 <li>Existing subcommands <code>login/link/status/unlink/logout</code> &mdash; semantics unchanged</li>
115 <li>Web app Clerk session + hosted-page precedent (<code>routes/pair.tsx</code>, <code>hostedPairing.ts</code>)</li>
116</ul>
117</div>
118
119<h2>Auth flow (hosted out-of-band OAuth, Clerk PKCE)</h2>
120
121<figure>
122<svg viewBox="0 0 820 470" width="100%" role="img" aria-label="Sequence diagram of the out-of-band OAuth flow">
123 <!-- lanes -->
124 <rect x="20" y="16" width="220" height="44" rx="8" fill="#121212" stroke="#2a2a2a"/>
125 <rect x="300" y="16" width="220" height="44" rx="8" fill="#121212" stroke="#2a2a2a"/>
126 <rect x="580" y="16" width="220" height="44" rx="8" fill="#121212" stroke="#2a2a2a"/>
127 <text x="130" y="43" fill="#fff" font-size="14" font-weight="600" text-anchor="middle">Remote box &#8212; t3 CLI</text>
128 <text x="410" y="43" fill="#fff" font-size="14" font-weight="600" text-anchor="middle">Laptop &#8212; app.t3.codes</text>
129 <text x="690" y="43" fill="#fff" font-size="14" font-weight="600" text-anchor="middle">Clerk</text>
130 <line x1="130" y1="60" x2="130" y2="450" stroke="#2a2a2a"/>
131 <line x1="410" y1="60" x2="410" y2="450" stroke="#2a2a2a"/>
132 <line x1="690" y1="60" x2="690" y2="450" stroke="#2a2a2a"/>
133
134 <!-- 1: generate + print -->
135 <rect x="30" y="80" width="200" height="34" rx="6" fill="#1e2a4a"/>
136 <text x="130" y="101" fill="#7c9cff" font-size="12.5" text-anchor="middle">1. gen verifier + challenge + state</text>
137
138 <!-- 2: user opens URL -->
139 <line x1="130" y1="140" x2="404" y2="140" stroke="#7c9cff" stroke-dasharray="5 4"/>
140 <polygon points="404,140 394,135 394,145" fill="#7c9cff"/>
141 <text x="267" y="132" fill="#a3a3a3" font-size="12.5" text-anchor="middle">2. user opens /connect#&#123;state,challenge&#125;</text>
142
143 <!-- 3: redirect to clerk -->
144 <line x1="410" y1="185" x2="684" y2="185" stroke="#e5e5e5"/>
145 <polygon points="684,185 674,180 674,190" fill="#e5e5e5"/>
146 <text x="547" y="177" fill="#a3a3a3" font-size="12.5" text-anchor="middle">3. sign in &#8594; /oauth/authorize (PKCE)</text>
147
148 <!-- 4: redirect back with code -->
149 <line x1="690" y1="230" x2="416" y2="230" stroke="#e5e5e5"/>
150 <polygon points="416,230 426,225 426,235" fill="#e5e5e5"/>
151 <text x="553" y="222" fill="#a3a3a3" font-size="12.5" text-anchor="middle">4. redirect /connect/callback?code&amp;state</text>
152
153 <!-- 5: page shows code -->
154 <rect x="310" y="252" width="200" height="34" rx="6" fill="#14301e"/>
155 <text x="410" y="273" fill="#4ade80" font-size="12.5" text-anchor="middle">5. shows account + authorization code</text>
156
157 <!-- 6: user enters code -->
158 <line x1="410" y1="316" x2="136" y2="316" stroke="#7c9cff" stroke-dasharray="5 4"/>
159 <polygon points="136,316 146,311 146,321" fill="#7c9cff"/>
160 <text x="273" y="308" fill="#a3a3a3" font-size="12.5" text-anchor="middle">6. user enters code in terminal</text>
161
162 <!-- 7: token exchange -->
163 <line x1="130" y1="360" x2="684" y2="360" stroke="#e5e5e5"/>
164 <polygon points="684,360 674,355 674,365" fill="#e5e5e5"/>
165 <text x="407" y="352" fill="#a3a3a3" font-size="12.5" text-anchor="middle">7. POST /oauth/token &#123;code + verifier&#125; &#8594; access/refresh tokens</text>
166
167 <!-- 8: done -->
168 <rect x="30" y="392" width="200" height="46" rx="6" fill="#14301e"/>
169 <text x="130" y="411" fill="#4ade80" font-size="12.5" text-anchor="middle">8. store token, set desired link,</text>
170 <text x="130" y="427" fill="#4ade80" font-size="12.5" text-anchor="middle">install relay client &#8594; Connected!</text>
171</svg>
172<figcaption>The verifier never leaves the box (steps 1&#8594;7), so the authorization code is worthless if observed. state/challenge ride the URL fragment &mdash; they are not secrets.</figcaption>
173</figure>
174
175<div class="card">
176<h3>Details that keep it simple</h3>
177<ul>
178 <li><strong>Stateless URL, no short-link service.</strong> The <code>/connect</code> page reads <code>state</code> + <code>code_challenge</code> from the URL fragment and builds the Clerk authorize URL client-side. ~100-char URL &mdash; fine to transfer into an SSH session.</li>
179 <li><strong>State check without a backend:</strong> the callback page displays one authorization blob of <code>code.state</code>; the CLI splits it and verifies <code>state</code> matches what it generated. One line on each side, preserves the loopback flow's CSRF check.</li>
180 <li><strong>Phishing is addressed with copy, not code:</strong> the callback page shows <em>which account</em> is being connected ("Connecting as theo@&hellip;") and warns: <em>"Only enter this code in a terminal session you started yourself."</em> No mechanism needed.</li>
181 <li><strong>Code expiry is a non-issue:</strong> Clerk auth codes live 10 minutes &mdash; the same timeout the existing loopback flow already uses. Wrong/expired code &#8594; friendly retry that reprints the URL.</li>
182 <li><strong>One external config step:</strong> register <code>https://app.t3.codes/connect/callback</code> as an allowed redirect URI on the existing Clerk CLI OAuth client. No new client, no new scopes.</li>
183</ul>
184</div>
185
186<h2>The phases (one PR, one commit each)</h2>
187<p class="kv">Ordering is dependency order: each phase is independently revertable and the tree is green at every boundary. Phases 1&ndash;3 are the PR; phase 4 ships separately later.</p>
188
189<table>
190 <tr><th style="width:1%">Phase</th><th>Scope</th><th>Files</th><th style="width:1%">~LOC</th></tr>
191 <tr>
192 <td><span class="pill core">1</span></td>
193 <td><strong>Hosted code page</strong> (web-only, purely additive, zero risk). Two static routes modeled on <code>pair.tsx</code>: <code>/connect</code> (ensure Clerk session, then client-side redirect to authorize &mdash; it's a static SPA, no server 302) and <code>/connect/callback</code> (validate params, show account + copyable code + safety warning). Both routes guard against non-hosted deployments &mdash; redirect to <code>/</code> unless <code>isHostedStaticApp()</code>, same pattern as <code>pair.tsx</code>, since this bundle also ships in local instances. Plus the Clerk dashboard redirect-URI entry.</td>
194 <td><code>apps/web/src/routes/connect.tsx</code><br><code>apps/web/src/routes/connect.callback.tsx</code></td>
195 <td>~200</td>
196 </tr>
197 <tr>
198 <td><span class="pill core">2</span></td>
199 <td><strong>CLI out-of-band OAuth flow + single command.</strong> Add an out-of-band OAuth login path to <code>CliTokenManager</code> (print URL, <code>Prompt.text</code> for the code, reuse <code>exchangeToken</code>). Make bare <code>t3 connect</code> a handler = login + link (subcommands untouched). Auto-pick headless mode inside SSH sessions (<code>SSH_CONNECTION</code>/<code>SSH_TTY</code> &mdash; nothing else); <code>--headless</code> flag as manual override. Loopback stays the default on desktop &mdash; no regression.</td>
200 <td><code>cloud/CliTokenManager.ts</code> (+60)<br><code>cloud/publicConfig.ts</code> (+10)<br><code>cli/connect.ts</code> (+60)</td>
201 <td>~150</td>
202 </tr>
203 <tr>
204 <td><span class="pill new">3</span></td>
205 <td><strong>Background on boot &mdash; Linux first</strong> (the SSH case). One new module: pinned runtime install to <code>~/.t3/runtime/versions/&lt;v&gt;</code> + <code>current</code> symlink, systemd <em>user</em> unit with absolute node/t3 paths, <code>enable-linger</code>. y/n prompt at the end of <code>connect</code>; teardown in <code>logout</code>. Install and service-start failures must land in a log file (under <code>~/.t3/userdata/logs/</code>) whose path is printed at connect time &mdash; systemd user units fail invisibly otherwise. Unit-file generation is pure string-building &#8594; trivially testable. macOS launchd / Windows follow as 3b/3c only if wanted.</td>
206 <td><code>cloud/bootService.ts</code> (new)<br><code>cli/connect.ts</code> (+prompt/teardown)</td>
207 <td>~250</td>
208 </tr>
209 <tr>
210 <td><span class="pill later">4</span></td>
211 <td><strong>Web-triggered update</strong> (optional follow-up PR; not part of this one, not needed for the core UX). Web detects <code>daemon version &lt; latest-on-channel</code> using the existing version-skew surface + hosted manifest; one authenticated "update" command &mdash; client says <em>update</em>, daemon resolves/verifies the version itself (never client-specified &mdash; that would be RCE). Stage install &#8594; verify &#8594; atomic symlink swap &#8594; <code>systemctl --user restart</code>. Progress streams reuse the <code>RelayClientInstallProgressEvent</code> pattern.</td>
212 <td>web banner + one control command + daemon update routine</td>
213 <td>later</td>
214 </tr>
215</table>
216
217<h3>Runtime layout (phase 3)</h3>
218<pre><code>~/.t3/runtime/
219├── versions/0.0.27/ &larr; npm install --prefix (gets native deps right: node-pty etc.)
220└── current -> versions/0.0.27
221
222~/.config/systemd/user/t3code.service &larr; ExecStart=/abs/path/node .../current/.../t3 serve
223loginctl enable-linger $USER &larr; survives SSH logout / reboot</code></pre>
224<p class="kv">Why a real <code>npm install</code> and not "reuse the npx binary": the npx cache is ephemeral and <code>t3</code> ships native deps (<code>node-pty</code>, <code>@ff-labs/fff-node</code>) that need per-platform prebuilds. Why pinned and not <code>npx t3@latest</code> in the unit: a boot-time registry fetch means the box may simply not come up (network down, PATH-less systemd env, nvm). Deterministic boot; updates happen out-of-band (phase 4 follow-up) or by re-running <code>npx t3 connect</code>.</p>
225
226<h2>Explicitly not doing</h2>
227<ul>
228 <li><span class="no">&#10007;</span> Relay / <code>infra</code> / contracts changes &mdash; none, in any phase</li>
229 <li><span class="no">&#10007;</span> Short-link service (<code>app.t3.codes/c/AB7K</code>) &mdash; only matters for hand-typing; revisit if ever needed</li>
230 <li><span class="no">&#10007;</span> RFC 8628 device grant &mdash; wrong UX direction, unverified Clerk support</li>
231 <li><span class="no">&#10007;</span> Auto-update loop in the daemon &mdash; web-triggered only (phase 4 follow-up), user stays in control</li>
232 <li><span class="no">&#10007;</span> Project auto-registration &mdash; workspace assumed set up; the web UI handles the rest</li>
233 <li><span class="no">&#10007;</span> Changing existing loopback flow, subcommands, or desktop behavior</li>
234</ul>
235
236<h2>Risks &amp; checks</h2>
237<ul>
238 <li><span class="warn">&#9888;</span> <strong>Clerk redirect URI:</strong> confirm the CLI OAuth client accepts the hosted redirect and that the token endpoint honors PKCE exchange for codes issued to it. Verify in staging before the phase 2 commit. <em>(Only external dependency in the plan.)</em></li>
239 <li><span class="warn">&#9888;</span> <strong>systemd user env is minimal:</strong> always write absolute paths for node + t3 into the unit; never rely on PATH. Service failures are invisible by default &mdash; hence the phase 3 requirement to log to a printed file path.</li>
240 <li><span class="warn">&#9888;</span> <strong>Linger prompt honesty:</strong> the y/n prompt should say the machine becomes reachable via T3 Connect whenever powered on &mdash; that's the feature, but say it.</li>
241 <li><span class="ok">&#10003;</span> Re-running <code>connect</code> when linked &#8594; idempotent: refresh token, re-confirm service, done.</li>
242</ul>
243
244<h2>Decision log</h2>
245<ul class="kv">
246 <li>Auth: hosted out-of-band OAuth redirect on Clerk PKCE (not relay-brokered pairing, not device grant) &mdash; chosen for minimal new surface.</li>
247 <li>URL: stateless static page, no backend short-link.</li>
248 <li>Service: real per-user login service (systemd user + linger first); detect + offer install, never silent.</li>
249 <li>Binary: pinned managed runtime under <code>~/.t3</code>; interactive <code>npx</code> usage untouched.</li>
250 <li>Updates: not always-latest; web UI surfaces available updates with one-click trigger (phase 4 follow-up).</li>
251 <li>Delivery: one PR with a commit per phase (green tree at every boundary), not separate PRs.</li>
252 <li>Workspace: assumed already set up; no auto-registration.</li>
253</ul>
254
255</main>
256</body>
257</html>
AGENTS.md+7
...@@ -118,6 +118,13 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real...@@ -118,6 +118,13 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real
118- One concern per PR. If the description says "also", split it.118- One concern per PR. If the description says "also", split it.
119- When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit.119- When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit.
120120
121## Plans and work artifacts
122
123- Do not commit implementation plans, research notes, or agent scratch files. Keep temporary working material outside the worktree. `.plans/` is gitignored only as a safety net for legacy tooling.
124- Track active maintainer work in the GitHub issue or project item that owns it. External proposals follow `CONTRIBUTING.md` and belong in Ideas discussions.
125- Put durable architecture, constraints, and decisions in `docs/internals/`. Update those docs when the product changes so agents find current facts instead of abandoned intentions.
126- A merged PR is the implementation record. Close or update its tracking item when the work lands; do not preserve a second checklist in the repository.
127
121## How it works128## How it works
122129
123Clients send typed WebSocket requests. The server turns them into _commands_, a pure _decider_ turns commands into persisted _events_, and a _projector_ derives the read model the UI renders. Provider CLIs run as subprocesses; per-provider _adapters_ translate their native protocols into orchestration events. Side effects run in queue-backed _reactors_ that emit _receipts_ when milestones land. Each turn ends with a _checkpoint_, a hidden git ref, so the app can diff and restore.130Clients send typed WebSocket requests. The server turns them into _commands_, a pure _decider_ turns commands into persisted _events_, and a _projector_ derives the read model the UI renders. Provider CLIs run as subprocesses; per-provider _adapters_ translate their native protocols into orchestration events. Side effects run in queue-backed _reactors_ that emit _receipts_ when milestones land. Each turn ends with a _checkpoint_, a hidden git ref, so the app can diff and restore.
apps/mobile/src/features/files/fileTree.test.ts+1-1
...@@ -68,7 +68,7 @@ describe("mobile file tree helpers", () => {...@@ -68,7 +68,7 @@ describe("mobile file tree helpers", () => {
68 const tree = buildFileTree([68 const tree = buildFileTree([
69 {69 {
70 kind: "file",70 kind: "file",
71 path: ".plans/19-version-control-phase-1-vcs-driver-foundation.md",71 path: "docs/internals/workspace-layout.md",
72 },72 },
73 {73 {
74 kind: "file",74 kind: "file",
apps/web/src/markdown-links.test.ts+4-4
...@@ -132,10 +132,10 @@ describe("resolveMarkdownFileLinkTarget", () => {...@@ -132,10 +132,10 @@ describe("resolveMarkdownFileLinkTarget", () => {
132describe("resolveInlineCodeFileLinkMeta", () => {132describe("resolveInlineCodeFileLinkMeta", () => {
133 it("links relative paths with file extensions", () => {133 it("links relative paths with file extensions", () => {
134 expect(134 expect(
135 resolveInlineCodeFileLinkMeta(".plans/worktree-management-v1.md", "/Users/julius/project"),135 resolveInlineCodeFileLinkMeta("docs/internals/workspace-layout.md", "/Users/julius/project"),
136 ).toMatchObject({136 ).toMatchObject({
137 targetPath: "/Users/julius/project/.plans/worktree-management-v1.md",137 targetPath: "/Users/julius/project/docs/internals/workspace-layout.md",
138 basename: "worktree-management-v1.md",138 basename: "workspace-layout.md",
139 });139 });
140 });140 });
141141
...@@ -270,7 +270,7 @@ describe("resolveInlineCodeFileLinkMeta", () => {...@@ -270,7 +270,7 @@ describe("resolveInlineCodeFileLinkMeta", () => {
270 });270 });
271271
272 it("ignores relative paths without a cwd to resolve against", () => {272 it("ignores relative paths without a cwd to resolve against", () => {
273 expect(resolveInlineCodeFileLinkMeta(".plans/worktree-management-v1.md")).toBeNull();273 expect(resolveInlineCodeFileLinkMeta("docs/internals/workspace-layout.md")).toBeNull();
274 });274 });
275});275});
276276
docs/README.md+1
...@@ -36,6 +36,7 @@ policy in [CONTRIBUTING.md](../CONTRIBUTING.md); agent rules in [AGENTS.md](../A...@@ -36,6 +36,7 @@ policy in [CONTRIBUTING.md](../CONTRIBUTING.md); agent rules in [AGENTS.md](../A
36- [Environment auth](./internals/environment-auth.md)36- [Environment auth](./internals/environment-auth.md)
37- [T3 Connect](./internals/t3-connect.md)37- [T3 Connect](./internals/t3-connect.md)
38- [CI gates](./internals/ci.md)38- [CI gates](./internals/ci.md)
39- [Engineering work artifacts](./internals/work-artifacts.md)
3940
40### Runbooks41### Runbooks
4142
docs/internals/work-artifacts.md created+25
...@@ -0,0 +1,25 @@
1# Engineering work artifacts
2
3> For maintainers. Using T3 Code? See [docs/user](../user/).
4
5Keep planned work out of the source tree. Code search should return the product as it exists, not a mix of current behavior and abandoned intentions.
6
7## Current facts belong in the docs
8
9Put durable architecture, constraints, and operational knowledge in `docs/internals/` or `docs/operations/`. Write these documents in the present tense and update them with the code they describe.
10
11When the reason for a decision will matter after implementation, record it in the relevant internal document. Use a separate decision record under `docs/internals/` only when the rationale does not fit cleanly beside the current architecture.
12
13## Planned work belongs in GitHub
14
15Track active maintainer work in its GitHub issue or project item. The tracking item should state the outcome, constraints, and acceptance criteria, then link the pull requests that implement it. Split large efforts into one durable specification and small work items that can each close independently.
16
17Close completed items. Update or delete invalidated work before starting the next implementation session. External proposals follow [CONTRIBUTING.md](../../CONTRIBUTING.md) and belong in Ideas discussions rather than issues.
18
19## Temporary work stays temporary
20
21Keep agent scratch files, exploratory research, transcripts, and session handoff notes outside the worktree. They are inputs to the work, not project documentation.
22
23`.plans/` is gitignored as a safety net for legacy tools. Its presence does not make it an accepted project artifact. Pull requests must not add implementation plans or temporary research under another name.
24
25A pull request records what changed and why. If a fact must survive after the pull request merges, update the relevant documentation. Otherwise, the tracking item and pull request are the record.
vite.config.ts-1
...@@ -28,7 +28,6 @@ export default defineConfig({...@@ -28,7 +28,6 @@ export default defineConfig({
28 ignorePatterns: [28 ignorePatterns: [
29 ".reference",29 ".reference",
30 ".repos/**",30 ".repos/**",
31 ".plans",
32 ".alchemy",31 ".alchemy",
33 "dist",32 "dist",
34 "dist-electron",33 "dist-electron",