@a9n-shoji/rvw 0.2.0 → 0.2.2

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 (105) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +28 -7
  3. package/dist/cli.mjs +492 -307
  4. package/dist/cli.mjs.map +3 -3
  5. package/dist/web/assets/{DocumentViewer-CNgHpCuL.js → DocumentViewer-ADzspLC4.js} +32 -32
  6. package/dist/web/assets/{markdown-source-map-_dS1q5Dg.js → MarkdownTable-DVJ0_MIC.js} +2 -2
  7. package/dist/web/assets/WalkthroughViewer-yQibj2tK.js +1 -0
  8. package/dist/web/assets/{abnfDiagram-N423BO3Z-0v-ndtqQ.js → abnfDiagram-N423BO3Z-6dQzEuPV.js} +1 -1
  9. package/dist/web/assets/architecture-TIHT7OUA-DEoBuEnc.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-T3A2C74G-WSNJ-tZE.js → architectureDiagram-T3A2C74G-DP7hHV7T.js} +1 -1
  11. package/dist/web/assets/{blockDiagram-VBNYF7ZC-lmMbEUwI.js → blockDiagram-VBNYF7ZC-ZbwzVKWP.js} +1 -1
  12. package/dist/web/assets/{c4Diagram-5PPSVZJV-BrN5YVV8.js → c4Diagram-5PPSVZJV-HVD9_uV4.js} +1 -1
  13. package/dist/web/assets/channel-DjgdV-zW.js +1 -0
  14. package/dist/web/assets/{chunk-2GRJ4B5K-gt2JPyrH.js → chunk-2GRJ4B5K-Bkxs2d4J.js} +1 -1
  15. package/dist/web/assets/{chunk-4I5QYGJK-Cg5GZ_-7.js → chunk-4I5QYGJK-BJHE6zC_.js} +1 -1
  16. package/dist/web/assets/{chunk-5RXB4S5H-BdhRGvxW.js → chunk-5RXB4S5H-B-52dfCQ.js} +1 -1
  17. package/dist/web/assets/{chunk-6Q2QTUOP-B1DYWNVJ.js → chunk-6Q2QTUOP-BpD0vQ66.js} +1 -1
  18. package/dist/web/assets/{chunk-7Z6QIM7H-D2YImamZ.js → chunk-7Z6QIM7H-BTI1qgAO.js} +1 -1
  19. package/dist/web/assets/{chunk-GF5L2VYU-CWtwL_CM.js → chunk-GF5L2VYU-B8BxoCCx.js} +1 -1
  20. package/dist/web/assets/{chunk-I66GZJ75-DdoGlxMt.js → chunk-I66GZJ75-zwi-Xf01.js} +1 -1
  21. package/dist/web/assets/{chunk-JQJVKLGR-CVWAp3XP.js → chunk-JQJVKLGR-D9mKBWUz.js} +1 -1
  22. package/dist/web/assets/{chunk-KBJHAD2P-BBacwg3J.js → chunk-KBJHAD2P-Qmtir3qc.js} +1 -1
  23. package/dist/web/assets/{chunk-NSK5VX7P-1AsXiBV3.js → chunk-NSK5VX7P-SP2vZdAo.js} +1 -1
  24. package/dist/web/assets/{chunk-QR6OTTB3-DL2s5pNp.js → chunk-QR6OTTB3-uVDCHurs.js} +1 -1
  25. package/dist/web/assets/{chunk-UBXNYLIW-mu3fkNQI.js → chunk-UBXNYLIW-CtNzBWOU.js} +1 -1
  26. package/dist/web/assets/{chunk-W5SLKNZC-BAwhMBBb.js → chunk-W5SLKNZC-BPfDqbmK.js} +1 -1
  27. package/dist/web/assets/{chunk-WRU74C26-C7IeXZaP.js → chunk-WRU74C26-BZkEOexq.js} +1 -1
  28. package/dist/web/assets/classDiagram-JCYQIIEL-B6Qjam7Y.js +1 -0
  29. package/dist/web/assets/classDiagram-v2-OCEON4UE-B6Qjam7Y.js +1 -0
  30. package/dist/web/assets/{cynefin-VYW2F7L2-DNisGvNv.js → cynefin-VYW2F7L2-BRisCyqC.js} +1 -1
  31. package/dist/web/assets/{cynefinDiagram-MW4NZA55-BA27gOTz.js → cynefinDiagram-MW4NZA55-ClIrmCkB.js} +1 -1
  32. package/dist/web/assets/{dagre-VZM6K2ZE-vo_1YAuM.js → dagre-VZM6K2ZE-X31uC_JB.js} +1 -1
  33. package/dist/web/assets/{diagram-7IWD3JNH-DLLdUj0_.js → diagram-7IWD3JNH-DBQFjl0i.js} +1 -1
  34. package/dist/web/assets/{diagram-B4RE2ZJO-BV0WyNA-.js → diagram-B4RE2ZJO-BZ7934Wg.js} +1 -1
  35. package/dist/web/assets/{diagram-LBJQPF4R-DXxKSx2U.js → diagram-LBJQPF4R-CcJqH9aY.js} +1 -1
  36. package/dist/web/assets/{diagram-Q27KOJAE-DsQ2GzIK.js → diagram-Q27KOJAE-DzM7eiil.js} +1 -1
  37. package/dist/web/assets/{diagram-UB23O5K3-DfjElFP1.js → diagram-UB23O5K3-CVCV-d3m.js} +1 -1
  38. package/dist/web/assets/{ebnfDiagram-BXEA7PRR-cYu0XV1a.js → ebnfDiagram-BXEA7PRR-D8xb9FBp.js} +1 -1
  39. package/dist/web/assets/{erDiagram-JOGREHBK-DnsLBHaV.js → erDiagram-JOGREHBK-1l-G_PMG.js} +1 -1
  40. package/dist/web/assets/eventmodeling-45OFAUF4-ndNeUBOZ.js +1 -0
  41. package/dist/web/assets/flowDiagram-UKHOOZJN-BVxK_6Tu.js +1 -0
  42. package/dist/web/assets/{ganttDiagram-PKOTCBZU-CVD5gq3E.js → ganttDiagram-PKOTCBZU-CDJ9a1tp.js} +1 -1
  43. package/dist/web/assets/{gitGraph-TEB2WS4Q-C_j0n-Kw.js → gitGraph-TEB2WS4Q-DOeTh5wB.js} +1 -1
  44. package/dist/web/assets/{gitGraphDiagram-DS77QQ5N-qACevJmW.js → gitGraphDiagram-DS77QQ5N-DYLxc8Nz.js} +1 -1
  45. package/dist/web/assets/index-9cOaPesh.js +384 -0
  46. package/dist/web/assets/index-kK3hngnz.css +1 -0
  47. package/dist/web/assets/{info-DKCQHKI2-Cmkbz3Yi.js → info-DKCQHKI2-B5u97QuQ.js} +1 -1
  48. package/dist/web/assets/{infoDiagram-6WML65LV-CDrAem7M.js → infoDiagram-6WML65LV-zosKW2OD.js} +1 -1
  49. package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-DXgCnFqW.js → ishikawaDiagram-WSZJBQD7-VYcc_au8.js} +1 -1
  50. package/dist/web/assets/{journeyDiagram-NVQOT4AX-1KW-I7DV.js → journeyDiagram-NVQOT4AX-C_nU8kP4.js} +1 -1
  51. package/dist/web/assets/{kanban-definition-27J2QSJJ-CCt75OuF.js → kanban-definition-27J2QSJJ-DybKaWfx.js} +1 -1
  52. package/dist/web/assets/{line-DQfkDi_r.js → line-DWsGplGj.js} +1 -1
  53. package/dist/web/assets/{mermaid-parser.core-B7TMMtY0.js → mermaid-parser.core-D_T_3oIQ.js} +3 -3
  54. package/dist/web/assets/{mermaid.core-B93x-7Mk.js → mermaid.core-BoLGzxB5.js} +4 -4
  55. package/dist/web/assets/{mindmap-definition-FAOFIHXS-Dyt28V6S.js → mindmap-definition-FAOFIHXS-CsdpqPIa.js} +1 -1
  56. package/dist/web/assets/{packet-7NZHBO7P-zQ0u35s3.js → packet-7NZHBO7P-CReAEF1h.js} +1 -1
  57. package/dist/web/assets/{pegDiagram-VL7TDLO6-et0KHGIE.js → pegDiagram-VL7TDLO6-CE9-NPMu.js} +1 -1
  58. package/dist/web/assets/{pie-RZYD4A2V-uur86LkQ.js → pie-RZYD4A2V-GYixXUka.js} +1 -1
  59. package/dist/web/assets/{pieDiagram-7S7Q4E2Y-c027WPTB.js → pieDiagram-7S7Q4E2Y-CqaAKNg2.js} +1 -1
  60. package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-BtSV4E-0.js → quadrantDiagram-CIZ2JOQS-eRMHJqfj.js} +1 -1
  61. package/dist/web/assets/{radar-I7S5WNFK-B_g0VZMF.js → radar-I7S5WNFK-BwHxP0iK.js} +1 -1
  62. package/dist/web/assets/{railroad-3IZDKUUU-VFLsM_Em.js → railroad-3IZDKUUU-AHc3O4Oz.js} +1 -1
  63. package/dist/web/assets/railroad-abnf-AHOZXSZD-ZKY5olmV.js +1 -0
  64. package/dist/web/assets/railroad-ebnf-EBAXGLYW-CrG1UCMo.js +1 -0
  65. package/dist/web/assets/railroad-peg-LSFZ7HO6-CoY6MIbj.js +1 -0
  66. package/dist/web/assets/{railroadDiagram-AXF67PYL-uOtnxRbQ.js → railroadDiagram-AXF67PYL-BhAxCokG.js} +1 -1
  67. package/dist/web/assets/{requirementDiagram-LRYGKXZP-DGSZAsj4.js → requirementDiagram-LRYGKXZP-BRW-kYhg.js} +1 -1
  68. package/dist/web/assets/{sankeyDiagram-W5VNT64P-CCDk8Vxt.js → sankeyDiagram-W5VNT64P-DfnYLbDJ.js} +1 -1
  69. package/dist/web/assets/{sequenceDiagram-SI44F4Z6-Ce-Jzdk5.js → sequenceDiagram-SI44F4Z6-WEkIPSg8.js} +1 -1
  70. package/dist/web/assets/{stateDiagram-OKZ733FA-BYpYzyEa.js → stateDiagram-OKZ733FA-CdN7veIN.js} +1 -1
  71. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-0xn0gbpn.js +1 -0
  72. package/dist/web/assets/{swimlanes-SLNWSIFB-BL-CUc6d.js → swimlanes-SLNWSIFB-g9wWLf5Y.js} +1 -1
  73. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-YJ3yh_d3.js +8 -0
  74. package/dist/web/assets/{timeline-definition-Z64GVDOM-p9pkof_K.js → timeline-definition-Z64GVDOM-DjkKZIPE.js} +1 -1
  75. package/dist/web/assets/{treeView-QDETBFTQ-dRhXTU9p.js → treeView-QDETBFTQ-WARa9r09.js} +1 -1
  76. package/dist/web/assets/{treemap-6X3UGDF4-CZHe3dIh.js → treemap-6X3UGDF4-CTFjdb6k.js} +1 -1
  77. package/dist/web/assets/{vennDiagram-T6HMQDX7-ipODJegx.js → vennDiagram-T6HMQDX7-CI9QrBwu.js} +1 -1
  78. package/dist/web/assets/{wardley-OPB4EBWU-CxHlVjkF.js → wardley-OPB4EBWU-Cfo_Nnla.js} +1 -1
  79. package/dist/web/assets/{wardleyDiagram-T6FBY63Y-YarkIncH.js → wardleyDiagram-T6FBY63Y-CzGJ4fzZ.js} +1 -1
  80. package/dist/web/assets/{xychartDiagram-ELKLHX3M-Cw1M-7a4.js → xychartDiagram-ELKLHX3M-BXCiD7gJ.js} +1 -1
  81. package/dist/web/index.html +2 -2
  82. package/migrations/010_comment_post_references.sql +17 -0
  83. package/package.json +2 -1
  84. package/skills/rvw/SKILL.md +79 -6
  85. package/skills/rvw-walkthrough/SKILL.md +1 -1
  86. package/skills/rvw-watch-comments/SKILL.md +217 -67
  87. package/skills/rvw-watch-comments/scripts/auto-ack.mjs +211 -0
  88. package/skills/rvw-watch-comments/scripts/preflight.mjs +108 -0
  89. package/skills/rvw-watch-comments/scripts/rvw-command.mjs +80 -0
  90. package/skills/rvw-watch-comments/scripts/watch-driver.mjs +272 -0
  91. package/skills/rvw-watch-comments/scripts/watch-state.mjs +223 -70
  92. package/dist/web/assets/WalkthroughViewer-BVB3ETHG.js +0 -1
  93. package/dist/web/assets/architecture-TIHT7OUA-BjGWtjPU.js +0 -1
  94. package/dist/web/assets/channel-DkH22XO0.js +0 -1
  95. package/dist/web/assets/classDiagram-JCYQIIEL-C6Z4rmZR.js +0 -1
  96. package/dist/web/assets/classDiagram-v2-OCEON4UE-C6Z4rmZR.js +0 -1
  97. package/dist/web/assets/eventmodeling-45OFAUF4-BUAcQ5NU.js +0 -1
  98. package/dist/web/assets/flowDiagram-UKHOOZJN-hAe9ojjP.js +0 -1
  99. package/dist/web/assets/index-Coye1BY5.js +0 -384
  100. package/dist/web/assets/index-DFdYNMZE.css +0 -1
  101. package/dist/web/assets/railroad-abnf-AHOZXSZD-TVRi9_2L.js +0 -1
  102. package/dist/web/assets/railroad-ebnf-EBAXGLYW-B5B-JrS4.js +0 -1
  103. package/dist/web/assets/railroad-peg-LSFZ7HO6-CfIpa6ba.js +0 -1
  104. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-BJFG9cb2.js +0 -1
  105. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-BS8L8fb1.js +0 -8
