revspec 0.5.0 → 0.7.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.
Files changed (47) hide show
  1. package/README.md +84 -67
  2. package/bin/revspec.ts +4 -38
  3. package/package.json +20 -3
  4. package/skills/revspec/SKILL.md +38 -31
  5. package/src/cli/reply.ts +1 -1
  6. package/src/cli/watch.ts +69 -41
  7. package/src/protocol/live-events.ts +6 -16
  8. package/src/state/review-state.ts +37 -24
  9. package/src/tui/app.ts +168 -107
  10. package/src/tui/comment-input.ts +21 -14
  11. package/src/tui/confirm.ts +4 -6
  12. package/src/tui/help.ts +77 -20
  13. package/src/tui/pager.ts +4 -2
  14. package/src/tui/search.ts +9 -4
  15. package/src/tui/spinner.ts +81 -0
  16. package/src/tui/status-bar.ts +9 -8
  17. package/src/tui/thread-list.ts +62 -22
  18. package/src/tui/ui/keymap.ts +55 -0
  19. package/.github/workflows/ci.yml +0 -18
  20. package/CLAUDE.md +0 -27
  21. package/bun.lock +0 -213
  22. package/docs/superpowers/plans/2026-03-14-live-ai-integration.md +0 -1877
  23. package/docs/superpowers/plans/2026-03-14-spectral-v1-implementation.md +0 -2139
  24. package/docs/superpowers/plans/2026-03-15-ui-refactor.md +0 -1025
  25. package/docs/superpowers/specs/2026-03-14-live-ai-integration-design.md +0 -518
  26. package/docs/superpowers/specs/2026-03-14-live-ai-integration-design.review.json +0 -65
  27. package/docs/superpowers/specs/2026-03-14-spec-review-tool-design.md +0 -331
  28. package/docs/superpowers/specs/2026-03-14-spec-review-tool-design.review.json +0 -141
  29. package/docs/superpowers/specs/claude-code-integration-notes.md +0 -26
  30. package/scripts/install-skill.sh +0 -20
  31. package/scripts/release.sh +0 -52
  32. package/test/cli-reply.test.ts +0 -140
  33. package/test/cli-watch.test.ts +0 -216
  34. package/test/cli.test.ts +0 -160
  35. package/test/e2e-live.test.ts +0 -171
  36. package/test/live-interaction.test.ts +0 -398
  37. package/test/opentui-smoke.test.ts +0 -12
  38. package/test/protocol/live-events.test.ts +0 -509
  39. package/test/protocol/live-merge.test.ts +0 -167
  40. package/test/protocol/merge.test.ts +0 -100
  41. package/test/protocol/read.test.ts +0 -92
  42. package/test/protocol/types.test.ts +0 -95
  43. package/test/protocol/write.test.ts +0 -72
  44. package/test/state/review-state.test.ts +0 -399
  45. package/test/tui/pager.test.ts +0 -159
  46. package/test/tui/ui/keybinds.test.ts +0 -71
  47. package/tsconfig.json +0 -14
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Revspec
2
2
 
3
- A review tool for AI-generated spec documents with real-time AI conversation. Comment on specific lines, get AI replies instantly, resolve discussions, and approve — all without leaving the terminal.
3
+ A review tool for AI-generated spec documents with real-time AI conversation. Comment on specific lines, get AI replies instantly, resolve discussions, submit for rewrites, and approve — all without leaving the terminal.
4
4
 
5
5
  ## Why
6
6
 
@@ -27,92 +27,114 @@ cd revspec && bun install && bun link
27
27
  revspec spec.md
28
28
  ```
29
29
 
30
- Opens a TUI in line mode with vim-style navigation. Press `c` on any line to open a thread and start commenting.
31
-
32
- ### Markdown rendering
33
-
34
- Revspec renders markdown in-place (toggle with `m`):
35
-
36
- - **Headings** — colored and bold, `#`–`######`
37
- - **Inline** — bold (`**`/`__`), italic (`*`/`_`), bold-italic (`***`), strikethrough (`~~`), `code`, [links](url)
38
- - **Fenced code blocks** — fence markers dimmed, body in green
39
- - **Tables** — box-drawing borders, header row bolded, auto-column-widths
40
- - **Lists** — unordered (`•`), ordered, task lists (`☐`/`☑`)
41
- - **Blockquotes** — bar gutter, italicized text
42
- - **Cursor line** highlighting across all elements
43
- - **Search highlights** — colored match segments
30
+ Opens a TUI with vim-style navigation. Press `c` on any line to open a thread and start commenting.
44
31
 
