@jeffober/thatch 0.2.4 → 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 (76) hide show
  1. package/README.md +56 -26
  2. package/artifacts/skills/common.md +1 -0
  3. package/artifacts/skills/thatch-clear-writing.md +103 -5
  4. package/artifacts/skills/thatch-code-archaeology.md +9 -0
  5. package/artifacts/skills/thatch-code-review.md +16 -8
  6. package/artifacts/skills/thatch-coordination.md +69 -0
  7. package/artifacts/skills/thatch-plan-refinement.md +33 -4
  8. package/artifacts/skills/thatch-pr-description.md +21 -13
  9. package/artifacts/skills/thatch-refine-personal.md +5 -0
  10. package/artifacts/skills/thatch-refine.md +7 -1
  11. package/artifacts/skills/thatch-review-context.md +55 -2
  12. package/artifacts/skills/thatch-review-followup.md +4 -4
  13. package/artifacts/skills/thatch-review-no-slop.md +15 -0
  14. package/artifacts/skills/thatch-review-response.md +1 -1
  15. package/artifacts/skills/thatch-review-synthesizer.md +21 -1
  16. package/bin/thatch +48 -15
  17. package/docs/assets/deepwiki-badge.svg +1 -0
  18. package/docs/dev/README.md +18 -10
  19. package/docs/dev/features/README.md +1 -1
  20. package/docs/dev/features/behavior-engine.md +10 -0
  21. package/docs/dev/features/cli.md +4 -1
  22. package/docs/dev/features/commands.md +40 -9
  23. package/docs/dev/features/cross-session-chat.md +194 -23
  24. package/docs/dev/features/database.md +4 -4
  25. package/docs/dev/features/notifications.md +1 -2
  26. package/docs/dev/features/nudge-pipeline.md +14 -0
  27. package/docs/dev/features/opencode-plugin.md +56 -15
  28. package/docs/dev/features/prediction-engine.md +71 -6
  29. package/docs/dev/features/qa-system.md +19 -3
  30. package/docs/dev/features/session-lifecycle.md +6 -6
  31. package/docs/dev/features/session-tabs.md +161 -0
  32. package/docs/dev/features/setup.md +5 -0
  33. package/docs/dev/features/watchers.md +62 -6
  34. package/docs/dev/gotchas.md +89 -0
  35. package/docs/dev/mcp-parity.md +5 -5
  36. package/docs/dev/setup-and-hooks.md +7 -2
  37. package/docs/dev/skills.md +12 -9
  38. package/docs/plans/generalized-session-heartbeat.md +212 -0
  39. package/docs/plans/prediction-consolidation.md +88 -68
  40. package/docs/user/README.md +29 -13
  41. package/docs/user/behavior-engine.md +7 -1
  42. package/docs/user/cli.md +5 -1
  43. package/docs/user/code-review.md +43 -2
  44. package/docs/user/commands.md +37 -10
  45. package/docs/user/cross-session-chat.md +76 -26
  46. package/docs/user/prediction-engine.md +23 -1
  47. package/docs/user/setup.md +16 -0
  48. package/docs/user/skills.md +9 -7
  49. package/docs/user/watchers.md +22 -7
  50. package/package.json +10 -2
  51. package/src/alerts.ts +1 -1
  52. package/src/capabilities.ts +20 -6
  53. package/src/chat.ts +358 -43
  54. package/src/commands.ts +123 -18
  55. package/src/db.ts +74 -3
  56. package/src/git.ts +3 -1
  57. package/src/hygiene.ts +5 -0
  58. package/src/mcp.ts +101 -9
  59. package/src/opencode/tui-plugin.ts +90 -0
  60. package/src/opencode/v2.ts +440 -66
  61. package/src/prediction.ts +90 -0
  62. package/src/prompts.ts +164 -29
  63. package/src/runtime.ts +298 -15
  64. package/src/scoring-engine.ts +40 -9
  65. package/src/session-db.ts +4 -4
  66. package/src/session-tab-shared.ts +231 -0
  67. package/src/setup.ts +9 -3
  68. package/src/skills.ts +6 -9
  69. package/src/tab-state.ts +125 -0
  70. package/src/tool-defs.ts +454 -76
  71. package/src/tools.ts +2 -0
  72. package/src/watchers.ts +263 -26
  73. package/docs/plans/intuition-drives.md +0 -147
  74. package/docs/plans/llm-alerts.md +0 -287
  75. package/docs/plans/thatch-actions.md +0 -179
  76. package/docs/plans/worktree-deletion-recovery.md +0 -246
@@ -23,8 +23,8 @@ The description is not a design doc, changelog, or implementation journal. It is
23
23
 
24
24
  Each section has one job:
25
25
  - SYNOPSIS: orient to the class of change and affected area.
26
- - PURPOSE: make the reader agree there is a problem worth solving.
27
- - DESCRIPTION: teach the system's current behavior and how this change alters it.
26
+ - PURPOSE: state the premise (the defect, or the pivot the change serves) and teach the feature-level mechanism that makes the diff legible.
27
+ - DESCRIPTION: teach the system's current behavior and how this change alters it; end with a NEXT STEPS subsection when a follow-up ticket continues the work.
28
28
  - WALK-THROUGH (when needed): walk the reviewer through the new workflow step by step.
29
29
  - NOTES: flag scope limits, intentional regressions, and things AI reviewers will misread.
