thurview 0.8.2 → 0.9.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thurview",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "Guided, evidence-anchored reviews of agent-written code. A coding agent authors the review; you read, ask, comment and decide in the browser.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,276 @@
1
+ ---
2
+ name: forge-review
3
+ description: Review a pull or merge request and post the review back to the forge - inline comments anchored to lines, a summary, a verdict, and a re-review that answers the previous pass point by point. Use when the user asks to review a PR or MR and post the comments, to re-review after a push, to approve or request changes on a pull or merge request, or invokes /forge-review. Built on thurview for the evidence, with the forge as the destination.
4
+ user-invocable: true
5
+ argument-hint: "[<number|url>] [--forge github|gitlab]"
6
+ ---
7
+
8
+ # forge-review
9
+
10
+ thurview authors the evidence; the forge is where the review lands. This skill
11
+ is the bridge between them, and it is the only one that posts.
12
+
13
+ ```mermaid
14
+ flowchart LR
15
+ A[forge status: what CI really did] --> B[forge prior: the previous pass]
16
+ B --> C[scaffold + graph: the evidence]
17
+ C --> D[publish: the reader approves the review before it is posted]
18
+ D --> E[forge submit: comments, summary, verdict]
19
+ E --> F[forge reply --resolve: only what is verified]
20
+ F -->|author pushes| A
21
+ ```
22
+
23
+ Run the CLI as `thurview`, or `npx -y thurview` when it is not on PATH. Every
24
+ command prints TOON on stdout, errors are structured on stdout too, exit code
25
+ 2 is a usage error. If a command answers `unknown command` or `unknown flag`
26
+ for something below, the installed CLI is older than this skill: run
27
+ `thurview update` and retry once.
28
+
29
+ ## Request
30
+
31
+ $ARGUMENTS
32
+
33
+ A number or URL names the change request. Empty means the change request open
34
+ from the current branch - `thurview forge status` with no `--change` finds it
35
+ through the active review's binding, and when there is none, ask which one
36
+ rather than guessing.
37
+
38
+ ## The word
39
+
40
+ GitHub calls it a pull request, GitLab a merge request. Everything here calls
41
+ it a **change request** and the CLI takes `--change <number|url>` on either
42
+ forge. Use the forge's own word only when you are writing to the author on
43
+ that forge.
44
+
45
+ Forge coverage, and what a third forge would need, is in
46
+ [Forges](references/forges.md). Read it when a call fails or the forge is not
47
+ github.com.
48
+
49
+ ## What this never does
50
+
51
+ Never merge, never close, never push to the branch under review. You usually
52
+ cannot push to a fork at all, so **every finding is a comment**. Merging is
53
+ the maintainer's decision and the CLI has no command for it.
54
+
55
+ ## Before you start
56
+
57
+ Read `~/.agent-rules/VOICE.md` **on every run**. Every comment, reply and
58
+ summary is published on the operator's account, and that file is the spec for
59
+ how they read - the sign-off line and its exact wording, the length budget per
60
+ comment, severity prefixes, and when to ask instead of assert. It changes; do
61
+ not work from memory of it, and do not copy it in here.
62
+
63
+ Two consequences of it shape everything below, so they are worth saying once:
64
+ a comment is a few lines and no more, and it ends with the sign-off as a plain
65
+ last line. A finding that will not fit is **two comments**, or one comment
66
+ carrying the claim and one suggestion with the evidence behind a permalink -
67
+ never a wall of prose on a line of someone's diff.
68
+
69
+ ## Workflow
70
+
71
+ ### 1. Ask the forge what it knows, before forming any opinion
72
+
73
+ ```sh
74
+ thurview forge status --change <ref>
75
+ ```
76
+
77
+ Record `change.head`. That commit is what this pass reviews, and a later pass
78
+ diffs against it to find what moved. Note `change.fromFork` and
79
+ `change.author`: a change request from outside the organisation is the case
80
+ every rule here was learned on.
81
+
82
+ ### 2. Establish what CI actually ran
83
+
84
+ `ci.verdict` is one sentence you can quote. `ci.trustworthy` is the only field
85
+ that means "the tests really passed here".
86
+
87
+ Two failures this answers, both of which shipped real bugs:
88
+
89
+ - **A fork change request has almost no CI.** `ci.baselineRan` is what the
90
+ target branch's own tip runs. One check here against twenty there means the
91
+ pipeline is not a gate on this change - the review is.
92
+ - **A green tick can mean "never ran".** `passed`, `failed`, `cancelled`,
93
+ `skipped` and `running` are counted separately because a cancelled job
94
+ asserted nothing while showing no failure. A title-gate failure that
95
+ cancels the test matrix leaves exactly that shape.
96
+
97
+ When `ci.trustworthy` is false, **say so in the summary comment in your own
98
+ words, with the counts**. An unstated gap is one the maintainer will assume
99
+ you checked.
100
+
101
+ ### 3. Read the previous pass back, point by point
102
+
103
+ ```sh
104
+ thurview forge prior --change <ref> # add --mine for this account's threads
105
+ ```
106
+
107
+ `summary.passes` of 0 means this is a first pass; skip to step 4.
108
+
109
+ Otherwise this is a **re-review, and answering the prior pass is the most
110
+ important thing you will do here**. A point raised and then silently dropped
111
+ teaches the author that review is noise.
112
+
113
+ Go through every open thread and give it exactly one of three words:
114
+
115
+ | Word | What you do |
116
+ | ------------------- | --------------------------------------------------------------- |
117
+ | addressed | Verify it at the current head, then reply and resolve (step 8) |
118
+ | partially addressed | Reply saying which part is done and which is not; leave it open |
119
+ | untouched | Reply asking for it again, or say why you are dropping it |
120
+
121
+ `threads[].atHead` is false when the thread was written against an older
122
+ commit, and `outdated` when the code under it moved. Neither means the point
123
+ was fixed - only reading the code at the current head means that.
124
+
125
+ ### 4. Diff only what moved
126
+
127
+ On a re-review, read the new work rather than the whole change again:
128
+
129
+ ```sh
130
+ git range-diff <base>...<previous head> <base>...<current head>
131
+ ```
132
+
133
+ The previous head is the one you recorded last pass, or `review.pinnedHead`
134
+ from `forge status`. Read the full diff only on a first pass.
135
+
136
+ ### 5. Get the evidence from thurview
137
+
138
+ ```sh
139
+ thurview scaffold --pr <ref> # pins base and head from the forge
140
+ thurview graph interfaces --review <id> # what the change moved in the visible surface
141
+ thurview graph impact --review <id> # what it reaches that the diff does not show
142
+ thurview graph callers <symbol> --review <id>
143
+ thurview graph tests-for <symbol> --review <id>
144
+ ```
145
+
146
+ The thurview skill owns these in full - `thurview skill` prints the path to
147
+ its SKILL.md, and its references sit beside it. Read them when you author the
148
+ document in step 6; do not re-derive structure from hunks.
149
+
150
+ Read every line you are about to comment on **at the pinned head**, with
151
+ `git show <head>:<path>`, never from the working tree.
152
+
153
+ ### 6. Decide who reads the review before the forge does
154
+
155
+ Default: **a human approves the pass before it is posted.** Author the
156
+ thurview document as the thurview skill describes, publish it, and wait:
157
+
158
+ ```sh
159
+ thurview publish --review <id> --open
160
+ thurview wait --review <id> --timeout <seconds>
161
+ ```
162
+
163
+ The reader sees every finding against its anchored code and approves or sends
164
+ it back; `wait.reason` of `accepted` is your signal to post. This is the whole
165
+ reason to go through thurview rather than straight to `gh`: comments land on
166
+ the operator's account, on a contributor's work, and are read as the
167
+ maintainer's word.
168
+
169
+ Post without that gate only when the user asked for an unattended run. Say in
170
+ the handover which of the two happened.
171
+
172
+ **Never put the thurview URL in a forge comment.** That server is local to
173
+ this machine; the author cannot open it, and the link leaks a path. Evidence
174
+ that must travel goes in a permalink - `status.permalink` shows the shape for
175
+ this forge, with the pinned head already in it.
176
+
177
+ ### 7. Write the pass
178
+
179
+ One JSON file, which is also what a human can read before it is posted:
180
+
181
+ ```json
182
+ {
183
+ "verdict": "request-changes",
184
+ "body": "<the summary, in VOICE.md's shape, ending with the sign-off>",
185
+ "comments": [
186
+ {
187
+ "path": "src/clip.rs",
188
+ "line": 44,
189
+ "startLine": 40,
190
+ "side": "head",
191
+ "body": "<one point, ending with the sign-off>"
192
+ }
193
+ ]
194
+ }
195
+ ```
196
+
197
+ `line` is the LAST line of the range and `startLine` the first. `side` is
198
+ `head` unless you are commenting on a line the change deleted. Both forges
199
+ refuse a comment on a line the diff does not touch, so anchor inside a hunk.
200
+
201
+ Per comment: one point, stated as a problem then a concrete suggestion,
202
+ within VOICE.md's length budget, sign-off last. **A finding you cannot
203
+ reproduce is a question, not an assertion** - give the mechanism and the
204
+ evidence, say what you could not reproduce, and ask the author to confirm.
205
+ That is the correct form, not a weaker one.
206
+
207
+ The summary body carries what has no line: what CI did and did not establish
208
+ (step 2), the security result (step 5 of
209
+ [Security surfaces](references/security-surfaces.md)), and who does what next.
210
+
211
+ Check it before it goes anywhere:
212
+
213
+ ```sh
214
+ thurview forge submit --change <ref> --file pass.json --dry-run
215
+ ```
216
+
217
+ `warnings` names every comment past the line budget. Split those, do not
218
+ shorten by deleting the suggestion.
219
+
220
+ ### 8. Submit
221
+
222
+ ```sh
223
+ thurview forge submit --change <ref> --file pass.json
224
+ ```
225
+
226
+ `verdict` is `comment`, `request-changes` or `approve`.
227
+
228
+ **Approving is a state change with consequences, and you say them out loud
229
+ before you do it.** It dismisses a standing request for changes, which is what
230
+ makes the change request mergeable, and where auto-merge is armed it merges
231
+ the code with no further human read. The CLI refuses an approve without
232
+ `--confirm` for that reason; the flag is not a formality, it is the point at
233
+ which you have told the user.
234
+
235
+ Record `submitted.head`. That is the commit the next pass diffs against.
236
+
237
+ ### 9. Resolve only what you verified
238
+
239
+ ```sh
240
+ thurview forge reply <threadId> --change <ref> --body "<answer>" --resolve --at <head>
241
+ ```
242
+
243
+ `--at` must be the current head, and the CLI refuses any other. Resolving a
244
+ thread tells the author a point was accepted; doing it without reading the
245
+ code at that head is worse than leaving it open. A thread deferred by
246
+ agreement may be resolved only when the agreement is written in the thread.
247
+
248
+ Reply without `--resolve` for a point that is partially addressed.
249
+
250
+ ### 10. Hand over
251
+
252
+ Tell the user, in a few lines:
253
+
254
+ - the change request, its head, and the verdict you posted
255
+ - what CI established, in one clause, when `ci.trustworthy` was false
256
+ - how many comments, and how many prior threads you resolved
257
+ - whether a human approved the pass first, or it was unattended
258
+ - what you are waiting for now - the author's push, or the maintainer's merge
259
+
260
+ ## Re-review after a push
261
+
262
+ Start at step 1 again. The head will have moved; `forge status` says so and
263
+ `review.pinnedHead` holds what you reviewed last. Re-pin with
264
+ `thurview scaffold --update --review <id>`, range-diff from the old head, and
265
+ answer the prior pass before reading anything new.
266
+
267
+ ## Completion criteria
268
+
269
+ Report completion only when all of these hold:
270
+
271
+ - `forge status` was read and its CI verdict is reflected in the summary.
272
+ - Every prior thread is marked addressed, partially addressed or untouched.
273
+ - Security is stated explicitly, findings or none.
274
+ - The pass is posted, with a verdict, and its head is recorded.
275
+ - Every thread you resolved was verified at the current head.
276
+ - Nothing was merged, closed or pushed.
@@ -0,0 +1,58 @@
1
+ # Forges
2
+
3
+ `thurview forge` speaks to GitHub through `gh` and to GitLab through `glab`.
4
+ Both are driven through `gh api` and `glab api` rather than their porcelain,
5
+ because the REST and GraphQL payloads are a contract and the porcelain is not.
6
+
7
+ Which adapter owns a host is decided by name for `github.com` and
8
+ `gitlab.com`, and read off the machine for everything else - a host is owned
9
+ when `gh auth status --hostname <host>` or `glab auth status --hostname
10
+ <host>` succeeds, which is where the answer for a self-hosted instance already
11
+ lives. `GH_HOST` and `GITLAB_HOST` are honoured. A host no adapter claims is
12
+ **refused**, not guessed at; pass `--forge github|gitlab` to name it.
13
+
14
+ ## What differs, and what the CLI does about it
15
+
16
+ | Behaviour | GitHub | GitLab |
17
+ | ---------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------- |
18
+ | The noun | pull request | merge request |
19
+ | A pass | one atomic review | N discussions plus one note; no atomic form exists |
20
+ | Requesting changes | `CHANGES_REQUESTED`, which blocks a merge | no such state; the account's approval is removed and the summary says it |
21
+ | A comment's anchor | a line range, `start_line` to `line` | one line; a range is anchored at its last line and `notes` says so |
22
+ | Thread identity | a GraphQL node id | a discussion id |
23
+ | Resolving | `resolveReviewThread` | `PUT .../discussions/<id>?resolved=true` |
24
+ | Staleness of a thread | `isOutdated` from the forge | not reported; compare `threads[].atHead` against the current head instead |
25
+ | CI | check runs plus commit statuses | the latest pipeline's jobs, read from the pipeline's own project so a fork's jobs are found |
26
+ | Fetching a head | `refs/pull/<n>/head` | `refs/merge-requests/<n>/head` |
27
+ | A line range permalink | `#L10-L20` | `#L10-20` |
28
+
29
+ Consequences worth knowing before you post:
30
+
31
+ - On GitLab a pass that fails half way through has already posted the
32
+ comments it got to. The inline comments go first and the summary last, so a
33
+ partial pass is still one the author can read, but check `submitted.comments`
34
+ against what you sent.
35
+ - `request-changes` on GitLab does not block a merge. If blocking matters,
36
+ say so in the summary and leave it to the maintainer.
37
+ - A multi-line finding on GitLab loses its range. Put the range in the
38
+ comment's own permalink.
39
+
40
+ ## What has been exercised
41
+
42
+ The GitHub adapter is driven by tests and against github.com. The GitLab
43
+ adapter is driven by the same tests - the calls it makes and the answers it
44
+ parses are asserted - but has not been run against a live GitLab instance. If
45
+ you are the first to point it at one, expect the friction to be in `glab api`'s
46
+ own flags rather than in the endpoints, and report what differed.
47
+
48
+ ## Adding a forge
49
+
50
+ Implement `Forge` in `src/forge/types.ts` and register it in `BUILTIN` in
51
+ `src/forge/index.ts`. The interface is the whole contract - `get`, `fetchRef`,
52
+ `checks`, `baseline`, `prior`, `submit`, `reply`, `permalink`, plus `owns` and
53
+ `whoami`. There is deliberately no `merge`, `close` or `push`.
54
+
55
+ Then drive it from `test/forge.test.ts`. The tests put a fake CLI on PATH
56
+ under the adapter's own binary name and answer from a fixture table, so an
57
+ adapter is proved by the calls it makes and the answers it parses rather than
58
+ by being asserted to exist.
@@ -0,0 +1,54 @@
1
+ # Security surfaces
2
+
3
+ A generic security pass produces generic findings. The checklist has to come
4
+ from what the change actually touches, so derive it from the diff and say what
5
+ you derived it from.
6
+
7
+ ## How to derive it
8
+
9
+ 1. List the surfaces the diff crosses. A surface is a place where the change
10
+ meets something it does not control - input it did not produce, a file
11
+ system, another process, a network peer, a platform API, a log.
12
+ 2. For each surface, take its questions from the table below.
13
+ 3. Ask each question against the code at the pinned head, not against the
14
+ hunk. A missing cleanup path is invisible in a diff that only adds lines.
15
+ 4. Anchor every finding to the line that answers it.
16
+ 5. **State the result explicitly, findings or none.** "No security findings"
17
+ is a result; an absent section is not one, and the maintainer cannot tell
18
+ the two apart.
19
+
20
+ ## Surfaces and their questions
21
+
22
+ | The change touches | Ask |
23
+ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | Text that reaches a model | Prompt injection - whose text is it, what can it make the agent do, what is quoted versus interpreted |
25
+ | Temporary files | Predictable name, permissions at creation, symlink races, TOCTOU between check and use, cleanup on every error path as well as the happy one |
26
+ | Attacker-controlled names | Path traversal, absolute paths, shell metacharacters, leading dashes read as flags, Unicode that normalises to something else |
27
+ | Reading untrusted bytes | Unbounded reads, allocation from a length the input chose, decompression ratios, what happens at the size limit rather than below it |
28
+ | Running another process | Argument construction, quoting per platform, whether a shell is involved at all, what the environment carries, working directory |
29
+ | Process or PTY lifetime | Orphans on error, exit status that can be forged or lost, signals, what happens when the child outlives the parent |
30
+ | Logs and telemetry | What reaches a log that should not - secrets, tokens, file contents, personal data - and whether log lines can be forged by input |
31
+ | Platform branches | Each branch checked separately; a guard that holds on Linux and not on Windows is the common shape |
32
+ | Authentication or identity | What is trusted, what is verified, what a caller can assert about itself |
33
+ | Serialised data | Deserialisation of attacker-controlled shapes, schema validation before use, defaults that silently accept |
34
+
35
+ ## Two worked examples
36
+
37
+ **A clipboard copy through a temporary file.** Surfaces: text that reaches a
38
+ model, temporary files, attacker-controlled names, unbounded reads, running
39
+ another process, platform branches, logs. Which yields, concretely - is the
40
+ temp file name predictable; what mode is it created with; is there a window
41
+ between creating and writing it; is it removed when the copy fails and not
42
+ only when it succeeds; can the copied text be read back by another user; does
43
+ the platform helper get its argument as an argument or through a shell; does
44
+ the content reach a log.
45
+
46
+ **A window lifecycle change that spawns a process.** Surfaces: running another
47
+ process, process lifetime, attacker-controlled names, platform branches.
48
+ Which yields - how the command line is built and escaped on each platform;
49
+ whether a window name is interpolated into it; whether the child is reaped;
50
+ whether its exit status can be forged by something the child does not control;
51
+ what the code does when the platform branch it was not written for runs.
52
+
53
+ The pattern in both: the surfaces come from the diff, the questions come from
54
+ the surfaces, and the answers come from the code at head.
@@ -19,6 +19,10 @@ request asks for.
19
19
  states what it did not examine. Read
