@jeffober/thatch 0.2.5 → 0.2.7
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-clear-writing.md +11 -0
- package/artifacts/skills/thatch-code-archaeology.md +9 -0
- package/artifacts/skills/thatch-code-review.md +25 -15
- 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-clarity.md +101 -0
- 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 +22 -2
- package/bin/thatch +103 -33
- 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 +23 -7
- package/docs/dev/features/commands.md +40 -9
- package/docs/dev/features/cross-session-chat.md +99 -27
- 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 +12 -8
- package/docs/user/README.md +22 -14
- package/docs/user/behavior-engine.md +7 -1
- package/docs/user/cli.md +28 -15
- package/docs/user/code-review.md +46 -4
- package/docs/user/commands.md +27 -7
- package/docs/user/cross-session-chat.md +48 -20
- package/docs/user/prediction-engine.md +12 -1
- package/docs/user/skills.md +9 -6
- package/package.json +1 -1
- package/src/capabilities.ts +20 -6
- package/src/chat.ts +56 -24
- 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 +55 -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 +55 -22
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: thatch-review-context
|
|
3
|
-
description: Gather project and feature context before a code review. Investigates PR descriptions, git history, ticket references, linked docs/tickets, docs, and
|
|
3
|
+
description: Gather project and feature context before a code review. Investigates PR descriptions, git history, ticket references, linked docs/tickets, docs, memory, and related PRs (stack members, release trains) to build a context brief that prevents false positives about intentionally deferred work and about pieces delivered by sibling PRs. Use before dispatching review specialists or running a solo review.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
You are a review context builder. Your job is to gather project context that gives reviewers the background they need to avoid false positives about intentionally deferred work, incomplete features, and multi-ticket dependencies.
|
|
@@ -85,7 +85,7 @@ If the matching CLI tool (`gh`, `glab`, etc.) is not on PATH and you cannot call
|
|
|
85
85
|
#### Detect the connected PR/MR
|
|
86
86
|
|
|
87
87
|
Use the current branch name (without remote or merge-base prefix) to look up an open PR/MR:
|
|
88
|
-
- **GitHub**: `gh pr view
|
|
88
|
+
- **GitHub**: `gh pr view <branch> --json number,title,state,headRefName,baseRefName,headRefOid,baseRefOid`. Fallback: `gh api repos/{owner}/{repo}/pulls --jq ".[] | select(.head.ref==\"<branch>\")"`.
|
|
89
89
|
- **GitLab**: `glab mr list --source-branch <branch>`.
|
|
90
90
|
- **Bitbucket Cloud**: REST `https://api.bitbucket.org/2.0/repositories/<owner>/<slug>/pullrequests?q=source.branch.name="<branch>"` (no widely-adopted CLI; call via available HTTP tool).
|
|
91
91
|
- **Azure DevOps**: `az repos pr list --source-branch <branch> --org <org-url> -r <project>`.
|
|
@@ -132,6 +132,46 @@ Directory structure conventions are often undocumented. They live in the repo's
|
|
|
132
132
|
|
|
133
133
|
This gives the pedantic specialist the structural context without having to discover the pattern independently.
|
|
134
134
|
|
|
135
|
+
### 11. Related PRs: stacks and release trains
|
|
136
|
+
|
|
137
|
+
A change is often one piece of a coordinated set of PRs. The other pieces are outside this diff, so specialists cannot see them. Two false-positive patterns result: a comment references work that a sibling PR delivers, or the change consumes a contract that a sibling PR defines. Discover the set and record what each piece does.
|
|
138
|
+
|
|
139
|
+
Two shapes to look for:
|
|
140
|
+
- **Stack**: PRs that merge into each other. This PR's base branch is another PR's head branch.
|
|
141
|
+
- **Release train**: PRs that all target the main branch but must merge or deploy in a specific order. The order lives in tracker relations or ticket DEPENDENCIES sections (the ticket section that names the change's blocking links; the links carry the ordering, the bullets carry only the rationale), not in the branch structure.
|
|
142
|
+
|
|
143
|
+
#### Discovery
|
|
144
|
+
|
|
145
|
+
- **Stack chaining**: if the connected PR's base branch is not the main branch, another PR probably owns that branch as its head. Find it (`gh pr view <base-branch>` on GitHub, `glab mr list --source-branch <base-branch>` on GitLab), then repeat from that PR until the chain reaches the main branch. Walk at most 5 links in each direction; record deeper stack members as `unexplored` register entries. Also look downward: `gh pr list --base <this-branch>` (GitHub) or `glab mr list --target-branch <this-branch>` (GitLab) finds PRs stacked on top of this one.
|
|
146
|
+
- **Tracker relations**: tickets surfaced by the earlier sources carry blockedBy/blocks relations, parent epics, and DEPENDENCIES sections that name the ordered set. Stack chaining cannot find a release train, because every PR in a train targets main; this route is the primary way trains surface. Map each ticket in the set to its PR/MR (`gh pr list --search "TICKET-ID"` on GitHub, or the tracker's attached-PR link); a ticket with no open PR stays a dependency recorded in the brief's Dependencies section, not a register entry.
|
|
147
|
+
- **Linked PRs** from sources 1 and 8. Deduplicate against the two methods above.
|
|
148
|
+
|
|
149
|
+
A sibling that already merged to main is also covered by the staleness check (source 3). Record it in the register anyway, for context.
|
|
150
|
+
|
|
151
|
+
#### Build the sibling register
|
|
152
|
+
|
|
153
|
+
For each sibling PR/MR:
|
|
154
|
+
- PR/MR number, title, state (open/merged/closed/draft)
|
|
155
|
+
- Its base branch, and for stacks its position in the chain
|
|
156
|
+
- One line: what it implements, from its description and diff stat
|
|
157
|
+
|
|
158
|
+
This field list is canonical: the brief section below and the synthesizer's Cross-PR notes appendix render the register as built here. An `unexplored` entry (a stack member beyond the walk cap) carries the number, title, state, base branch, and chain position, but no what-it-implements line and no consistency verdicts; the sibling's description and diff were not read.
|
|
159
|
+
|
|
160
|
+
#### Targeted consistency verification
|
|
161
|
+
|
|
162
|
+
Read every sibling's description in full. Open a sibling's diff only to verify the specific contracts, symbols, and references this change consumes or mentions. Never run a full second review of a sibling. Checks:
|
|
163
|
+
|
|
164
|
+
1. **Contract check**: an API, type, schema, config key, or flag this change consumes from a sibling matches the sibling's actual implementation at its head SHA.
|
|
165
|
+
2. **Reference check**: a comment or doc in this change references a thing X. X exists in this change or in a sibling. If neither delivers X, the reference is dangling.
|
|
166
|
+
3. **Ordering check**: the change or its ticket claims a merge or release order for the set. Verify the claim is stated and consistent with the stack chain or the tracker relations.
|
|
167
|
+
4. **Overlap check**: two PRs in the set implement the same thing differently.
|
|
168
|
+
|
|
169
|
+
Tag each check that applied: `consistent`, `inconsistent` (with the evidence), or `unclear`.
|
|
170
|
+
|
|
171
|
+
#### Early-out
|
|
172
|
+
|
|
173
|
+
If the discovery routes above surfaced no related PR/MR and no coordinated ticket set, state `Standalone change — no sibling register` and skip this source. A coordinated ticket set with no open PRs is not standalone: record it under Dependencies. Most personal projects land here; the source costs one look.
|
|
174
|
+
|
|
135
175
|
## Context brief format
|
|
136
176
|
|
|
137
177
|
Produce a structured brief:
|
|
@@ -154,6 +194,7 @@ List each deferred piece:
|
|
|
154
194
|
- What other tickets or PRs this depends on (must merge first)
|
|
155
195
|
- What tickets or PRs depend on this (are blocked by it)
|
|
156
196
|
- Whether base branches have landed (for stacked PRs)
|
|
197
|
+
- **Release order**: when a ticket or the PR description claims a merge or deploy order for a set of changes, record the claimed order and where it is claimed (which ticket's DEPENDENCIES section, which tracker relation, or which PR description)
|
|
157
198
|
- **Staleness**: whether main has moved past the merge-base since the branch point. When the staleness check (source 3) found commits touching this change's paths, list them here — the synthesizer re-verifies behavioral findings against current main before classifying them
|
|
158
199
|
|
|
159
200
|
### Relevant constraints
|
|
@@ -175,6 +216,18 @@ If context gathering found a connected PR/MR but no prior reviews by other users
|
|
|
175
216
|
|
|
176
217
|
If context gathering found no connected PR/MR (local-branch review), state `Local-branch review — no prior-comment fetch`. This is not a gap; the existing review procedure applies.
|
|
177
218
|
|
|
219
|
+
### Sibling PR context (only when related PRs were detected)
|
|
220
|
+
|
|
221
|
+
List the sibling register as built above (one entry per sibling):
|
|
222
|
+
- PR/MR number, title, state, base branch, and for stacks the chain position
|
|
223
|
+
- What it implements (one line)
|
|
224
|
+
- Consistency verdicts from the targeted checks (`consistent` / `inconsistent` with evidence / `unclear`), only for the checks that applied
|
|
225
|
+
- An `unexplored` entry renders its fields only; the missing what-it-implements line and verdicts record the walk cap, not a gap
|
|
226
|
+
|
|
227
|
+
Whether base branches have landed is recorded under Dependencies above, not here.
|
|
228
|
+
|
|
229
|
+
If discovery found no related PRs and no coordinated ticket set, state `Standalone change — no sibling register`. This is not a gap; the change is not part of a coordinated set. A coordinated ticket set with no open PRs is not standalone: its claimed order is recorded under Dependencies.
|
|
230
|
+
|
|
178
231
|
## If context is sparse
|
|
179
232
|
|
|
180
233
|
Not every PR will have rich context. If you find minimal context:
|
|
@@ -30,7 +30,7 @@ Run `git fetch origin` (NOT `git pull`). Inspect `git remote -v` to recognize th
|
|
|
30
30
|
- `dev.azure.com` → Azure DevOps
|
|
31
31
|
|
|
32
32
|
Probe for an open PR/MR on the current branch:
|
|
33
|
-
- **GitHub**: `gh pr view
|
|
33
|
+
- **GitHub**: `gh pr view <branch> --json number,title,headRefOid,baseRefOid,baseRefName`
|
|
34
34
|
- **GitLab**: `glab mr list --source-branch <branch>`
|
|
35
35
|
|
|
36
36
|
If no PR/MR is found or no VCS CLI is available, you cannot re-check. Report this and stop.
|
|
@@ -79,7 +79,7 @@ Compare `LAST_ROUND_SHA` to `CURRENT_SHA`:
|
|
|
79
79
|
|
|
80
80
|
**Force-push check**: run `git merge-base --is-ancestor <LAST_ROUND_SHA> <CURRENT_SHA>`. If it fails, the author rewrote history (force-pushed). Line numbers may have drifted. You will need to locate each finding's equivalent location in the new HEAD by matching surrounding context, not by line number.
|
|
81
81
|
|
|
82
|
-
**Merge-from-
|
|
82
|
+
**Merge-from-base check**: if the author merged the default branch into the branch between rounds — or, on a stacked PR, merged or rebased onto the PR's base branch — plain `git log <LAST_ROUND_SHA>..<CURRENT_SHA>` and `git diff <LAST_ROUND_SHA> <CURRENT_SHA>` both pull in every base commit, burying the author's actual response commits. To see only the author's commits, run `git log <LAST_ROUND_SHA>..<CURRENT_SHA> --not origin/<default-branch>` after `git fetch origin` (usually main or master). On a stacked PR (Step 1's probe returned a base branch that is not the default branch — `baseRefName` on GitHub, the MR's target branch on GitLab), also fetch the base branch and exclude it too: `git fetch origin <base-branch>`, then `git log <LAST_ROUND_SHA>..<CURRENT_SHA> --not origin/<default-branch> origin/<base-branch>`. Then read each remaining commit's diff with `git show <sha>`. Do not assume a large diff stat means the author rewrote the PR. Check for a merge commit first.
|
|
83
83
|
|
|
84
84
|
### Read author responses
|
|
85
85
|
|
|
@@ -152,7 +152,7 @@ Produce a structured report for the user:
|
|
|
152
152
|
- PR/MR number, title
|
|
153
153
|
- Last-reviewed SHA, current SHA
|
|
154
154
|
- Whether the author pushed changes, responded to comments, or both
|
|
155
|
-
- Force-push or merge-from-
|
|
155
|
+
- Force-push or merge-from-base detected (if applicable)
|
|
156
156
|
|
|
157
157
|
### Finding verification
|
|
158
158
|
|
|
@@ -275,7 +275,7 @@ After all findings have been dealt with (adequately addressed and replied to, or
|
|
|
275
275
|
### How to offer
|
|
276
276
|
|
|
277
277
|
Present the offer as a choice:
|
|
278
|
-
1. **Re-run the full structured review**: load `thatch-code-review` on the current HEAD. It will dispatch all
|
|
278
|
+
1. **Re-run the full structured review**: load `thatch-code-review` on the current HEAD. It will dispatch all ten specialists on the merge-base..HEAD range. The existing follow-up detection will cross-reference your prior comments. On Claude Code or Cursor (no sub-agent support), run each specialist skill in sequence, then run `thatch-review-synthesizer` to aggregate.
|
|
279
279
|
2. **End the re-review here**: the user confirmed their concerns were addressed (or decided how to handle the unaddressed ones) and does not want another full round.
|
|
280
280
|
|
|
281
281
|
The offer is just an offer. The user may decline. They may have only wanted to confirm their concerns were addressed.
|
|
@@ -80,7 +80,7 @@ Identify the PR and VCS host:
|
|
|
80
80
|
recognize the VCS host: `github.com` -> GitHub (`gh`), `gitlab.*` ->
|
|
81
81
|
GitLab (`glab`), `bitbucket.org` -> Bitbucket Cloud, `dev.azure.com` ->
|
|
82
82
|
Azure DevOps.
|
|
83
|
-
- Probe for an open PR on the current branch: `gh pr view
|
|
83
|
+
- Probe for an open PR on the current branch: `gh pr view <branch>
|
|
84
84
|
--json number,title,headRefOid` (GitHub) or equivalent.
|
|
85
85
|
|
|
86
86
|
Fetch ALL review comments:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: thatch-review-synthesizer
|
|
3
|
-
description: Verify and synthesize findings from multiple review specialist skills into a single deduplicated, severity-grouped report. Use after running one or more thatch-review-* specialist skills.
|
|
3
|
+
description: Verify and synthesize findings from multiple review specialist skills into a single deduplicated, severity-grouped report. Cross-references prior review comments and sibling PR registers when provided. Use after running one or more thatch-review-* specialist skills.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are a review synthesizer. You have received findings from one or more review specialists (pedantic, acceptance, state-flow, economy, no-slop, breadcrumbs, mark-and-sweep, highlights, technique). Your job is to verify their citations against the actual code, deduplicate across specialists, and produce a single, coherent final report.
|
|
6
|
+
You are a review synthesizer. You have received findings from one or more review specialists (pedantic, acceptance, state-flow, economy, no-slop, clarity, breadcrumbs, mark-and-sweep, highlights, technique). Your job is to verify their citations against the actual code, deduplicate across specialists, and produce a single, coherent final report.
|
|
7
7
|
|
|
8
8
|
## Static analysis only
|
|
9
9
|
You review code by reading it. Do NOT run tests, linters, compilers, or any build commands.
|
|
@@ -95,6 +95,14 @@ New findings that match a prior comment tagged `addressed` (preliminary) are sti
|
|
|
95
95
|
|
|
96
96
|
When the VCS resolve flag (GitLab `resolved`, GitHub `isResolved`) disagrees with the code-state verdict (e.g., thread marked resolved in the UI but the bug persists), trust the code state and call out the disagreement in the appendix entry.
|
|
97
97
|
|
|
98
|
+
## Cross-reference against the sibling PR register
|
|
99
|
+
|
|
100
|
+
If your input includes a **sibling register** (built via `thatch-review-context` source #11 — related PRs detected as stack members or release-train siblings), apply it during classification, before finalizing any finding whose claim crosses PR boundaries.
|
|
101
|
+
|
|
102
|
+
- A finding claiming a referenced thing is **missing** ("comments reference X but nothing implements X", "flagged as unused or undefined") is REJECTED — or moved to the Cross-PR notes appendix as a stack note — when the register shows a sibling PR delivers it. Cite the sibling PR and its register entry as the reason.
|
|
103
|
+
- A finding that this change **consumes a contract a sibling does not actually provide** (shape mismatch, renamed symbol, different semantics at the sibling's head) is CONFIRMED, with the `Provenance` field citing the sibling PR.
|
|
104
|
+
- A problem discovered **in the sibling itself** is not a finding of this review by default. Record it in the Cross-PR notes appendix so it can be raised in the sibling's own review. Escalate it to a finding of THIS review only when the inconsistency affects this change's safety or correctness, or when flagging it now heads off a bug the author will hit in imminent follow-up work in the same project.
|
|
105
|
+
|
|
98
106
|
## Severity calibration
|
|
99
107
|
|
|
100
108
|
Assign final severity based on YOUR verification:
|
|
@@ -323,6 +331,18 @@ List every entry from the prior-comments register here. For each entry, produce
|
|
|
323
331
|
|
|
324
332
|
If no prior-comments register was provided (local-branch review or first round on the PR/MR), state `None — no prior review comments to cross-reference` and move on.
|
|
325
333
|
|
|
334
|
+
### Cross-PR notes (appendix — only when a sibling register was provided)
|
|
335
|
+
|
|
336
|
+
List every sibling in the register here. For each sibling, produce a one-block record:
|
|
337
|
+
1. **PR/MR number, title, state, base branch**, and for stacks its position in the chain
|
|
338
|
+
2. **What it implements** (one line from the register)
|
|
339
|
+
3. **Consistency verdicts**: `consistent` / `inconsistent` (with the evidence) / `unclear`, per targeted check that applied
|
|
340
|
+
4. **Notes for the sibling's own review**: problems discovered in the sibling that belong in its review, not this one
|
|
341
|
+
|
|
342
|
+
An `unexplored` entry renders item 1's fields and states `unexplored — not read` for items 2 and 3.
|
|
343
|
+
|
|
344
|
+
Omit this section entirely when no sibling register was provided (standalone change or local-branch review).
|
|
345
|
+
|
|
326
346
|
### Coverage gaps
|
|
327
347
|
|
|
328
348
|
Note which files or areas were NOT covered by any specialist.
|
package/bin/thatch
CHANGED
|
@@ -13,17 +13,30 @@ import { resolveOpencodeDbPath, SessionDB, partToTimelineEntry, partToTranscript
|
|
|
13
13
|
import { chatTailDiff, chatTailBacklog, filterChatTailRows, formatChatTailJsonl, CHAT_TAIL_DEFAULT_LIMIT, parseChatTimeBound, CHAT_STALE_MS, CHAT_RENUDGE_MINUTES, chatLiveness, humanAge, splitChatRoster, sortChatRoster, mcpSessionID, isoMinutesAgo, renderChatParticipant, resolveRegisteredPredecessor, type ChatSessionRow, type ChatTailFilter } from "../src/chat";
|
|
14
14
|
import { chatEnabled, loadConfig } from "../src/config";
|
|
15
15
|
import { chatNotificationNudge } from "../src/prompts";
|
|
16
|
+
import { chatOpenTabs as chatOpenTabsProvider } from "../src/tab-state";
|
|
16
17
|
import { createDebugLog } from "../src/debug";
|
|
17
18
|
|
|
18
19
|
const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
19
20
|
const dbPath = process.env.THATCH_DB_PATH ?? join(configHome, "thatch", "thatch.db");
|
|
20
|
-
const db = new ThatchDB(dbPath);
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const argv = process.argv.slice(2);
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Help is checked before the DB is opened or any subcommand runs, and
|
|
25
|
+
// against the WHOLE argument list - not just the leading flags - so
|
|
26
|
+
// `thatch chat list -h` and `thatch -h chat list` print usage instead of
|
|
27
|
+
// executing. Asking for help is not an error: exit 0, where a bad
|
|
28
|
+
// command still exits 1.
|
|
29
|
+
if (argv.includes("-h") || argv.includes("--help")) usage(0);
|
|
30
|
+
|
|
31
|
+
const [command, ...args] = argv;
|
|
32
|
+
|
|
33
|
+
const db: ThatchDB = new ThatchDB(dbPath, { chatOpenTabs: chatOpenTabsProvider(() => db.chatTabClosures()) });
|
|
34
|
+
|
|
35
|
+
function usage(exitCode = 1): never {
|
|
25
36
|
console.error(`USAGE
|
|
26
37
|
thatch --version, -v Print version and exit
|
|
38
|
+
thatch -h, --help Print this usage and exit (the flag
|
|
39
|
+
works anywhere in the command line)
|
|
27
40
|
thatch stores List all stores
|
|
28
41
|
thatch list [store] List memory labels in a store
|
|
29
42
|
thatch show <label> [store] Display a memory by label
|
|
@@ -51,13 +64,16 @@ function usage(): never {
|
|
|
51
64
|
Full conversation as OpenAI chat-completions JSONL
|
|
52
65
|
thatch session search <query> [--session <id>] [--regex] [--limit N]
|
|
53
66
|
JSONL of matching parts across sessions (decoded content)
|
|
54
|
-
thatch chat list [--stale N|all]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
thatch chat list [--recent] [--stale N|all]
|
|
68
|
+
Registered chat sessions. Default shows
|
|
69
|
+
only open sessions (plus MCP rows, which
|
|
70
|
+
read mail at their next prompt): name,
|
|
71
|
+
session id, age, status, project, topic.
|
|
72
|
+
Tab-less fresh sessions collapse into a
|
|
73
|
+
hidden-count note; --recent shows them.
|
|
74
|
+
--stale adds the stale section: N bounds
|
|
75
|
+
it to sessions seen in the last N days
|
|
76
|
+
(default 1); all shows every stale one.
|
|
61
77
|
thatch chat tail [--once] [--limit N|all] [--match RE]
|
|
62
78
|
[--from NAME] [--to NAME] [--since DT] [--until DT]
|
|
63
79
|
Follow cross-session chat as a JSONL
|
|
@@ -74,7 +90,7 @@ function usage(): never {
|
|
|
74
90
|
|
|
75
91
|
Stores default to the repo detected from git remote. Use "global" for the
|
|
76
92
|
global store. Use "all" with search to search project + global together.`);
|
|
77
|
-
process.exit(
|
|
93
|
+
process.exit(exitCode);
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
async function defaultStore(): Promise<string> {
|
|
@@ -424,27 +440,42 @@ async function sessionCommand(rest: string[]): Promise<void> {
|
|
|
424
440
|
const CHAT_ROSTER_HEADER = ["NAME", "SESSION ID", "AGE", "STATUS", "PROJECT", "TOPIC"];
|
|
425
441
|
|
|
426
442
|
/**
|
|
427
|
-
* Renders the `chat list` roster as
|
|
428
|
-
* an aligned-column table under a header row. Section headers
|
|
429
|
-
* the
|
|
430
|
-
* green/red status, and a muted italic topic.
|
|
443
|
+
* Renders the `chat list` roster as three sections - Active, Recent, and
|
|
444
|
+
* Stale - each an aligned-column table under a header row. Section headers
|
|
445
|
+
* are bold, the Recent and Stale explainers are muted italic; rows use
|
|
446
|
+
* green names, dim age, green/yellow/red status, and a muted italic topic.
|
|
447
|
+
* The STATUS column carries the liveness verdict: open (tab open now),
|
|
448
|
+
* recent (host alive, no open tab), active/idle (MCP rows), stale (host
|
|
449
|
+
* presumed gone).
|
|
450
|
+
* showRecent = false (the CLI default without `--recent`) collapses the
|
|
451
|
+
* Recent section into a muted hidden-count note pointing at the flag.
|
|
452
|
+
* Unlike the Stale hide - which the caller does by pre-filtering rows
|
|
453
|
+
* before this function, dropping them silently - the recent hide needs
|
|
454
|
+
* the rows in hand to count them, so it happens here as a note. An
|
|
455
|
+
* all-recent roster therefore renders the note alone, with no section
|
|
456
|
+
* header: there is no Recent section on the page to head, while the
|
|
457
|
+
* stale hidden note rides under a header because its section IS
|
|
458
|
+
* displayed;
|
|
431
459
|
* staleCapMs bounds how old a stale row is displayed (null = no bound);
|
|
432
460
|
* rows past the cap collapse into a muted hidden-count note so the reader
|
|
433
461
|
* knows rows exist that the window left out.
|
|
462
|
+
* now is the render's single clock sample: the split, every per-row
|
|
463
|
+
* verdict, and the caller's pre-filter must agree, or a row aging past
|
|
464
|
+
* the staleness boundary mid-render could land in two sections at once.
|
|
434
465
|
* Color is applied only when stdout is an interactive terminal (and
|
|
435
466
|
* NO_COLOR is unset), so piped output - grep, redirect, QA runners - stays
|
|
436
467
|
* clean plain text.
|
|
437
468
|
*/
|
|
438
|
-
function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null = null): string {
|
|
469
|
+
function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null = null, openTabs?: ReadonlySet<string>, showRecent = true, now = Date.now()): string {
|
|
439
470
|
const colored = process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
440
471
|
const paint = (code: string, s: string) => (colored ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
441
|
-
const { active, stale, staleHidden } = splitChatRoster(sortChatRoster(sessions),
|
|
472
|
+
const { active, recent, stale, staleHidden } = splitChatRoster(sortChatRoster(sessions), now, staleCapMs, openTabs);
|
|
442
473
|
const renderTable = (rows: ChatSessionRow[]) => {
|
|
443
474
|
const cells = (r: ChatSessionRow) => [
|
|
444
475
|
r.name,
|
|
445
476
|
r.session_id,
|
|
446
477
|
humanAge(r.last_seen),
|
|
447
|
-
chatLiveness(r),
|
|
478
|
+
chatLiveness(r, now, openTabs),
|
|
448
479
|
r.project ?? "unknown",
|
|
449
480
|
r.topic ?? "",
|
|
450
481
|
];
|
|
@@ -455,11 +486,14 @@ function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null
|
|
|
455
486
|
const header = align(CHAT_ROSTER_HEADER).map((h) => paint("1", h)).join(" ");
|
|
456
487
|
const body = rows.map((r) => {
|
|
457
488
|
const [name, sessionID, age, status, project, topic] = align(cells(r));
|
|
489
|
+
// Green = present or reachable (open/active), yellow = alive host
|
|
490
|
+
// without a tab (recent, wake not guaranteed), red = stale.
|
|
491
|
+
const statusColor = status === "stale" ? "31" : status === "recent" ? "33" : "32";
|
|
458
492
|
return [
|
|
459
493
|
paint("32", name),
|
|
460
494
|
paint("2", sessionID),
|
|
461
495
|
paint("2", age),
|
|
462
|
-
paint(
|
|
496
|
+
paint(statusColor, status),
|
|
463
497
|
project,
|
|
464
498
|
topic ? paint("3;90", topic) : "",
|
|
465
499
|
].join(" ").trimEnd();
|
|
@@ -470,6 +504,20 @@ function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null
|
|
|
470
504
|
if (active.length > 0) {
|
|
471
505
|
parts.push([paint("1", "# Active Sessions"), renderTable(active)].join("\n"));
|
|
472
506
|
}
|
|
507
|
+
if (recent.length > 0) {
|
|
508
|
+
if (showRecent) {
|
|
509
|
+
const explainer = "These sessions have a fresh heartbeat but no open tab, so a wake may not reach them until their tab reopens; headless subordinates may still read mail on their own turns.";
|
|
510
|
+
parts.push([paint("1", "# Recent Sessions"), paint("3;90", explainer), renderTable(recent)].join("\n"));
|
|
511
|
+
} else {
|
|
512
|
+
// Bare note, no section header: there is no Recent section on the
|
|
513
|
+
// page, so a header would announce a section the view does not
|
|
514
|
+
// render. The stale hidden note rides under a header because with
|
|
515
|
+
// --stale its section IS rendered.
|
|
516
|
+
parts.push(
|
|
517
|
+
paint("3;90", `${recent.length} recent session${recent.length === 1 ? "" : "s"} hidden (alive, no open tab); --recent to show`),
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
473
521
|
if (stale.length > 0 || staleHidden > 0) {
|
|
474
522
|
const lines = [paint("1", "# Stale Sessions")];
|
|
475
523
|
if (stale.length > 0) {
|
|
@@ -499,13 +547,24 @@ async function chatCommand(rest: string[]): Promise<void> {
|
|
|
499
547
|
// sessions from burying the fresh signal. --stale all restores the
|
|
500
548
|
// unbounded view. A bad value dies loudly rather than silently
|
|
501
549
|
// shrinking the roster.
|
|
502
|
-
// Default:
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
550
|
+
// Default: open sessions only (plus MCP rows, which read mail at
|
|
551
|
+
// their next prompt - "open" is meaningless for them). --recent
|
|
552
|
+
// opts into the Recent section (alive, no open tab). The two hidden
|
|
553
|
+
// defaults work differently: --recent's leaves a muted count note
|
|
554
|
+
// in the renderer (it needs the rows in hand to count them);
|
|
555
|
+
// --stale's drops the rows silently in the pre-filter below, and
|
|
556
|
+
// only the empty-roster message points at the flag. Unlike --stale
|
|
557
|
+
// it takes no window value: recent rows are bounded by the heartbeat
|
|
558
|
+
// window itself, so there is nothing to cap. The value is parsed by
|
|
559
|
+
// hand (not flagValue) because bare --stale is meaningful.
|
|
560
|
+
const showRecent = restArgs.includes("--recent");
|
|
506
561
|
const showStale = restArgs.includes("--stale");
|
|
507
562
|
const staleIdx = restArgs.indexOf("--stale");
|
|
508
|
-
const
|
|
563
|
+
const rawNext = showStale ? restArgs[staleIdx + 1] : undefined;
|
|
564
|
+
// A token starting with "--" is a sibling flag (e.g. "--stale
|
|
565
|
+
// --recent"), not --stale's value: --stale stays bare instead of
|
|
566
|
+
// eating the flag.
|
|
567
|
+
const staleNext = rawNext !== undefined && rawNext.startsWith("--") ? undefined : rawNext;
|
|
509
568
|
let staleCapMs: number | null = 24 * 60 * 60 * 1000;
|
|
510
569
|
if (staleNext !== undefined) {
|
|
511
570
|
if (staleNext === "all") staleCapMs = null;
|
|
@@ -514,17 +573,28 @@ async function chatCommand(rest: string[]): Promise<void> {
|
|
|
514
573
|
process.exit(1);
|
|
515
574
|
} else staleCapMs = Number(staleNext) * 24 * 60 * 60 * 1000;
|
|
516
575
|
}
|
|
517
|
-
|
|
518
|
-
|
|
576
|
+
// The open-tab set is read once per invocation at render time: rows
|
|
577
|
+
// with a tab render "open", fresh rows without one "recent".
|
|
578
|
+
const openTabs = chatOpenTabsProvider(() => db.chatTabClosures())();
|
|
579
|
+
// One clock sample per render: the pre-filter, the split, and every
|
|
580
|
+
// per-row verdict must agree, or a row aging past the staleness
|
|
581
|
+
// boundary mid-render could land in two sections at once.
|
|
582
|
+
const now = Date.now();
|
|
583
|
+
const registered = db.listChatSessions();
|
|
584
|
+
// Stale rows are hidden here (not in the renderer) so the
|
|
585
|
+
// emptiness check can tell an empty directory from a stale-only
|
|
586
|
+
// one; recent rows are left in because formatChatRoster needs them
|
|
587
|
+
// to render the hidden-count note.
|
|
588
|
+
const sessions = registered.filter((r) => showStale || chatLiveness(r, now, openTabs) !== "stale");
|
|
519
589
|
if (sessions.length === 0) {
|
|
520
590
|
console.log(
|
|
521
|
-
|
|
591
|
+
registered.length === 0
|
|
522
592
|
? "No sessions registered. Sessions join the directory automatically on their first idle (opencode), or via chat_register."
|
|
523
593
|
: "No active sessions. (Stale sessions are hidden; run with --stale to include them.)",
|
|
524
594
|
);
|
|
525
595
|
break;
|
|
526
596
|
}
|
|
527
|
-
console.log(formatChatRoster(sessions, showStale ? staleCapMs : null));
|
|
597
|
+
console.log(formatChatRoster(sessions, showStale ? staleCapMs : null, openTabs, showRecent, now));
|
|
528
598
|
break;
|
|
529
599
|
}
|
|
530
600
|
|
|
@@ -1050,7 +1120,7 @@ switch (command) {
|
|
|
1050
1120
|
const sessionID = data.session_id ?? data.conversation_id;
|
|
1051
1121
|
|
|
1052
1122
|
const { peekQueue, getMissedCount, incrementMissedCount } = await import("../src/extract-queue");
|
|
1053
|
-
const { extractionNudge, claudeWriteNudge, claudeRecallNudge, predictionNudge, behaviorNudge } = await import("../src/prompts");
|
|
1123
|
+
const { extractionNudge, claudeWriteNudge, claudeRecallNudge, predictionNudge, behaviorNudge, mcpToolName } = await import("../src/prompts");
|
|
1054
1124
|
const { detectSkew, readNpmCacheForUpdate } = await import("../src/version-check");
|
|
1055
1125
|
|
|
1056
1126
|
let repo = "unknown";
|
|
@@ -1144,10 +1214,10 @@ switch (command) {
|
|
|
1144
1214
|
parts.push(claudeRecallNudge(matches));
|
|
1145
1215
|
}
|
|
1146
1216
|
if (predictions && predictions.length > 0) {
|
|
1147
|
-
parts.push(predictionNudge(predictions));
|
|
1217
|
+
parts.push(predictionNudge(predictions, mcpToolName));
|
|
1148
1218
|
}
|
|
1149
1219
|
if (behaviors && behaviors.length > 0) {
|
|
1150
|
-
parts.push(behaviorNudge(behaviors));
|
|
1220
|
+
parts.push(behaviorNudge(behaviors, mcpToolName));
|
|
1151
1221
|
}
|
|
1152
1222
|
|
|
1153
1223
|
if (parts.length > 0) {
|
|
@@ -1296,13 +1366,13 @@ switch (command) {
|
|
|
1296
1366
|
if (repo === "unknown") repo = "global";
|
|
1297
1367
|
|
|
1298
1368
|
const { sidebandPredictions, sidebandSocketPath } = await import("../src/sideband");
|
|
1299
|
-
const { predictionNudge } = await import("../src/prompts");
|
|
1369
|
+
const { predictionNudge, mcpToolName } = await import("../src/prompts");
|
|
1300
1370
|
const sockPath = sidebandSocketPath(dbPath);
|
|
1301
1371
|
const threshold = parseFloat(process.env.THATCH_PREDICTION_THRESHOLD ?? "0.60");
|
|
1302
1372
|
const predictions = await sidebandPredictions(sockPath, promptText, [repo, "global"], threshold, 5);
|
|
1303
1373
|
|
|
1304
1374
|
if (predictions && predictions.length > 0) {
|
|
1305
|
-
const output = predictionNudge(predictions);
|
|
1375
|
+
const output = predictionNudge(predictions, mcpToolName);
|
|
1306
1376
|
if (isJson) console.log(JSON.stringify({ additional_context: output }));
|
|
1307
1377
|
else console.log(output);
|
|
1308
1378
|
} else {
|
package/docs/dev/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Shared core
|
|
|
27
27
|
├── git.ts → detect repo identity (store name) + worktree-deletion recovery (repo_paths cache)
|
|
28
28
|
├── hygiene.ts → hygiene report (pending dedups, stale, orphaned branches)
|
|
29
29
|
├── prompts.ts → system prompt, compaction, reminders, recall/prediction/behavior nudges, prompt cores, CLAUDE.md instructions
|
|
30
|
-
├── commands.ts → /thatch slash-command registry: actions
|
|
30
|
+
├── commands.ts → /thatch slash-command registry: actions (prompt cores + self-contained skill couchings) + wrap-ups, per-host sync
|
|
31
31
|
├── sideband.ts → Unix socket server + client: warm-model semantic match for hook processes
|
|
32
32
|
├── skills.ts → SKILL.md content + installer (shared + opencode-only arrays)
|
|
33
33
|
├── scoring-engine.ts → generic four-table scoring base (shared by prediction + behavior engines)
|
|
@@ -91,7 +91,7 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
91
91
|
| `index.ts` | Dual-shape OpenCode plugin entry: the merged default export (`{ id, setup, server }`) loads on opencode v1 (reads `default.server`) and v2 (reads `default.setup`, strips excess keys). Lazy-imports the host adapter so each host's SDK resolves only under its own runtime. |
|
|
92
92
|
| `opencode/v1.ts` + `opencode/v2.ts` | Host adapters. v1 builds HostCapabilities from the PluginInput client and returns the hooks object; v2 registers through the promise-context domains (ToolEditor, session.hook, event.subscribe) and degrades the surfaces v2 lacks. v2 also wires the session-tab host flow (create -> move -> chat-register -> rpc emit -> prompt) through the `CoreContext.sessionTabHost` seam. |
|
|
93
93
|
| `opencode/tui-plugin.ts` | The v2 TUI CLI plugin (the package's `./tui` entrypoint): listens for `rpc.thatch-tabs.tab-opened` and `rpc.thatch-tabs.tab-closed` events and opens/closes the subordinate session's tab via `ui.tabs.open`/`ui.tabs.close`. See [features/session-tabs.md](features/session-tabs.md). |
|
|
94
|
-
| `capabilities.ts` | The `HostCapabilities` seam: the host operations the shared runtime needs (session create/prompt/get/list/messages/delete, toast, compaction trigger, app exit). `capabilitiesFromClient` bridges the v1 SDK client. |
|
|
94
|
+
| `capabilities.ts` | The `HostCapabilities` seam: the host operations the shared runtime needs (session create/prompt/get/list/messages/delete, toast, compaction trigger, session close-out - app exit on v1, the session's own tab on v2). `capabilitiesFromClient` bridges the v1 SDK client. |
|
|
95
95
|
| `runtime.ts` | Shared plugin runtime: wires DB, model, extraction; the nudge tiers, system prompt injection, session event handling, child-session bookkeeping, wrap-up resolution. Host-agnostic: consumed by both adapters. |
|
|
96
96
|
| `os-args.ts` | Pure argv / OS-command-line helpers for startup session resolution (`-s`/`-c`). SDK-free. |
|
|
97
97
|
| `setup.ts` | `thatch setup --claude` / `--cursor` installer. Writes MCP config (`.mcp.json` / `.cursor/mcp.json`), appends to CLAUDE.md / AGENTS.md (idempotent), installs hooks in settings.json / hooks.json, installs skills. |
|
|
@@ -108,10 +108,11 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
108
108
|
| `watchers.ts` | In-memory watcher registry and background poller for event-driven notifications (opencode-only). Three sources: GitHub PRs and branches via `gh api` (injected `GhRunner`), and local shell commands via `bash -c` (injected `CommandRunner`) run as condition variables - only the exit code is read, never the command's output. Diffs against last-seen state, queues events (pointer data plus machine status like check conclusions and exit codes, never external content) per session, and delivers them through a plugin-supplied callback when the session is idle. The `pollSeconds` getter surfaces the poll cadence to tool output and notifications. Process-lifetime state by design - no SQLite. |
|
|
109
109
|
| `chat.ts` | Cross-session chat between opencode sessions on one machine (opencode-only). `ChatStore` holds the shared directory and inbox in SQLite so any process can read/write them; `ChatPoller` heartbeats hosted sessions and delivers wake prompts through an injected callback when the recipient is idle - only the recipient's host process delivers, so there is no cross-process double-delivery. Heartbeat staleness flags crashed sessions; a per-recipient nudge rate cap is the anti-loop hard brake. See [features/cross-session-chat.md](features/cross-session-chat.md). |
|
|
110
110
|
| `chat-names.ts` | The static display-name pool for the chat directory: whimsical geek-culture names in fnord's Nomenclater style, baked in so name assignment never costs a model call. The pool is the fallback base for registrations with no usable session title. |
|
|
111
|
+
| `tab-state.ts` | Read-only open-tab reader for the chat roster: globs every opencode channel's `tabs.json` (global + cwd scopes), subtracts fresh confirmed tab closes, and composes the provider the roster surfaces and broadcast targeting consume. Degrades to an empty set - never a false OPEN. See [features/cross-session-chat.md](features/cross-session-chat.md). |
|
|
111
112
|
| `sideband.ts` | Unix domain socket server + client. The MCP server (long-lived, warm model) runs `SidebandServer` so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Handles three methods: `match` (recall nudge), `predictions` (prediction auto-fire), and `behaviors` (behavior auto-fire). Socket path is a hash of the DB path — both processes compute it independently. |
|
|
112
113
|
| `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), prediction nudge (`predictionNudge`), behavior nudge (`behaviorNudge`), prediction verb selection (`predictionVerb`), watcher notification (`watcherNotificationNudge`), version warning (`versionWarningNudge`), chat wake notification (`chatNotificationNudge`), chat transcript echo text and echo recognizer (`chatEchoText` / `isChatEchoParts`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. Also the prompt cores (`defragCore`, `hygieneCore`, `reflectCore`, `extractionCore`) - host-agnostic behavior instruction bodies shared by nudges and `/thatch` commands, parameterized by a `ToolNamer` for per-host tool spellings. See [features/commands.md](features/commands.md). |
|
|
113
|
-
| `commands.ts` | Slash-command registry and sync. `actionDefs(tool)` renders the on-demand actions (defrag, extract, hygiene, reflect) from the prompt cores; the wrap-up commands (compact, exit) are bespoke greenlight-gated templates. Renderers produce full command files for opencode (`installOpencodeCommands`, synced by the plugin at init) and Claude Code (`installClaudeCommands`, synced by setup); `compilePrompts` in `mcp.ts` exposes the same actions as MCP prompts for Cursor. A parity-guard test pins the three sets to the documented exclusions. See [features/commands.md](features/commands.md). |
|
|
114
|
-
| `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (
|
|
114
|
+
| `commands.ts` | Slash-command registry and sync. `actionDefs(tool)` renders the on-demand actions: the nudge-backed ones (defrag, extract, hygiene, reflect) from the prompt cores, and the self-contained ones (refine, whois, and the review-mode couchings peer-review, self-review) inline in the registry; the wrap-up commands (compact, exit) are bespoke greenlight-gated templates. Renderers produce full command files for opencode (`installOpencodeCommands`, synced by the plugin at init) and Claude Code (`installClaudeCommands`, synced by setup); `compilePrompts` in `mcp.ts` exposes the same actions as MCP prompts for Cursor. A parity-guard test pins the three sets to the documented exclusions. See [features/commands.md](features/commands.md). |
|
|
115
|
+
| `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (everything that works on all three hosts: the memory workflows, the review specialists — whose roster comes from `REVIEW_SPECIALISTS` in prompts.ts — plus the synthesizer, context, followup, and response skills, the walkthrough and archaeology skills, the refinement family, the writing skills, and verification/export) and `OPENCODE_ONLY_SKILLS` (the code-review coordinator and thatch-coordination — they need sub-agent support, so they are not installed for Claude Code or Cursor). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
|
|
115
116
|
| `scoring-engine.ts` | Generic four-table scoring engine with Bayesian confidence. Shared base for prediction and behavior engines — each wraps it with table-specific names. |
|
|
116
117
|
| `prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names. |
|
|
117
118
|
| `behavior.ts` | Thin wrapper around `ScoringEngine` with behavior-specific table names. |
|
|
@@ -129,7 +130,7 @@ methods. The v1 hook shapes:
|
|
|
129
130
|
|------|-------------|
|
|
130
131
|
| `experimental.chat.system.transform` | Appends the thatch system prompt (store names, usage rules). |
|
|
131
132
|
| `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
|
|
132
|
-
| `command.execute.before` | Arms the wrap-up greenlight check: `/thatch/compact` and `/thatch/exit` mark their session in `pendingWrapUp`. The next `session.status` idle resolves it by inspecting the final assistant message for the greenlight token and, when present, triggering the
|
|
133
|
+
| `command.execute.before` | Arms the wrap-up greenlight check: `/thatch/compact` and `/thatch/exit` mark their session in `pendingWrapUp`. The next `session.status` idle resolves it by inspecting the final assistant message for the greenlight token and, when present, triggering the host action (compact: v1 `session_compact` via `executeCommand`, v2 `session.compact` on the promise domain; exit: record the session's watcher deaths, unregister it from chat, then v1 `app.exit` via `publish` or v2 the exit-tab-closed rpc event closing the session's own tab). See [features/session-lifecycle.md](features/session-lifecycle.md). |
|
|
133
134
|
| `experimental.compaction.autocontinue` | Clears the compacting flag so `chat.message` nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. The `chat.message` hook also clears the flag if it fires for a session still in the compacting set but the incoming message has no compaction-type part — this handles compaction failure, where the session would otherwise be stuck with nudges off forever. |
|
|
134
135
|
| `tool.execute.after` | Buffers every non-`thatch_*`, non-`skill`, non-`task` tool call into the session's extraction buffer. (Skill/task are excluded — buffering them creates a feedback loop where the nudge triggers a skill load, which gets buffered, which triggers another nudge.) Memory writes (`thatch_memory_remember`) and `thatch_extraction_done` drain the buffer and reset the missed-nudge counter. For child sessions (`childToParent.has(sessionID)`), also tracks metrics: `remember` with `overwrite:false` → new++, `overwrite:true` → updated++, `forget` → deleted++. For `thatch_chat_*` tool calls, builds the transcript echo (`chatEchoText`) and posts it as a visible non-synthetic `noReply` part. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
|
|
135
136
|
| `chat.message` | Recall nudge: embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). The same embedding also feeds the prediction auto-fire (`db.scorePredictionNudge`, injects `[thatch] User decision model`) and the behavior auto-fire (`db.scoreBehaviorNudge`, injects `[thatch] Situational behaviors`). All three nudges fire independently in separate try/catch blocks with separate synthetic parts. There is deliberately NO extraction nudge here - extraction is plugin-driven at session idle (see the extraction feature doc); the old model-driven handshake raced its own state machine and was removed. Chat transcript echoes (non-synthetic `noReply` parts whose text is entirely `[chat]`-prefixed bubbles) are skipped before any tier — no model turn reads them, so nudging them wastes an embedding. Skipped entirely while the session is compacting (tool calls are blocked during summary generation), and for task-dispatched sub-agent sessions (`childToParent` without `extractionChildren`) - their tool lists may exclude the thatch tools and their work is driven by the dispatch prompt. |
|
|
@@ -268,14 +269,16 @@ extraction cycle
|
|
|
268
269
|
→ MCP path (Claude Code/Cursor): unchanged — no SDK client, no child
|
|
269
270
|
sessions; extract-queue.ts + flush-tools drives the nudge via hooks
|
|
270
271
|
|
|
271
|
-
toast notifications (opencode-only, TUI-rendered
|
|
272
|
+
toast notifications (opencode-only, TUI-rendered; the bridge's toast
|
|
273
|
+
event -> the TUI plugin's ui.toast.show on v2)
|
|
272
274
|
→ client.tui.showToast — best-effort, silently ignored if TUI not
|
|
273
275
|
connected (headless mode); model-invisible (goes to the user only,
|
|
274
276
|
not the conversation history — the inverse of synthetic nudge parts,
|
|
275
277
|
which are model-visible but TUI-hidden)
|
|
276
278
|
→ extraction metrics: `[thatch] new: N, updated: M, deleted: K` (success
|
|
277
|
-
variant, 4s) — fires when an extraction child goes idle
|
|
278
|
-
|
|
279
|
+
variant, 4s) — fires when an extraction child goes idle, but ONLY when
|
|
280
|
+
memories were actually written; no-save runs show nothing (no
|
|
281
|
+
notification fatigue)
|
|
279
282
|
→ recall matches: `[thatch] recalled N memories` (info variant, 3s) —
|
|
280
283
|
fires when chat.message matches stored memories
|
|
281
284
|
→ prediction matches: `[thatch] N predictions surfaced` (info variant,
|
|
@@ -23,7 +23,7 @@ system, see [../skills.md](../skills.md). For non-obvious invariants, see
|
|
|
23
23
|
| [behavior-engine.md](behavior-engine.md) | Behavior engine (LLM self-discipline rules, ham/spam, auto-fire) |
|
|
24
24
|
| [hygiene.md](hygiene.md) | Hygiene system (duplicate, stale, orphaned branch signals) |
|
|
25
25
|
| [deduplication.md](deduplication.md) | Deduplication system (find pairs, verdict lifecycle) |
|
|
26
|
-
| [commands.md](commands.md) | Slash commands (on-demand actions
|
|
26
|
+
| [commands.md](commands.md) | Slash commands (on-demand actions: prompt cores and skill couchings, wrap-ups, host sync) |
|
|
27
27
|
| [sideband.md](sideband.md) | Sideband IPC (Unix socket for MCP host hook processes) |
|
|
28
28
|
| [compaction-recovery.md](compaction-recovery.md) | Compaction recovery (opencode compaction hooks) |
|
|
29
29
|
| [session-lifecycle.md](session-lifecycle.md) | Session lifecycle management (opencode events, child tracking, wrap-up commands) |
|
|
@@ -113,6 +113,8 @@ Parameters: `behavior` (statement, semantic match at cosine >= 0.85),
|
|
|
113
113
|
- Injects `behaviorNudge(items)` as a separate synthetic text part with a
|
|
114
114
|
`[thatch] Situational behaviors` header.
|
|
115
115
|
- 0-evidence behaviors use "consider"; behaviors with evidence use "do".
|
|
116
|
+
- The block ends with a snapshot footer (same as the prediction nudge): the
|
|
117
|
+
labels are a submission-time snapshot, not a live view.
|
|
116
118
|
- Independent try/catch — a behavior failure does not block recall or
|
|
117
119
|
prediction.
|
|
118
120
|
- Toast: `[thatch] N behaviors surfaced`.
|
|
@@ -250,6 +252,14 @@ behavior_provenance(
|
|
|
250
252
|
)
|
|
251
253
|
```
|
|
252
254
|
|
|
255
|
+
Timestamp semantics: a matcher's `updated_at` is the last change to the
|
|
256
|
+
matcher row OR its edge set — `createEdge` bumps it when a new edge lands
|
|
257
|
+
(atomically with the insert), and deleting an item bumps the matchers whose
|
|
258
|
+
edges cascade away (atomically with the delete). The matcher's own text may
|
|
259
|
+
legitimately be older than its links (a corrected behavior re-links the
|
|
260
|
+
existing matcher without touching its text). A behavior's `updated_at` is
|
|
261
|
+
written at creation and then moved by the last confidence change.
|
|
262
|
+
|
|
253
263
|
## Key invariants
|
|
254
264
|
|
|
255
265
|
1. **The firewall principle does NOT apply.** The LLM grading its own
|
package/docs/dev/features/cli.md
CHANGED
|
@@ -27,7 +27,13 @@ The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, M
|
|
|
27
27
|
|
|
28
28
|
## Global behavior
|
|
29
29
|
|
|
30
|
-
-
|
|
30
|
+
- `--version` and `-v` print the package version and exit 0.
|
|
31
|
+
- `-h` and `--help` print usage and exit 0. The check runs before the DB
|
|
32
|
+
is opened and before any subcommand dispatch, and scans the WHOLE
|
|
33
|
+
argument list - not just the leading flags - so `thatch chat list -h`
|
|
34
|
+
and `thatch -h chat list` show usage instead of executing. Explicit
|
|
35
|
+
help exits 0 where a usage error exits 1.
|
|
36
|
+
- No other short flags. No subcommand aliases.
|
|
31
37
|
- Unknown command or missing required arg calls `usage()` and exits 1.
|
|
32
38
|
- Args are positional, parsed by hand from `process.argv.slice(2)`. No arg-parsing library.
|
|
33
39
|
- DB opened once at startup, closed at end (except `mcp` which closes early).
|
|
@@ -47,11 +53,21 @@ Inherits stdio, exits with the child's exit code. Errors and exits 1 if none fou
|
|
|
47
53
|
Read-only window on the cross-session chat directory ([cross-session-chat.md](cross-session-chat.md)):
|
|
48
54
|
|
|
49
55
|
- `chat list` renders the roster as aligned columns under a header row
|
|
50
|
-
(NAME/AGE/STATUS/PROJECT/TOPIC), colorized only when stdout
|
|
51
|
-
(`formatChatRoster()` in `bin/thatch`; pipes and QA runners
|
|
52
|
-
text). Rows are split into Active and Stale
|
|
53
|
-
`splitChatRoster()
|
|
54
|
-
|
|
56
|
+
(NAME/SESSION ID/AGE/STATUS/PROJECT/TOPIC), colorized only when stdout
|
|
57
|
+
is a TTY (`formatChatRoster()` in `bin/thatch`; pipes and QA runners
|
|
58
|
+
get plain text). Rows are split into Active, Recent, and Stale
|
|
59
|
+
sections by `splitChatRoster()` (it takes the open-tab set, so fresh
|
|
60
|
+
opencode rows split into open versus recent); the status column is
|
|
61
|
+
`chatLiveness()`'s verdict
|
|
62
|
+
(`open`/`recent`/`stale` for opencode rows - the open-tab reader feeds
|
|
63
|
+
the open/recent split - `active`/`idle` for MCP rows). The Recent and
|
|
64
|
+
Stale sections are flag-guarded, and the two guards hide differently:
|
|
65
|
+
without `--recent` the Recent section is replaced by a muted
|
|
66
|
+
hidden-count note (the renderer keeps the rows to count them), while
|
|
67
|
+
without `--stale` stale rows are dropped entirely by a pre-filter in
|
|
68
|
+
the command case - only the empty-roster message points at the flag.
|
|
69
|
+
`--stale [N|all]` reveals the stale rows under a one-day display cap
|
|
70
|
+
whose overflow collapses into a hidden-count note.
|
|
55
71
|
- `chat tail` follows the message stream as JSONL: one `ChatTailEvent`
|
|
56
72
|
per line via `formatChatTailJsonl()` (a bare `JSON.stringify`; no ANSI,
|
|
57
73
|
no markdown rendering, no local-time conversion - the tail is a log for
|
|
@@ -113,7 +129,7 @@ Read-only window on the cross-session chat directory ([cross-session-chat.md](cr
|
|
|
113
129
|
## Key invariants
|
|
114
130
|
|
|
115
131
|
- No arg-parsing library. Args are positional, parsed by hand from `process.argv.slice(2)`.
|
|
116
|
-
-
|
|
132
|
+
- `--version`/`-v` prints the version; `-h`/`--help` prints usage from anywhere in the argument list (checked upstream of dispatch, before the DB opens) and exits 0. No other short flags. No subcommand aliases.
|
|
117
133
|
- DB opened once at startup, closed at end (except `mcp` which closes early).
|
|
118
134
|
- Store name auto-detected from git remote. "global" is the shared store. "all" (search only) means project + global.
|
|
119
135
|
- Unknown command or missing required arg calls `usage()` and exits 1.
|