@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/README.md +2 -2
- package/artifacts/skills/thatch-plan-refinement.md +80 -0
- package/bin/thatch +17 -8
- package/docs/dev/README.md +3 -3
- package/docs/dev/features/cli.md +1 -1
- package/docs/dev/features/database.md +25 -0
- package/docs/dev/features/multi-host.md +3 -1
- package/docs/dev/features/qa-system.md +1 -1
- package/docs/dev/features/repo-identity.md +110 -0
- package/docs/dev/features/watchers.md +8 -0
- package/docs/dev/skills.md +1 -0
- package/docs/plans/worktree-deletion-recovery.md +246 -0
- package/docs/user/README.md +2 -1
- package/docs/user/skills.md +1 -0
- package/docs/user/watchers.md +7 -2
- package/package.json +1 -1
- package/src/db.ts +57 -0
- package/src/git.ts +198 -17
- package/src/hygiene.ts +6 -2
- package/src/index.ts +27 -9
- package/src/mcp.ts +4 -2
- package/src/prompts.ts +2 -0
- package/src/skills.ts +1 -0
- package/src/tool-defs.ts +11 -4
- package/src/watchers.ts +26 -0
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
|
-
|
|
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
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
|
|
1436
|
-
"via bash -c about every 60s, killed at a 30s
|
|
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
|
-
|
|
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,
|
|
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>;
|