20
20
  [Code explainer](references/code-explainer.md) and follow that instead.
21
21
 
22
+ Posting a review back to a forge - inline comments on a pull or merge request,
23
+ a verdict, a re-review that answers the previous one - is the `forge-review`
24
+ skill, not this one. This skill's document is read in the browser.
25
+
22
26
  The agent studies the change and writes a short document in which every claim
23
27
  about code is anchored to an exact file and line range at a pinned commit.
24
28
  thurview validates those anchors, seals a revision, and serves it in the
@@ -86,7 +90,7 @@ request:
86
90
 
87
91
  ```sh
88
92
  thurview scaffold # current branch vs its trunk fork point
89
- thurview scaffold --pr 123 # pull request (needs gh)
93
+ thurview scaffold --pr 123 # pull or merge request (needs gh or glab)
90
94
  thurview scaffold --base <ref> --head <ref>
91
95
  ```
92
96
 
@@ -249,7 +253,9 @@ Tell the user, in a few lines and nothing more:
249
253
  - which theme source you used: the user's request, the project's design
250
254
  system (name the files), or the default skin
251
255
  - when the review has no map, why not, in one clause
252
- - that you are now waiting for their questions and their decision
256
+ - that you are now waiting for their questions and their decision, and that
257
+ a question asked after you stop waiting is queued rather than lost - the
258
+ page tells them which of the two it is
253
259
 