30
30
  - robots.txt (when needed): dense, machine-addressed context that heads off recurring false positives from automated reviewers.
@@ -35,7 +35,7 @@ AI-authored PRs tend to be large. The description is the reader's lever against
35
35
 
36
36
  ## Structure
37
37
 
38
- Sections, in order: SYNOPSIS, PURPOSE, DESCRIPTION, WALK-THROUGH (conditional), NOTES (conditional), robots.txt (conditional, always last).
38
+ Sections, in order: SYNOPSIS, PURPOSE, DESCRIPTION (optionally ending with a `### NEXT STEPS` subsection), WALK-THROUGH (conditional), NOTES (conditional), robots.txt (conditional, always last).
39
39
 
40
40
  **SYNOPSIS, PURPOSE, and DESCRIPTION are mandatory** - even for a one-line, single-concern change. A tiny PR gets short sections, not fewer sections. WALK-THROUGH and NOTES appear only when warranted; omit them when empty. robots.txt appears only after an automated reviewer has produced a false positive worth heading off (see its section); it is always the final section.
41
41
 
@@ -45,6 +45,8 @@ Sections, in order: SYNOPSIS, PURPOSE, DESCRIPTION, WALK-THROUGH (conditional),
45
45
 
46
46
  One to two lines. Orient only. The reader should come away knowing the class of change (bugfix, refactor, feature, revert, infra, cleanup) and the affected area.
47
47
 
48
+ Lead with the change: what it does or enables. A very brief statement of the problem it fixes may follow, after the change - do X to fix Y, never Y alone. Even a safety fix describes itself by behavior, not by certification: "the schema lets the worker clean up after itself", not "makes the schema safe to deploy".
49
+
48
50
  SYNOPSIS may use subsystem-specific terms as scan anchors (that is its job). It does not define them. The first teaching section (PURPOSE or DESCRIPTION) picks up that burden. An outsider who does not know the terms should still be able to orient from the plain-English nouns around them. Do not force all project context into SYNOPSIS; it is a TLDR, not the teaching section.
49
51
 
50
52
  Include a ticket link if one exists. Format: `Ticket: [PLAT-139](https://<host>/browse/PLAT-139)` on its own line directly under the SYNOPSIS text.
@@ -59,7 +61,7 @@ Do not fabricate ticket IDs. If no signal exists and the user says `none`, omit
59
61
 
60
62
  ### PURPOSE
61
63
 
62
- Frame the problem. Lead with the conclusion, then provide the evidence, not the other way around. State what is wrong, then explain why. For example:
64
+ Frame the premise: the defect, or the pivot the change serves. Lead with the conclusion, then provide the evidence, not the other way around. State what is wrong, then explain why. For example:
63
65
 
64
66
  - "The export endpoint loads every row into memory before serializing, so a large tenant runs the pod out of memory" not "the endpoint allocates a slice per row, and with enough rows... which is why the pod runs out of memory."
65
67
  - "This handler runs one query per result row, so a page of 50 items fires 51 queries" not "the handler loops over results issuing a query each time... resulting in an N+1 query pattern."
@@ -67,11 +69,11 @@ Frame the problem. Lead with the conclusion, then provide the evidence, not the
67
69
 
68
70
  The reader should know what the problem is before they read why it happens.
69
71
 
70
- No solution yet. The reader should finish this section agreeing there is something to address, even with zero code knowledge.
72
+ State the premise in one sentence: what is wrong (the defect), or what the change is for (the pivot it serves). Then teach, at the feature level, how the affected thing works - the shortest version that makes DESCRIPTION's details parse. For a defect, the reader finishes the section agreeing there is something to address, even with zero code knowledge. For a foundation or preventive change, the reader finishes understanding what this is and how the feature works; that understanding is the motivation.
71
73
 
72
- Separate the problem from its context. The problem statement is one paragraph. Hazard rating and prior-work context go in a separate paragraph. The reader who already agrees there is a problem can skip the rating; the reader who does not should not have to wade through it to find the problem.
74
+ PURPOSE stays at feature altitude. Table-level mechanics, column lists, and function names belong to DESCRIPTION.
73
75
 
74
- If the change is preventive (no current defect), say why it is worth fixing despite the low risk. "No incident has hit this shape" is a demotivation, not a motivation. Answer "why now?": a prior PR identified it as a follow-up, the API still permits a failure shape, or the next planned change would make it worse. The reviewer's question is "should I spend time on this?" - give them the answer.
76
+ No failure narratives and no urgency arguments. Do not narrate a sequence of failures to justify the change, and do not argue that it must land before something else exists. State the harm as fact where it belongs: harm evidence lives in DESCRIPTION layer 1 as existing behavior, and hazard facts live in NOTES.
75
77
 
76
78
  When a ticket description is available, use its problem statement as the seed for PURPOSE. But verify it against the actual code. Ticket descriptions can be stale or wrong. If the ticket says "the frobnicator crashes on null input" but the diff shows you fixing a race condition in the frobnicator, the PURPOSE describes the race condition, not the null input. Flag the discrepancy to the user.
77
79
 
@@ -79,13 +81,15 @@ When a ticket description is available, use its problem statement as the seed fo
79
81
 
80
82
  How much structure DESCRIPTION needs depends on the PR's complexity.
81
83
 