45
32
  ### Keybindings
46
33
 
34
+ **Navigation**
35
+
47
36
  | Key | Action |
48
37
  |-----|--------|
49
38
  | `j/k` | Move cursor down/up |
50
39
  | `gg` / `G` | Go to top / bottom |
51
40
  | `Ctrl+D/U` | Half page down/up |
52
- | `m` | Toggle markdown / line mode |
53
- | `c` | Open thread / comment on line |
54
- | `r` | Resolve thread (toggle) |
55
- | `R` | Resolve all pending |
56
- | `dd` | Delete draft comment (double-tap) |
57
- | `/` | Search |
41
+ | `zz` | Center cursor line in viewport |
42
+ | `/` | Search (smartcase) |
58
43
  | `n/N` | Next/prev search match |
44
+ | `Esc` | Clear search highlights |
59
45
  | `]t/[t` | Next/prev thread |
60
46
  | `]r/[r` | Next/prev unread AI reply |
61
- | `l` | List threads |
62
- | `a` | Approve spec |
63
- | `:w` | Merge changes to review JSON |
64
- | `:wq` | Merge and quit |
65
- | `:q` | Quit (only if merged) |
66
- | `:q!` | Quit without merging |
47
+
48
+ **Review**
49
+
50
+ | Key | Action |
51
+ |-----|--------|
52
+ | `c` | Open thread / comment on line |
53
+ | `r` | Resolve thread (toggle) |
54
+ | `R` | Resolve all pending |
55
+ | `dd` | Delete thread (with confirm) |
56
+ | `t` | List threads |
57
+ | `S` | Submit for rewrite (AI updates spec, TUI reloads) |
58
+ | `A` | Approve spec (finalize and exit) |
59
+
60
+ **Commands**
61
+
62
+ | Key | Action |
63
+ |-----|--------|
64
+ | `:q` | Quit (warns if unresolved threads) |
65
+ | `:q!` | Force quit |
66
+ | `:{N}` | Jump to line N |
67
+ | `Ctrl+C` | Force quit |
67
68
  | `?` | Help |
68
69
 
70
+ **Popups**
71
+
72
+ | Key | Action |
73
+ |-----|--------|
74
+ | `y/Enter` | Confirm / select |
75
+ | `q/Esc` | Cancel / close |
76
+
69
77
  ### Thread popup
70
78
 
71
79
  The thread popup has two modes:
72
80
 
73
81
  - **Insert mode** — type your comment, `Tab` sends, `Esc` switches to normal mode
74
- - **Normal mode** — `j/k` and `Ctrl+D/U` scroll the conversation history, `c` to reply, `r` to resolve, `Esc` to close
82
+ - **Normal mode** — `j/k` and `Ctrl+D/U` scroll the conversation, `gg/G` top/bottom, `c` to reply, `r` to resolve, `q/Esc` to close
83
+
84
+ ### Markdown rendering
85
+
86
+ Revspec renders markdown in-place:
87
+
88
+ - **Headings** — colored and bold, `#`–`######`
89
+ - **Inline** — bold, italic, bold-italic, strikethrough, `code`, links
90
+ - **Fenced code blocks** — markers dimmed, body in green
91
+ - **Tables** — box-drawing borders, header row bolded, auto-column-widths
92
+ - **Lists** — unordered, ordered, task lists
93
+ - **Blockquotes** — bar gutter, italicized text
94
+ - **Cursor line** highlighting and **search highlights**
75
95
 
76
96
  ## Live AI Integration
77
97
 
78
- Revspec supports real-time communication with AI coding tools (Claude Code, opencode, etc.) via two CLI subcommands:
98
+ Revspec communicates with AI coding tools (Claude Code, etc.) via CLI subcommands:
79
99
 