254
260
  The page explains its own controls; do not describe them.
255
261
 
@@ -265,9 +271,16 @@ nothing: keep `--timeout` under that limit and run `wait` again on `timeout`.
265
271
  When the tool can run a command in the background and wake you when it exits,
266
272
  run `wait` that way, so the user has the terminal back while they read.
267
273
 
274
+ While `wait` runs the reader's page says an agent is listening, and says the
275
+ opposite within seconds of it returning. Do not loop it to look present: a
276
+ question asked with nobody waiting is queued, not lost, and `thurview`
277
+ reports it as `needsAgent` the next time you run any command in the
278
+ worktree.
279
+
268
280
  `wait.reason` says what happened, with the threads that need you:
269
281
 
270
- - `question`: an "Ask now" thread. Answer each thread in `threads` with
282
+ - `question`: a thread the reader sent to you. Answer each thread in
283
+ `threads` with
271
284
  `thurview threads reply <threadId> --review <id> --body "<answer>"`. Do
272
285
  not change the document for a question. Wait again.
273
286
  - `awaiting-agent-updates`: the reader submitted with "Request changes".
@@ -27,7 +27,7 @@ warns when the branch moved past them.
27
27
  | `accepted` | Terminal. Cannot be republished. |
28
28
  | `closed` | Terminal. Ended without approval. Cannot be republished. |