84
+ The spine of DESCRIPTION is the walkthrough: what the change does and how it works, readable straight through. Justify only where the overall behavior is unintuitive, and keep the why to one brief, high-level sentence: "The constraints had to be applied in a follow-up migration, because the ORM cannot express database-level referential actions." Never interleave defenses with the mechanics - a paragraph where every change sentence is followed by two or three sentences of workaround detail makes the reader skip around to reconstruct the what-and-how, like business logic buried under `if err != nil` blocks. Detailed workaround narratives are NOTES bullets, stated as facts; prose does not repeat NOTES. Factual scope stays in prose.
85
+
82
86
  **Small, self-evident change** (add a flag, guard a branch, rename a symbol, bump a constant): collapse to a sentence or two stating what the PR adds/changes. Do not manufacture a "how the code behaves today" paragraph for a change whose intent already makes the diff obvious. That is padding, not scaffolding.
83
87
 
84
88
  **Medium change with non-obvious before/after mechanics**: three layers, in order. Each layer is as short as possible while remaining honest.
85
89
 
86
90
  1. **How the existing code behaves.** High-level narrative, not a function walk. Name the key components, their contract, and the specific behavior that matters for this change. Introduce decision points explicitly - they become anchors for layer 2.
87
91
  2. **What this PR changes.** For each decision point named in layer 1, say what it does now. Keep parallelism: same names, same order. The reader's mental model from layer 1 is the load-bearing structure here.