@@ -1,13 +1,14 @@
1
1
  ---
2
2
  name: rvw-watch-comments
3
- description: Continuously watch all Pull Requests saved in the local rvw database for new root comments and replies, durably queue them, delegate investigation, and post final rvw replies. Use when a user asks an Agent task to monitor, watch, poll, or continuously address new rvw review comments, optionally allowing fixes and pushes only for Pull Requests authored by the authenticated GitHub user.
3
+ description: Continuously watch all Pull Requests saved in the local rvw database for new root comments and replies, durably queue them, acknowledge them immediately, investigate or delegate bounded batches, and replace the acknowledgement with a final rvw reply. Use when a user asks an Agent task to monitor, watch, poll, or continuously address new rvw review comments, optionally allowing fixes and pushes only for Pull Requests authored by the authenticated GitHub user.
4
4
  ---
5
5
 
6
6
  # Watch rvw comments
7
7
 
8
- Run one long-lived parent task as the intake and durable-state owner. Delegate PR batches to workers;
9
- never ask rvw to launch or manage an Agent. Use the `rvw` Skill for per-comment reading, exact-source
10
- inspection, replies, and synchronization.
8
+ Run one long-lived parent task as the intake and durable-state owner. The bundled driver owns the
9
+ watch process, cursor resume, RFC 7464 parsing, ingestion, and optional immediate acknowledgement;
10
+ do not recreate that plumbing. rvw never launches or manages an Agent. Use the `rvw` Skill for
11
+ exact-source inspection, final edits, authorized fixes, and synchronization.
11
12
 
