@jeffober/thatch 0.2.5 → 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 +48 -25
- package/artifacts/skills/common.md +1 -0
- package/artifacts/skills/thatch-code-archaeology.md +9 -0
- package/artifacts/skills/thatch-code-review.md +16 -8
- package/artifacts/skills/thatch-plan-refinement.md +33 -4
- 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-response.md +1 -1
- package/artifacts/skills/thatch-review-synthesizer.md +21 -1
- package/bin/thatch +21 -12
- package/docs/dev/README.md +11 -8
- package/docs/dev/features/README.md +1 -1
- package/docs/dev/features/behavior-engine.md +10 -0
- package/docs/dev/features/cli.md +2 -1
- package/docs/dev/features/commands.md +40 -9
- package/docs/dev/features/cross-session-chat.md +85 -21
- package/docs/dev/features/database.md +4 -4
- package/docs/dev/features/nudge-pipeline.md +14 -0
- package/docs/dev/features/opencode-plugin.md +41 -15
- package/docs/dev/features/prediction-engine.md +27 -1
- 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 +11 -5
- package/docs/dev/gotchas.md +21 -0
- package/docs/dev/mcp-parity.md +3 -3
- package/docs/dev/skills.md +11 -8
- package/docs/user/README.md +19 -12
- package/docs/user/behavior-engine.md +7 -1
- package/docs/user/cli.md +2 -1
- package/docs/user/code-review.md +43 -2
- package/docs/user/commands.md +27 -7
- package/docs/user/cross-session-chat.md +47 -20
- package/docs/user/prediction-engine.md +12 -1
- package/docs/user/skills.md +8 -6
- package/package.json +1 -1
- package/src/capabilities.ts +20 -6
- package/src/chat.ts +39 -17
- package/src/commands.ts +109 -18
- package/src/db.ts +23 -2
- package/src/mcp.ts +6 -2
- package/src/opencode/tui-plugin.ts +29 -9
- package/src/opencode/v2.ts +247 -66
- package/src/prompts.ts +54 -6
- package/src/runtime.ts +122 -17
- package/src/scoring-engine.ts +40 -9
- package/src/session-tab-shared.ts +55 -4
- package/src/skills.ts +5 -9
- package/src/tab-state.ts +125 -0
- package/src/tool-defs.ts +42 -21
package/src/tool-defs.ts
CHANGED
|
@@ -133,6 +133,13 @@ export interface CoreContext {
|
|
|
133
133
|
* authoritative over the caller-claimed `as` argument. Returns null when
|
|
134
134
|
* no fresh mapping exists (Cursor, unknown server process). */
|
|
135
135
|
chatDerivedIdentity?: () => string | null;
|
|
136
|
+
/** Reads the sessions that have an opencode tab open RIGHT NOW (the
|
|
137
|
+
* per-channel tabs.json reader plus confirmed-close overrides, computed
|
|
138
|
+
* fresh at each call). Roster rows carrying a tab render "open"; fresh
|
|
139
|
+
* rows without one render "recent", and broadcast targets only open
|
|
140
|
+
* rows. Unwired (tests, hosts with no local opencode state) means no
|
|
141
|
+
* tab signal: everything degrades to "recent", never a false "open". */
|
|
142
|
+
openTabs?: () => ReadonlySet<string>;
|
|
136
143
|
/** Host-executed session-tab operations, wired ONLY by the opencode v2
|
|
137
144
|
* adapter (post-construction into the shared CoreContext, because the
|
|
138
145
|
* implementation closes over both the runtime's db and the v2 plugin
|
|
@@ -2131,12 +2138,14 @@ const chatRegisterDef: ToolDef = {
|
|
|
2131
2138
|
};
|
|
2132
2139
|
|
|
2133
2140
|
/** Lists registered sessions with liveness. Stale means the session's host
|
|
2134
|
-
* process has not heartbeat-ed it recently - it is gone or hung.
|
|
2141
|
+
* process has not heartbeat-ed it recently - it is gone or hung. Fresh
|
|
2142
|
+
* rows split into open (a tab is open now) and recent (no known tab). */
|
|
2135
2143
|
const chatListDef: ToolDef = {
|
|
2136
2144
|
name: "chat_list",
|
|
2137
2145
|
description:
|
|
2138
2146
|
"List sessions registered in the cross-session chat directory, with a " +
|
|
2139
|
-
"liveness marker (
|
|
2147
|
+
"liveness marker (open = a tab is open now, recent = host alive but no " +
|
|
2148
|
+
"open tab, stale = host likely gone), each session's " +
|
|
2140
2149
|
"project and topic, and your own unread count.",
|
|
2141
2150
|
args: {
|
|
2142
2151
|
as: mcpIdentityArg(),
|
|
@@ -2149,10 +2158,11 @@ const chatListDef: ToolDef = {
|
|
|
2149
2158
|
return "No sessions registered. Sessions join the directory automatically on their first idle (opencode), or via chat_register - you would be the first.";
|
|
2150
2159
|
}
|
|
2151
2160
|
const unread = ctx.db.unreadChatCount(identity.sessionID);
|
|
2161
|
+
const openTabs = ctx.openTabs?.() ?? new Set<string>();
|
|
2152
2162
|
const { active, stale } = splitChatRoster(sortChatRoster(sessions));
|
|
2153
2163
|
const line = (s: (typeof sessions)[number]) => {
|
|
2154
2164
|
const self = s.session_id === identity.sessionID;
|
|
2155
|
-
const liveness = chatLiveness(s);
|
|
2165
|
+
const liveness = chatLiveness(s, Date.now(), openTabs);
|
|
2156
2166
|
const project = s.project ? ` project:${s.project}` : "";
|
|
2157
2167
|
// The age turns the liveness bucket into a concrete last-check-in;
|
|
2158
2168
|
// the checkout kind tells sessions on a shared tree who sits in a
|
|
@@ -2178,13 +2188,17 @@ const chatListDef: ToolDef = {
|
|
|
2178
2188
|
// The name-stability note closes the roster because it answers the
|
|
2179
2189
|
// question a reader asking "is this the same X as before?" needs:
|
|
2180
2190
|
// assigned names are unique forever; only hook-anchored MCP identities
|
|
2181
|
-
// can be revisited by an old name.
|
|
2191
|
+
// can be revisited by an old name. The liveness legend decodes the
|
|
2192
|
+
// per-row verdicts: presence (open) is distinct from a live host
|
|
2193
|
+
// (recent) and from a stopped one (stale).
|
|
2182
2194
|
return (
|
|
2183
2195
|
`[chat] ${sessions.length} session${sessions.length === 1 ? "" : "s"} registered\n\n` +
|
|
2184
2196
|
`${sections.join("\n\n")}\n\n` +
|
|
2197
|
+
`Liveness: open = the session has an opencode tab right now; recent = ` +
|
|
2198
|
+
`its harness is alive but no tab is open (wake delivery not guaranteed); ` +
|
|
2199
|
+
`stale = its harness has missed its heartbeat and may be gone. ` +
|
|
2185
2200
|
`Names are assigned by thatch and never reused or reassigned, so the ` +
|
|
2186
|
-
`same name is always the same session
|
|
2187
|
-
`idle has not been seen recently.`
|
|
2201
|
+
`same name is always the same session.`
|
|
2188
2202
|
);
|
|
2189
2203
|
},
|
|
2190
2204
|
};
|
|
@@ -2226,17 +2240,21 @@ const chatSendDef: ToolDef = {
|
|
|
2226
2240
|
? `\n\nDUPLICATE SUPPRESSED: an identical message sent moments ago is still UNREAD in the recipient's inbox, so no second copy was created - the original delivery stands. The same body delivers again once they have read it (or a few seconds past the dedupe window).`
|
|
2227
2241
|
: "";
|
|
2228
2242
|
// State the recipient's ACTUAL liveness: the boilerplate "waits until
|
|
2229
|
-
// idle" is only true for a
|
|
2230
|
-
//
|
|
2231
|
-
//
|
|
2243
|
+
// idle" is only true for a session with an open tab. A recent row's
|
|
2244
|
+
// harness is alive but the tab-less wake can be swallowed without
|
|
2245
|
+
// running a turn - say so instead of promising a wake that may never
|
|
2246
|
+
// fire. A stale recipient's harness is probably gone entirely.
|
|
2232
2247
|
const row = ctx.db.findChatSession(session_id);
|
|
2233
|
-
const liveness = row ? chatLiveness(row) : "stale";
|
|
2248
|
+
const liveness = row ? chatLiveness(row, Date.now(), ctx.openTabs?.()) : "stale";
|
|
2249
|
+
const lastSeen = row ? humanAge(row.last_seen) : "just now";
|
|
2234
2250
|
const delivery =
|
|
2235
|
-
liveness === "
|
|
2236
|
-
? `The recipient is
|
|
2237
|
-
: liveness === "
|
|
2238
|
-
? `
|
|
2239
|
-
:
|
|
2251
|
+
liveness === "open"
|
|
2252
|
+
? `The recipient is open (last seen ${lastSeen}) and will be woken when its session goes idle.`
|
|
2253
|
+
: liveness === "recent"
|
|
2254
|
+
? `NOTE: the recipient is RECENT (last seen ${lastSeen}): its harness is alive, but the session has NO OPEN TAB. The message waits in its inbox, but wake delivery is not guaranteed - a tab-less session may not run a turn on the wake. Daemon-resident sessions (subordinates) usually still pick the message up on their next activity; a closed tab may sit unread. If you expected a live window, check chat_list.`
|
|
2255
|
+
: liveness === "active"
|
|
2256
|
+
? `The recipient is an MCP-host session; it sees this message at its next prompt.`
|
|
2257
|
+
: `NOTE: the recipient is STALE (last seen ${row ? humanAge(row.last_seen) : "unknown"}). Its harness may no longer be running - the message waits unread until that session resumes, and no wake will fire meanwhile. If you meant a different session, check chat_list and resend.`;
|
|
2240
2258
|
return (
|
|
2241
2259
|
`[sent] to ${name} (${session_id.slice(0, 12)})\n\n` +
|
|
2242
2260
|
`${delivery}${splitNote}${dedupeNote}`
|
|
@@ -2344,11 +2362,12 @@ const chatBroadcastDef: ToolDef = {
|
|
|
2344
2362
|
"Broadcast one message to every other registered session on this " +
|
|
2345
2363
|
"machine at once - for announcements and open questions (\"which of " +
|
|
2346
2364
|
"you is working on X?\", \"main just moved, rebase if you are based on " +
|
|
2347
|
-
"it\").
|
|
2348
|
-
"
|
|
2349
|
-
"
|
|
2350
|
-
"
|
|
2351
|
-
"
|
|
2365
|
+
"it\"). Opencode sessions without an open tab (stale, or fresh but " +
|
|
2366
|
+
"tab-less) are skipped and reported: they are not reliably wakeable, so " +
|
|
2367
|
+
"messaging them would spend a turn on a delivery that likely never " +
|
|
2368
|
+
"runs. MCP sessions receive it at their next prompt. Each recipient is " +
|
|
2369
|
+
"woken like any chat message, so broadcast sparingly: every open " +
|
|
2370
|
+
"session spends a model turn on it.",
|
|
2352
2371
|
args: {
|
|
2353
2372
|
body: z.string().describe(
|
|
2354
2373
|
`Message body, delivered to every other registered session. Bodies over ${CHAT_SPLIT_PART_LEN} characters ` +
|
|
@@ -2368,7 +2387,7 @@ const chatBroadcastDef: ToolDef = {
|
|
|
2368
2387
|
`recipients: ${result.recipients.length > 0 ? result.recipients.join(", ") : "(none)"}`,
|
|
2369
2388
|
];
|
|
2370
2389
|
if (result.parts > 1) lines.push(`sent as ${result.parts} parts per recipient (the body exceeded ${CHAT_SPLIT_PART_LEN} characters and was auto-split)`);
|
|
2371
|
-
if (result.skipped.length > 0) lines.push(`skipped stale: ${result.skipped.join(", ")}`);
|
|
2390
|
+
if (result.skipped.length > 0) lines.push(`skipped (stale or no open tab): ${result.skipped.join(", ")}`);
|
|
2372
2391
|
if (result.deduped > 0) lines.push(`duplicate suppressed: an identical broadcast sent moments ago is still UNREAD at ${result.deduped} recipient${result.deduped === 1 ? "" : "s"} - the original delivery stands; the same body delivers again to a recipient once they have read it`);
|
|
2373
2392
|
return (
|
|
2374
2393
|
lines.join("\n") +
|
|
@@ -2625,6 +2644,7 @@ export interface CoreContextExtensions {
|
|
|
2625
2644
|
drainExtractionQueue?: CoreContext["drainExtractionQueue"];
|
|
2626
2645
|
watcherRegistry?: WatcherRegistry;
|
|
2627
2646
|
projectDir?: string;
|
|
2647
|
+
openTabs?: CoreContext["openTabs"];
|
|
2628
2648
|
}
|
|
2629
2649
|
|
|
2630
2650
|
/**
|
|
@@ -2652,6 +2672,7 @@ export function buildCoreContext(
|
|
|
2652
2672
|
drainExtractionQueue: extensions?.drainExtractionQueue,
|
|
2653
2673
|
watchers: extensions?.watcherRegistry,
|
|
2654
2674
|
projectDir: extensions?.projectDir,
|
|
2675
|
+
openTabs: extensions?.openTabs,
|
|
2655
2676
|
};
|
|
2656
2677
|
}
|
|
2657
2678
|
|