80
100
  ### `revspec watch <file.md>`
81
101
 
82
- Blocks until the reviewer adds comments, then returns them with spec context:
102
+ Blocks until the reviewer acts, then returns structured output:
83
103
 
84
104
  ```
85
105
  === New Comments ===
86
- Thread: t1 (line 14)
106
+ Thread: x1a3f (line 14)
87
107
  Context:
88
108
  12: The system uses polling...
89
109
  > 14: it sends a notification via webhook.
90
110
  16: resource state.
91
111
  [reviewer]: this is unclear
92
112
 
93
- To reply: revspec reply spec.md t1 "<your response>"
113
+ To reply: revspec reply spec.md x1a3f "<your response>"
94
114
  When done replying, run: revspec watch spec.md
95
115
  ```
96
116
 
117
+ Watch exits on three events:
118
+ - **Comment/reply** — returns thread content for AI to respond
119
+ - **Submit (`S`)** — returns resolved thread summaries for AI to rewrite the spec
120
+ - **Approve (`A`)** — spec is finalized
121
+ - **Session end** — reviewer quit the TUI
122
+
97
123
  ### `revspec reply <file.md> <threadId> "<text>"`
98
124
 
99
- Sends an AI reply that appears instantly in the reviewer's TUI:
100
-
101
- ```bash
102
- revspec reply spec.md t1 "Good point. I'll clarify the polling vs webhook distinction."
103
- ```
125
+ Sends an AI reply that appears instantly in the reviewer's TUI.
104
126
 
105
127
  ### The loop
106
128
 
107
129
  ```
108
130
  1. AI generates spec
109
- 2. AI launches: revspec spec.md (in tmux pane or separate terminal)
131
+ 2. AI launches: revspec spec.md (in tmux pane)
110
132
  3. AI runs: revspec watch spec.md (blocks)
111
- 4. Reviewer comments on lines in the TUI
112
- 5. Watch returns with comments AI replies → watch again
113
- 6. Reviewer resolves threadsapproves
114
- 7. AI reads review JSON, rewrites spec, launches new round
115
- 8. Repeat until clean approval
133
+ 4. Reviewer comments AI replies watch again
134
+ 5. Reviewer resolves threadspresses S (submit)
135
+ 6. Watch returns resolved thread summaries AI rewrites spec
136
+ 7. TUI reloads with new spec reviewer continues reviewing
137
+ 8. Repeat 3-7 until A (approve)
116
138
  ```
117
139
 
118
140
  ### Claude Code skill
@@ -125,39 +147,34 @@ Install the `/revspec` skill for Claude Code:
125
147
 
126
148
  Then use `/revspec` in Claude Code after generating a spec.
127
149
 
150
+ ## Testing
151
+
152
+ ```bash
153
+ bun run test # Unit + integration (~3s)
154
+ bun run test:e2e # E2E snapshot tests (~7s)
155
+ bun run test:all # Everything
156
+ ```
157
+
158
+ E2E tests use `bun-pty` to spawn revspec in a pseudo-terminal (80x24), send keystrokes, capture plain-text output, and compare snapshots.
159
+
128
160
  ## Protocol
129
161
 
130
- Communication happens through a JSONL file (`spec.review.live.jsonl`) — append-only, both sides write to it. On session end, events are merged into `spec.review.json`.
162
+ Communication happens through a JSONL file (`spec.review.jsonl`) — append-only, both sides write to it. The JSONL is the single source of truth for the review session.
131
163
 
132
164
  ### Event types
133
165
 
134
166
  ```jsonl
135
- {"type":"comment","threadId":"t1","line":14,"author":"reviewer","text":"unclear","ts":1710400000}
136
- {"type":"reply","threadId":"t1","author":"owner","text":"I'll fix it","ts":1710400005}
137
- {"type":"resolve","threadId":"t1","author":"reviewer","ts":1710400010}
138
- {"type":"approve","author":"reviewer","ts":1710400050}
167
+ {"type":"comment","threadId":"x1a3f","line":14,"author":"reviewer","text":"unclear","ts":1710400000}
168
+ {"type":"reply","threadId":"x1a3f","author":"owner","text":"I'll fix it","ts":1710400005}
169
+ {"type":"resolve","threadId":"x1a3f","author":"reviewer","ts":1710400010}
170
+ {"type":"submit","author":"reviewer","ts":1710400050}
171
+ {"type":"approve","author":"reviewer","ts":1710400060}
172
+ {"type":"session-end","author":"reviewer","ts":1710400070}
139
173
  ```
