@jeffober/thatch 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +8 -1
  2. package/artifacts/skills/thatch-clear-writing.md +103 -5
  3. package/artifacts/skills/thatch-coordination.md +69 -0
  4. package/artifacts/skills/thatch-pr-description.md +21 -13
  5. package/artifacts/skills/thatch-review-no-slop.md +15 -0
  6. package/bin/thatch +27 -3
  7. package/docs/assets/deepwiki-badge.svg +1 -0
  8. package/docs/dev/README.md +7 -2
  9. package/docs/dev/features/cli.md +2 -0
  10. package/docs/dev/features/cross-session-chat.md +118 -11
  11. package/docs/dev/features/notifications.md +1 -2
  12. package/docs/dev/features/opencode-plugin.md +18 -3
  13. package/docs/dev/features/prediction-engine.md +44 -5
  14. package/docs/dev/features/session-tabs.md +155 -0
  15. package/docs/dev/features/setup.md +5 -0
  16. package/docs/dev/features/watchers.md +62 -6
  17. package/docs/dev/gotchas.md +68 -0
  18. package/docs/dev/mcp-parity.md +2 -2
  19. package/docs/dev/setup-and-hooks.md +7 -2
  20. package/docs/dev/skills.md +1 -1
  21. package/docs/plans/generalized-session-heartbeat.md +212 -0
  22. package/docs/plans/prediction-consolidation.md +88 -68
  23. package/docs/user/README.md +12 -3
  24. package/docs/user/cli.md +3 -0
  25. package/docs/user/commands.md +11 -4
  26. package/docs/user/cross-session-chat.md +31 -8
  27. package/docs/user/prediction-engine.md +11 -0
  28. package/docs/user/setup.md +16 -0
  29. package/docs/user/skills.md +1 -1
  30. package/docs/user/watchers.md +22 -7
  31. package/package.json +10 -2
  32. package/src/alerts.ts +1 -1
  33. package/src/chat.ts +320 -27
  34. package/src/commands.ts +14 -0
  35. package/src/db.ts +51 -1
  36. package/src/git.ts +3 -1
  37. package/src/hygiene.ts +5 -0
  38. package/src/mcp.ts +95 -7
  39. package/src/opencode/tui-plugin.ts +70 -0
  40. package/src/opencode/v2.ts +197 -4
  41. package/src/prediction.ts +90 -0
  42. package/src/prompts.ts +110 -23
  43. package/src/runtime.ts +187 -9
  44. package/src/session-db.ts +4 -4
  45. package/src/session-tab-shared.ts +180 -0
  46. package/src/setup.ts +9 -3
  47. package/src/skills.ts +1 -0
  48. package/src/tool-defs.ts +412 -55
  49. package/src/tools.ts +2 -0
  50. package/src/watchers.ts +263 -26
  51. package/docs/plans/intuition-drives.md +0 -147
  52. package/docs/plans/llm-alerts.md +0 -287
  53. package/docs/plans/thatch-actions.md +0 -179
  54. package/docs/plans/worktree-deletion-recovery.md +0 -246
