@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.
- package/README.md +56 -26
- package/artifacts/skills/common.md +1 -0
- package/artifacts/skills/thatch-clear-writing.md +103 -5
- package/artifacts/skills/thatch-code-archaeology.md +9 -0
- package/artifacts/skills/thatch-code-review.md +16 -8
- package/artifacts/skills/thatch-coordination.md +69 -0
- package/artifacts/skills/thatch-plan-refinement.md +33 -4
- package/artifacts/skills/thatch-pr-description.md +21 -13
- package/artifacts/skills/thatch-refine-personal.md +5 -0
- package/artifacts/skills/thatch-refine.md +7 -1
- package/artifacts/skills/thatch-review-context.md +55 -2
- package/artifacts/skills/thatch-review-followup.md +4 -4
- package/artifacts/skills/thatch-review-no-slop.md +15 -0
- package/artifacts/skills/thatch-review-response.md +1 -1
- package/artifacts/skills/thatch-review-synthesizer.md +21 -1
- package/bin/thatch +48 -15
- package/docs/assets/deepwiki-badge.svg +1 -0
- package/docs/dev/README.md +18 -10
- package/docs/dev/features/README.md +1 -1
- package/docs/dev/features/behavior-engine.md +10 -0
- package/docs/dev/features/cli.md +4 -1
- package/docs/dev/features/commands.md +40 -9
- package/docs/dev/features/cross-session-chat.md +194 -23
- package/docs/dev/features/database.md +4 -4
- package/docs/dev/features/notifications.md +1 -2
- package/docs/dev/features/nudge-pipeline.md +14 -0
- package/docs/dev/features/opencode-plugin.md +56 -15
- package/docs/dev/features/prediction-engine.md +71 -6
- 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 +161 -0
- package/docs/dev/features/setup.md +5 -0
- package/docs/dev/features/watchers.md +62 -6
- package/docs/dev/gotchas.md +89 -0
- package/docs/dev/mcp-parity.md +5 -5
- package/docs/dev/setup-and-hooks.md +7 -2
- package/docs/dev/skills.md +12 -9
- package/docs/plans/generalized-session-heartbeat.md +212 -0
- package/docs/plans/prediction-consolidation.md +88 -68
- package/docs/user/README.md +29 -13
- package/docs/user/behavior-engine.md +7 -1
- package/docs/user/cli.md +5 -1
- package/docs/user/code-review.md +43 -2
- package/docs/user/commands.md +37 -10
- package/docs/user/cross-session-chat.md +76 -26
- package/docs/user/prediction-engine.md +23 -1
- package/docs/user/setup.md +16 -0
- package/docs/user/skills.md +9 -7
- package/docs/user/watchers.md +22 -7
- package/package.json +10 -2
- package/src/alerts.ts +1 -1
- package/src/capabilities.ts +20 -6
- package/src/chat.ts +358 -43
- package/src/commands.ts +123 -18
- package/src/db.ts +74 -3
- package/src/git.ts +3 -1
- package/src/hygiene.ts +5 -0
- package/src/mcp.ts +101 -9
- package/src/opencode/tui-plugin.ts +90 -0
- package/src/opencode/v2.ts +440 -66
- package/src/prediction.ts +90 -0
- package/src/prompts.ts +164 -29
- package/src/runtime.ts +298 -15
- package/src/scoring-engine.ts +40 -9
- package/src/session-db.ts +4 -4
- package/src/session-tab-shared.ts +231 -0
- package/src/setup.ts +9 -3
- package/src/skills.ts +6 -9
- package/src/tab-state.ts +125 -0
- package/src/tool-defs.ts +454 -76
- package/src/tools.ts +2 -0
- package/src/watchers.ts +263 -26
- package/docs/plans/intuition-drives.md +0 -147
- package/docs/plans/llm-alerts.md +0 -287
- package/docs/plans/thatch-actions.md +0 -179
- 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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
74
|
+
PURPOSE stays at feature altitude. Table-level mechanics, column lists, and function names belong to DESCRIPTION.
|
|
73
75
|
|
|
74
|
-
|
|
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
|
|
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
|
|
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
|
|
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 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
|
|
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(
|
|
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)
|
|
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
|
|
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>
|