12
13
  ## Fix policy
13
14
 
@@ -27,9 +28,9 @@ rvw replies remain allowed. Never resolve unless the user separately changes tha
27
28
 
28
29
  ## Durable task state
29
30
 
30
- Use the bundled `scripts/watch-state.mjs` with Node 24. Give it one task-private absolute SQLite path
31
- outside every reviewed repository. The tool stores identifiers, cursors, leases, retries, and generated
32
- post IDs, but never comment bodies or source. Separate watch tasks use separate state databases.
31
+ Use Node 24 and one task-private absolute SQLite path outside every reviewed repository. The state
32
+ tool stores identifiers, cursors, leases, retries, and generated post IDs, but never comment bodies or
33
+ source. Separate watch tasks use separate state databases.
33
34
 
34
35
  Initialize once after running `gh api user --jq .login`:
35
36
 
@@ -40,84 +41,174 @@ node '<SKILL_DIR>/scripts/watch-state.mjs' init \
40
41
  --own-mode 'investigate-and-reply'
41
42
  ```
42
43
 
43
- Omit `--expected-login` and force `investigate-and-reply` when identity is unavailable. On restart,
44
- run `recover`, then `status`. Initialization rejects a policy change for an existing task.
44
+ Omit `--expected-login` and force `investigate-and-reply` when identity is unavailable.
45
+ Initialization rejects a policy change for an existing task.
46
+
47
+ On restart, run `recover`, then `status`. Both expose `quarantinedBatches`; `status` also exposes
48
+ recoverable `inFlightBatches` with lease IDs and status posts.
45
49
 
46
50
  ```bash
47
51
  node '<SKILL_DIR>/scripts/watch-state.mjs' recover --state '<TASK_STATE_DB>'
48
52
  node '<SKILL_DIR>/scripts/watch-state.mjs' status --state '<TASK_STATE_DB>'