88
- 3. **Why that fixes PURPOSE.** One or two sentences linking the mechanics in layer 2 to the harm named in PURPOSE. If the fix has limits (doesn't cover case Z, follow-up needed), call them out here in one line.
92
+ 3. **Why that fixes PURPOSE** (when PURPOSE names a defect). One or two sentences stating the net effect as fact, linking the mechanics in layer 2 to the harm named in PURPOSE. When PURPOSE is a pivot statement - the change is the feature itself - omit this layer. If the fix has limits (doesn't cover case Z, follow-up needed), call them out here in one line.
89
93
 
90
94
  The structure is simple: PURPOSE names a harm, layer 1 names the mechanism, layer 2 changes the mechanism, layer 3 shows the harm is gone. Names carry through. The reader builds one model, not four.
91
95
 
@@ -93,6 +97,8 @@ The structure is simple: PURPOSE names a harm, layer 1 names the mechanism, laye
93
97
 
94
98
  Organize by **workflow or data-flow**, not by touched symbol. A bullet list of API changes reads as disconnected touchpoints. A walkthrough following one object through the system reads as a story. If the change touches a request lifecycle, follow the request. If it touches a data pipeline, follow the data.
95
99
 
100
+ **Forward scope: `### NEXT STEPS`.** When the PR hands off to a planned follow-up ticket, do not leave it inline in DESCRIPTION prose. End DESCRIPTION with a `### NEXT STEPS` subsection: what the follow-up does, framed by the economic property that justifies the split ("amortizing the cost of updates to the cache"), not just the raw number. Link the ticket when one exists. NOTES `Remaining work:` stays for unfinished work inside this PR.
101
+
96
102
  ### WALK-THROUGH
97
103
 
98
104
  When the change has a sequence of steps - a lifecycle, a workflow, a data flow through the system - split the mechanics into a numbered WALK-THROUGH. Keep DESCRIPTION to root-cause + design concept only.
@@ -143,7 +149,7 @@ Always a `## NOTES` markdown header. Never a bare `Notes:` line ending a section
143
149
  Use NOTES for:
144
150
  - Intentional behavior changes that look like regressions.
145
151
  - Scope disclaimers (what this PR does NOT do) - max two sentences, no hedging.
146
- - Deferred follow-up items (`Remaining work:` bullet list).
152
+ - Deferred follow-up items (`Remaining work:` bullet list) - planned forward work with its own ticket goes in a DESCRIPTION `### NEXT STEPS` subsection instead.
147
153
  - Ticket scope mismatches (when the PR partially addresses the ticket).
148
154
  - Anything AI reviewers (Cursor BugBot, CodeRabbit) will misread.
149
155
 
@@ -398,8 +404,8 @@ That reading alone conveys the shape and scope of both changes. The prose is for
398
404
  1. **Gather**: `git log $MERGE_BASE..HEAD`, `git diff $MERGE_BASE..HEAD --stat`, commit messages, current branch name. Apply the ticket resolution order (SYNOPSIS section) - infer first, ask only if nothing matches. Skim the diff; identify decision points (components or branches where behavior differs before vs after). Identify the workflow or lifecycle path if the change has one.
399
405
  2. **Read ticket context**: if a ticket is resolved, fetch its description. Use it to seed PURPOSE and identify reviewer-relevant plan changes: revisions, pivots caused by code reality, discoveries after the ticket was written, and deferred or unnecessary ticket items.
400
406
  3. **Read project context for stacked work**: if the PR refers to related PRs, follow-ups, deferred design, upstream dependencies, or unexplained project terms, read the linked source material before drafting. Build a private term map. If a term controls the PR and you cannot resolve it, ask the user.
401
- 4. **Draft PURPOSE first**, in one breath, without looking at code. If you cannot state the harm in two sentences, you do not understand the PR yet. Go back and read. When a ticket is available, seed PURPOSE from its problem statement, but verify against the code.
402
- 5. **Write DESCRIPTION**: choose the right depth (collapsed for small, three-layer for medium, root-cause + concept for large with WALK-THROUGH). Name everything you will refer to later. Organize by workflow/data-flow, not by touched symbol. Add a one- or two-sentence context bridge only when stacked-project context is needed to understand this PR's delta.
407
+ 4. **Draft PURPOSE first**, in one breath, without looking at code. If you cannot state the premise in one sentence and the feature story in a few more, you do not understand the PR yet. Go back and read. When a ticket is available, seed PURPOSE from its problem statement, but verify against the code.
408
+ 5. **Write DESCRIPTION**: choose the right depth (collapsed for small, three-layer for medium, root-cause + concept for large with WALK-THROUGH). Name everything you will refer to later. Organize by workflow/data-flow, not by touched symbol. Add a one- or two-sentence context bridge only when stacked-project context is needed to understand this PR's delta. End with a `### NEXT STEPS` subsection when a follow-up ticket continues the work.
403
409
  6. **Write WALK-THROUGH** (if needed): numbered steps in execution order. Bold the step leads. State what changed, not what you did.
404
410
  7. **Write SYNOPSIS last** - it is a compression of PURPOSE + the core mechanic.
405
411
  8. **Bold and italicize the save points** in the allowed locations. Bold phrases that name the change (components, mechanisms, behaviors). Italicize phrases that orient to significance (conclusions, payoffs), at most one per paragraph. Apply the scan check: read only the bold and italic fragments and verify the shape comes through.
@@ -407,22 +413,25 @@ That reading alone conveys the shape and scope of both changes. The prose is for
407
413
  10. **robots.txt only when earned**: do not add it during initial authoring. It is added later, in response to an automated reviewer's false positive (see its section). When you do add it, use the collapsed `<details>` format with `<summary>robots.txt</summary>`, keep the editing HTML comment at the top of the block, and keep the human-warning line first after the comment.
408
414
  11. **Verify links**: check ticket/PR links, repo file links, and external docs where tooling allows. Do not claim an unverified link was verified.
409
415
  12. **Verify prose style and core principle**: check each rule in Prose style. Cut design-narration, authoring-sequence, and buzzwordy abstractions. Verify plain ASCII. Verify first-use definitions for subsystem jargon. Verify project-private labels are translated before use. Verify plain English nouns (no code-domain nouns in prose). Verify conclusion-first in PURPOSE. Verify NOTES bullets are either short pointers or self-contained. Verify one-paragraph-one-physical-line for GitHub rendering. The robots.txt section is exempt from these prose rules below its human-warning line.
410
- 13. **Clarity pass**: read the draft as a cold reviewer skimming the PR. Rewrite before returning it; do not tell the user you performed this pass. Check: does every project-specific term get plain-English meaning before or with the label? Are ambiguous terms qualified or replaced with concrete behavior? Did you replace code/comment shorthand with reader-facing behavior when the shorthand is less clear? Does every process word name object/action/effect? Does every contrast name old and new behavior? Does every causal sentence show the middle step? Did you preserve connective tissue instead of compressing meaning into noun stacks? If the draft got longer, cut a lower-value claim instead of compressing a high-value one.
416
+ 13. **Clarity pass**: read the draft as a cold reviewer skimming the PR. Rewrite before returning it; do not tell the user you performed this pass. Check: does every project-specific term get plain-English meaning before or with the label? Are ambiguous terms qualified or replaced with concrete behavior? Did you replace code/comment shorthand with reader-facing behavior when the shorthand is less clear? Does every process word name object/action/effect? Does every contrast name old and new behavior? Does every causal sentence show the middle step? Did you preserve connective tissue instead of compressing meaning into noun stacks? Does the SYNOPSIS lead with the change (do X to fix Y, never Y alone)? Is the DESCRIPTION spine readable straight through, with no interleaved defenses? Is there any failure narrative or urgency argument left in PURPOSE? If the draft got longer, cut a lower-value claim instead of compressing a high-value one.
411
417
  14. **Verify length**: estimate the PR's complexity (see the point-based budgets in Length and style). If the body exceeds the budget, step up the abstraction ladder first (replace mechanism sequences with the principle that subsumes them). If still over, cut the lowest-value claim entirely. Do not compress terminology or cut connective tissue to hit a word count.
412
418
  15. **Submit** via `gh pr create --body "$(cat <<'EOF' ... EOF)"` to preserve formatting.
413
419
 
414
420
  ## Anti-patterns
415
421
 
416
422
  - **Burying the harm.** If PURPOSE is a paragraph of setup before the actual problem, rewrite. Problem first.
423
+ - **Failure-saga PURPOSE.** Narrating a sequence of failures or arguing urgency ("this PR prevents X before Y exists") to justify a change. State the defect or pivot in one factual sentence, teach the mechanism, and put hazard facts in NOTES.
417
424
  - **Layer 2 introduces new names.** If layer 2 mentions components layer 1 never named, the scaffold broke. Go back, add them to layer 1, or drop them from layer 2.
418
425
  - **Emphasizing filler words.** Bolding "this", "the", "we", or adjectives produces scan noise. Bold only nouns and verbs that carry meaning.
419
426
  - **Bolding filler or context.** Bolding history, hazard detail, justification, or connective tissue creates scan noise. Bold the change itself (components, mechanisms, behaviors); italicize the significance (conclusions, payoffs). Not the scaffolding around them.
420
427
  - **Over-italicizing.** More than one italicized phrase per paragraph dilutes the signal. Pick the one phrase that carries the paragraph's point.
421
428
  - **Design-doc DESCRIPTION.** If layer 1 is a function walkthrough or architecture essay, you are writing for yourself, not the reviewer. Compress to the decision points.
429
+ - **Interleaved defenses.** A DESCRIPTION where every change sentence is followed by two or three sentences of workaround detail. State what IS and walk the reader through; the unintuitive part earns one brief, high-level why; detailed workarounds are NOTES facts.
422
430
  - **Matching the diff line-by-line.** The description is orthogonal to the diff, not a prose projection of it.
423
431
  - **Over-claiming scope.** A fix for a sloppy implementation is "fix a sloppy implementation," not "harden the foo subsystem." Match energy to reality.
424
432
  - **Narrating your own process.** Any sentence about the order you did things, which commit does what, or what a later commit will add. Reads as AI slop. Change-as-it-stands only.
425
433
  - **Orphaned synopsis.** A bold lead paragraph with no `## SYNOPSIS` header above a body that does use headers. Add the header; it is mandatory.
434
+ - **Problem-only synopsis.** A TLDR that describes the problem being solved without stating what the change does. Even a safety fix leads with its behavior; the problem gets a brief clause after it.
426
435
  - **Bare `Notes:` line.** Use `## NOTES` header or omit the section entirely.
427
436
  - **Organizing by touched symbol.** A bullet list of "changed X in file A, changed Y in file B" reads as disconnected touchpoints. Follow the data flow.
428
437
  - **Buzzwordy abstractions.** "identity contract", "canonical hash", "check in isolation", "shared seam" - if it sounds like it is trying to sound smart, replace it with plain concrete wording. If "seam" means a conceptual border, say "boundary". If it means common code, say "shared helper", "shared function", or "shared SQL expression".
@@ -438,7 +447,6 @@ That reading alone conveys the shape and scope of both changes. The prose is for
438
447
  - **Implementation verbs in prose.** "Publish the key" reads as jargon to someone who does not know the method is called `publish`. Say "store the key in the in-memory cache".
439
448
  - **Code-domain nouns in prose.** "Row read", "waiter", "zeroized", "backlog" (as jargon) are technically English but belong in source files, not sentences. Replace with plain English: "reading the row from the database", "a request waiting for a worker", "overwritten with zeros", "the queued scan jobs".
440
449
  - **Conclusion buried at the end.** PURPOSE that builds to its point in the last sentence forces the reader to absorb the explanation before knowing what is being explained. Lead with the problem, then explain why.
441
- - **Hazard rating without motivation.** "No incident has hit this shape" answers "how bad?" but not "why review this?" Answer "why now?" so the reviewer can decide whether to invest time.
442
450
  - **NOTES bullets that are both wordy and terse.** A bullet that compresses a paragraph of rationale into one dense sentence reads as a private note. Either flag the thing and point to the full explanation, or write a self-contained sentence or two. Not the middle thing.
443
451
  - **robots.txt as a suppression lever.** Writing "ignore the finding about X" or "skip checking X" in robots.txt turns it into a rubber stamp and a prompt-injection surface. Explain what X does and the intent behind it so the reviewer clears its own false positive; never instruct it to stop looking.
444
452
  - **Preemptive robots.txt.** Adding the section before any automated reviewer has actually produced a false positive. It is a response to a demonstrated misread, not scaffolding.
@@ -32,6 +32,11 @@ below; where they disagree, these win.
32
32
  - **Degraded review is fine.** If the host cannot dispatch sub-agents, a
33
33
  same-context self-review pass is usually enough. Saying so out loud, as
34
34
  the core skill requires, is still worthwhile but the stakes are lower.
35
+ - **Round counts drop, not mode selection.** These deltas relax how many
36
+ rounds a run needs. If a deep-mode trigger in `thatch-plan-refinement`
37
+ is met (shared persistent state, many files or hosts, a new
38
+ abstraction, table, or contract, or the user asks for depth), the lens
39
+ fan-out still runs.
35
40
 
36
41
  ## Lens weights
37
42
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: thatch-refine
3
- description: Classify the current project (audience, lifecycle, surface) and refine a plan under that type's requirements. Entry point for the thatch-refine-* family. Use when the user asks to refine a plan, runs /thatch/refine, or when a plan is about to be implemented and the project type should shape the refinement bar. For a bare "refine this plan" request, load this skill rather than thatch-plan-refinement directly - the router picks the type and applies its delta on top of the core loop.
3
+ description: Classify the current project (audience, lifecycle, surface) and refine a plan under that type's requirements. Entry point for the thatch-refine-* family. Use when the user asks to refine a plan, runs /thatch/refine, or when a plan is about to be implemented and the project type should shape the refinement bar. For a bare "refine this plan" request, load this skill rather than thatch-plan-refinement directly - the router picks the type and applies its delta on top of the core loop. Deep-mode triggers in the core skill are binding; a type delta relaxes round counts, never mode selection.
4
4
  ---
5
5
 
6
6
  You are the entry point for project-type-aware plan refinement. The core
@@ -68,6 +68,12 @@ pass.
68
68
  4. Load `thatch-plan-refinement` (the core loop) and the type's skill.
69
69
  5. Run the core loop with the type skill's deltas applied. Where a delta
70
70
  and the core skill disagree, the delta wins; that is its whole job.
71
+ One exception: mode selection. A delta relaxes round counts, skip
72
+ thresholds, and plan sections. It never turns off deep mode. When a
73
+ deep-mode trigger in `thatch-plan-refinement` is met (shared
74
+ persistent state, many files or hosts, a new abstraction, table, or
75
+ contract, or the user asks for depth), the lens fan-out runs
76
+ regardless of type.
71
77
 
72
78
  If no type fits (the repo serves a niche the table misses), run plain
73
79
  `thatch-plan-refinement` and say that the requirements are the generic
@@ -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 memory to build a context brief that prevents false positives about intentionally deferred work. Use before dispatching review specialists or running a solo review.
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 --head <branch> --json number,title,state,headRefName,baseRefName,headRefOid,baseRefOid`. Fallback: `gh api repos/{owner}/{repo}/pulls --jq ".[] | select(.head.ref==\"<branch>\")"`.
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 --head <branch> --json number,title,headRefOid,baseRefOid`
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-main check**: if the author merged main into the branch between rounds, plain `git log <LAST_ROUND_SHA>..<CURRENT_SHA>` and `git diff <LAST_ROUND_SHA> <CURRENT_SHA>` both pull in every main 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/main` after `git fetch origin`. 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.
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-main detected (if applicable)
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 eight 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.
278
+ 1. **Re-run the full structured review**: load `thatch-code-review` on the current HEAD. It will dispatch all nine 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.
@@ -41,12 +41,25 @@ Comments that reference the AI, the user, or the conversation:
41
41
  - Comments mentioning specific ticket numbers for resolved issues
42
42
  - Commented-out code with "// removed" or "// old" annotations
43
43
 
44
+ ### Metaphors standing in for the mechanism
45
+ A word that labels behavior the comment never states:
46
+ - "a quarantine" where the behavior is "re-enqueued rows get a fresh timestamp so they don't starve the rest"
47
+ - Euphemisms for plain terms: "not bumped by re-touches" instead of "later UPDATEs of the same row leave it unchanged"
48
+ The behavior replaces the metaphor. A metaphor verb attached to the stated mechanism is fine ("the sweep heals the dropped ids"); a metaphor noun doing the mechanism's job is not.
49
+
50
+ ### Telegraphic compression
51
+ Verbless clause chains or label-colon chains where full sentences belong:
52
+ - "failures wait out the backoff delay, idle cycles the short idle cadence, productive cycles nothing"
53
+ - "A claimed id with no recompute row was deleted or soft-deleted mid-drain: its cache row is deleted"
54
+ Full parallel sentences with the causal clause are the fix; the missing "because" is usually the point.
55
+
44
56
  ## What is NOT slop
45
57
 
46
58
  - Comments explaining *why* the code behaves a certain way
47
59
  - Comments explaining tradeoffs or design decisions
48
60
  - Comments explaining non-obvious behavior
49
61
  - Docstrings describing function contracts
62
+ - Load-bearing precision: pinned terms of art, genuine lists, error-class contracts, cross-references that carry the why
50
63
  - Legitimate TODOs for future work
51
64
  - User-visible strings whose tone/content is required by the feature or by an external protocol
52
65
  - Unchanged legacy text outside the touched scope unless the current change makes it newly wrong or newly suspicious
@@ -66,6 +79,8 @@ Do NOT report issues in files you did not actually read.
66
79
  - **CHANGE_NARRATION**: Comments describing the change being made, not the code's behavior
67
80
  - **FOURTH_WALL**: Comments referencing the AI, the user, or the conversation
68
81
  - **AI_STYLE_TELL**: Typography, verbosity, hedging, filler, superlatives, meta-commentary
82
+ - **MECHANISM_METAPHOR**: A metaphor or euphemism labeling behavior the comment never states
83
+ - **TELEGRAPHIC_COMPRESSION**: Verbless clause chains or label-colon chains where full sentences with the named cause belong
69
84
  - **STALE_ARTIFACT**: TODOs referencing completed work, commented-out code, resolved ticket references
70
85
 
71
86
  For slop findings, the source of truth is usually the project's writing norms and the surrounding code intent; use "N/A — mechanical finding" for the producer chain.
@@ -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 --head <branch>
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,6 +1,6 @@
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
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.
@@ -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,11 +13,12 @@ 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
+ const db: ThatchDB = new ThatchDB(dbPath, { chatOpenTabs: chatOpenTabsProvider(() => db.chatTabClosures()) });
21
22
 
22
23
  const [command, ...args] = process.argv.slice(2);
23
24
 
@@ -242,9 +243,11 @@ function emit(output: string, versionWarning: string | null, isJson: boolean, ch
242
243
  else console.log(output);
243
244
  }
244
245
 
245
- /** Resolve the project directory from host-specific env vars or cwd. */
246
+ /** Resolve the project directory from host-specific env vars or cwd.
247
+ * THATCH_PROJECT_DIR (pinned by project-local MCP configs) leads the
248
+ * chain, mirroring the server's resolution in src/mcp.ts. */
246
249
  function projectDir(): string {
247
- return process.env.CURSOR_PROJECT_DIR ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
250
+ return process.env.THATCH_PROJECT_DIR ?? process.env.CURSOR_PROJECT_DIR ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
248
251
  }
249
252
 
250
253
  // ---------------------------------------------------------------------------
@@ -425,7 +428,9 @@ const CHAT_ROSTER_HEADER = ["NAME", "SESSION ID", "AGE", "STATUS", "PROJECT", "T
425
428
  * Renders the `chat list` roster as two sections - Active and Stale - each
426
429
  * an aligned-column table under a header row. Section headers are bold,
427
430
  * the stale explainer is muted italic; rows use green names, dim age,
428
- * green/red status, and a muted italic topic.
431
+ * green/red status, and a muted italic topic. The STATUS column carries
432
+ * the liveness verdict: open (tab open now), recent (host alive, no open
433
+ * tab), active/idle (MCP rows), stale (host presumed gone).
429
434
  * staleCapMs bounds how old a stale row is displayed (null = no bound);
430
435
  * rows past the cap collapse into a muted hidden-count note so the reader
431
436
  * knows rows exist that the window left out.
@@ -433,7 +438,7 @@ const CHAT_ROSTER_HEADER = ["NAME", "SESSION ID", "AGE", "STATUS", "PROJECT", "T
433
438
  * NO_COLOR is unset), so piped output - grep, redirect, QA runners - stays
434
439
  * clean plain text.
435
440
  */
436
- function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null = null): string {
441
+ function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null = null, openTabs?: ReadonlySet<string>): string {
437
442
  const colored = process.stdout.isTTY === true && !process.env.NO_COLOR;
438
443
  const paint = (code: string, s: string) => (colored ? `\x1b[${code}m${s}\x1b[0m` : s);
439
444
  const { active, stale, staleHidden } = splitChatRoster(sortChatRoster(sessions), Date.now(), staleCapMs);
@@ -442,7 +447,7 @@ function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null
442
447
  r.name,
443
448
  r.session_id,
444
449
  humanAge(r.last_seen),
445
- chatLiveness(r),
450
+ chatLiveness(r, Date.now(), openTabs),
446
451
  r.project ?? "unknown",
447
452
  r.topic ?? "",
448
453
  ];
@@ -453,11 +458,14 @@ function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null
453
458
  const header = align(CHAT_ROSTER_HEADER).map((h) => paint("1", h)).join(" ");
454
459
  const body = rows.map((r) => {
455
460
  const [name, sessionID, age, status, project, topic] = align(cells(r));
461
+ // Green = present or reachable (open/active), yellow = alive host
462
+ // without a tab (recent, wake not guaranteed), red = stale.
463
+ const statusColor = status === "stale" ? "31" : status === "recent" ? "33" : "32";
456
464
  return [
457
465
  paint("32", name),
458
466
  paint("2", sessionID),
459
467
  paint("2", age),
460
- paint(status === "stale" ? "31" : "32", status),
468
+ paint(statusColor, status),
461
469
  project,
462
470
  topic ? paint("3;90", topic) : "",
463
471
  ].join(" ").trimEnd();
@@ -512,8 +520,11 @@ async function chatCommand(rest: string[]): Promise<void> {
512
520
  process.exit(1);
513
521
  } else staleCapMs = Number(staleNext) * 24 * 60 * 60 * 1000;
514
522
  }
523
+ // The open-tab set is read once per invocation at render time: rows
524
+ // with a tab render "open", fresh rows without one "recent".
525
+ const openTabs = chatOpenTabsProvider(() => db.chatTabClosures())();
515
526
  let sessions = db.listChatSessions();
516
- if (!showStale) sessions = sessions.filter((r) => chatLiveness(r) === "fresh");
527
+ if (!showStale) sessions = sessions.filter((r) => chatLiveness(r, Date.now(), openTabs) !== "stale");
517
528
  if (sessions.length === 0) {
518
529
  console.log(
519
530
  showStale
@@ -522,7 +533,7 @@ async function chatCommand(rest: string[]): Promise<void> {
522
533
  );
523
534
  break;
524
535
  }
525
- console.log(formatChatRoster(sessions, showStale ? staleCapMs : null));
536
+ console.log(formatChatRoster(sessions, showStale ? staleCapMs : null, openTabs));
526
537
  break;
527
538
  }
528
539
 
@@ -1048,7 +1059,7 @@ switch (command) {
1048
1059
  const sessionID = data.session_id ?? data.conversation_id;
1049
1060
 
1050
1061
  const { peekQueue, getMissedCount, incrementMissedCount } = await import("../src/extract-queue");
1051
- const { extractionNudge, claudeWriteNudge, claudeRecallNudge, predictionNudge, behaviorNudge } = await import("../src/prompts");
1062
+ const { extractionNudge, claudeWriteNudge, claudeRecallNudge, predictionNudge, behaviorNudge, mcpToolName } = await import("../src/prompts");
1052
1063
  const { detectSkew, readNpmCacheForUpdate } = await import("../src/version-check");
1053
1064
 
1054
1065
  let repo = "unknown";
@@ -1064,7 +1075,20 @@ switch (command) {
1064
1075
  // This is the primary delivery channel for version warnings because
1065
1076
  // flush-tools fires on every prompt, including conversational turns
1066
1077
  // with no tool calls.
1067
- const versionWarning = detectSkew(dbPath) ?? readNpmCacheForUpdate(dbPath);
1078
+ const skew = detectSkew(dbPath);
1079
+ const versionWarning = skew ?? readNpmCacheForUpdate(dbPath);
1080
+ // Server-stale gate (issue #6's residual): under SKEW the running
1081
+ // MCP server predates the upgrade, so its compiled-at-start tool
1082
+ // list may lack the tools the nudges reference. The extraction tier
1083
+ // is the only one that names a tool for the model to call
1084
+ // (get_extraction_payload) - it degrades to a single restart
1085
+ // instruction, and the missed-count is not incremented (the
1086
+ // escalation counter counts ignored nudges; none was delivered).
1087
+ // The queue is file-backed and peek-not-drain, so nothing is lost:
1088
+ // it extracts after the restart. Tiers 2/3 reference only
1089
+ // long-standing tools and the sideband is the old server's own warm
1090
+ // model - both keep firing under skew.
1091
+ const serverStale = skew !== null;
1068
1092
  // Chat check: with a hook session ID this always prints the caller's
1069
1093
  // assigned identity (and its unread count), so the model knows who it
1070
1094
  // is before any chat tool call; with no mail the line says so.
@@ -1090,6 +1114,15 @@ switch (command) {
1090
1114
  // not the full payload - the sub-agent fetches it via
1091
1115
  // get_extraction_payload.
1092
1116
  const interactions = sessionID ? peekQueue(sessionID) : [];
1117
+ if (interactions.length > 0 && serverStale) {
1118
+ emit(
1119
+ `[thatch] The MCP server is stale: the package was upgraded but this session still runs the old server, so extraction is paused until the session restarts. The ${interactions.length} queued interaction${interactions.length === 1 ? "" : "s"} are safe (file-backed) and will extract then. Restart the session to resume.`,
1120
+ versionWarning,
1121
+ isJson,
1122
+ chatLine,
1123
+ );
1124
+ break;
1125
+ }
1093
1126
  if (interactions.length > 0) {
1094
1127
  const missed = sessionID ? getMissedCount(sessionID) : 0;
1095
1128
  if (sessionID) incrementMissedCount(sessionID);
@@ -1120,10 +1153,10 @@ switch (command) {
1120
1153
  parts.push(claudeRecallNudge(matches));
1121
1154
  }
1122
1155
  if (predictions && predictions.length > 0) {
1123
- parts.push(predictionNudge(predictions));
1156
+ parts.push(predictionNudge(predictions, mcpToolName));
1124
1157
  }
1125
1158
  if (behaviors && behaviors.length > 0) {
1126
- parts.push(behaviorNudge(behaviors));
1159
+ parts.push(behaviorNudge(behaviors, mcpToolName));
1127
1160
  }
1128
1161
 
1129
1162
  if (parts.length > 0) {
@@ -1272,13 +1305,13 @@ switch (command) {
1272
1305
  if (repo === "unknown") repo = "global";
1273
1306
 
1274
1307
  const { sidebandPredictions, sidebandSocketPath } = await import("../src/sideband");
1275
- const { predictionNudge } = await import("../src/prompts");
1308
+ const { predictionNudge, mcpToolName } = await import("../src/prompts");
1276
1309
  const sockPath = sidebandSocketPath(dbPath);
1277
1310
  const threshold = parseFloat(process.env.THATCH_PREDICTION_THRESHOLD ?? "0.60");
1278
1311
  const predictions = await sidebandPredictions(sockPath, promptText, [repo, "global"], threshold, 5);
1279
1312
 
1280
1313
  if (predictions && predictions.length > 0) {
1281
- const output = predictionNudge(predictions);
1314
+ const output = predictionNudge(predictions, mcpToolName);
1282
1315
  if (isJson) console.log(JSON.stringify({ additional_context: output }));
1283
1316
  else console.log(output);
1284
1317
  } else {
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="109" height="20" role="img" aria-label="Ask DeepWiki"><title>Ask DeepWiki</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="109" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="24" height="20" fill="black"/><rect x="24" width="85" height="20" fill="#007ec6"/><rect width="109" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAPoAAAD6AG1e1JrAAACIUlEQVRYw+2XP2gUQRTGv7d3JhYWQawkIBaCVWzETos0gmAnVoKNTSoLK0E7EQQrC20sBVFBtLPQRgTBtBaKjYgIQUSxiJq7fT+LvCGPJZdNLt4dQj5Y3u7sznzfzLw/s9IO/icANiniCujEfWdsQgBLxAbsbraPZcmBfcAt4DNwKQsZNfEUsAB8YRV12LfA6ZHuedg7AO7ed/c+UBcbQhbiu+6wXIM6EnZWkszMJe2Ke0n6I2la0v51hFeSMLN6OwIK6jJwEBdUYb2MAxRSz6sY4geiahFgLe1Hwq6YWe3us8AN4FQQU4QM64SPY69Xyr43fADgNjAPXHb3peSsD4FDQ0VLEjAHvIhBPS6AHvA1bBO9JPAXcHFbIRtJ5yzwIQZ9CZwAusDJENIkLqsG8DpNyIZJwSUk9wLHgakcesC1JCCjiHm1kYDNOEjptCxpSVK/8f73OFLxPLAYM3oCzEX7MXf/lJbc/8kWpA4HgQdpYI9IWAbeh83whi84cHXL+58EPBoQhnmm94EzwE3gZ2p/Dhzdbhg+TaRr01x7ftb4/jBwFzjXrCvDpmLW9crVLNeR9CaapoGemb2TdCERW1tNaBNQ8jktwmozqwtpqRNtdWAjARYk39IS12ZWRX7vmJmA71nQZgi36gN7gCvAj5xc3P0jcH7kx7Ik5IC73wsh14GZsZySow5002l4Jr0b6+m4eSyvpAn8lEzsx2QHo8RfUrlN+uPq4ksAAAAASUVORK5CYII="/><text aria-hidden="true" x="655" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="750">Ask DeepWiki</text><text x="655" y="140" transform="scale(.1)" fill="#fff" textLength="750">Ask DeepWiki</text></g></svg>