@jeffober/thatch 0.2.4 → 0.2.6
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.
- package/README.md +56 -26
- package/artifacts/skills/common.md +1 -0
- package/artifacts/skills/thatch-clear-writing.md +103 -5
- package/artifacts/skills/thatch-code-archaeology.md +9 -0
- package/artifacts/skills/thatch-code-review.md +16 -8
- package/artifacts/skills/thatch-coordination.md +69 -0
- package/artifacts/skills/thatch-plan-refinement.md +33 -4
- package/artifacts/skills/thatch-pr-description.md +21 -13
- package/artifacts/skills/thatch-refine-personal.md +5 -0
- package/artifacts/skills/thatch-refine.md +7 -1
- package/artifacts/skills/thatch-review-context.md +55 -2
- package/artifacts/skills/thatch-review-followup.md +4 -4
- package/artifacts/skills/thatch-review-no-slop.md +15 -0
- package/artifacts/skills/thatch-review-response.md +1 -1
- package/artifacts/skills/thatch-review-synthesizer.md +21 -1
- package/bin/thatch +48 -15
- package/docs/assets/deepwiki-badge.svg +1 -0
- package/docs/dev/README.md +18 -10
- package/docs/dev/features/README.md +1 -1
- package/docs/dev/features/behavior-engine.md +10 -0
- package/docs/dev/features/cli.md +4 -1
- package/docs/dev/features/commands.md +40 -9
- package/docs/dev/features/cross-session-chat.md +194 -23
- package/docs/dev/features/database.md +4 -4
- package/docs/dev/features/notifications.md +1 -2
- package/docs/dev/features/nudge-pipeline.md +14 -0
- package/docs/dev/features/opencode-plugin.md +56 -15
- package/docs/dev/features/prediction-engine.md +71 -6
- package/docs/dev/features/qa-system.md +19 -3
- package/docs/dev/features/session-lifecycle.md +6 -6
- package/docs/dev/features/session-tabs.md +161 -0
- package/docs/dev/features/setup.md +5 -0
- package/docs/dev/features/watchers.md +62 -6
- package/docs/dev/gotchas.md +89 -0
- package/docs/dev/mcp-parity.md +5 -5
- package/docs/dev/setup-and-hooks.md +7 -2
- package/docs/dev/skills.md +12 -9
- package/docs/plans/generalized-session-heartbeat.md +212 -0
- package/docs/plans/prediction-consolidation.md +88 -68
- package/docs/user/README.md +29 -13
- package/docs/user/behavior-engine.md +7 -1
- package/docs/user/cli.md +5 -1
- package/docs/user/code-review.md +43 -2
- package/docs/user/commands.md +37 -10
- package/docs/user/cross-session-chat.md +76 -26
- package/docs/user/prediction-engine.md +23 -1
- package/docs/user/setup.md +16 -0
- package/docs/user/skills.md +9 -7
- package/docs/user/watchers.md +22 -7
- package/package.json +10 -2
- package/src/alerts.ts +1 -1
- package/src/capabilities.ts +20 -6
- package/src/chat.ts +358 -43
- package/src/commands.ts +123 -18
- package/src/db.ts +74 -3
- package/src/git.ts +3 -1
- package/src/hygiene.ts +5 -0
- package/src/mcp.ts +101 -9
- package/src/opencode/tui-plugin.ts +90 -0
- package/src/opencode/v2.ts +440 -66
- package/src/prediction.ts +90 -0
- package/src/prompts.ts +164 -29
- package/src/runtime.ts +298 -15
- package/src/scoring-engine.ts +40 -9
- package/src/session-db.ts +4 -4
- package/src/session-tab-shared.ts +231 -0
- package/src/setup.ts +9 -3
- package/src/skills.ts +6 -9
- package/src/tab-state.ts +125 -0
- package/src/tool-defs.ts +454 -76
- package/src/tools.ts +2 -0
- package/src/watchers.ts +263 -26
- package/docs/plans/intuition-drives.md +0 -147
- package/docs/plans/llm-alerts.md +0 -287
- package/docs/plans/thatch-actions.md +0 -179
- 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.
|