49
53
  ```
50
54
 
51
- Both outputs expose `quarantinedBatches`. Before resuming intake, edit every extant `statusPostId` in
52
- those batches to `⚠️ 対応を継続できませんでした` with the recorded error. Repeating this exact edit is
53
- safe and prevents an interrupted third attempt from leaving `確認中` indefinitely.
55
+ Before resuming intake, edit every extant `statusPostId` in quarantined batches to
56
+ `⚠️ 対応を継続できませんでした` with the recorded error. Repeating that exact edit is safe and
57
+ prevents an interrupted third attempt from leaving `確認中` indefinitely.
54
58
 
55
59
  ## Start or resume intake
56
60
 
57
- 1. Require `protocolVersion` 2 and `agent.transport`, `comment.watch`, `comment.read`, `comment.reply`,
58
- `comment.edit`, and `pullRequest.sync`; stop when `rvw agent status --json` selects `unavailable`.
59
- 2. Start `rvw comment watch --json-seq` when state has no cursor. Otherwise pass the exact saved cursor
60
- with `--after`. A cursorless start intentionally skips every existing comment.
61
- 3. Parse each RFC 7464 frame and pass that single JSON value to `ingest` over closed stdin:
61
+ Run the single preflight command. It concurrently detects `rvw` and verifies Node `>=24.15.0`.
62
+ Require `protocolVersion` 3 and `agent.transport`, `comment.watch`, `comment.read`, `comment.reply`,
63
+ `comment.edit`, `comment.codeReferences`, and `pullRequest.sync`, and report agent status and ping in
64
+ one JSON value. Stop when `ok` is false. A disconnected ping is diagnostic when status safely selects
65
+ direct-database transport; an unavailable selected transport is fatal.
66
+
67
+ ```bash
68
+ node '<SKILL_DIR>/scripts/preflight.mjs'
69
+ ```
70
+
71
+ Start the bundled driver with the state path. `--auto-ack` is the normal mode: it claims an eligible
72
+ PR batch, re-reads every thread, creates `🔎 確認中です…` (or restores it when retrying that batch),
73
+ records suppression, and emits
74
+ one `batch-acknowledged` JSON line containing the lease and operations. The first `watch-ready` line
75
+ means monitoring is established. The driver chooses cursorless start only when state has no cursor;
76
+ that intentionally skips all existing comments. Otherwise it resumes from the exact durable cursor.
77
+ Before each initial connection or reconnect, it auto-acknowledges any eligible event that was durably
78
+ ingested before an earlier driver interruption.
62
79
 
63
80
  ```bash
64
- node '<SKILL_DIR>/scripts/watch-state.mjs' ingest --state '<TASK_STATE_DB>'
81
+ node '<SKILL_DIR>/scripts/watch-driver.mjs' '<TASK_STATE_DB>' --auto-ack
65
82
  ```
66
83
 
67
- `ingest` commits an event and its cursor atomically. A crash before that commit causes rvw to replay the
68
- event. Deleted posts and already-suppressed task replies advance the cursor without creating work. Do
69
- not construct or edit cursors.
84
+ The driver polls rvw once per second. After an unexpected EOF or process exit it re-reads the durable
85
+ cursor and reconnects after 1, 2, 4, 8, then 16 seconds, capped at 30 seconds. Five short-lived
86
+ reconnect failures are terminal; a run lasting at least 30 seconds resets that budget. Protocol error
87
+ frames are terminal because retrying an invalid cursor or incompatible contract cannot recover.
70
88
 
71
- ## Batch and delegate
89
+ Driver exit codes are stable:
72
90
 
73
- Run `list` to find eligible PR batches. Re-read every returned comment URI with `rvw comment get`; the
74
- watch event is only a minimal trigger. Coalesce the current batch by PR and comment.
91
+ | Exit | Meaning |
92
+ | ---- | ------------------------------------------------------------------------------------------------------------------- |
93
+ | `0` | Graceful `SIGINT` / `SIGTERM` stop after forwarding termination to rvw. |
94
+ | `20` | rvw process, startup, protocol error frame, or reconnect budget failure. |
95
+ | `21` | Malformed, non-RFC-7464, or truncated watch output. |
96
+ | `22` | Durable state status or ingest failure. |
97
+ | `23` | Automatic acknowledgement failed; its claimed lease has already been returned to retry or quarantine when possible. |
98
+
99
+ Without `--auto-ack`, the driver emits `pending` lines and leaves the batch unclaimed. An external
100
+ monitor can also wait independently without hand-written polling:
75
101
 
76
102
  ```bash
77
- node '<SKILL_DIR>/scripts/watch-state.mjs' list --state '<TASK_STATE_DB>'
103
+ node '<SKILL_DIR>/scripts/watch-state.mjs' wait --state '<TASK_STATE_DB>'
78
104
  ```
79
105
 
80
- Choose the mode, then claim the PR. For a write-capable batch, pass the canonical `owner/repository` as
81
- `--write-key`; the state tool prevents another write-capable batch for that repository. Omit it for
82
- investigate-only work.
106
+ `wait` immediately returns existing eligible work or waits for the pending set to change from empty
107
+ to non-empty, then prints one JSON line with `pullRequests` and `pending`. Add `--follow` to emit every
108
+ later empty-to-non-empty transition. The driver and `ingest` commit each event and cursor atomically;
109
+ a crash before that commit causes rvw to replay it. Never construct or edit cursors.
110
+
111
+ ## Acknowledge and process a batch
112
+
113
+ With the normal auto-ack driver, consume its `batch-acknowledged` object directly. Each operation has
114
+ `commentRef`, the batch-operation-stable `idempotencyKey`, `statusPostId`, `acknowledgement`, `status`, and the
115
+ fresh `comment get` result as `thread`. A disappeared thread has `status: "gone"` and is not
116
+ acknowledged. If intake runs without auto-ack, invoke the same complete fast path once for the PR:
83
117
 
84
118
  ```bash
