@jeffober/thatch 0.1.16 → 0.1.17

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.
@@ -0,0 +1,399 @@
1
+ ---
2
+ name: ticket-description
3
+ description: Draft a ticket description using instructional-design scaffolding (SYNOPSIS / PROBLEM / BACKGROUND / PROPOSED APPROACH / ACCEPTANCE CRITERIA / RELEASE PLAN / VALIDATION / DEPENDENCIES / NOTES) with bold and italic emphasis on the phrases that carry the meaning, so a reader skimming only the emphasized fragments still gets the story. Use when the user asks you to write, draft, or file a ticket, issue, or work item (Linear or Jira). Do NOT use for PR descriptions (use pr-description), commit messages, or design docs.
4
+ ---
5
+
6
+ # Writing ticket descriptions
7
+
8
+ ## The reader
9
+
10
+ You are writing for two people who share one trait: neither has your context, and neither knows this subsystem's jargon.
11
+
12
+ The first reader is a **lead or product manager** scanning a backlog. They decide priority, sequence, and assignment. They read SYNOPSIS and PROBLEM, skim ACCEPTANCE CRITERIA and DEPENDENCIES, and move on. They have 30 seconds per ticket in a planning context. They know general engineering vocabulary but may not know subsystem-specific acronyms or terms.
13
+
14
+ The second reader is an **implementer** picking up the ticket cold. They need to start work without coming back to ask "what did you mean by..." They read everything, carefully, and the ticket is their starting specification. They may be new to the subsystem and need terms defined.
15
+
16
+ The description is not a design doc. It is a specification that makes work startable. A design doc (when one is warranted) may live in repo docs, Linear, Notion, Archbee, Google Docs, Figma, or another system; link it from the ticket instead of inlining it. The ticket's job is to define the problem, propose a direction, and set the gates that define "done."
17
+
18
+ This is **instructional design**, not documentation. Each section has a cognitive goal:
19
+ - SYNOPSIS: orient to the class of work and affected area.
20
+ - PROBLEM: make the reader agree there is something worth solving.
21
+ - BACKGROUND: give the implementer the context they would otherwise re-derive.
22
+ - PROPOSED APPROACH: set the direction without over-constraining the implementation.
23
+ - ACCEPTANCE CRITERIA: define "done" as observable, testable bullets.
24
+ - RELEASE PLAN: sequence the rollout when prod coordination is needed.
25
+ - VALIDATION: define how to verify the change in the deployed system.
26
+ - DEPENDENCIES: explain ordering constraints, not just list them.
27
+ - NOTES: flag scope limits, intentional non-goals, and things that look in-scope but aren't.
28
+
29
+ Layer the knowledge so each section builds on the previous one. The reader should never encounter a concept in section N that was not introduced in section N-1.
30
+
31
+ ## Structure
32
+
33
+ Sections, in order: SYNOPSIS, PROBLEM, BACKGROUND (conditional), PROPOSED APPROACH, ACCEPTANCE CRITERIA, RELEASE PLAN (conditional), VALIDATION (conditional), DEPENDENCIES (conditional), NOTES (conditional).
34
+
35
+ **SYNOPSIS, PROBLEM, PROPOSED APPROACH, and ACCEPTANCE CRITERIA are mandatory.** A small ticket gets short sections, not fewer sections. Conditional sections appear only when warranted; omit them when empty. Never emit an empty header.
36
+
37
+ **Scale the body to the work's conceptual size.** A one-line fix gets a one-line PROBLEM, a one-line PROPOSED APPROACH, and one or two acceptance criteria. A multi-PR feature gets full sections with real depth. The implementer should be able to start work after reading the ticket, but they should not have to read a novel to fix a guard clause.
38
+
39
+ ### SYNOPSIS
40
+
41
+ One to two lines. Orient only. The reader should come away knowing the class of work (bugfix, feature, refactor, investigation, cleanup) and the affected area.
42
+
43
+ SYNOPSIS may use subsystem-specific terms as scan anchors (that is its job). It does not define them. BACKGROUND (or PROBLEM, when there is no BACKGROUND) 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.
44
+
45
+ Include a ticket link if referencing a parent or related ticket. Do not link the ticket to itself.
46
+
47
+ ### PROBLEM
48
+
49
+ Frame the harm. Lead with the conclusion, then provide the evidence, not the other way around. State what is wrong, then explain why. For example:
50
+
51
+ - "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."
52
+ - "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."
53
+ - "The retry loop has no cap, so a persistent failure spins forever" not "the loop re-enters on error without a counter... meaning it never terminates."
54
+
55
+ The reader should know what the problem is before they read why it happens.
56
+
57
+ No solution yet. The reader should finish this section agreeing there is something to address, even with zero code knowledge.
58
+
59
+ Cover three things, each in its own paragraph:
60
+ 1. **What is wrong**, concretely. Name the component, the behavior, and the failure mode.
61
+ 2. **Why it matters.** Incident history, risk rating, customer impact, or "preventive: blocks a class of bug we keep almost-hitting."
62
+ 3. **Hazard rating and motivation.** How likely is the bad outcome, and under what conditions? Then answer "why now?": if the risk is low, what motivates this ticket? 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 lead's question is "should I prioritize this?" - give them the answer. _PLAT-135's ticket did this well: it named the cross-wait, then said "why it never fired" and rated the window as once-per-process cold load._
63
+
64
+ If the problem is preventive (no current defect), say so plainly. Then say why it is worth fixing despite the low risk.
65
+
66
+ ### BACKGROUND (conditional)
67
+
68
+ Context the implementer needs that is not part of the problem itself. Prior tickets, prior PRs, architectural constraints, decisions already made. This is the section that prevents the implementer from re-deriving what the ticket author already knows.
69
+
70
+ BACKGROUND is also where subsystem-specific terms and acronyms get defined on first use. If the ticket uses terms a newcomer to the area would not know (a crypto term like DEK, an auth term like the SAML assertion, a scanning term like a ScanRun, a results term like the frozen materialized view), define them here in a brief gloss on first appearance. After the first definition, use the term freely throughout the rest of the ticket. If there is no BACKGROUND section, definitions go in PROBLEM.
71
+
72
+ Link the source material the implementer needs. For repo files, use `github.com` links rather than relative paths; tickets render outside the repo. For external integrations, link the vendor docs for the exact integration touch points the ticket depends on (auth flow, webhook format, API endpoint, SDK call, cloud resource behavior). Also link any user-provided planning artifacts that define intent or UX: Linear, Notion, Archbee, Google Docs, Figma, or similar. Do not invent external docs; link what is relevant and available.
73
+
74
+ When referencing prior work, link the ticket or PR and state what it did and what it left behind. "PLAT-134 (#6838) made key I/O executor-explicit, removing the unconditional deadlock. One narrower hazard survived it" gives the implementer the full arc in two sentences.
75
+
76
+ Omit when the problem is self-contained. A one-line "follow-up from #6838" can live in PROBLEM instead.
77
+
78
+ ### PROPOSED APPROACH
79
+
80
+ The design concept. Not a full implementation plan. Enough that the implementer knows the direction: "load key material outside the mutex" not "restructure ActiveKey to use cachedKey/publish helpers with a publish function that zeroizes the loser's DEK." The implementer writes the implementation plan; the ticket sets the compass heading.
81
+
82
+ For investigative tickets, this is "investigate X and propose a solution" rather than a concrete approach. Say so explicitly so the implementer knows the ticket expects analysis before implementation.
83
+
84
+ This is where the ticket diverges most from a PR description. A PR description describes a change that already happened. A ticket proposes one that hasn't. The prose is prescriptive ("the fix should...") rather than descriptive ("this PR moves...").
85
+
86
+ ### ACCEPTANCE CRITERIA
87
+
88
+ The checklist. Each criterion is a bullet, testable and concrete. These are what the implementer and reviewer use to decide "done." Ordered by dependency where it matters.
89
+
90
+ Good criteria name the **observable behavior**, not the implementation:
91
+ - "no DB or KMS I/O occurs while km.mu is held" (observable, testable)
92
+ - "a regression test demonstrates that a pool-path cold load cannot block transaction-path callers" (observable, testable)
93
+
94
+ Bad criteria are vague:
95
+ - "improve the locking situation" (untestable)
96
+ - "restructure ActiveKey" (implementation detail, not a behavior)
97
+
98
+ If the criterion has a test shape, name it: "SetMaxOpenConns(1) with the connection held by a tx that also resolves the key." This gives the implementer the test design without dictating the implementation.
99
+
100
+ ### RELEASE PLAN (conditional)
101
+
102
+ Rollout strategy for tickets that change production behavior. Only when the change touches prod in a way that requires coordination. Covers:
103
+ - Feature flags (name, default state, per-tenant overrides if relevant).
104
+ - Migration ordering (what must deploy before what, and why).
105
+ - Deploy sequence (which services, in what order).
106
+ - Rollback path (what to do if it goes wrong).
107
+
108
+ A pure internal refactor with no operator-visible behavior change does not need this section. A migration that gates on a backfill completing does.
109
+
110
+ ### VALIDATION (conditional)
111
+
112
+ How to verify the change is correct in the deployed system. Distinct from acceptance criteria (which are about the code) because validation is about the **deployed system**. Covers:
113
+ - Monitoring dashboards or panels to watch.
114
+ - Metrics or signals that confirm the change is live and correct.
115
+ - Smoke tests or manual verification steps.
116
+ - The specific signal that says "this shipped and is working."
117
+
118
+ Only when prod verification is non-trivial. A pure refactor with good tests does not need this. A backfill worker that runs for days post-merge does.
119
+
120
+ ### DEPENDENCIES (conditional)
121
+
122
+ Blocking relationships and ordering constraints. "Blocked by PLAT-126" is the tracker field; the *why* belongs here: "PLAT-125 partitions by location_fingerprint, so it can only run after backfill completes (count == 0), not merely after PLAT-124 merges." That kind of nuance is what prevents a well-intentioned lead from reordering the queue.
123
+
124
+ Distinguish hard data dependencies (cannot run until X completes) from soft dependencies (should run after X for cleanliness, but technically works independently). Say which kind it is.
125
+
126
+ ### NOTES
127
+
128
+ Always a `## NOTES` markdown header. Never a bare `Notes:` line ending a section.
129
+
130
+ Use NOTES for:
131
+ - Intentional non-goals (things that look in-scope but are deliberately excluded).
132
+ - Scope limits (what this ticket does NOT do).
133
+ - Consciously declined alternatives (approaches considered and rejected, with a one-line reason).
134
+ - Context that doesn't fit any other section but would cause confusion if omitted.
135
+
136
+ One bullet per item. Bold the **leading noun** in each bullet as the scan anchor.
137
+
138
+ Each bullet is either a short flag or a self-contained explanation. A short flag names the thing and points to where the full rationale lives. A self-contained explanation states the thing and its reason in plain English, in one or two sentences that a reader with no prior context can parse. Do not compress a paragraph into a subordinate clause. If the rationale needs more than two sentences, it belongs in a design doc or the PROPOSED APPROACH, not in NOTES.
139
+
140
+ If there are no notes, omit the section entirely. Do not emit an empty `## NOTES` header.
141
+
142
+ ## Emphasis for scanning: bold and italics
143
+
144
+ Bold and italicize the "save points" - the phrases that carry meaning - not whole sentences. A reader who skims *only* the bold and italic fragments should get the story; full prose is for when they want detail.
145
+
146
+ **What to bold:** phrases that name the **work itself** - the component, the failure mode, the mechanism, the behavior the ticket asks for. These are the nouns and verbs that would appear in a diff of the design, not a diff of the code. "**circular wait** in the **KeyManager**" names the failure mode and the component. "**load key material outside the mutex**" names the mechanism.
147
+
148
+ **What to italicize:** phrases that orient the reader to **significance** - the conclusion a paragraph is building toward, or the outcome that makes the work matter. _There is a circular wait in the KeyManager_ in PROBLEM. _the circular wait cannot form_ in a payoff sentence. Italics mark the load-bearing insight; bolds mark the load-bearing nouns and verbs.
149
+
150
+ **What NOT to bold or italicize:** history ("PLAT-134 removed the deadlock"), hazard detail ("it only triggers during the first key fetch"), justification, or connective tissue. These are context and support; they do not carry the story.
151
+
152
+ **Where formatting goes:**
153
+ - SYNOPSIS: bold the key noun phrases (scan anchor for the whole ticket).
154
+ - PROBLEM: bold and italicize the conclusion phrase (the problem statement), then bold key nouns in the explanation. Italicize the motivation insight if there is one.
155
+ - PROPOSED APPROACH: bold the mechanism nouns and verbs. Italicize the directional conclusion if there is one.
156
+ - ACCEPTANCE CRITERIA bullet leads: bold the observable behavior being verified.
157
+ - DEPENDENCIES and NOTES bullets: bold the leading noun.
158
+
159
+ Rules:
160
+ - Bold once per concept per section.
161
+ - Prefer bolding things (components, states, thresholds, identifiers) and verbs of change over adjectives.
162
+ - Italicize at most one phrase per paragraph. If two phrases compete, pick the one that carries the paragraph's point.
163
+ - Never bold a whole sentence. Never italicize a whole sentence. Never bold a whole bullet.
164
+ - In bullets, the leading noun is usually the right anchor; bold it, leave the qualifier plain.
165
+ - A phrase may be both bold and italicized when it is both a work-naming noun and the paragraph's load-bearing insight: **_circular wait_**. Use sparingly.
166
+
167
+ Scan check before submitting: read only the bold and italic fragments top-to-bottom. If that reading does not convey the shape of the work, you bolded the wrong words (or the prose is padding around them).
168
+
169
+ ## Prose style
170
+
171
+ These are hard rules, not suggestions.
172
+
173
+ 1. **One idea per sentence.** Split clause-chains. If a sentence has more than one comma separating independent clauses, it is two sentences. One topic per paragraph: if a paragraph shifts from the problem to its risk rating, split it. The reader who skims paragraph leads should get the section's structure.
174
+ 2. **Literal phrasing beats compressed idiom.** Write for an implementer who has never seen this subsystem: "the worker holds the lock across the database read and the network call" not "lock spans I/O." Implementation verbs are compressed idiom: do not use method names as verbs in prose. "store the result in the in-memory cache" not "publish it". Prefer plain English nouns over code-domain nouns, even when the code-domain noun is technically correct: "reading the row from the database" not "the row read"; "a request waiting for a worker" not "a waiter"; "overwritten with zeros" not "zeroized"; "the queued scan jobs" not "the backlog". If a word sounds like it belongs in a source file rather than a sentence, replace it.
175
+ 3. **Define subsystem-specific terms on first use.** If a term is specific to this subsystem (not general engineering vocabulary), expand or briefly gloss it the first time it appears. "the DEK (the symmetric key used to encrypt secrets at rest)" not just "the DEK". "the SAML assertion (the signed XML document an identity provider returns to prove who the user is)" not just "the assertion". After the first definition, use the term freely. Definitions go in BACKGROUND, or PROBLEM if there is no BACKGROUND.
176
+ 4. **No design-narration phrases.** Delete "it anchors the design", "the fix", "this change", "the new approach". State the fact plainly.
177
+ 5. **No authoring-sequence narration.** Never describe the order you discovered things in, which draft had what, or what a later revision will add. Describe the ticket as it stands.
178
+ 6. **Italics for orientation asides.** Use `_italics_` for a one-phrase aside that orients the reader: _This is where the deadlock lived_. Not for emphasis.
179
+ 7. **Plain ASCII only.** No smart quotes, smart apostrophes, em dashes, en dashes, ellipsis glyphs, or arrow glyphs. Use plain hyphens (sparingly) for asides; semicolons are often better.
180
+ 8. **No buzzwordy abstractions.** If a phrase sounds like it is trying to sound smart, cut or replace it. Name the code and behavior directly. "identity contract" -> "the struct field that identifies the owner". "canonical hash" -> "the SHA-256 we compute on ingestion".
181
+ 9. **Prescriptive voice.** The ticket proposes work that hasn't happened yet. Use "the fix should...", "the implementation must...", "the worker should..." not "this PR moves..." or "the code now...". The PR description uses descriptive voice; the ticket uses prescriptive.
182
+ 10. **Default to a numbered outline for steps.** A flat list, with an occasional inline branch ("if the path is not in the registry, return a 404; otherwise continue to step 6"), covers almost every workflow. The bar for a diagram or Mermaid chart is high: reach for one only when the flow genuinely needs two dimensions to read - a graph network with many nodes and edges, or a workflow that recurses, forks, and rejoins in ways a linear outline cannot follow without constant back-references. If the flow is one or two levels deep and does not recurse or redirect, an outline is clearer and cheaper to maintain than a diagram.
183
+ 11. **Avoid loaded language.** Use neutral, current terminology unless the repo's established name is different. Prefer allowlist/blocklist over whitelist/blacklist, primary/replica over master/slave, and precise role names over moralized labels like "bad" or "evil".
184
+
185
+ ## Length and style
186
+
187
+ Target: the implementer can start work after one read. The lead can prioritize after reading SYNOPSIS and PROBLEM.
188
+
189
+ Scale the body to the work's conceptual size:
190
+ - Small fix (one guard, one flag, one constant): one paragraph per mandatory section, one to three acceptance criteria. ~20-line ceiling.
191
+ - Moderate change (new function, restructured flow, multi-file refactor): full sections with real depth. No hard ceiling, but each paragraph earns its place.
192
+ - Large change (new feature, multi-PR effort, migration sequence): full sections plus RELEASE PLAN and VALIDATION. May run a full page. If PROPOSED APPROACH is longer than the rest of the ticket combined, the ticket is probably a design doc wearing a ticket costume; split it.
193
+
194
+ Inside sections, telegraphic bullet style is fine for criteria and plans: lowercase start, abbreviations (`w/`, `1x`, `~`), parenthetical shorthand. Full sentences for PROBLEM and BACKGROUND prose.
195
+
196
+ **Tracker rendering**: many trackers (Linear, Jira) render markdown. Put each narrative paragraph on one physical line. Separate paragraphs with a blank line. Bullets stay one per line.
197
+
198
+ ## Codebase comparison
199
+
200
+ Before drafting, verify the problem statement against the current codebase. If the ticket proposes changing a function that was refactored last month, or cites an incident that was mitigated differently, the ticket is stale before it is filed.
201
+
202
+ Comparison process:
203
+ 1. Read the code the ticket references. Confirm the behavior described in PROBLEM still exists.
204
+ 2. Check git log for recent changes to the same files. If the behavior was already fixed, do not file the ticket; tell the user.
205
+ 3. If the behavior exists but was partially mitigated, note the mitigation in BACKGROUND so the implementer knows what is already in place.
206
+ 4. If the ticket is a follow-up to a merged PR, check the merged code to confirm the follow-up is still needed.
207
+
208
+ When the codebase contradicts the ticket, flag the discrepancy to the user before drafting. Do not silently paper over the gap.
209
+
210
+ ## Search before create
211
+
212
+ Before filing a new ticket, search for likely duplicates in the tracker. Use the proposed title, problem keywords, affected subsystem, and any incident/customer/project references. If a plausible duplicate exists, surface it to the user before drafting or filing.
213
+
214
+ If the user still wants a new ticket, make the relationship explicit: related follow-up, supersedes, duplicates, or narrower implementation slice. Do not silently create parallel tickets for the same problem.
215
+
216
+ ## Link verification
217
+
218
+ Before filing or updating a ticket, verify every link you can. Tickets are usually read outside the repo, so broken links are worse than no links.
219
+
220
+ - **Repo file links** - use `github.com` links rather than relative paths; verify the path exists on the relevant branch.
221
+ - **External docs** - if tooling can fetch the URL, confirm it resolves and appears to be the intended doc. If you cannot verify a user-provided doc link, keep it but do not claim you verified it.
222
+ - **Ticket, PR, and design links** - verify the ID/title matches the thing being referenced.
223
+
224
+ ## Linear labels
225
+
226
+ When filing a Linear ticket, add obvious labels when they clarify triage. Labels are filing metadata, not ticket-description prose.
227
+
228
+ - **Bug** - user-visible broken behavior, incorrect result, regression, or production defect.
229
+ - **tech-debt** - cleanup, drift, dead code, consistency work, or known design debt with no direct behavior change.
230
+ - **refactor** - internal reshaping with intentionally unchanged behavior.
231
+ - **Feature** - new user-facing capability or new supported workflow.
232
+ - **Improvement** - behavior already exists but should become clearer, safer, faster, or easier to use.
233
+ - **Support** - work driven by customer/support handling rather than product roadmap.
234
+ - **infra** - Docker, mise, CI, codegen, config, deployment, or cross-cutting dev infrastructure.
235
+ - **frontend**, **go**, **hono**, **proto** - use only when the implementation surface is clearly in that area.
236
+
237
+ Do not guess initiative labels (`track-*`, `wave-*`). Use them only when the user or existing project context names the initiative.
238
+
239
+ ## What does NOT belong in the ticket
240
+
241
+ - **Implementation detail.** The ticket sets the direction; the implementer chooses the mechanism. If the acceptance criteria read like a build plan, they are over-specified.
242
+ - **Full design docs.** Link them from BACKGROUND, do not inline them. They may live in repo docs, Linear, Notion, Archbee, Google Docs, Figma, or another planning system. If the ticket IS the design doc, split it into a design doc plus a ticket that references it.
243
+ - **AI attribution, co-author lines, "generated with" footers.**
244
+ - **Backstory that does not affect the work.** "We explored approach A then B then landed on C" is noise. The consciously declined alternatives go in NOTES as one-liners.
245
+ - **Authoring-process narration.** "I was investigating X and noticed Y" is session context, not ticket content. State the problem plainly.
246
+
247
+ ## Worked examples
248
+
249
+ ### Small fix
250
+
251
+ ## SYNOPSIS
252
+
253
+ **Guard clause fix** in the **scan activity handler**: reject negative cursor values instead of wrapping them.
254
+
255
+ ## PROBLEM
256
+
257
+ The scan activity endpoint accepts a cursor query parameter without validating its sign. A negative cursor passes through to the keyset query, where Postgres treats it as a valid bigint but the index scan produces unpredictable ordering. A client that sends `?cursor=-1` gets a page of results that appears valid but skips rows. No incident has been reported; the endpoint is internal and cursors are generated by the API, not user input. The risk is that a future caller constructs a cursor from untrusted input.
258
+
259
+ ## PROPOSED APPROACH
260
+
261
+ Validate the cursor at the handler boundary before passing it to the query layer. Reject negative values with a 400 response. The query layer should not see a cursor it cannot handle.
262
+
263
+ ## ACCEPTANCE CRITERIA
264
+
265
+ - **negative cursor returns 400** - a request with `?cursor=-1` receives a 400 response with a clear error message, not a 200 with partial results.
266
+ - **zero and positive cursors unchanged** - existing valid cursor values continue to paginate normally.
267
+
268
+ ## NOTES
269
+
270
+ - **not a query-layer change** - the guard belongs in the handler, not the query builder. The query layer trusts its inputs by design.
271
+
272
+ ### Moderate change (PLAT-135 as it would appear under this skill)
273
+
274
+ ## SYNOPSIS
275
+
276
+ **Concurrency fix** in **secret storage**: cold-cache **tenant key loads** should not hold the **KeyManager mutex** across DB and KMS I/O.
277
+
278
+ ## PROBLEM
279
+
280
+ On a cold cache, KeyManager holds its mutex across the key row read and the KMS unwrap. A caller loading through the pool can win the mutex and then wait for a pool connection, while every pool connection is held by transaction callers blocked on that same mutex. That is a circular wait, and mutex waits do not respect context deadlines.
281
+
282
+ No incident has hit this shape. The window only exists during the once-per-process cold load, and it requires mixed pool-path and transaction-path callers with full pool saturation in the same instant. PLAT-134 (#6838) removed the unconditional deadlock in this code; this closes the remaining conditional one.
283
+
284
+ ## BACKGROUND
285
+
286
+ PLAT-134 (#6838) made key I/O executor-explicit: `ActiveKey(ctx, exec)` and `GetOrCreateKey(ctx, exec)` take an explicit executor, and the KeyManager holds no database handle. That removed the hold-and-wait deadlock where a transaction holder could trigger a hidden second connection acquisition. The mutex-across-I/O shape survived because it predates PLAT-134; it has been identical since the KeyManager was introduced.
287
+
288
+ ## PROPOSED APPROACH
289
+
290
+ Load key material outside the mutex. Cold loaders run their I/O on their own executor, then take the mutex only to publish the cached entry. Duplicate loads under race are acceptable: they unwrap the same wrapped DEK, producing the same result. Creation needs separate serialization because the schema has no uniqueness on active key rows.
291
+
292
+ ## ACCEPTANCE CRITERIA
293
+
294
+ - **no DB or KMS I/O while km.mu is held** - the mutex guards only the cache field, never held across a query or KMS call.
295
+ - **cache semantics unchanged** - load-only `ActiveKey`, creation confined to `GetOrCreateKey`, cached entry always corresponds to a committed row.
296
+ - **regression test** - a cold pool-path load cannot block transaction-path callers. Test shape: `SetMaxOpenConns(1)` with the connection held by a transaction that also resolves the key.
297
+
298
+ ## NOTES
299
+
300
+ - **no singleflight on the load path** - any shared wait ties waiters to the winner's executor and recreates the cross-wait. Duplicate unwraps are idempotent and cheap.
301
+ - **multi-replica key-creation race** is consciously out of scope. The deployment shape is one API pod per tenant; cross-process creation is a known, accepted hazard.
302
+
303
+ ### Large change with RELEASE PLAN and VALIDATION
304
+
305
+ ## SYNOPSIS
306
+
307
+ **Backfill worker** for **location fingerprints**: compute and populate `location_fingerprint` on existing rows to unblock the M2 unique index.
308
+
309
+ ## PROBLEM
310
+
311
+ PLAT-123 added the `location_fingerprint` column and made `NewLocation` compute it on write. New rows get fingerprints, but all existing rows have NULL. The M2 plan requires a unique index on `(secret_id, source_id, location_fingerprint)`, which cannot be created while NULLs exist. The dedup pass (PLAT-125) also needs fingerprints to group duplicate locations; without them, un-fingerprinted duplicates do not group together and latent dupes survive into the index creation.
312
+
313
+ ## BACKGROUND
314
+
315
+ PLAT-123 (#6777) added the column and write-path computation. The fingerprint is `hex(sha256(source_type + NUL-delimited column-name/value pairs))` with a fixed authority map (PLAT-122). The column is nullable; old rows remain NULL until this backfill completes. The backfill must finish before PLAT-125 (dedup) and PLAT-126 (index + NOT NULL) can run.
316
+
317
+ ## PROPOSED APPROACH
318
+
319
+ A daemon goroutine in scanner-api walks the `webapi_secretlocation` table in keyset-paginated batches, computing and updating `location_fingerprint` for rows where it is NULL. The walk is paced (batch size and sleep interval) to avoid saturating the database. A kill-switch feature flag disables the daemon without redeployment. The daemon quiesces when no NULL rows remain.
320
+
321
+ ## ACCEPTANCE CRITERIA
322
+
323
+ - **daemon computes fingerprints** for all NULL rows, keyed by `WHERE location_fingerprint IS NULL`, walking in keyset-paginated batches.
324
+ - **paced walk** - batch size and sleep interval are configurable; defaults do not saturate the database under normal load.
325
+ - **kill switch** - a feature flag disables the daemon at startup; the flag defaults to enabled.
326
+ - **quiescence** - when no NULL rows remain, the daemon stops scanning and logs a quiescent message.
327
+ - **gauge** - a Prometheus gauge reports the remaining NULL count, primed at daemon start.
328
+
329
+ ## RELEASE PLAN
330
+
331
+ - Deploy the worker behind a kill-switch flag (default enabled, no per-tenant overrides).
332
+ - Monitor the burndown gauge and `thogctl locations-missing-fingerprint` TOTAL count per tenant.
333
+ - The worker runs for days on large tenants (~61M rows at ~200 rows/s). No Friday releases.
334
+ - Rollback: disable the kill-switch flag. The daemon stops on next pod restart. Partially filled rows are safe; the walk resumes from the cursor on restart.
335
+
336
+ ## VALIDATION
337
+
338
+ - Watch the burndown gauge in Grafana for each tenant. The count should decrease monotonically (with restart resets).
339
+ - Run `thogctl locations-missing-fingerprint` per tenant. TOTAL == 0 means the backfill is complete for that tenant.
340
+ - Skim scanner-api logs for `backfill batch failed` or `unknown_source_type` to rule out non-cadence failures.
341
+ - After TOTAL == 0 for all tenants, PLAT-125 (dedup) can proceed.
342
+
343
+ ## DEPENDENCIES
344
+
345
+ - **hard dependency on PLAT-123** - the column and write-path computation must be live in prod before the backfill starts. The backfill targets `WHERE location_fingerprint IS NULL`; without the column, there is nothing to fill.
346
+ - **PLAT-125 (dedup) is hard-blocked on this** - dedup groups by `location_fingerprint`; running it while NULLs exist would miss real duplicates.
347
+ - **PLAT-126 (index + NOT NULL) is hard-blocked on PLAT-125** - the unique index cannot be created while duplicate rows exist.
348
+
349
+ ## NOTES
350
+
351
+ - **cursor reset on restart** - the daemon does not persist its cursor. A pod restart resets to the beginning of the walk. Paced batching makes this safe (re-walking filled rows is a no-op), but large tenants take longer to converge after a restart.
352
+ - **unfillable rows** - some source types may lack a fingerprint extractor. The daemon counts these separately and does not retry them. The fleet survey confirmed zero unfillable source types in production, but the code path exists as insurance.
353
+
354
+ Scan the bolds only:
355
+ > Guard clause fix ... scan activity handler ... negative cursor returns 400 ... zero and positive cursors unchanged ... not a query-layer change ... Concurrency fix ... secret storage ... tenant key loads ... KeyManager mutex ... negative cursor returns 400 ... no DB or KMS I/O while km.mu is held ... cache semantics unchanged ... regression test ... no singleflight on the load path ... multi-replica key-creation race ... Backfill worker ... location fingerprints ... daemon computes fingerprints ... paced walk ... kill switch ... quiescence ... gauge ... hard dependency on PLAT-123 ... PLAT-125 (dedup) is hard-blocked on this ... PLAT-126 (index + NOT NULL) is hard-blocked on PLAT-125 ... cursor reset on restart ... unfillable rows
356
+
357
+ That reading alone conveys the shape, scope, and gates of all three tickets.
358
+
359
+ ## Process
360
+
361
+ 1. **Gather**: read the code the ticket references. Check `git log` for recent changes to the same files. Identify the problem, the prior work, and the proposed direction.
362
+ 2. **Search before create**: search Linear/Jira for likely duplicate tickets. If one exists, surface it before drafting or filing.
363
+ 3. **Codebase comparison**: verify the problem still exists in the current code. If it was already fixed, tell the user. If it was partially mitigated, note the mitigation.
364
+ 4. **Draft PROBLEM first**, in one breath, without looking at code. If you cannot state the harm in three sentences, you do not understand the problem yet. Go back and read.
365
+ 5. **Write BACKGROUND** (if needed): what prior work led here, what was done, what was left behind.
366
+ 6. **Write PROPOSED APPROACH**: the compass heading, not the map. Prescriptive voice.
367
+ 7. **Write ACCEPTANCE CRITERIA**: observable, testable bullets. Name the test shape where relevant.
368
+ 8. **Write RELEASE PLAN** (if needed): flags, ordering, rollback.
369
+ 9. **Write VALIDATION** (if needed): monitoring, signals, smoke tests.
370
+ 10. **Write DEPENDENCIES** (if needed): what blocks this, what this blocks, and why.
371
+ 11. **Write SYNOPSIS last** - it is a compression of PROBLEM + the core approach.
372
+ 12. **Bold and italicize the save points** in the allowed locations. Bold phrases that name the work (components, mechanisms, behaviors). Italicize phrases that orient to significance (conclusions, payoffs), at most one per paragraph. Apply the scan check.
373
+ 13. **Add NOTES** for non-goals, declined alternatives, and scope limits. Use `## NOTES` header. Omit if empty.
374
+ 14. **Suggest Linear labels** when the ticket is being filed in Linear. Use obvious stable labels only; do not guess initiative labels.
375
+ 15. **Verify links**: check repo file links, external docs, tickets, PRs, and design artifacts where tooling allows. Do not claim an unverified link was verified.
376
+ 16. **Verify prose style**: check each rule in Prose style. Cut design-narration, authoring-sequence, and buzzwordy abstractions. Verify plain ASCII. Verify prescriptive voice (not descriptive). Verify first-use definitions for subsystem jargon in BACKGROUND or PROBLEM. Verify plain English nouns (no code-domain nouns in prose). Verify conclusion-first in PROBLEM. Verify NOTES bullets are either short pointers or self-contained.
377
+ 17. **Verify length**: if the body exceeds what the work's conceptual size warrants, cut until each line earns its place. If PROPOSED APPROACH dominates, consider splitting into a design doc plus ticket.
378
+
379
+ ## Anti-patterns
380
+
381
+ - **Burying the harm.** If PROBLEM is a paragraph of setup before the actual problem, rewrite. Problem first.
382
+ - **Implementation plan as acceptance criteria.** "Restructure ActiveKey to use a publish helper" is an implementation step, not an observable behavior. Replace with "no DB or KMS I/O occurs while km.mu is held."
383
+ - **Bolding expository prose without distinction.** Bolds in PROBLEM, BACKGROUND, or PROPOSED APPROACH are fine when they name the work (components, mechanisms). They are noise when they bold filler or context. Bold the change; italicize the significance; leave the scaffolding plain.
384
+ - **Over-italicizing.** More than one italicized phrase per paragraph dilutes the signal. Pick the one phrase that carries the paragraph's point.
385
+ - **Design-doc ticket.** If PROPOSED APPROACH is longer than the rest of the ticket combined, the ticket is a design doc wearing a ticket costume. Split it: design doc in `docs/`, ticket that references it.
386
+ - **Missing hazard rating.** PROBLEM without a "how likely is this" paragraph leaves the lead guessing at priority. Always rate the risk, even if it is "no current defect; preventive."
387
+ - **Descriptive voice.** "This PR moves the lock outside the I/O" is a PR description. "The fix should load key material outside the mutex" is a ticket. Use prescriptive voice.
388
+ - **Vague acceptance criteria.** "Improve the locking situation" is untestable. Name the observable behavior the implementer and reviewer can check.
389
+ - **Dependencies without the why.** "Blocked by PLAT-124" is the tracker field. The ticket should explain why: "PLAT-125 groups by location_fingerprint, so it can only run after backfill completes." Without the why, a lead may reorder the queue and break a hard data dependency.
390
+ - **Bare `Notes:` line.** Use `## NOTES` header or omit the section entirely.
391
+ - **Loaded language.** Using avoidable loaded terms when neutral terms exist. Prefer allowlist/blocklist, primary/replica, and precise role names.
392
+ - **Smart punctuation.** Em dashes, smart quotes, ellipsis glyphs. Use plain ASCII.
393
+ - **Splitting paragraphs across lines.** Many trackers render each line break as a visible break. One paragraph = one physical line.
394
+ - **Ungrounded jargon.** Subsystem-specific terms used in SYNOPSIS or PROBLEM but never defined in BACKGROUND. The implementer reads "frozen matview reverification" and hits a wall before they start. Define on first use in BACKGROUND (or PROBLEM if no BACKGROUND).
395
+ - **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".
396
+ - **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".
397
+ - **Conclusion buried at the end.** PROBLEM 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.
398
+ - **Hazard rating without motivation.** "No incident has hit this shape" answers "how bad?" but not "why prioritize this?" Answer "why now?" so the lead can decide whether to schedule it.
399
+ - **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.
package/bin/thatch CHANGED
@@ -28,7 +28,7 @@ function usage(): never {
28
28
  thatch prime Prime project memory (runs the primer skill via opencode, cursor, or claude)
29
29
  thatch buffer-batch Append a PostToolBatch payload to the queue (Claude Code hook only)
30
30
  thatch buffer-tool Append a single postToolUse interaction to the queue (Cursor hook only)
31
- thatch flush-tools [--json] Drain the queue + print an extraction nudge (hook only)
31
+ thatch flush-tools [--json] Peek the queue + print an extraction nudge (hook only)
32
32
  thatch setup --claude [--cursor] [--global]
33
33
  Install config + instructions + hooks + skills
34
34
 
@@ -30,7 +30,8 @@ Shared core
30
30
 
31
31
  OpenCode plugin path
32
32
  ├── index.ts → plugin entry: wires DB/model/extraction, registers tools + hooks
33
- └── tools.ts → thin opencode tool() wrappers over tool-defs
33
+ ├── tools.ts → thin opencode tool() wrappers over tool-defs
34
+ └── extraction.ts → in-memory ring buffer + shared payload builders
34
35
 
35
36
  MCP server path
36
37
  ├── mcp.ts → stdio JSON-RPC server: z.toJSONSchema() for tools/list,
@@ -38,6 +39,7 @@ MCP server path
38
39
  │ opens sideband socket for warm-model match queries
39
40
  ├── sideband.ts → Unix socket: SidebandServer (embed + search via warm model)
40
41
  │ and sidebandMatch (thin client for hook processes)
42
+ ├── extract-queue.ts → file-backed JSONL queue (Claude Code + Cursor hooks)
41
43
  └── setup.ts → `thatch setup --claude` / `--cursor`: writes .mcp.json,
42
44
  CLAUDE.md / AGENTS.md, settings/hooks JSON, installs skills
43
45
 
@@ -58,10 +60,11 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
58
60
  | `git.ts` | Parse `owner/repo` from git remote. Worktree-safe fallback chain. |
59
61
  | `db.ts` | SQLite schema, CRUD for entries/stores, brute-force cosine search (`search` = pure scoring, `recall` = search + telemetry stamping), dedup-pair verdict tracking. |
60
62
  | `embeddings.ts` | Lazy-load the embedding model. Expose `queryEmbed`/`passageEmbed` and the model `name` (stored as an informational tag). `MockEmbeddingModel` for tests. |
61
- | `extraction.ts` | Buffers non-thatch tool interactions per session and serializes them into the JSON payload the extraction nudge carries. (opencode-only — no MCP equivalent.) |
63
+ | `extraction.ts` | Per-session in-memory ring buffer (cap 20) that buffers non-thatch tool interactions and serializes them into the JSON payload the extraction nudge carries. The in-memory pipeline is opencode-only, but the payload builders (`buildExtractionPayload`, `deriveTitle`) are shared by both paths — `extract-queue.ts` imports `deriveTitle`, `bin/thatch` imports `buildExtractionPayload`. `summarizeArgs` is used internally by `buildExtractionPayload`. |
64
+ | `extract-queue.ts` | File-backed per-session JSONL queue (caps 20, oldest dropped). Shared by the Claude Code and Cursor hook paths, which fire one-shot per event with no cross-call state. This is the MCP-side equivalent of `extraction.ts`. |
62
65
  | `sideband.ts` | Unix domain socket server + client. The MCP server (long-lived, warm model) runs `SidebandServer` so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Socket path is a hash of the DB path — both processes compute it independently. |
63
66
  | `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. |
64
- | `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (10 skills: fact-extractor, dedup-classifier, project-primer, 5 review specialists, review synthesizer, session reflection — work on both opencode and Claude Code) and `OPENCODE_ONLY_SKILLS` (1 skill: code-review coordinator — requires sub-agent support, not installed for Claude Code). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
67
+ | `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (18 skills: fact-extractor, dedup-classifier, project-primer, 6 review specialists, review synthesizer, review context, workflow research, change walkthrough, code walkthrough, session reflection, pr-description, ticket-description, split-overlarge-pr — work on both opencode and Claude Code) and `OPENCODE_ONLY_SKILLS` (1 skill: code-review coordinator — requires sub-agent support, not installed for Claude Code). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
65
68
 
66
69
  ## Plugin hooks
67
70
 
@@ -72,8 +75,8 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
72
75
  | `experimental.chat.system.transform` | Appends the thatch system prompt (store names, usage rules). |
73
76
  | `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
74
77
  | `experimental.compaction.autocontinue` | Clears the compacting flag so `chat.message` nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. |
75
- | `tool.execute.after` | Buffers every non-`thatch_*` tool call into the session's extraction buffer. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
76
- | `chat.message` | Two priority tiers: (a) if extraction buffer has interactions, flushes them as a synthetic text part carrying the extraction nudge + JSON payload; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
78
+ | `tool.execute.after` | Buffers every non-`thatch_*`, non-`skill`, non-`task` tool call into the session's extraction buffer. (Skill/task are excluded — buffering them creates a feedback loop where the nudge triggers a skill load, which gets buffered, which triggers another nudge.) Memory writes (`thatch_memory_remember`) and `thatch_extraction_done` drain the buffer and reset the missed-nudge counter. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
79
+ | `chat.message` | Two priority tiers: (a) if extraction buffer has interactions, **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge + JSON payload — the buffer persists until the agent writes a memory or calls `thatch_extraction_done`, so ignored nudges repeat and escalate (polite → insistent → ALL-CAPS) via the `missedNudges` counter; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
77
80
  | `event` (`session.created`) | Sends the session-start reminder via `client.session.prompt`, carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. The session id is at `event.properties.info.id`. |
78
81
  | `dispose` | Closes the DB. |
79
82
 
@@ -108,6 +111,16 @@ Two of these hooks were dead for weeks because failures were invisible.
108
111
  wraps them in `tool()` with a `thatch_` prefix; the MCP server wraps them
109
112
  in `z.object()` for validation and `z.toJSONSchema()` for the protocol.
110
113
  Adding a tool means adding one entry to `TOOL_DEFS`.
114
+ 9. **Proactive save is prompt-instructed, not hook-driven.** All three system
115
+ prompts include a "Before Responding" section instructing the agent to
116
+ check for durable knowledge (via `thatch_memory_recall` for dedup, then
117
+ `thatch_memory_remember`) before composing a final response after
118
+ substantial work. No plugin hook fires between generation and response
119
+ delivery, so the only viable path is prompt instruction. The existing
120
+ `chat.message` extraction nudge stays as a fallback for the false-negative
121
+ case (agent forgot to save, next user turn arrives with buffer still
122
+ pending). This is an experiment — model reliability on meta-instructions
123
+ is uncertain.
111
124
 
112
125
  ## Data flow
113
126
 
@@ -139,8 +152,14 @@ dedup cycle (agent-driven)
139
152
  (overwriting or forgetting an entry clears its verdicts → can re-flag)
140
153
 
141
154
  extraction cycle (agent-driven)
142
- → tool.execute.after buffers non-thatch tool calls per session (max 20)
143
- → next chat.message flushes the buffer into a nudge part with JSON payload
155
+ → tool.execute.after buffers non-thatch, non-skill, non-task tool calls
156
+ per session (max 20)
157
+ → next chat.message peeks the buffer (does NOT drain it) and injects a
158
+ nudge part with JSON payload; missed nudges escalate (polite → insistent
159
+ → ALL-CAPS) via the missedNudges counter
160
+ → drain: thatch_memory_remember in the session (or a child sub-agent via
161
+ the childToParent Map) OR thatch_extraction_done — both clear the buffer
162
+ and reset the missed-nudge counter
144
163
  → agent loads thatch-fact-extractor skill, saves facts via thatch_memory_remember
145
164
 
146
165
  prompt-aware recall nudge (both paths)
@@ -167,11 +186,14 @@ hygiene heartbeat (session start)
167
186
  (default `~/.config/thatch/thatch.db`), WAL mode, 5s busy timeout.
168
187
  - Tables: `stores(name PK)`,
169
188
  `entries(slug, store, label, content, embedding BLOB, model, branch,
170
- confidence, created_at, updated_at, recall_count, last_recalled_at,
171
- PK(slug, store))`,
189
+ confidence, archived, created_at, updated_at, recall_count,
190
+ last_recalled_at, PK(slug, store))`,
172
191
  `dedup_pairs(store, slug_a, slug_b, status, checked_at, PK(store, slug_a, slug_b))`.
173
- - `recall_count`/`last_recalled_at` are added to pre-existing databases by an
174
- idempotent column migration at init (`PRAGMA table_info` + `ALTER TABLE`).
192
+ - `recall_count`, `last_recalled_at`, and `archived` are added to pre-existing
193
+ databases by an idempotent column migration at init (`PRAGMA table_info` +
194
+ `ALTER TABLE`). The `archived` column is `INTEGER NOT NULL DEFAULT 0` (0 =
195
+ live, 1 = archived); search, dedup, and staleness queries all exclude
196
+ archived entries by default.
175
197
  - Embeddings are raw Float32Array bytes. Serialization honors
176
198
  `byteOffset`/`byteLength` — transformers.js can return views into larger
177
199
  tensor buffers, and serializing the whole backing buffer corrupts vectors.
@@ -194,7 +216,7 @@ hygiene heartbeat (session start)
194
216
  bun install # deps
195
217
  bun test # full suite; no network, no real config dirs
196
218
  mise run check # typecheck + bun test + markdownlint (the CI gate)
197
- opencode # self-hosts via .opencode/plugins/thatch.ts
219
+ opencode # self-host via opencode.json plugin path (see root README)
198
220
  ```
199
221
 
200
222
  The markdownlint gate lints `README.md` and `docs/` (excluding the historical
@@ -227,9 +249,11 @@ authenticate through GitHub's OIDC exchange instead. Prerequisites:
227
249
  - A Trusted Publisher configured on npmjs.com for `@jeffober/thatch` pointing at
228
250
  the `sysread/thatch` repo and this workflow file.
229
251
  - npm >= 11.5.1 (the OIDC exchange needs it). `publish.yml` installs
230
- `setup-node@v4` (node 24) and `npm install -g npm@latest`; it deliberately
231
- sets **no `registry-url`**, which would write a token-expecting `.npmrc` that
232
- preempts the OIDC exchange.
233
-
234
- CI (`.github/workflows/ci.yml`) runs `bun test` on every push/PR to `main` —
235
- the never-merge-broken guard before a release.
252
+ `setup-node@v4` (node 24) and `npm install -g npm@11` — npm 12 shipped a
253
+ sigstore bug that broke OIDC publishing; the pin prevents it. The workflow
254
+ deliberately sets **no `registry-url`**, which would write a
255
+ token-expecting `.npmrc` that preempts the OIDC exchange.
256
+
257
+ CI (`.github/workflows/ci.yml`) runs `tsc` (typecheck), `bun test`, and
258
+ `markdownlint-cli2` on every push/PR to `main` — the never-merge-broken
259
+ guard before a release.
@@ -35,6 +35,15 @@ here first. These are the things that have already cost time.
35
35
  - **`overwrite: true` clears stale dedup verdicts** for that slug. Forgetting an
36
36
  entry clears all verdicts involving it. Both make a pair eligible for
37
37
  re-reporting by `find_duplicates`. This is intended.
38
+ - **Archived memories are excluded by default.** `search`, `findDuplicates`,
39
+ and `staleEntryCount` all filter `WHERE archived = 0`. To search archived
40
+ memories, pass `includeArchived: true` to `thatch_memory_recall`. To archive
41
+ a memory, write it with `archived: true`; to unarchive, `archived: false`.
42
+ - **Updating an archived memory requires explicit `archived` param.** If the
43
+ entry is already archived and a `remember` call omits the `archived` param,
44
+ the tool returns an error (`db.ts:211`). Pass `archived: true` to keep it
45
+ archived or `archived: false` to unarchive. This guard prevents accidental
46
+ unarchival via an unrelated content update.
38
47
 
39
48
  ## Hooks (opencode)
40
49
 
@@ -42,11 +51,29 @@ here first. These are the things that have already cost time.
42
51
  the hook object returned by `server`. Moving it into the `event` handler
43
52
  (where `session.created` lives) silently never fires it — the event bus has
44
53
  no such event. This was dead for weeks because the failure was invisible.
54
+ - **`tool.execute.after` excludes `skill` and `task` tools**, not just
55
+ `thatch_*`. Buffering them creates a feedback loop: the nudge triggers a
56
+ skill load, which gets buffered, which triggers another nudge on the next
57
+ turn.
45
58
  - **Hook failures are logged with a `[thatch]` prefix and never swallowed.** Two
46
59
  hooks were dead for weeks before failures were made visible. If you add a
47
60
  hook, log on failure.
48
61
  - **`chat.message` has two priority tiers**: extraction nudge first (returns
49
62
  early), then the prompt-aware recall nudge. Don't run both in one turn.
63
+ - **The extraction nudge peeks, never flushes.** The buffer is NOT drained
64
+ on nudge delivery (`index.ts:169` — `peek()` call). It persists until the
65
+ agent writes a memory or calls `thatch_extraction_done`. Ignored nudges
66
+ accumulate; the `missedNudges` counter escalates the tone (polite at 0-1
67
+ misses, insistent at 2, ALL-CAPS at 3+). The counter resets when the
68
+ buffer drains.
69
+ - **A child sub-agent's `thatch_memory_remember` drains the parent's buffer**
70
+ via the `childToParent` Map (`index.ts:65` declaration, `index.ts:131`
71
+ lookup). Without this, dispatching the fact-extractor as a background task
72
+ would write memories in the child but never clear the parent's queue — the
73
+ nudge would replay every turn. `thatch_extraction_done` is the
74
+ belt-and-suspenders explicit acknowledgment: it drains the buffer without
75
+ requiring a memory write (covers cases where the sub-agent errors out or the
76
+ host doesn't expose parent-child session relationships).
50
77
 
51
78
  ## Hooks (Claude Code / Cursor)
52
79