@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.
Files changed (51) hide show
  1. package/README.md +48 -25
  2. package/artifacts/skills/common.md +1 -0
  3. package/artifacts/skills/thatch-code-archaeology.md +9 -0
  4. package/artifacts/skills/thatch-code-review.md +16 -8
  5. package/artifacts/skills/thatch-plan-refinement.md +33 -4
  6. package/artifacts/skills/thatch-refine-personal.md +5 -0
  7. package/artifacts/skills/thatch-refine.md +7 -1
  8. package/artifacts/skills/thatch-review-context.md +55 -2
  9. package/artifacts/skills/thatch-review-followup.md +4 -4
  10. package/artifacts/skills/thatch-review-response.md +1 -1
  11. package/artifacts/skills/thatch-review-synthesizer.md +21 -1
  12. package/bin/thatch +21 -12
  13. package/docs/dev/README.md +11 -8
  14. package/docs/dev/features/README.md +1 -1
  15. package/docs/dev/features/behavior-engine.md +10 -0
  16. package/docs/dev/features/cli.md +2 -1
  17. package/docs/dev/features/commands.md +40 -9
  18. package/docs/dev/features/cross-session-chat.md +85 -21
  19. package/docs/dev/features/database.md +4 -4
  20. package/docs/dev/features/nudge-pipeline.md +14 -0
  21. package/docs/dev/features/opencode-plugin.md +41 -15
  22. package/docs/dev/features/prediction-engine.md +27 -1
  23. package/docs/dev/features/qa-system.md +19 -3
  24. package/docs/dev/features/session-lifecycle.md +6 -6
  25. package/docs/dev/features/session-tabs.md +11 -5
  26. package/docs/dev/gotchas.md +21 -0
  27. package/docs/dev/mcp-parity.md +3 -3
  28. package/docs/dev/skills.md +11 -8
  29. package/docs/user/README.md +19 -12
  30. package/docs/user/behavior-engine.md +7 -1
  31. package/docs/user/cli.md +2 -1
  32. package/docs/user/code-review.md +43 -2
  33. package/docs/user/commands.md +27 -7
  34. package/docs/user/cross-session-chat.md +47 -20
  35. package/docs/user/prediction-engine.md +12 -1
  36. package/docs/user/skills.md +8 -6
  37. package/package.json +1 -1
  38. package/src/capabilities.ts +20 -6
  39. package/src/chat.ts +39 -17
  40. package/src/commands.ts +109 -18
  41. package/src/db.ts +23 -2
  42. package/src/mcp.ts +6 -2
  43. package/src/opencode/tui-plugin.ts +29 -9
  44. package/src/opencode/v2.ts +247 -66
  45. package/src/prompts.ts +54 -6
  46. package/src/runtime.ts +122 -17
  47. package/src/scoring-engine.ts +40 -9
  48. package/src/session-tab-shared.ts +55 -4
  49. package/src/skills.ts +5 -9
  50. package/src/tab-state.ts +125 -0
  51. 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 (fresh = active, stale = likely gone), each session's " +
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. A name whose row shows stale or ` +
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 live host. A stale recipient's harness is
2230
- // probably gone - say so, so the sender can reroute instead of waiting
2231
- // on a wake that will never fire.
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 === "fresh"
2236
- ? `The recipient is fresh (last seen ${row ? humanAge(row.last_seen) : "just now"}) and will be woken when its session goes idle.`
2237
- : liveness === "active"
2238
- ? `The recipient is an MCP-host session; it sees this message at its next prompt.`
2239
- : `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.`;
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\"). Stale opencode sessions (harness stopped heartbeating) are skipped and " +
2348
- "reported, since they will never read the mail; MCP sessions receive " +
2349
- "it at their next prompt. Each recipient is woken like any chat " +
2350
- "message, so broadcast sparingly: every live session spends a model " +
2351
- "turn on it.",
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