@jeffober/thatch 0.1.46 → 0.1.47

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/src/index.ts CHANGED
@@ -2,9 +2,9 @@ import { join } from "node:path";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { createDebugLog } from "./debug";
4
4
  import type { Plugin } from "@opencode-ai/plugin";
5
- import { ThatchDB } from "./db";
5
+ import { ThatchDB, repoPathCache } from "./db";
6
6
  import { BgeEmbeddingModel } from "./embeddings";
7
- import { detectRepo, detectWorktreeKind } from "./git";
7
+ import { detectRepo, detectWorktreeKind, resolveSpawnCwd } from "./git";
8
8
  import { createTools } from "./tools";
9
9
  import {
10
10
  systemPrompt,
@@ -24,7 +24,7 @@ import { installOpencodeCommands, COMPACT_READY_TOKEN, EXIT_READY_TOKEN } from "
24
24
  import { hygieneReport } from "./hygiene";
25
25
  import { seedDefaultBehaviors } from "./seed-behaviors";
26
26
  import { startVersionChecker, stopVersionChecker, getVersionChecker, readOnDiskVersion, compareSemver } from "./version-check";
27
- import { WatcherRegistry, ghApiRun, ghAvailable } from "./watchers";
27
+ import { WatcherRegistry, ghApiRun, ghAvailable, runWatchedCommand, withCwdFallback } from "./watchers";
28
28
  import { watcherNotificationNudge, chatNotificationNudge, chatEchoText, isChatEchoParts } from "./prompts";
29
29
  import { ChatPoller, createWakeGate, isDefaultSessionTitle } from "./chat";
30
30
  import { chatEnabled, chatAutoRegister, loadConfig } from "./config";
@@ -162,24 +162,31 @@ export function startupSessionId(osArgs: string[]): string | null {
162
162
  return startupSessionIdFromArgv(process.argv) ?? startupSessionIdFromArgv(osArgs);
163
163
  }
164
164
 
165
- export const server: Plugin = async ({ client, worktree }) => {
165
+ export const server: Plugin = async ({ client, worktree, directory }) => {
166
166
  // The opencode server's cwd is wherever the server happened to start;
167
167
  // `worktree` is the project this plugin instance actually serves.
168
- const repo = await detectRepo(worktree);
168
+ // `directory` is the session's own directory - on resume after the
169
+ // worktree was deleted, opencode reassigns worktree to "/" (it walks up
170
+ // from the missing dir, finds no .git, and boots the global project) but
171
+ // still forwards the original path as directory, which is the only input
172
+ // the repo_paths cache can recover identity from.
169
173
  const home = process.env.HOME ?? "/tmp";
170
174
  const configHome = process.env.XDG_CONFIG_HOME ?? join(home, ".config");
171
175
  const dbPath = process.env.THATCH_DB_PATH ?? join(configHome, "thatch", "thatch.db");
172
176
  const modelName = process.env.THATCH_MODEL ?? "Xenova/bge-small-en-v1.5";
173
177
 
178
+ // The DB must exist before identity detection: detectRepo consults the
179
+ // repo_paths cache when the session directory is gone.
180
+ const db = new ThatchDB(dbPath);
181
+ const repoCache = repoPathCache(db);
182
+ const repo = await detectRepo(directory ?? worktree, repoCache);
183
+
174
184
  // Whether cross-session chat is on (chat.enabled, default on). Read once
175
185
  // at init: it gates the chat poller and the prompt's chat section. The
176
186
  // chat tools re-read the file per call, so a toggle takes effect there
177
187
  // without a restart.
178
188
  const chatOn = chatEnabled(loadConfig(dbPath).config);
179
189
 
180
- const db = new ThatchDB(dbPath);
181
- // Opt-in diagnostics (THATCH_DEBUG), written to debug.log beside the db.
182
- // See debug.ts for the tag filter semantics.
183
190
  const debug = createDebugLog(dbPath);
184
191
  const model = new BgeEmbeddingModel(modelName);
185
192
  const extraction = new ExtractionPipeline();
@@ -244,6 +251,16 @@ export const server: Plugin = async ({ client, worktree }) => {
244
251
  // triggers a model turn even when the user is away. Events carry pointer
245
252
  // data plus machine status (check conclusions, exit codes); the model
246
253
  // fetches logs and further details itself with gh.
254
+ // Watched-command runner: resolves the spawn cwd per poll. When the
255
+ // project directory is deleted mid-watch (worktree merged and cleaned up),
256
+ // the poll falls back to the repo_paths cache's main checkout instead of
257
+ // spinning ENOENT errors until the TTL. The wrapper logs the fallback
258
+ // once per dead directory - per-cycle error logging is the existing
259
+ // failure mode and it spams.
260
+ const watchedCommandRunner = withCwdFallback(runWatchedCommand, (cwd) => resolveSpawnCwd(cwd, repoCache), (from, to) => {
261
+ console.error(`[thatch] project directory ${from} deleted; watcher falling back to main checkout ${to}`);
262
+ });
263
+
247
264
  const watchers = new WatcherRegistry({
248
265
  deliver: async (sessionID, events) => {
249
266
  // Events carry their watch's target label from the registry, so the
@@ -276,6 +293,7 @@ export const server: Plugin = async ({ client, worktree }) => {
276
293
  },
277
294
  canDeliver: canPromptSession,
278
295
  ghRunner: ghApiRun,
296
+ commandRunner: watchedCommandRunner,
279
297
  });
280
298
  // gh presence decides whether watch_create and watch_branch_create work;
281
299
  // checked lazily by the tools, but log once at startup so misconfiguration
@@ -1298,7 +1316,7 @@ export const server: Plugin = async ({ client, worktree }) => {
1298
1316
 
1299
1317
  let hygiene: string | null = null;
1300
1318
  try {
1301
- hygiene = await hygieneReport(db, repo, worktree);
1319
+ hygiene = await hygieneReport(db, repo, worktree, repoCache);
1302
1320
  } catch (err) {
1303
1321
  console.error(`[thatch] hygiene report failed: ${err}`);
1304
1322
  }
package/src/mcp.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { z } from "zod";
3
- import { ThatchDB } from "./db";
3
+ import { ThatchDB, repoPathCache } from "./db";
4
4
  import { mcpSessionID } from "./chat";
5
5
  import { BgeEmbeddingModel } from "./embeddings";
6
6
  import { detectRepo } from "./git";
@@ -121,14 +121,16 @@ export function compilePrompts(): Map<string, ActionDef> {
121
121
  */
122
122
  export async function runMcpServer(): Promise<void> {
123
123
  const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
124
- const repo = await detectRepo(projectDir);
125
124
 
126
125
  const home = process.env.HOME ?? "/tmp";
127
126
  const configHome = process.env.XDG_CONFIG_HOME ?? `${home}/.config`;
128
127
  const dbPath = process.env.THATCH_DB_PATH ?? `${configHome}/thatch/thatch.db`;
129
128
  const modelName = process.env.THATCH_MODEL ?? "Xenova/bge-small-en-v1.5";
130
129
 
130
+ // The DB must exist before identity detection: detectRepo consults the
131
+ // repo_paths cache when the project directory is gone (deleted worktree).
131
132
  const db = new ThatchDB(dbPath);
133
+ const repo = await detectRepo(projectDir, repoPathCache(db));
132
134
  const model = new BgeEmbeddingModel(modelName);
133
135
 
134
136
  await seedDefaultBehaviors(db, model);
package/src/prompts.ts CHANGED
@@ -99,6 +99,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
99
99
  - \`thatch-code-review\` - full multi-agent code review (dispatches 8 specialists + synthesizer). Requires sub-agent support.
100
100
  - \`thatch-review-followup\` - alternate entrypoint for follow-up review rounds. Verifies whether the author's responses and code changes since your last review adequately addressed prior findings, offers to reply on resolved items, then optionally re-runs the full structured review.
101
101
  - \`thatch-code-archaeology\` - investigate an existing feature, debug an unfamiliar area, or begin a new ticket. Explores the code base from multiple angles (data model, state flow, git history, sibling features) before proposing changes. The research skill; pair with \`thatch-coding-workflow\`.
102
+ - \`thatch-plan-refinement\` - refine a plan before implementing it: fresh-context reviewer rounds until consensus, with an optional deep-mode lens fan-out (reuse, alternatives, hidden problems, safe-to-modify, archaeology). Use when a plan for a code change is about to be built or the user asks to stress-test a plan.
102
103
  - \`thatch-change-walkthrough\` - produces a teaching walkthrough of a diff with a specific format (SYNOPSIS + per-workflow orient/mechanism/numbered-stages + change overlay mirroring the same numbers) and calibrated prose rules. Load the skill for the format and prose rules, not just for the research method.
103
104
  - \`thatch-code-walkthrough\` - produces a teaching walkthrough for a feature/workflow as it stands today with a specific format (SYNOPSIS + per-workflow orient/mechanism/numbered-stages + Key files) and calibrated prose rules. Also use proactively to draft high-level docs for new or undocumented features (suitable for pkg/README.md or docs/features/<name>.md). Load the skill for the format and prose rules, not just for the research method.
104
105
  - \`thatch-review-context\` - gather project context (PRs, tickets, TODOs, deferred work) before a review.
@@ -408,6 +409,7 @@ Thatch ships skills for code review, project investigation, and memory
408
409
  workflows. The host auto-discovers them, but reach for them proactively:
409
410
 
410
411
  - \`thatch-code-archaeology\` - investigate an existing feature, debug an unfamiliar area, or begin a new ticket. Explores the code base from multiple angles (data model, state flow, git history, sibling features) before proposing changes. The research skill; pair with \`thatch-coding-workflow\`.
412
+ - \`thatch-plan-refinement\` - refine a plan before implementing it: fresh-context reviewer rounds until consensus, with an optional deep-mode lens fan-out (reuse, alternatives, hidden problems, safe-to-modify, archaeology). Use when a plan for a code change is about to be built or the user asks to stress-test a plan.
411
413
  - \`thatch-change-walkthrough\` - produces a teaching walkthrough of a diff with a specific format (SYNOPSIS + per-workflow orient/mechanism/numbered-stages + change overlay mirroring the same numbers) and calibrated prose rules. Load the skill for the format and prose rules, not just for the research method.
412
414
  - \`thatch-code-walkthrough\` - produces a teaching walkthrough for a feature/workflow as it stands today with a specific format (SYNOPSIS + per-workflow orient/mechanism/numbered-stages + Key files) and calibrated prose rules. Also use proactively to draft high-level docs for new or undocumented features (suitable for pkg/README.md or docs/features/<name>.md). Load the skill for the format and prose rules, not just for the research method.
413
415
  - \`thatch-review-context\` - gather project context (PRs, tickets, TODOs, deferred work) before a review.
package/src/skills.ts CHANGED
@@ -70,6 +70,7 @@ function loadSharedSkills(): SkillDef[] {
70
70
  "thatch-code-walkthrough",
71
71
  "thatch-session-reflection",
72
72
  "thatch-coding-workflow",
73
+ "thatch-plan-refinement",
73
74
  "thatch-pr-description",
74
75
  "thatch-ticket-description",
75
76
  "thatch-split-overlarge-pr",
package/src/tool-defs.ts CHANGED
@@ -1432,8 +1432,9 @@ const watchCommandCreateDef: ToolDef = {
1432
1432
  "per-run timeout kills a blocking command each cycle, wasting up to " +
1433
1433
  "30s of the shared poll cycle on a check that can never succeed). " +
1434
1434
  "Use this for long or unknown-duration waits on any local condition " +
1435
- "instead of sleep-polling. The command runs in this project directory " +
1436
- "via bash -c about every 60s, killed at a 30s per-run timeout " +
1435
+ "instead of sleep-polling. The command runs in the project directory " +
1436
+ "(or the cd path) via bash -c about every 60s, killed at a 30s " +
1437
+ "per-run timeout " +
1437
1438
  "(THATCH_WATCH_COMMAND_TIMEOUT_SECONDS overrides; a timed-out run " +
1438
1439
  "means not-done-yet). One-shot by design: it fires once on the first " +
1439
1440
  "exit 0, auto-cancels, and the notification carries only the exit code " +
@@ -1448,6 +1449,11 @@ const watchCommandCreateDef: ToolDef = {
1448
1449
  "The shell command to run as a condition: a fast, idempotent status " +
1449
1450
  "check that exits 0 when the wait is over. Its output is discarded.",
1450
1451
  ),
1452
+ cd: z.string().optional().describe(
1453
+ "Directory the command runs in (the bash cwd). Defaults to the " +
1454
+ "project directory. Use only when the condition must be checked " +
1455
+ "somewhere else; a nonexistent path is rejected at registration.",
1456
+ ),
1451
1457
  },
1452
1458
  opencodeOnly: true,
1453
1459
  async execute(args, ctx, host) {
@@ -1457,11 +1463,12 @@ const watchCommandCreateDef: ToolDef = {
1457
1463
  if (!ctx.watchers) {
1458
1464
  return "Watching is unavailable: no watcher registry was wired by this host.";
1459
1465
  }
1460
- if (!ctx.projectDir) {
1466
+ const dir = (args.cd as string | undefined) ?? ctx.projectDir;
1467
+ if (!dir) {
1461
1468
  return "Watching is unavailable: no project directory was wired by this host.";
1462
1469
  }
1463
1470
  const command = args.command as string;
1464
- const result = await ctx.watchers.createCommand(host.sessionID, command, ctx.projectDir);
1471
+ const result = await ctx.watchers.createCommand(host.sessionID, command, dir);
1465
1472
  if (!result.ok) return `Watcher not created: ${result.error}`;
1466
1473
  const w = result.watcher;
1467
1474
  return (
package/src/watchers.ts CHANGED
@@ -233,6 +233,32 @@ export interface CommandRunResult {
233
233
 
234
234
  export type CommandRunner = (command: string, cwd: string, timeoutMs: number) => Promise<CommandRunResult>;
235
235
 
236
+ /**
237
+ * Wraps a CommandRunner so the spawn cwd is resolved per poll: the given
238
+ * resolver returns the directory to run in (the project dir when alive, or
239
+ * the cached main checkout when the worktree was deleted mid-watch), or null
240
+ * to keep the original cwd (which then fails as before). A fallback fires
241
+ * at most once per dead directory - onFallback, when given, reports it so
242
+ * the host can log; per-cycle logging is the poll loop's existing failure
243
+ * mode and it spams. Exported for the fallback unit tests; the plugin wires
244
+ * the production instance.
245
+ */
246
+ export function withCwdFallback(
247
+ base: CommandRunner,
248
+ resolve: (cwd: string) => Promise<string | null>,
249
+ onFallback?: (from: string, to: string) => void,
250
+ ): CommandRunner {
251
+ const logged = new Set<string>();
252
+ return async (command, cwd, timeoutMs) => {
253
+ const resolved = await resolve(cwd);
254
+ if (resolved && resolved !== cwd && !logged.has(cwd)) {
255
+ logged.add(cwd);
256
+ onFallback?.(cwd, resolved);
257
+ }
258
+ return base(command, resolved ?? cwd, timeoutMs);
259
+ };
260
+ }
261
+
236
262
  export interface WatcherRegistryOptions {
237
263
  /** Delivers a batch of events for a session. Injected so tests never spawn. */
238
264
  deliver: (sessionID: string, events: WatcherEvent[]) => Promise<void>;