85
- node '<SKILL_DIR>/scripts/watch-state.mjs' claim \
119
+ node '<SKILL_DIR>/scripts/auto-ack.mjs' \
86
120
  --state '<TASK_STATE_DB>' \
87
121
  --pull-request '<PR_URL>'
88
122
  ```
89
123
 
90
- Immediately acknowledge every extant claimed thread before delegating. Each operation contains a
91
- thread-stable `idempotencyKey` and nullable `statusPostId`:
124
+ For a null `statusPostId`, auto-ack sends exactly `{ "body": "🔎 確認中です…",
125
+ "idempotencyKey": "<BATCH_OPERATION_KEY>" }` to `rvw comment reply`, then records the returned post. It omits
126
+ `authorLabel` and `relatedCommitOid`, so an uncertain retry has the identical payload. For an existing
127
+ status post in the same retried batch it sends
128
+ `{ "body": "🔎 確認中です…", "relatedCommitOid": null }` to `comment edit`. A later batch for the
129
+ same thread has a new key and null `statusPostId`, so it creates another acknowledgement and never
130
+ rewrites the previous final answer.
131
+ The acknowledgement's watch event is suppressed even when intake queued it before the post ID was
132
+ recorded.
92
133
 
93
- - When `statusPostId` is null, create exactly `🔎 確認中です…` with `rvw comment reply`. Pass the
94
- operation key as `idempotencyKey`; omit `authorLabel` and `relatedCommitOid` so an uncertain retry has
95
- the identical payload. Record the returned post with `ack` over closed stdin:
134
+ The auto-ack claim initially has no repository write reservation. This lets acknowledgement remain
135
+ fast without a live GitHub round trip. If the batch later passes every fix-and-push check, reserve its
136
+ verified head repository immediately before the first code write:
96
137
 
97
138
  ```bash
98
- node '<SKILL_DIR>/scripts/watch-state.mjs' ack \
99
- --state '<TASK_STATE_DB>' --lease '<LEASE_ID>'
139
+ node '<SKILL_DIR>/scripts/watch-state.mjs' reserve-write \
140
+ --state '<TASK_STATE_DB>' \
141
+ --lease '<LEASE_ID>' \
142
+ --write-key '<HEAD_OWNER>/<HEAD_REPOSITORY>'
100
143
  ```
101
144
 
102
- Pass `{ "commentRef": "rvw://comment/...", "postId": "..." }`. This immediately suppresses the
103
- acknowledgement's own watch event, including when intake queued it first.
145
+ The unique reservation prevents two leases from writing the same repository. A manually invoked
146
+ `auto-ack` may instead receive `--write-key` when that identity was already verified.
147
+
148
+ ## Investigate directly or delegate
149
+
150
+ For an `investigate-and-reply` batch containing only one or two comments with a focused source scope,
151
+ the parent may investigate directly when doing so will not materially delay intake handling. Do not
152
+ pay worker startup and result-relay cost for those small batches. Delegate broader investigation,
153
+ multiple unrelated comments, or any authorized fix-and-push batch to one fresh worker per PR. The
154
+ driver continues intake independently while the parent or worker investigates.
155
+
156
+ Workers never access the task state or post rvw replies. Give a worker the raw comment URIs, policy,
157
+ expected login, repository location, live head identity when relevant, lease ID, and one absolute
158
+ result path outside the reviewed repository. Require an atomic write (temporary sibling followed by
159
+ rename) of exactly this final JSON shape:
160
+
161
+ ```json
162
+ {
163
+ "leaseId": "<LEASE_ID>",
164
+ "pullRequest": "https://github.com/owner/repository/pull/123",
165
+ "outcomes": [
166
+ {
167
+ "commentRef": "rvw://comment/uuid",
168
+ "body": "📝 調査結果\n\nThe failure is handled by [the retry guard](rvw-ref:retry-guard).",
169
+ "relatedCommitOid": "0123456789abcdef0123456789abcdef01234567",
170
+ "references": [
171
+ {
172
+ "id": "retry-guard",
173
+ "label": "Retry guard",
174
+ "path": "src/request-handler.ts",
175
+ "startLine": 18,
176
+ "endLine": 24
177
+ }
178
+ ],
179
+ "pushStatus": "not-needed"
180
+ }
181
+ ]
182
+ }
183
+ ```
104
184
 
105
- - When `statusPostId` is present, replace that post with the same acknowledgement through
106
- `rvw comment edit <URI> --post <STATUS_POST_ID> --stdin --json`; set `relatedCommitOid` to null. This
107
- reuses one status reply when a human adds another reply to the same thread.
185
+ `pushStatus` is `not-attempted`, `not-needed`, or `pushed`. `relatedCommitOid` is the exact available PR
186
+ commit containing every referenced path and may identify investigation evidence even when no change
187
+ was made. Set it to null only when `references` is empty. `references` is always the complete array for
188
+ that outcome. The worker's completion notification only signals that the file is ready. The parent
189
+ reads and validates the file after that notification and never depends on relayed message text for the
190
+ result. Accept no progress, plans, or partial findings as the final result.
108
191
 
109
- Do not acknowledge a thread that disappeared before its initial read. Give one fresh worker the raw
110
- comment URIs, policy, expected login, repository location, and live head identity. Keep the parent as
111
- sole watcher and state owner. Accept only a final structured result with one concise outcome body per
112
- comment, commit OID, and push status. Do not post other progress, plans, or partial findings.
192
+ For every concrete claim about code behavior, an implemented change, or relevant test coverage, use
193
+ typed references by default so the reviewer can open the exact evidence. Select the smallest useful
194
+ committed range, include a signature plus the relevant body for multi-line behavior, link every
195
+ declaration from `body` as `rvw-ref:<referenceId>`, and keep IDs unique within the post. A repository
196
+ comment target already opens its exact source; do not duplicate it unless a separately labeled range
197
+ adds navigation value. Omit references only for outcomes without useful code evidence, uncommitted
198
+ evidence, terminal errors, or target-only evidence where another link would not help navigation.
113
199
 
114
- Re-read each complete thread immediately before applying the result. Replace its recorded status post
115
- with exactly one final outcome:
200
+ Re-read each extant thread immediately before applying a direct or file result. Replace its recorded
201
+ status post with exactly one final outcome:
116
202
 
117
203
  - `✅ 対応しました` followed by the change, commit, and test result.
118
204
  - `📝 調査結果` followed by the conclusion when no code change was made.
119
205
  - `⚠️ 対応を継続できませんでした` followed by the terminal reason.
120
206
 
207
+ Validate the outcome's body, `relatedCommitOid`, and complete `references` array against the freshly
208
+ read thread, then pass all three fields to `rvw comment edit`. A result without references must send
209
+ `references: []`; set `relatedCommitOid` to null unless the post needs that commit for repository links
210
+ or images. Never leave references from the acknowledgement or a previous retry on the status post.
211
+
121
212
  Finish the lease only after every required final edit succeeds:
122
213
 
123
214
  ```bash