29
29
 
30
- "Ask now" does not change the status. "Submit review" with "Request changes"
30
+ Asking the agent a question does not change the status. "Submit review" with "Request changes"
31
31
  sets `awaiting-agent-updates`; with "Approve" sets `accepted`; with "Close"
32
32
  sets `closed`.
33
33
 
@@ -36,21 +36,62 @@ Dismissal is separate: the reader removes the review from the active list and
36
36
 
37
37
  ## Threads
38
38
 
39
- Two kinds, chosen by the reader when creating one:
39
+ Two kinds, chosen by the reader when creating one. In the browser these are
40
+ the two buttons on the comment box, one click each:
40
41
 
41
- - `ask` mode (a question): delivered at once. `wait` returns `question`.
42
- Answer with `threads reply`. It stays open until the reader resolves it;
43
- open questions never block a republish.
44
- - `review` mode (a comment): held as pending until the reader submits. Then
45
- `wait` returns `awaiting-agent-updates` with the submitted threads.
42
+ - `ask` mode (a question, "Send to the agent"): submitted on creation and
43
+ delivered at once. `wait` returns `question`. Answer with `threads reply`.
44
+ Open questions never block a republish.
45
+ - `review` mode (a comment, "Add to the review"): held as pending until the
46
+ reader submits. Then `wait` returns `awaiting-agent-updates` with the
47
+ submitted threads.
46
48
 