140
174
 
141
- ### Review JSON
142
-
143
- ```json
144
- {
145
- "file": "spec.md",
146
- "threads": [
147
- {
148
- "id": "t1",
149
- "line": 14,
150
- "status": "resolved",
151
- "messages": [
152
- { "author": "reviewer", "text": "this is unclear", "ts": 1710400000 },
153
- { "author": "owner", "text": "I'll restructure this section", "ts": 1710400005 }
154
- ]
155
- }
156
- ]
157
- }
158
- ```
175
+ The `submit` event acts as a round delimiter — the AI rewrites the spec, and the TUI reloads. Events before a `submit` reference the previous spec version.
159
176
 
160
- Thread statuses: `open` (owner's turn), `pending` (reviewer's turn), `resolved`, `outdated`.
177
+ Thread statuses: `open` (awaiting AI reply), `pending` (AI replied, awaiting reviewer), `resolved`, `outdated`.
161
178
 
162
179
  ## License
163
180
 
package/bin/revspec.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { existsSync } from "fs";
3
3
  import { resolve, basename, extname, dirname, join } from "path";
4
- import { readReviewFile } from "../src/protocol/read";
5
4
  import { runTui } from "../src/tui/app";
6
- import { readEventsFromOffset } from "../src/protocol/live-events";
7
5
 
8
6
  const args = process.argv.slice(2);
9
7
  const subcommand = args[0];
@@ -33,7 +31,7 @@ if (subcommand === "reply") {
33
31
  }
34
32
 
35
33
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
36
- console.log("Usage: revspec <file.md> [--tui|--nvim|--web]");
34
+ console.log("Usage: revspec <file.md>");
37
35
  process.exit(0);
38
36
  }
39
37
 
@@ -56,40 +54,8 @@ if (!existsSync(specPath)) {
56
54
  process.exit(1);
57
55
  }
58
56
 
59
- // 2. Derive review/jsonl paths from spec filename
60
- // e.g. spec.md -> spec.review.json, spec.review.live.jsonl
61
- const specDir = dirname(specPath);
62
- const specBase = basename(specPath, extname(specPath)); // e.g. "spec"
63
- const reviewPath = join(specDir, `${specBase}.review.json`);
64
- const jsonlPath = join(specDir, `${specBase}.review.live.jsonl`);
65
- const draftPath = join(specDir, `${specBase}.review.draft.json`);
57
+ // 2. Launch TUI
58
+ const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
59
+ await runTui(specPath, pkg.version);
66
60
 
67
- // 3. Launch TUI (skip if REVSPEC_SKIP_TUI=1)
68
- if (process.env.REVSPEC_SKIP_TUI !== "1") {
69
- const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
70
- await runTui(specPath, reviewPath, draftPath, pkg.version);
71
- }
72
-
73
- // 4. After TUI exits, check if approved via JSONL
74
- if (existsSync(jsonlPath)) {
75
- const { events } = readEventsFromOffset(jsonlPath, 0);
76
- const wasApproved = events.some(e => e.type === "approve");
77
- if (wasApproved && existsSync(reviewPath)) {
78
- console.log(`APPROVED: ${reviewPath}`);
79
- process.exit(0);
80
- }
81
- }
82
-
83
- // 5. Check if review file exists with open/pending threads
84
- const existingReview = readReviewFile(reviewPath);
85
- if (existingReview) {
86
- const hasOpenOrPending = existingReview.threads.some(
87
- (t) => t.status === "open" || t.status === "pending"
88
- );
89
- if (hasOpenOrPending) {
90
- process.stdout.write(`${reviewPath}\n`);
91
- }
92
- }
93
-
94
- // Otherwise print nothing
95
61
  process.exit(0);