@@ -125,28 +216,32 @@ node '<SKILL_DIR>/scripts/watch-state.mjs' complete \
125
216
  --state '<TASK_STATE_DB>' --lease '<LEASE_ID>'
126
217
  ```
127
218
 
128
- Pass `{ "postIds": [] }` over closed stdin. The field remains available to suppress any exceptional
219
+ Pass `{ "postIds": [] }` over closed stdin. The field remains available to suppress exceptional
129
220
  additional task-created posts. If a thread or its recorded status post disappeared during work,
130
- complete it without creating a replacement and report it as gone. A watched status-post deletion
131
- clears the mapping and rotates its idempotency key, so a later human follow-up can create a fresh one.
221
+ complete it without creating a replacement and report it as gone. Comment and reply bodies are UTF-8
222
+ GFM Markdown up to 64 KiB, not 4 KiB; a 4093-byte result is within the contract.
132
223
 
133
224
  ## Choose the worker mode
134
225
 
135
- Use `fix-and-push` only when all checks succeed immediately before the first write:
226
+ Use `fix-and-push` only when all checks succeed immediately before reserving and making the first
227
+ write:
136
228
 
137
229
  1. The immutable task policy allows it.
138
230
  2. `gh api user --jq .login` still equals the expected login, case-insensitively.
139
231
  3. `rvw comment get <URI> --live --json` reports the same PR author login.
140
232
  4. Live `headRepository.owner`, `headRepository.name`, branch, and head OID are all present.
141
233
  5. The intended push URL, branch, and current remote head exactly match those live values.
234
+ 6. `reserve-write` succeeds for the live head repository.
142
235
 
143
- Otherwise downgrade to `investigate-and-reply`. Never infer ownership or a push target from the base
144
- repository, branch name alone, local Git author, remote name, or rvw `authorLabel`.
236
+ Otherwise use `investigate-and-reply`. Never infer ownership or a push target from the base repository,
237
+ branch name alone, local Git author, remote name, or rvw `authorLabel`.
145
238
 
146
239
  ### Investigate and reply
147
240
 
148
- Inspect exact and surrounding source read-only and return one concise final outcome per affected
149
- comment. The parent edits the recorded status post; do not add another final reply.
241
+ Inspect exact and surrounding source read-only and produce one concise final outcome per affected
242
+ comment. Use the exact commit that supports the conclusion as `relatedCommitOid` and follow the code
243
+ evidence defaults above even though no commit was pushed. The parent edits the recorded status post;
244
+ do not add another final reply.
150
245
 
151
246
  ### Fix and push an owned PR
152
247
 
@@ -158,18 +253,73 @@ retrying; never repeat the implementation blindly.
158
253
 
159
254
  After GitHub exposes the pushed head, run `rvw pr sync --repository '<WORKTREE>' --stdin --json`
160
255
  without comment updates. Then edit each status post with its final body and the synchronized head as
161
- `relatedCommitOid`. If no code change is appropriate, edit the status post without a related commit.
256
+ `relatedCommitOid`. Follow the code evidence defaults above: link the implemented behavior and relevant
257
+ test ranges from the final body and include the post's complete typed `references` array at that exact
258
+ head. If the outcome has no useful code evidence, send `references: []` explicitly rather than
259
+ retaining stale declarations.
162
260
 
163
261
  ## Failure and stop
164
262
 
165
- Report a failed lease through `fail` with `{ "error": "...", "retryable": true }` over stdin. The
166
- state tool retains the same batch, status posts, and idempotency keys, retries after about 10 seconds
167
- and then 1 minute, and quarantines it after the third failed attempt. Leave `🔎 確認中です…` unchanged
168
- for a scheduled retry. Before a non-retryable failure or the third failed attempt, edit every extant
169
- status post to the terminal warning form, then call `fail`. On a recovered retry, immediately restore
170
- the acknowledgement before work. `fail` returns affected operations when it quarantines a batch;
171
- `recover` and `status` expose all quarantined operations for restart cleanup. Continue unrelated PRs.
172
-
173
- On graceful stop, stop dispatching, let the active write operation reach a safe boundary, terminate the
174
- watch process, and report `status`. Resume with the stored cursor and `recover`; never start the same
175
- task without its state database.
263
+ Report a failed lease through `fail` with `{ "error": "...", "retryable": true }` over closed stdin.
264
+ The state tool retains the same batch, status posts, and idempotency keys, retries after about 10
265
+ seconds and then 1 minute, and quarantines it after the third failed attempt. Leave
266
+ `🔎 確認中です…` unchanged for a scheduled retry. Before a non-retryable failure or the third failed
267
+ attempt, edit every extant status post to the terminal warning form, then call `fail`. On a recovered
268
+ retry, auto-ack restores the acknowledgement before work. Continue unrelated PRs.
269
+
270
+ On graceful stop, stop dispatching, let active writes reach a safe boundary, terminate the driver,
271
+ and report `status`. Resume with the stored cursor and `recover`; never start the same task twice with
272
+ one state database.
273
+
274
+ ## Bundled CLI contract reference
275
+
276
+ Successful commands write exactly one newline-terminated JSON object to stdout, except `wait --follow`
277
+ and the long-lived driver, which write one object per transition. State-command errors and driver
278
+ fatal errors write JSON to stderr with a nonzero exit; auto-ack returns its structured failure on
279
+ stdout with a nonzero exit. Commands marked with stdin read one complete JSON object through EOF.
280
+
281
+ | Command | Arguments | stdin JSON | Success JSON |
282
+ | --------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
283
+ | `init` | `--state PATH [--expected-login LOGIN] [--own-mode investigate-and-reply\|fix-and-push]` | none | `{ok,state,taskId,databaseId,cursor,expectedGitHubLogin,ownPullRequests,batches,inFlightBatches,quarantinedBatches}` |
284
+ | `ingest` | `--state PATH` | `ready`, `comment-posted`, or `stopped` frame from rvw | `{ok,status,cursor[,sequence]}`; event and cursor commit atomically |
285
+ | `list` | `--state PATH` | none | `{ok,pending:[{pullRequest,batchId,eventCount,firstSequence,commentRefs}]}` |
286
+ | `wait` | `--state PATH [--interval-ms N] [--follow]` | none | `{ok,type:"pending",pullRequests,pending}` on empty-to-non-empty |
287
+ | `claim` | `--state PATH --pull-request URL [--write-key owner/repo]` | none | `{ok,leaseId,batchId,pullRequest,attempts,writeKey,events,operations}` |
288
+ | `reserve-write` | `--state PATH --lease ID --write-key owner/repo` | none | `{ok,leaseId,batchId,pullRequest,writeKey,status}` |
289
+ | `ack` | `--state PATH --lease ID` | `{commentRef,postId}` | `{ok,batchId,commentRef,statusPostId,status}` |
290
+ | `complete` | `--state PATH --lease ID` | `{postIds:string[]}` | `{ok,batchId,status:"completed",suppressedPostIds}` |
291
+ | `fail` | `--state PATH --lease ID` | `{error:string,retryable:boolean}` | `{ok,batchId,status,attempts,nextAttemptAt[,operations]}` |
292
+ | `recover` | `--state PATH` | none | `{ok,recovered,pending,quarantined,quarantinedBatches}` |
293
+ | `status` | `--state PATH` | none | task policy, cursor, batch counts, `inFlightBatches`, and `quarantinedBatches` |
294
+
295
+ Frame schemas accepted by `ingest`:
296
+
297
+ ```json
298
+ { "type": "ready", "databaseId": "32 lowercase hex", "cursor": "opaque", "anchoredAtCurrent": true }
299
+ ```
300
+
301
+ ```json
302
+ {
303
+ "type": "comment-posted",
304
+ "cursor": "opaque",
305
+ "event": {
306
+ "sequence": 1,
307
+ "postId": "id",
308
+ "commentRef": "rvw://comment/uuid",
309
+ "pullRequestUrl": "https://github.com/owner/repo/pull/123",
310
+ "createdAt": "ISO-8601",
311
+ "deleted": false
312
+ }
313
+ }
314
+ ```
315
+
316
+ Claim `operations` are `{commentRef,idempotencyKey,statusPostId}`. Claim `events` are
317
+ `{sequence,postId,commentRef,pullRequestUrl}`. State schema additions are created with
318
+ idempotent local migrations; existing state databases remain readable and retain their cursors,
319
+ leases, and unfinished batch keys and status posts.
320
+
321
+ | Script | Invocation | Output |
322
+ | --------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
323
+ | Preflight | `node scripts/preflight.mjs` | One aggregate `{ok,node,rvw,agent,checks,errors}` object. |
324
+ | Driver | `node scripts/watch-driver.mjs STATE [--auto-ack]` | `watch-ready`, `pending`, `batch-acknowledged`, and reconnect JSON lines. |
325
+ | Auto-ack | `node scripts/auto-ack.mjs --state STATE --pull-request URL [--write-key owner/repo]` | Claimed lease plus `{events,operations}`; each operation includes the fresh thread or `gone`. |
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { runRvw, successfulJson } from "./rvw-command.mjs";
6
+
7
+ const ACKNOWLEDGEMENT_BODY = "🔎 確認中です…";
8
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
9
+ const stateScript = path.join(scriptDirectory, "watch-state.mjs");
10
+
11
+ function fail(message, details) {
12
+ const error = new Error(message);
13
+ error.details = details;
14
+ throw error;
15
+ }
16
+
17
+ function parseOptions(values) {
18
+ const options = {};
19
+ for (let index = 0; index < values.length; index += 2) {
20
+ const key = values[index];
21
+ const value = values[index + 1];
22
+ if (!key?.startsWith("--") || value === undefined || value.startsWith("--")) {
23
+ fail(`Expected --name value, received ${key ?? ""}`);
24
+ }
25
+ options[key.slice(2)] = value;
26
+ }
27
+ return options;
28
+ }
29
+
30
+ function required(options, key) {
31
+ const value = options[key];
32
+ if (typeof value !== "string" || value.length === 0) fail(`--${key} is required`);
33
+ return value;
34
+ }
35
+
36
+ async function runState(state, command, args = [], input) {
37
+ const { spawn } = await import("node:child_process");
38
+ const child = spawn(process.execPath, [stateScript, command, "--state", state, ...args], {
39
+ stdio: ["pipe", "pipe", "pipe"],
40
+ });
41
+ const stdout = [];
42
+ const stderr = [];
43
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
44
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
45
+ child.stdin.end(input === undefined ? undefined : JSON.stringify(input));
46
+ const code = await new Promise((resolve, reject) => {
47
+ child.once("error", reject);
48
+ child.once("close", resolve);
49
+ });
50
+ const stdoutText = Buffer.concat(stdout).toString("utf8");
51
+ const stderrText = Buffer.concat(stderr).toString("utf8");
52
+ let json = null;
53
+ try {
54
+ json = JSON.parse(stdoutText);
55
+ } catch {
56
+ // Report the original output below.
57
+ }
58
+ if (code !== 0 || !json?.ok) {
59
+ fail(`watch-state ${command} failed`, { code, stdout: stdoutText, stderr: stderrText });
60
+ }
61
+ return json;
62
+ }
63
+
64
+ function rvwFailure(command, result) {
65
+ return {
66
+ command,
67
+ exitCode: result.code,
68
+ signal: result.signal,
69
+ output: result.json,
70
+ stderr: result.stderr.trim() || null,
71
+ stdout: result.json ? null : result.stdout.trim() || null,
72
+ };
73
+ }
74
+
75
+ function isGone(result) {
76
+ return (
77
+ result.json?.ok === false &&
78
+ ["COMMENT_NOT_FOUND", "NOT_FOUND"].includes(result.json?.error?.code)
79
+ );
80
+ }
81
+
82
+ async function acknowledgeOperation(state, leaseId, operation, threadResult) {
83
+ if (isGone(threadResult)) {
84
+ return {
85
+ ...operation,
86
+ status: "gone",
87
+ acknowledgement: "skipped",
88
+ thread: null,
89
+ };
90
+ }
91
+ if (!successfulJson(threadResult)) {
92
+ fail(`rvw comment get failed for ${operation.commentRef}`, {
93
+ failure: rvwFailure("comment get", threadResult),
94
+ });
95
+ }
96
+ if (operation.statusPostId === null) {
97
+ const reply = await runRvw(["comment", "reply", operation.commentRef, "--stdin", "--json"], {
98
+ input: {
99
+ body: ACKNOWLEDGEMENT_BODY,
100
+ idempotencyKey: operation.idempotencyKey,
101
+ },
102
+ });
103
+ if (!successfulJson(reply) || typeof reply.json?.post?.id !== "string") {
104
+ fail(`rvw comment reply failed for ${operation.commentRef}`, {
105
+ failure: rvwFailure("comment reply", reply),
106
+ });
107
+ }
108
+ await runState(state, "ack", ["--lease", leaseId], {
109
+ commentRef: operation.commentRef,
110
+ postId: reply.json.post.id,
111
+ });
112
+ return {
113
+ ...operation,
114
+ statusPostId: reply.json.post.id,
115
+ status: "acknowledged",
116
+ acknowledgement: "created",
117
+ thread: threadResult.json,
118
+ };
119
+ }
120
+ const edit = await runRvw(
121
+ [
122
+ "comment",
123
+ "edit",
124
+ operation.commentRef,
125
+ "--post",
126
+ operation.statusPostId,
127
+ "--stdin",
128
+ "--json",
129
+ ],
130
+ { input: { body: ACKNOWLEDGEMENT_BODY, relatedCommitOid: null } },
131
+ );
132
+ if (!successfulJson(edit)) {
133
+ fail(`rvw comment edit failed for ${operation.commentRef}`, {
134
+ failure: rvwFailure("comment edit", edit),
135
+ });
136
+ }
137
+ await runState(state, "ack", ["--lease", leaseId], {
138
+ commentRef: operation.commentRef,
139
+ postId: operation.statusPostId,
140
+ });
141
+ return {
142
+ ...operation,
143
+ status: "acknowledged",
144
+ acknowledgement: "restored",
145
+ thread: threadResult.json,
146
+ };
147
+ }
148
+
149
+ async function main() {
150
+ const options = parseOptions(process.argv.slice(2));
151
+ const state = path.resolve(required(options, "state"));
152
+ const pullRequest = required(options, "pull-request");
153
+ let claimed = null;
154
+ try {
155
+ const claimArgs = ["--pull-request", pullRequest];
156
+ if (options["write-key"]) claimArgs.push("--write-key", options["write-key"]);
157
+ claimed = await runState(state, "claim", claimArgs);
158
+ const threadResults = await Promise.all(
159
+ claimed.operations.map((operation) =>
160
+ runRvw(["comment", "get", operation.commentRef, "--json"]),
161
+ ),
162
+ );
163
+ const operations = [];
164
+ for (let index = 0; index < claimed.operations.length; index += 1) {
165
+ operations.push(
166
+ await acknowledgeOperation(
167
+ state,
168
+ claimed.leaseId,
169
+ claimed.operations[index],
170
+ threadResults[index],
171
+ ),
172
+ );
173
+ }
174
+ process.stdout.write(
175
+ `${JSON.stringify({
176
+ ok: true,
177
+ type: "acknowledged",
178
+ leaseId: claimed.leaseId,
179
+ batchId: claimed.batchId,
180
+ pullRequest: claimed.pullRequest,
181
+ attempts: claimed.attempts,
182
+ writeKey: claimed.writeKey,
183
+ events: claimed.events,
184
+ operations,
185
+ })}\n`,
186
+ );
187
+ } catch (error) {
188
+ let leaseFailure = null;
189
+ if (claimed?.leaseId) {
190
+ try {
191
+ leaseFailure = await runState(state, "fail", ["--lease", claimed.leaseId], {
192
+ error: error instanceof Error ? error.message : String(error),
193
+ retryable: true,
194
+ });
195
+ } catch (failError) {
196
+ leaseFailure = { ok: false, error: String(failError) };
197
+ }
198
+ }
199
+ process.stdout.write(
200
+ `${JSON.stringify({
201
+ ok: false,
202
+ error: error instanceof Error ? error.message : String(error),
203
+ details: error?.details ?? null,
204
+ leaseFailure,
205
+ })}\n`,
206
+ );
207
+ process.exitCode = 1;
208
+ }
209
+ }
210
+
211
+ await main();