47
49
  Targets: a document block (with an optional quoted selection), a file line
48
50
  on the base or head side, a map node, or the whole review.
49
51
 
52
+ ### Status, `submitted` and `needsAgent`
53
+
54
+ Two flags and one derived predicate decide whether a thread reaches you.
55
+ `needsAgent` is the whole queue: `wait` reports it, `threads list --open`
56
+ counts it, and a thread outside it will not be delivered to anyone.
57
+
58
+ ```text
59
+ needsAgent = status is open AND submitted AND the last message is the reader's
60
+ ```
61
+
62
+ | Transition | status | submitted |
63
+ | ------------------------------ | -------------------- | ------------- |
64
+ | reader creates an `ask` thread | `open` | `true` |
65
+ | reader creates a `review` one | `open` | `false` |
66
+ | reader submits the review | unchanged | `true` (all) |
67
+ | **reader writes in a thread** | **forced to `open`** | `true` if ask |
68
+ | agent replies | unchanged | unchanged |
69
+ | `threads resolve` / Resolve | `resolved` | unchanged |
70
+ | Reopen | `open` | unchanged |
71
+
72
+ A message from the reader always reopens the thread. It has to: a reply that
73
+ left the thread resolved would sit at `needsAgent: false`, invisible to
74
+ `wait` and to `threads list --open`, and the reader would be writing to
75
+ nobody while the page still offered them a Reply button. Publishing a new
76
+ revision never touches a thread's status.
77
+
50
78
  `publish` after the first revision requires zero open submitted comment