package/package.json CHANGED
@@ -1,16 +1,33 @@
1
1
  {
2
2
  "name": "revspec",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
+ "description": "Terminal-based spec review tool with real-time AI conversation",
4
5
  "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/icyrainz/revspec.git"
10
+ },
11
+ "keywords": ["spec", "review", "tui", "ai", "claude", "vim", "terminal"],
5
12
  "bin": {
6
13
  "revspec": "./bin/revspec.ts"
7
14
  },
15
+ "files": [
16
+ "bin/",
17
+ "src/",
18
+ "skills/",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
8
22
  "scripts": {
9
23
  "start": "bun run bin/revspec.ts",
10
- "test": "bun test"
24
+ "test": "bun test test/unit test/integration",
25
+ "test:e2e": "bun test test/e2e",
26
+ "test:all": "bun test"
11
27
  },
12
28
  "devDependencies": {
13
- "@types/bun": "latest"
29
+ "@types/bun": "latest",
30
+ "bun-pty": "^0.4.8"
14
31
  },
15
32
  "peerDependencies": {
16
33
  "typescript": "^5"
@@ -38,7 +38,8 @@ echo $TMUX
38
38
 
39
39
  **If tmux is available:**
40
40
  ```bash
41
- tmux split-window -v "revspec <spec-file>"
41
+ # Split from Claude Code's own pane (not the user's active window)
42
+ tmux split-window -t "$TMUX_PANE" -v "revspec <spec-file>"
42
43
  ```
43
44
 
44
45
  **If no tmux:**
@@ -55,57 +56,64 @@ revspec watch <spec-file>
55
56
  This blocks until the reviewer adds comments. When it returns, you'll see output like:
56
57
 
57
58
  ```
58
- --- New threads ---
59
-
60
- [t1] line 14 (new):
59
+ === New Comments ===
60
+ Thread: x1a3f (line 14)
61
61
  Context:
62
- 12: The system uses polling...
63
- >14: it sends a notification via webhook.
64
- 16: resource state.
65
- Comment: "this is unclear"
62
+ 12: The system uses polling...
63
+ > 14: it sends a notification via webhook.
64
+ 16: resource state.
65
+ [reviewer]: this is unclear
66
+ To reply: revspec reply spec.md x1a3f "<your reply>"
66
67
 
67
- To reply: revspec reply spec.md <threadId> "<your response>"
68
68
  When done replying, run: revspec watch spec.md
69
69
  ```
70
70
 
71
71
  **For each comment:** Read the context, understand the concern, and reply thoughtfully:
72
72
 
73
73
  ```bash
74
- revspec reply <spec-file> t1 "Good point. I'll clarify — it uses polling to detect changes, then sends a webhook notification to downstream services."
74
+ revspec reply <spec-file> x1a3f "Good point. I'll clarify — it uses polling to detect changes, then sends a webhook notification to downstream services."
75
75
  ```
76
76
 
77
77
  After replying to all comments, run `revspec watch` again to wait for the next batch.
78
78
 
79
- **If watch returns "Session ended. Reviewer exited revspec."** — the reviewer closed the TUI. Check the review JSON for resolved threads that require spec changes (see Step 4). If no resolved threads, stop and wait — the user can invoke `/revspec` again later to resume.
79
+ **If watch returns "Session ended. Reviewer exited revspec."** — the reviewer closed the TUI. Stop and wait — the user can invoke `/revspec` again later.
80
80
 
81
81
  **Important:** Your replies should be substantive — address the concern, explain your reasoning, or acknowledge the change you'll make. Don't just say "noted" or "will fix."
82
82
 
83
- ## Step 4: Handle Session End or Approval
83
+ ## Step 4: Handle Submit, Session End, or Approval
84
84
 
85
- The watch loop ends in one of two ways:
85
+ The watch loop ends in one of three ways:
86
86
 
87
- ### Session ended (reviewer exited with `:q`)
87
+ ### Submit (reviewer pressed `S`)
88
+
89
+ Watch returns resolved thread summaries:
90
+
91
+ ```
92
+ === Submit: Rewrite Requested ===
88
93
 
89
- Read the review JSON and check for **resolved threads**. Resolved = the reviewer acknowledged your reply and wants you to make that change.
94
+ Resolved threads:
95
+ x1a3f (line 14): "this is unclear"
96
+ → AI: "I'll clarify the polling vs webhook distinction."
90
97
 
91
- **If resolved threads with actionable feedback exist:**
92
- 1. Rewrite the spec incorporating the feedback from resolved threads
93
- 2. Commit the updated spec
94
- 3. Append a round marker to the JSONL:
95
- ```bash
96
- echo '{"type":"round","author":"owner","round":2,"ts":'$(date +%s000)'}' >> <spec-file>.review.live.jsonl
97
- ```
98
- 4. Launch a new revspec session (go back to Step 2) so the reviewer can verify the changes
98
+ Rewrite the spec incorporating the above, then run: revspec watch spec.md
99
+ ```
100
+
101
+ 1. Read the current spec and compare against the resolved threads
102
+ 2. If changes are needed: rewrite the spec and save — this triggers the TUI to reload automatically
103
+ 3. If the spec already reflects the feedback (e.g., crash recovery): skip rewrite
104
+ 4. Run `revspec watch` again — the reviewer is still in the TUI and will see the new content
105
+
106
+ **Important:** Watch may re-output the submit summary on crash recovery. Always verify whether the spec already incorporates the feedback before rewriting.
107
+
108
+ ### Session ended (reviewer exited with `:q`)
99
109
 
100
- **If no resolved threads (or only open/pending threads):**
101
- The reviewer left without resolving anything — stop and wait. The user can invoke `/revspec` again later.
110
+ The reviewer closed the TUI. Stop and wait — the user can invoke `/revspec` again later.
102
111
 
103
- ### Approved (reviewer pressed `a`)
112
+ ### Approved (reviewer pressed `A`)
104
113
 
105
114
  Watch returns:
106
115
  ```
107
116
  Review approved.
108
- Review file: <path-to-review.json>
109
117
  ```
110
118
 
111
119
  The spec is finalized. Report: "Spec approved and finalized at `<spec-file>`. Ready to proceed with implementation."
@@ -115,7 +123,6 @@ The spec is finalized. Report: "Spec approved and finalized at `<spec-file>`. Re
115
123
  - **Open** — under discussion, AI replied, reviewer hasn't responded yet
116
124
  - **Pending** — owner replied, waiting for reviewer to read
117
125
  - **Resolved** — reviewer acknowledged the plan, AI should make the change
118
- - **Approve** — spec is final, proceed to implementation
119
126
 
120
127
  ## Loop Summary
121
128
 
@@ -123,9 +130,9 @@ The spec is finalized. Report: "Spec approved and finalized at `<spec-file>`. Re
123
130
  1. Launch revspec on spec file
124
131
  2. Watch for comments
125
132
  3. Reply to each comment
126
- 4. Watch again (repeat 2-3 until session-end or approval)
127
- 5. On session-end: check for resolved threads
128
- 6. If resolved threads need spec changes: rewrite spec, launch new round (go to 1)
133
+ 4. Watch again (repeat 2-3 until submit/session-end/approval)
134
+ 5. On submit: verify and rewrite spec if needed (triggers TUI reload), go to step 2
135
+ 6. On session-end: stop, user will invoke /revspec again if needed
129
136
  7. On approval: spec is finalized, done
130
137
  ```
131
138
 
package/src/cli/reply.ts CHANGED
@@ -26,7 +26,7 @@ export function runReply(
26
26
  // Derive JSONL path
27
27
  const dir = dirname(specPath);
28
28
  const base = basename(specPath, ".md");
29
- const jsonlPath = `${dir}/${base}.review.live.jsonl`;
29
+ const jsonlPath = `${dir}/${base}.review.jsonl`;
30
30
 
31
31
  // Validate JSONL exists
32
32
  if (!existsSync(jsonlPath)) {
package/src/cli/watch.ts CHANGED
@@ -19,9 +19,9 @@ export async function runWatch(specFile: string): Promise<void> {
19
19
  // Derive paths
20
20
  const dir = dirname(specPath);
21
21
  const base = basename(specPath, ".md");
22
- const jsonlPath = join(dir, `${base}.review.live.jsonl`);
23
- const offsetPath = join(dir, `${base}.review.live.offset`);
24
- const lockPath = join(dir, `${base}.review.live.lock`);
22
+ const jsonlPath = join(dir, `${base}.review.jsonl`);
23
+ const offsetPath = join(dir, `${base}.review.offset`);
24
+ const lockPath = join(dir, `${base}.review.lock`);
25
25
  const reviewPath = join(dir, `${base}.review.json`);
26
26
 
27
27
  // Handle lock file
@@ -73,7 +73,7 @@ export async function runWatch(specFile: string): Promise<void> {
73
73
  offset
74
74
  );
75
75
  if (result.approved) {
76
- console.log(`Review approved.\nReview file: ${reviewPath}`);
76
+ console.log("Review approved.");
77
77
  cleanupFiles(lockPath, offsetPath);
78
78
  } else if (result.output) {
79
79
  process.stdout.write(result.output);
@@ -99,7 +99,7 @@ export async function runWatch(specFile: string): Promise<void> {
99
99
  );
100
100
 
101
101
  if (result.approved) {
102
- console.log(`Review approved.\nReview file: ${reviewPath}`);
102
+ console.log("Review approved.");
103
103
  cleanupFiles(lockPath, offsetPath);
104
104
  process.exit(0);
105
105
  }
@@ -135,7 +135,7 @@ export async function runWatch(specFile: string): Promise<void> {
135
135
 
136
136
  // Also watch the directory for the JSONL file to appear
137
137
  const dirWatcher = fsWatch(dir, (eventType, filename) => {
138
- if (filename && filename.endsWith(".live.jsonl")) {
138
+ if (filename && filename.endsWith(".jsonl")) {
139
139
  setupWatcher();
140
140
  }
141
141
  });
@@ -180,7 +180,24 @@ function processNewEvents(
180
180
 
181
181
  const { events, newOffset } = readEventsFromOffset(jsonlPath, offset);
182
182
 
183
+ // Recovery: detect pending unprocessed submit
183
184
  if (events.length === 0) {
185
+ const { events: allEvents } = readEventsFromOffset(jsonlPath, 0);
186
+ const lastSubmitIdx = allEvents.findLastIndex(e => e.type === "submit");
187
+ if (lastSubmitIdx >= 0) {
188
+ const afterSubmit = allEvents.slice(lastSubmitIdx + 1);
189
+ const hasNewActivity = afterSubmit.some(e =>
190
+ e.type === "comment" || e.type === "reply" ||
191
+ e.type === "approve" || e.type === "session-end"
192
+ );
193
+ if (!hasNewActivity) {
194
+ const roundStart = findCurrentRoundStartIndex(allEvents);
195
+ const currentRoundThreads = replayEventsToThreads(allEvents.slice(roundStart));
196
+ const resolved = currentRoundThreads.filter(t => t.status === "resolved");
197
+ const output = formatSubmitOutput(resolved, specPath);
198
+ return { approved: false, output, newOffset: offset };
199
+ }
200
+ }
184
201
  return { approved: false, output: "", newOffset: offset };
185
202
  }
186
203
 
@@ -193,6 +210,17 @@ function processNewEvents(
193
210
  return { approved: true, output: "", newOffset };
194
211
  }
195
212
 
213
+ // Check for submit event — priority over session-end
214
+ const hasSubmit = events.some((e) => e.type === "submit");
215
+ if (hasSubmit) {
216
+ const { events: allEvents } = readEventsFromOffset(jsonlPath, 0);
217
+ const roundStart = findCurrentRoundStartIndex(allEvents);
218
+ const currentRoundThreads = replayEventsToThreads(allEvents.slice(roundStart));
219
+ const resolved = currentRoundThreads.filter(t => t.status === "resolved");
220
+ const output = formatSubmitOutput(resolved, specPath);
221
+ return { approved: false, output, newOffset };
222
+ }
223
+
196
224
  // Check for session-end — TUI exited, break the loop
197
225
  const hasSessionEnd = events.some((e) => e.type === "session-end");
198
226
  if (hasSessionEnd) {
@@ -233,8 +261,6 @@ function formatWatchOutput(
233
261
  // Group events by type
234
262
  const newCommentThreadIds: string[] = [];
235
263
  const replyThreadIds: string[] = [];
236
- const resolvedThreadIds: string[] = [];
237
- const deletedThreadIds: string[] = [];
238
264
 
239
265
  const seen = new Set<string>();
240
266
 
@@ -249,14 +275,6 @@ function formatWatchOutput(
249
275
  if (!replyThreadIds.includes(tid)) {
250
276
  replyThreadIds.push(tid);
251
277
  }
252
- } else if (event.type === "resolve") {
253
- if (!resolvedThreadIds.includes(tid)) {
254
- resolvedThreadIds.push(tid);
255
- }
256
- } else if (event.type === "delete") {
257
- if (!deletedThreadIds.includes(tid)) {
258
- deletedThreadIds.push(tid);
259
- }
260
278
  }
261
279
  }
262
280
 
@@ -315,31 +333,6 @@ function formatWatchOutput(
315
333
  }
316
334
  }
317
335
 
318
- // Resolved threads
319
- if (resolvedThreadIds.length > 0) {
320
- lines.push("=== Resolved ===");
321
- for (const tid of resolvedThreadIds) {
322
- const thread = threadsById.get(tid);
323
- if (!thread) continue;
324
- lines.push(`Thread: ${tid} (line ${thread.line}) — resolved`);
325
- lines.push("");
326
- }
327
- }
328
-
329
- // Deleted threads
330
- if (deletedThreadIds.length > 0) {
331
- lines.push("=== Deleted ===");
332
- for (const tid of deletedThreadIds) {
333
- const thread = threadsById.get(tid);
334
- if (thread) {
335
- lines.push(`Thread: ${tid} (line ${thread.line}) — deleted`);
336
- } else {
337
- lines.push(`Thread: ${tid} — deleted`);
338
- }
339
- lines.push("");
340
- }
341
- }
342
-
343
336
  // Add footer instruction
344
337
  const hasActionable = newCommentThreadIds.length > 0 || replyThreadIds.length > 0;
345
338
  if (hasActionable) {
@@ -368,6 +361,41 @@ function getContext(
368
361
  return result;
369
362
  }
370
363
 
364
+ function findCurrentRoundStartIndex(events: LiveEvent[]): number {
365
+ let count = 0;
366
+ for (let i = events.length - 1; i >= 0; i--) {
367
+ if (events[i].type === "submit") {
368
+ count++;
369
+ if (count === 2) return i + 1;
370
+ }
371
+ }
372
+ return 0;
373
+ }
374
+
375
+ function formatSubmitOutput(
376
+ resolvedThreads: Thread[],
377
+ specPath: string
378
+ ): string {
379
+ const lines: string[] = [];
380
+ lines.push("=== Submit: Rewrite Requested ===");
381
+ lines.push("");
382
+ if (resolvedThreads.length > 0) {
383
+ lines.push("Resolved threads:");
384
+ for (const t of resolvedThreads) {
385
+ const reviewerMsgs = t.messages.filter(m => m.author === "reviewer");
386
+ const ownerMsgs = t.messages.filter(m => m.author === "owner");
387
+ lines.push(` ${t.id} (line ${t.line}): "${reviewerMsgs.map(m => m.text).join("; ")}"`);
388
+ if (ownerMsgs.length > 0) {
389
+ lines.push(` → AI: "${ownerMsgs.map(m => m.text).join("; ")}"`);
390
+ }
391
+ }
392
+ lines.push("");
393
+ }
394
+ lines.push(`Rewrite the spec incorporating the above, then run: revspec watch ${basename(specPath)}`);
395
+ lines.push("");
396
+ return lines.join("\n");
397
+ }
398
+
371
399
  function cleanupFiles(lockPath: string, offsetPath: string): void {
372
400
  if (existsSync(lockPath)) unlinkSync(lockPath);
373
401
  if (existsSync(offsetPath)) unlinkSync(offsetPath);