@@ -1,246 +0,0 @@
1
- # Plan: survive worktree deletion (repo path cache + cwd fallback)
2
-
3
- Status: CONSENSUS (round 5, five fresh-context review rounds). Ready to
4
- implement.
5
-
6
- ## Problem
7
-
8
- opencode sessions run inside git worktrees (`~/dev/wt-<repo>/<branch>`).
9
- When the worktree is deleted after its branch merges:
10
-
11
- - **Store identity corruption.** `detectRepo` (src/git.ts:78) runs
12
- `git remote get-url origin` and `git rev-parse --git-common-dir` with
13
- `cwd` set to the session directory. When that directory is deleted, Bun
14
- rejects the spawn (ENOENT at chdir; verified empirically), both probes
15
- throw, and the final fallback (src/git.ts:105) returns the directory
16
- basename -- for a wt worktree, the branch name. That plausible-but-wrong
17
- name becomes the project store, and memories silently target a store no
18
- future session reads. The `"unknown"` sentinel (which degrades to the
19
- global store, src/tool-defs.ts:38) is not reachable on this path: a real
20
- deleted-worktree path is never empty, so the `|| "unknown"` at
21
- src/git.ts:105 never fires for it. (A degenerate input like `/` does
22
- return `"unknown"` today; that case is unaffected by this plan.)
23
- - **Watcher cwd pinning.** `watch_command_create` captures the project dir
24
- at registration (src/tool-defs.ts:1460-1464) and the poll runner spawns
25
- `bash -c` there every cycle (src/watchers.ts:440). A deleted cwd makes
26
- every poll reject; the watcher spins errors forever.
27
-
28
- Only four surfaces pin a cwd for spawning: src/git.ts:82, src/git.ts:92,
29
- src/git.ts:119 (`listBranches`), and src/watchers.ts:440. The `gh api`
30
- spawn (src/watchers.ts:318) uses absolute REST paths and no cwd -- it is
31
- unaffected. Watchers are in-memory only by documented contract
32
- (src/watchers.ts:19-28).
33
-
34
- **Host behavior when the directory is gone (verified in the opencode
35
- monorepo, packages/opencode/src/project + packages/core/src/project.ts and
36
- packages/core/src/fs-util.ts):** opencode does not refuse to resume. It
37
- walks up from the missing directory, finds no `.git`, and boots the global
38
- project with `worktree = "/"` while still passing the session's original
39
- (deleted) path as `directory` (packages/opencode/src/plugin/index.ts:153-157).
40
- Separately, Bun cannot launch at all from a deleted cwd, so per-prompt hook
41
- processes in that case never run -- they are not a recovery surface. The
42
- recovery surfaces are: resumed opencode sessions (via `ctx.directory`) and
43
- any host that spawns from a healthy cwd while passing the deleted path via
44
- `CLAUDE_PROJECT_DIR` / `CURSOR_PROJECT_DIR` (env has priority at
45
- src/git.ts:79).
46
-
47
- ## Design
48
-
49
- ### 1. `repo_paths` cache table
50
-
51
- ```sql
52
- CREATE TABLE IF NOT EXISTS repo_paths (
53
- worktree_path TEXT PRIMARY KEY,
54
- main_path TEXT NOT NULL,
55
- repo_slug TEXT NOT NULL,
56
- resolved_at INTEGER NOT NULL
57
- );
58
- ```
59
-
60
- - Follows the existing `CREATE TABLE IF NOT EXISTS` + ad-hoc migration
61
- pattern (src/db.ts:89+). Writes use `ON CONFLICT DO UPDATE` upsert
62
- (existing idiom, src/db.ts:544).
63
- - **Key normalization.** The key is `realpathSync(dir)` at write time (the
64
- directory exists then). On macOS, `/var/...` and `/private/var/...` are
65
- the same directory with different path forms. Reads try the raw path
66
- first, then `realpathSync(dirname(dir)) + basename(dir)` for the
67
- deleted-directory case.
68
- - **Cache access is injected, not imported.** src/git.ts stays DB-free.
69
- `detectRepo` and the new helpers take an optional cache accessor
70
- (`{ get(dir): Row | null; put(row): void }`); callers with a DB in scope
71
- pass an adapter over the `repo_paths` table. Where the DB does not exist
72
- yet, creation is reordered ahead of detection: src/index.ts:168 vs :171
73
- and src/mcp.ts:124 vs :131. Callers without a cache behave as today plus
74
- the `"unknown"`-on-deleted-dir fix.
75
- - **Write trigger.** Only on successful resolution via the remote or
76
- common-dir probes -- never from the basename fallback (caching that
77
- output would store the exact garbage the cache exists to fix). Writes
78
- are read-then-maybe-write: skip when the row already matches.
79
- Frequency note: in the opencode plugin, detection runs once at init
80
- (src/index.ts:168); the flush-tools hook runs per prompt but only on the
81
- Claude Code / Cursor hosts (bin/thatch:973-974, :1006).
82
-
83
- ### 2. main_path derivation
84
-
85
- New helper in src/git.ts, run unconditionally at cache-write time:
86
-
87
- ```ts
88
- // resolveMainCheckout(dir): Promise<string | null>
89
- // Runs `git rev-parse --path-format=absolute --git-common-dir` with cwd=dir.
90
- // Returns null when git fails or is not a repo.
91
- ```
92
-
93
- `--path-format=absolute` makes the output absolute in every live case
94
- (verified empirically): a linked worktree reports the main checkout's
95
- absolute `.git`, a root checkout reports its own absolute `.git`. So
96
- `main_path = dirname(output)` uniformly. A bare repo reports
97
- `/path/repo.git`, which fails the `endsWith("/.git")` check; the resulting
98
- nonsense `main_path` fails validation and is handled by the eviction rules
99
- below -- documented at the write site, not special-cased.
100
-
101
- detectRepo's public signature stays `Promise<string>`; a sibling helper
102
- `resolveMainPath(dir, cache?): Promise<string | null>` reads and validates
103
- a cache row for consumers that need the path.
104
-
105
- ### 3. detectRepo recovery for a deleted directory
106
-
107
- When both git probes throw:
108
-
109
- 1. `statSync(dir)` fails (directory gone): consult the cache.
110
- - Cache hit, and the cached `main_path` validates (below): return the
111
- cached `repo_slug`.
112
- - Cache hit with failed validation: return `"unknown"`.
113
- - Cache miss: return `"unknown"`, never the basename. This preserves
114
- the documented contract -- unidentifiable contexts must be `"unknown"`,
115
- never a plausible wrong identity (src/tool-defs.ts:33-41).
116
- 2. Directory exists but is not a git repo: basename fallback unchanged
117
- (its legitimate use).
118
-
119
- **Validation and eviction rules (definitive-invalidation only).**
120
- Validation of a cache hit runs, with cwd = cached `main_path`:
121
-
122
- 1. `statSync(main_path)` -- if it throws (main checkout also gone), the
123
- row is KEPT (the checkout may be re-cloned) and this call degrades to
124
- `"unknown"`. Transient spawn errors also keep the row. Never evict on
125
- an error that might be transient -- a wrongly evicted row permanently
126
- downgrades identity, which is worse than the status quo it replaces.
127
- 2. `git rev-parse --git-common-dir` -- a throw here (not a nonzero exit
128
- from a live dir) is likewise treated as transient; row kept.
129
- 3. Identity check: `git remote get-url origin` in `main_path` must parse
130
- to the cached `repo_slug`. A nonzero exit or unparseable remote on a
131
- live directory is DEFINITIVE (the path exists but is not the recorded
132
- repo -- re-cloned, replaced, or no longer a repo): evict the row,
133
- return `"unknown"`. A thrown spawn error is transient and keeps the
134
- row.
135
-
136
- **Plugin identity recovery on resume.** The opencode plugin detects
137
- identity from `ctx.directory` (falling back to `ctx.worktree` when
138
- absent) instead of `ctx.worktree` alone. This touches the destructure and
139
- type at src/index.ts:165-168, not just the call. For a resumed session in
140
- a deleted worktree, the framework hands the plugin the original deleted
141
- path as `directory` and `"/"` as `worktree`; keying on `directory` lets
142
- the cache return the true store identity. Tool execution dirs
143
- (`ctx.projectDir`) continue to use the framework's worktree value.
144
-
145
- **CLI consistency.** `bin/thatch` `defaultStore()` (bin/thatch:76-82) lacks
146
- the `unknown -> global` mapping that the tool layer applies. The mapping is
147
- added there so all surfaces agree. `detectRepo` consumers that benefit from
148
- the `"unknown"` change are enumerated for tests: `chatHookLine`
149
- (bin/thatch:100), `flush-predictions` (bin/thatch:1217), `reminder`
150
- (bin/thatch:729), `hygiene` (bin/thatch:769).
151
-
152
- ### 4. Shared cwd fallback at the pinned spawn sites
153
-
154
- A helper in src/git.ts:
155
-
156
- ```ts
157
- // resolveSpawnCwd(dir, cache?): Promise<string | null>
158
- // dir if it exists; else the validated cached main_path; else null.
159
- ```
160
-
161
- - `listBranches` (src/git.ts:119) uses it directly: valid pinned cwd >
162
- cached main checkout > current error behavior. `runWatchedCommand` gets
163
- the same fallback via the plugin's runner wrapper (section 5) rather
164
- than calling the helper itself. Branch visibility is unaffected by the
165
- fallback (worktrees share `refs/heads` and config; only HEAD and the
166
- index are per-worktree). For `listBranches` this upgrades a fail-safe
167
- (`[]` -> hygiene skips the orphan check) to a correct answer.
168
- - Inside `detectRepo` itself, the two probes use the same fallback so a
169
- deleted worktree with a valid cache recovers without special casing.
170
- - **Surfacing.** The watcher poll loop logs the fallback once per watcher
171
- (a flag, not every cycle -- poll errors already log every cycle at
172
- src/watchers.ts:1071-1073 and spam is the existing failure mode). There
173
- is no LLM-facing surface at poll time; the notification text is not
174
- modified.
175
-
176
- ### 5. Watcher fallback resolved per poll via the injected runner
177
-
178
- The plugin wires a `commandRunner` into the registry
179
- (src/watchers.ts:234, option declared at :251, currently passed only by
180
- tests; the plugin will start passing one at src/index.ts:247-279, where db
181
- is in scope). The plugin's runner is a wrapper that resolves the cwd per
182
- poll:
183
-
184
- ```ts
185
- // plugin scope, where db is available
186
- const runner = async (command, cwd, timeoutMs) => {
187
- const resolved = await resolveSpawnCwd(cwd, cacheAdapter);
188
- if (resolved && resolved !== cwd && !loggedFallback.has(cwd)) {
189
- console.error(`worktree deleted; watcher falling back to ${resolved}`);
190
- loggedFallback.add(cwd); // key on the dead cwd: same once-per-watcher granularity
191
- }
192
- return runWatchedCommand(command, resolved ?? cwd, timeoutMs);
193
- };
194
- ```
195
-
196
- - Resolution order: explicit `cd` > watcher cwd (if it still exists) >
197
- cached main checkout > fail. This is the only mechanism that actually
198
- recovers a watcher whose worktree dies mid-watch -- a fallback captured
199
- at registration would be the same live path as the cwd and therefore
200
- useless, which the previous plan revision got wrong.
201
- - The registry keeps its no-DB contract with no new options: the cache
202
- adapter is closed over in plugin scope, outside the registry.
203
- - New optional param `cd` (z.string()) on `watch_command_create`,
204
- validated at registration: the baseline run (src/watchers.ts:971-985)
205
- spawns with it, so a dead path fails registration with the existing
206
- friendly error (src/watchers.ts:441-443).
207
- - `cd` is not persisted to SQLite; watchers are in-memory by contract
208
- (src/watchers.ts:19-28). A watcher whose fallback path itself later
209
- dies fails its polls -- acceptable under the same lifetime contract.
210
- - `watch_create` and `watch_branch_create` get no `cd`: they run only gh
211
- REST calls with absolute paths (src/watchers.ts:595-607); their only
212
- directory-sensitive input is the init-time `ctx.defaultStore`, which
213
- step 3 fixes at the source.
214
-
215
- ## Tests
216
-
217
- - `tests/git-integration.test.ts` (real tmp repos, existing scaffolding):
218
- `git worktree add` + `rm -rf` the worktree; assert cache-backed slug and
219
- main-path recovery; cache miss yields `"unknown"`, not the branch name;
220
- existing non-git dir still yields the basename; realpath canonicalization
221
- via a symlinked temp path; recycled main_path with a different origin
222
- evicts (definitive) and yields `"unknown"`; missing main_path (stat
223
- throws) keeps the row and yields `"unknown"`.
224
- - `tests/watchers.test.ts` (injected `commandRunner` harness, lines 781+):
225
- resolution ordering (cd > cwd > captured fallback), fallback logged once,
226
- dead cd rejected at registration, watcher registered against a healthy
227
- dir whose fallback was captured before deletion keeps polling.
228
- - `tests/tool-defs.test.ts`: `cd` schema presence and validation; the
229
- existing unknown-to-global tests (lines 326, 477) cover the tool side.
230
-
231
- ## Accepted risks and deferred items
232
-
233
- - Rows for dead worktrees accumulate until definitively invalidated;
234
- bounded by worktrees ever seen. Optional prune deferred.
235
- - Poll-time fallback is not surfaced to the LLM mid-poll (console.error
236
- once per watcher). The eventual notification reports the condition
237
- result, same as today.
238
- - A fallback watcher runs its command from the main checkout root, so
239
- commands using relative paths may observe different state -- potentially
240
- a false exit 0 instead of a failure. Strictly better than today's
241
- permanent error spin, but a real semantic difference worth knowing.
242
- - Bare-repo `main_path` is nonsense until first validation evicts it;
243
- documented at the write site rather than special-cased.
244
- - `mkdtemp`/git path-form divergence claim verified in direction (Bun
245
- normalizes cwd to the realpath form); the exact tmpdir form difference
246
- is covered defensively by the dual read-path anyway.