51
79
  threads. Resolve a thread only when its requested change is present. Do not
52
80
  rewrite or merge threads.
53
81
 
82
+ ### Presence: what the reader is told
83
+
84
+ While `thurview wait` runs it writes a heartbeat to
85
+ `${THURVIEW_HOME:-~/.thurview}/agents/<reviewId>.json`, and the browser reads
86
+ it back as one of two sentences: an agent is listening now, or nothing is
87
+ listening and what you send is queued until one checks in. Nothing else
88
+ writes it, so presence is never inferred and never faked. A heartbeat older
89
+ than 15 seconds is a dead `wait`, not an agent.
90
+
91
+ That is why a question asked while you are away is not lost and does not need
92
+ you to sit in `wait`: it is queued, `thurview` reports it as `needsAgent` the
93
+ next time you run any command in the worktree, and you answer it then.
94
+
54
95
  ```sh
55
96
  thurview threads list --review <id> [--open]
56
97
  thurview threads get <threadId> --review <id>
@@ -64,6 +105,7 @@ thurview threads resolve <threadId> --review <id>
64
105
  ${THURVIEW_HOME:-~/.thurview}/
65
106
  ├── THURVIEW.md user guidance (optional)
66
107
  ├── server.json running server, if any
108
+ ├── agents/<id>.json heartbeat of a running `wait`, removed when it ends
67
109
  └── reviews/<id>/
68
110
  ├── review.md you edit
69
111
  ├── data.yaml you edit
@@ -90,3 +132,8 @@ again.
90
132
 
91
133
  `thurview threads get <id>` truncates bodies over 1500 characters; pass
92
134
  `--full` when the hint says so.
135
+
136
+ While `wait` runs, the reader's page says an agent is listening; when it
137
+ returns, the page says the opposite within seconds. Do not leave `wait`
138
+ running to look present when you are not going to answer, and do not loop it
139
+ to keep a queue drained: the queue survives you, and the reader is told so.