opencode-forgekeeper 2.4.1 → 3.0.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/README.md +494 -43
- package/dist/index.js +2854 -509
- package/dist/tui.js +819 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
An [OpenCode](https://opencode.ai) plugin that tracks the issues or PRs/MRs a session works on.
|
|
4
4
|
It prefixes the session title with their references, shows the PR/MR status in the prompt
|
|
5
|
-
footer, and
|
|
5
|
+
footer, and surfaces new review feedback in a persistent inbox. It supports GitLab and GitHub. The status
|
|
6
6
|
and review notifications are optional; see [Keep only session titles](#keep-only-session-titles).
|
|
7
7
|
|
|
8
8
|
Requires OpenCode 2.2 or later. It replaces
|
|
@@ -43,6 +43,32 @@ toasts.
|
|
|
43
43
|
Without a forge CLI, titles still follow targets and branches, and `/forge-status` reports
|
|
44
44
|
what's missing, such as `glab is not installed`.
|
|
45
45
|
|
|
46
|
+
## Upgrade from forgekeeper 2.x
|
|
47
|
+
|
|
48
|
+
Forgekeeper 3 changes where review notifications go, and renames its options:
|
|
49
|
+
|
|
50
|
+
- **Notifications:** review feedback no longer resumes the session or asks the agent to
|
|
51
|
+
address it. It goes to the `/forge-feedback` inbox. To also post it to the session, set
|
|
52
|
+
`delivery.reviews` to `"passive"` or `"summary"`. See
|
|
53
|
+
[Review notifications](#review-notifications).
|
|
54
|
+
- **Options:** Forgekeeper no longer reads the former option names. It shows an error toast
|
|
55
|
+
that names each one, and uses that option's default instead. For example, a former
|
|
56
|
+
`reviewStatus: false` leaves status on, so rename it to `status: false` before you upgrade.
|
|
57
|
+
- **Monitoring:** Forgekeeper also watches recent sessions with PR/MR targets that no CLI
|
|
58
|
+
has shown, and announces conflicts, failed jobs, and merges of their targets. See
|
|
59
|
+
[Which sessions the server watches](#which-sessions-the-server-watches) and
|
|
60
|
+
[Merge request updates](#merge-request-updates). Set `watch.lifecycle` to `false` to
|
|
61
|
+
stop the merge request updates.
|
|
62
|
+
|
|
63
|
+
Rename the former options as follows:
|
|
64
|
+
|
|
65
|
+
| Former option | Now |
|
|
66
|
+
| ------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
67
|
+
| `reviewStatus` | `status` |
|
|
68
|
+
| `notifyHumanReviews` | `watch.humanReviews` |
|
|
69
|
+
| `notifyAutomatedReviews`, `notifyDuoReview` | `watch.automatedReviews`, and `watch.reviewBots` for review bots |
|
|
70
|
+
| `resumeSession` | `delivery.reviews`: `"summary"` starts a turn, and `"passive"` queues for the next turn |
|
|
71
|
+
|
|
46
72
|
## Session targets
|
|
47
73
|
|
|
48
74
|
The server registers the `set_session_target` tool and adds instructions to the agent's
|
|
@@ -189,9 +215,38 @@ The server caches each session's status and polls every 2 minutes while a CLI sh
|
|
|
189
215
|
PR/MR, or every 30 seconds while an automated review runs. Each change reaches the CLIs as
|
|
190
216
|
an RPC event.
|
|
191
217
|
|
|
218
|
+
Sessions in the same checkout share cached status and human-feedback fetches for the same
|
|
219
|
+
forge, host, project, and PR/MR number. Overlapping requests share one in-flight fetch.
|
|
220
|
+
Successful status results remain fresh for the polling interval, capped at 30 seconds
|
|
221
|
+
while an automated review runs. Complete human-feedback results remain fresh for 60 seconds.
|
|
222
|
+
Failures and incomplete feedback are not retained for reuse. Each cache keeps at most
|
|
223
|
+
1,000 entries and is cleared when the plugin stops.
|
|
224
|
+
|
|
225
|
+
Session timers, review baselines, and acknowledgements remain independent. Manual refresh
|
|
226
|
+
invalidates the selected session's target caches and feedback fetch timestamps. Completed turns invalidate the checkout's caches
|
|
227
|
+
before notifying watched sessions, so changes such as pushes can be observed. Caches are
|
|
228
|
+
not shared between checkouts, whose CLI credentials and repository context may differ.
|
|
229
|
+
|
|
192
230
|
### Which sessions the server watches
|
|
193
231
|
|
|
194
|
-
The server
|
|
232
|
+
The server discovers recent sessions at startup, every 5 minutes, and after target changes
|
|
233
|
+
or completed turns. A session qualifies when it is a non-archived root session in the
|
|
234
|
+
checkout, has an explicit PR/MR target, and has a user message in the last 72 hours.
|
|
235
|
+
A compaction drops earlier messages from the session's context, so when the context has no
|
|
236
|
+
user message, the newest compaction counts instead, since compactions run only during
|
|
237
|
+
turns. Viewing a tab, changing its title, or receiving a background notification does not
|
|
238
|
+
extend that window. Sessions without readable conversation activity are skipped. Discovery
|
|
239
|
+
reads conversation timestamps locally; it does not send conversations to a model.
|
|
240
|
+
|
|
241
|
+
Discovered sessions poll without a CLI lease while at least one explicit target is open.
|
|
242
|
+
Targets that couldn't be resolved, such as while the forge returns nothing for them, keep
|
|
243
|
+
being retried, even when the session's other targets closed or merged. While none of the
|
|
244
|
+
targets resolves, retries back off like failed lookups. Once every target is found and
|
|
245
|
+
closed or merged, polling stops. Discovery reevaluates the activity window
|
|
246
|
+
every 5 minutes. It runs only while OpenCode has this checkout's plugin instance loaded;
|
|
247
|
+
it is not a separate scheduler and cannot wake an unloaded location or stopped service.
|
|
248
|
+
|
|
249
|
+
The server also watches sessions that a CLI has shown since it started. The CLI renews a
|
|
195
250
|
lease on each of them every minute, and a lease that isn't renewed for 3 minutes ends, such
|
|
196
251
|
as after the CLI exits. A session keeps polling while it's on screen in any CLI. A hidden
|
|
197
252
|
session with a lease keeps polling while it has a running automated review, human feedback
|
|
@@ -209,7 +264,7 @@ lookup or fetch ran is left to its new checkout. A review that finishes during t
|
|
|
209
264
|
go unannounced, because the new instance never saw it running. If the session can't be
|
|
210
265
|
read, the notification is retried later instead of being dropped.
|
|
211
266
|
|
|
212
|
-
After that read, the server checks the targets again and
|
|
267
|
+
After that read, the server checks the targets again and records feedback for the
|
|
213
268
|
PRs/MRs that are still targets. A target removed in the meantime isn't mentioned.
|
|
214
269
|
|
|
215
270
|
When the server plugin stops, such as on a reload, no new lookup, fetch, notification,
|
|
@@ -222,30 +277,185 @@ sent again.
|
|
|
222
277
|
|
|
223
278
|
## Review notifications
|
|
224
279
|
|
|
225
|
-
For each explicit target that is an open PR/MR, the server
|
|
280
|
+
For each explicit target that is an open PR/MR, the server records new review feedback.
|
|
226
281
|
A target that merges or closes gets no more notifications, and a notification waiting for a
|
|
227
282
|
retry is dropped.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
`
|
|
232
|
-
|
|
233
|
-
|
|
283
|
+
Feedback goes to a persistent inbox. The `delivery.reviews` option sets what else
|
|
284
|
+
an update does:
|
|
285
|
+
|
|
286
|
+
- `inbox`, the default, posts nothing to the session.
|
|
287
|
+
- `passive` queues a session message with `resume: false`, which starts no turn. It
|
|
288
|
+
doesn't mark the session unread, because the TUI marks sessions unread only when a
|
|
289
|
+
turn ends. When something else wakes the session, the message can run as its own
|
|
290
|
+
step; Forgekeeper then sends only the notice, without tools or the session's history.
|
|
291
|
+
- `summary` queues a message that starts a summary turn, described below. A turn that
|
|
292
|
+
ends marks the session unread. Updates without comments, such as Copilot reviews,
|
|
293
|
+
get a passive message instead.
|
|
294
|
+
|
|
295
|
+
Session messages contain the notification title, when the newest comment was written, and
|
|
296
|
+
links, not review bodies or instructions to act. Updates without comments, such as Copilot
|
|
297
|
+
reviews, show when Forgekeeper detected them instead. Times are absolute and local, with
|
|
298
|
+
their time zone, such as `Oct 28, 2026 at 3:25 PM GMT+1`, so they stay accurate in the
|
|
299
|
+
session's history. `/forge-feedback` shows the same time, and a summary starts by saying it.
|
|
300
|
+
|
|
301
|
+
Each update keeps the mode it was recorded in, so changing `delivery.reviews`
|
|
302
|
+
affects later updates only: switching to `summary` never starts turns for updates already
|
|
303
|
+
in the inbox. Summarize those with Ctrl+R instead. Stable message IDs deduplicate
|
|
304
|
+
automatic retries, including after reloads. Updates that haven't reached their session are
|
|
305
|
+
retried every minute and after a reload, each on its own, so one that keeps failing doesn't
|
|
306
|
+
hold the others back. Delivery doesn't hold up the inbox: `/forge-feedback`, pausing, and
|
|
307
|
+
acknowledging work while a summary reads its comments, and an update in flight isn't sent
|
|
308
|
+
again by a retry. An update acknowledged or cleared while its comments are read isn't
|
|
309
|
+
posted, and one paused meanwhile waits until monitoring resumes. An update whose session was deleted, archived, or moved, or that no
|
|
310
|
+
longer targets the PR/MR, is dropped, including when that changed while its comments were
|
|
311
|
+
read. Switching `delivery.reviews` to `inbox` stops retries of review updates, and
|
|
312
|
+
leaves them undelivered, so they go again once it's switched back. Merge request updates
|
|
313
|
+
follow `delivery.lifecycle` the same way. A summary requested from `/forge-feedback` is a
|
|
314
|
+
new request each time.
|
|
315
|
+
|
|
316
|
+
### Summary turns
|
|
317
|
+
|
|
318
|
+
A summary turn runs after any active turn finishes, including its tool calls. Forgekeeper
|
|
319
|
+
reads the announced comments, at most 20 of them and 4,000 characters each, with context
|
|
320
|
+
the summary can't fetch itself:
|
|
321
|
+
|
|
322
|
+
- Earlier replies in each comment's thread, at most 5 of 1,000 characters each. GitHub
|
|
323
|
+
provides only the comment a reply answers.
|
|
324
|
+
- The code an inline comment is on, at most 3,000 characters. On GitLab, that's 10 lines
|
|
325
|
+
on either side of the commented line, from the MR's head commit, or from its base commit
|
|
326
|
+
for a removed line, for at most 10 files. On GitHub, it's the end of the diff hunk. A
|
|
327
|
+
file that can't be read leaves out only its code.
|
|
328
|
+
|
|
329
|
+
The PR/MR's author wrote that code, so the summary treats it as untrusted, like the
|
|
330
|
+
comments. Forgekeeper keeps all of this in plugin storage until the next user prompt, or for
|
|
331
|
+
at most 7 days. It never enters the session's history, where later turns with tools could
|
|
332
|
+
read it.
|
|
333
|
+
|
|
334
|
+
#### Linked notes
|
|
335
|
+
|
|
336
|
+
On GitLab, review comments often link to supporting notes, such as an issue in another
|
|
337
|
+
project with further details. A summary can read those with one tool,
|
|
338
|
+
`fetch_linked_review_note(url)`, which accepts only links that the announced comments'
|
|
339
|
+
own text contains:
|
|
340
|
+
|
|
341
|
+
- HTTPS links without credentials or a port, on the MR's GitLab host, to an issue, work
|
|
342
|
+
item, or MR note (`…/-/issues/1#note_2`). Notes in other projects count. Links in thread
|
|
343
|
+
replies, in code, or in a linked note don't, so nothing a linked note says can widen
|
|
344
|
+
what the summary reads.
|
|
345
|
+
- At most 5 links per summary, each at most 300 characters, without duplicates or links
|
|
346
|
+
to the announced comments.
|
|
347
|
+
|
|
348
|
+
Forgekeeper stores these links with the summary's record before posting its message. The
|
|
349
|
+
tool reads a note with one fixed GraphQL query through `glab`, and keeps it only if the
|
|
350
|
+
note GitLab returns, of any note type, has the link's note ID and names the same project,
|
|
351
|
+
issue or MR, and note. GitLab returns nothing for notes you can't read. The model supplies
|
|
352
|
+
only the link: never a host, query, method, or header.
|
|
353
|
+
|
|
354
|
+
Each summary gets 5 calls. Every call counts, including repeats, refused links, and failed
|
|
355
|
+
reads, and is spent before the read, so neither concurrent calls nor a reload restore it.
|
|
356
|
+
A linked note keeps at most 4,000 characters, and all of them together at most 12,000.
|
|
357
|
+
The comments come first in the request's 40,000-character budget. Linked notes, with
|
|
358
|
+
their links and status lines, get only what the comments leave; notes that don't fit are
|
|
359
|
+
reported as left out, and without room for the readable links, the request offers no tool.
|
|
360
|
+
|
|
361
|
+
The tool returns only a receipt that names the link, which is what stays in the session's
|
|
362
|
+
history. The note's text goes to the summary's record, and from there only into the
|
|
363
|
+
summary's next requests, as a `<linked-note>` block. Every request that can hold linked
|
|
364
|
+
notes, including the last one, which has no tool, keeps the instructions that treat them
|
|
365
|
+
as untrusted like the comments.
|
|
366
|
+
|
|
367
|
+
The tool works only for the summary of the session's latest model request, and checks
|
|
368
|
+
the session's context again on each call, so a user prompt ends its access even before
|
|
369
|
+
the next request. A record older than 7 days counts as missing in every check, not just
|
|
370
|
+
at the next startup. After a reload, the tool refuses until the next request. A stopped
|
|
371
|
+
instance writes nothing, and cleanup waits for its calls, so a late call can't overwrite
|
|
372
|
+
the budget its replacement uses. Ordinary requests never include the tool, and summaries
|
|
373
|
+
without links, notices, and steps whose summary can't be read get no tools at all.
|
|
374
|
+
GitHub summaries don't read linked notes.
|
|
375
|
+
|
|
376
|
+
Forgekeeper recognizes a step whose input is one of its messages in two independent ways:
|
|
377
|
+
|
|
378
|
+
- Every message it posts ends with a tag naming its ID, such as
|
|
379
|
+
`[forgekeeper:msg_forgekeeper_…]`, which the context hook finds in the request it's
|
|
380
|
+
about to send. This doesn't depend on stored state, so a failed restore or an expired
|
|
381
|
+
record can't let the message run as an ordinary request. A tag in a user prompt, such
|
|
382
|
+
as a pasted transcript, doesn't count when the session's context shows the prompt.
|
|
383
|
+
- It tracks every message it posts until a user prompt follows that message, and reads the
|
|
384
|
+
session's context before each request. A message still in the queue stays tracked while
|
|
385
|
+
earlier steps run, and a reminder sent after the message doesn't end its step.
|
|
386
|
+
|
|
387
|
+
Before each model request that either way identifies, its context hook:
|
|
388
|
+
|
|
389
|
+
- removes every tool from the request, except the linked-note tool while the summary has
|
|
390
|
+
links left to read;
|
|
391
|
+
- replaces the system prompt with summary instructions that treat comments as untrusted;
|
|
392
|
+
- replaces the conversation with one message holding the comments and the linked notes
|
|
393
|
+
read so far, or the notice, so the request resends neither the session's history nor
|
|
394
|
+
the step's own tool calls;
|
|
395
|
+
- caps the output at 1,200 tokens.
|
|
396
|
+
|
|
397
|
+
If the review text is missing, such as after its record expired, the request asks for a
|
|
398
|
+
short reply saying so, still without tools or history. If the hook can't build the
|
|
399
|
+
replacement message, the request fails rather than keeping any of the session's messages.
|
|
400
|
+
|
|
401
|
+
As backstops, Forgekeeper refuses every tool execution other than the linked-note tool,
|
|
402
|
+
which checks its own calls and asks for no permissions, and every permission request,
|
|
403
|
+
while the session's latest model request was one of these, and treats a step it can't classify as
|
|
404
|
+
one, such as when the session's context can't be read, or when a compaction left no
|
|
405
|
+
input in it. The turn ends at the next user prompt. A later turn that another plugin starts
|
|
406
|
+
without a user prompt also runs without tools. The summary is the only part that stays in
|
|
407
|
+
the session's history.
|
|
408
|
+
|
|
409
|
+
`/forge-feedback` lists pending feedback for
|
|
410
|
+
the current checkout. Select an update to open its PR/MR or comment links, press Ctrl+D
|
|
411
|
+
to acknowledge it, press Ctrl+R to summarize it in its session in any delivery mode, or
|
|
412
|
+
press Ctrl+S to pause or resume review monitoring. Acknowledgement
|
|
413
|
+
only dismisses the local update; it does not reply to or resolve a remote discussion.
|
|
414
|
+
The home and session footers show the pending count, and connected CLIs in the checkout
|
|
415
|
+
show a toast even when the session's tab has never been visited. With session tabs on,
|
|
416
|
+
those CLIs also reopen the session's tab, without leaving the session you're in.
|
|
417
|
+
|
|
418
|
+
An update leaves the inbox when you acknowledge it, or when it no longer needs attention:
|
|
419
|
+
|
|
420
|
+
- Once every comment it announced is resolved, by anyone. Comments that can't be resolved,
|
|
421
|
+
such as GitLab comments outside a thread or GitHub review bodies, need acknowledging,
|
|
422
|
+
and so do updates without comments, such as Copilot reviews.
|
|
423
|
+
- Once its PR/MR merges or closes, or its session stops targeting it.
|
|
424
|
+
|
|
425
|
+
Forgekeeper checks resolution and PR/MR state only for sessions it monitors. Resolution
|
|
426
|
+
counts only from a fetch that no later change made obsolete, and state only after a lookup
|
|
427
|
+
finishes, so a reload doesn't clear updates before the session's targets are known. A
|
|
428
|
+
target change clears that target's updates right away, whether or not Forgekeeper
|
|
429
|
+
monitors the session. Working on the feedback, with or without the agent, doesn't clear an
|
|
430
|
+
update by itself.
|
|
431
|
+
|
|
432
|
+
The inbox retains the newest 500 updates per checkout across plugin reloads. It stores
|
|
433
|
+
session IDs, target URLs, notification titles, comment URLs, and timestamps, not comment
|
|
434
|
+
bodies or credentials. A pause persists across reloads and leaves status display available.
|
|
435
|
+
|
|
436
|
+
PRs/MRs from the title or the branch don't get review notifications.
|
|
234
437
|
|
|
235
438
|
Before it sends a notification, the server checks that the PR/MR is still one of the
|
|
236
439
|
session's targets. A lookup that started before the targets changed is discarded, so a
|
|
237
440
|
removed target never triggers a notification.
|
|
238
441
|
|
|
239
|
-
| Notification | GitLab
|
|
240
|
-
| ------------------------------------------ |
|
|
241
|
-
| An automated review finished with feedback | GitLab Duo
|
|
242
|
-
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
442
|
+
| Notification | GitLab | GitHub |
|
|
443
|
+
| ------------------------------------------ | -------------------------------- | -------------- |
|
|
444
|
+
| An automated review finished with feedback | GitLab Duo | GitHub Copilot |
|
|
445
|
+
| Comments from review bots | Security reviewers, `reviewBots` | `reviewBots` |
|
|
446
|
+
| New human review feedback | MR comments | PR comments |
|
|
447
|
+
|
|
448
|
+
- **Duo reviews**: new unresolved inline findings from the GitLab Duo bot create inbox
|
|
449
|
+
updates after the findings remain unchanged for five minutes. Forgekeeper does not need
|
|
450
|
+
to observe the review running. Summary-only reviews, approvals without inline findings,
|
|
451
|
+
system notes, and resolved findings send nothing. Comment IDs persist independently of
|
|
452
|
+
human feedback, including an empty baseline when monitoring starts without findings.
|
|
453
|
+
Existing findings become the initial baseline; new findings posted while OpenCode is
|
|
454
|
+
offline are detected after monitoring resumes if a baseline was already stored.
|
|
455
|
+
When available, `duoCreatedSession.id` groups findings by code-review workflow run.
|
|
456
|
+
A linked workflow must be terminal before delivery, and the watcher refetches before
|
|
457
|
+
notifying. Older GitLab versions and permission-hidden workflows use comment IDs and
|
|
458
|
+
the settling window without a workflow link. Separate runs use distinct inbox IDs.
|
|
249
459
|
- **Copilot reviews**: the server notifies when a submitted, undismissed review has at least
|
|
250
460
|
one inline comment. A review without inline comments, such as one that
|
|
251
461
|
found nothing or couldn't run for lack of quota, sends nothing. Copilot reviews are
|
|
@@ -255,15 +465,43 @@ removed target never triggers a notification.
|
|
|
255
465
|
review is requested.
|
|
256
466
|
- **Human reviews**: new comments and replies from people other than you and bots are
|
|
257
467
|
announced once none of their unresolved comments has changed for 5 minutes. Expect one
|
|
258
|
-
|
|
468
|
+
update 5 to 7 minutes after the review goes quiet. Resolved threads are skipped.
|
|
469
|
+
- **Review bots**: comments from the bot accounts that `reviewBots` names are announced
|
|
470
|
+
like human reviews, including comments without a file or line, such as a security
|
|
471
|
+
review's findings. `reviewBots` maps each host to exact usernames, ignoring case. By
|
|
472
|
+
default, it names GitLab.com's security reviewers for `gitlab-org`:
|
|
473
|
+
|
|
474
|
+
```json
|
|
475
|
+
{
|
|
476
|
+
"reviewBots": {
|
|
477
|
+
"gitlab.com": ["duo-security-review-gitlab-org", "ai-appsec-security-mr-reviewer-gitlab-org"]
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Duo Security Review has one account per top-level group, so add yours, such as
|
|
483
|
+
`duo-security-review-acme`, and a self-managed instance's accounts under its host. Your
|
|
484
|
+
list replaces the defaults, and `watch.reviewBots: false` turns this off. Names have no
|
|
485
|
+
wildcards, since a group owner can create a bot account with any free name, such as one
|
|
486
|
+
that starts like a security reviewer's. Only accounts that the forge flags as bots count,
|
|
487
|
+
and only on the host they're listed for. `watch.reviewBots` controls them on its own,
|
|
488
|
+
apart from `watch.humanReviews` and `watch.automatedReviews`. GitLab Duo's code review
|
|
489
|
+
findings always go through their own watcher instead. Internal notes, such as Duo
|
|
490
|
+
Security Review's, count when your account can read them.
|
|
259
491
|
|
|
260
492
|
The first look at a PR/MR records its newest comment as a baseline, so older comments never
|
|
261
493
|
count as new. Announced comments are stored per session and PR/MR, so a restart replays
|
|
262
494
|
nothing but still catches comments posted in the meantime.
|
|
263
495
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
496
|
+
Feedback counts as announced once the inbox has it. If Forgekeeper can't record it, such as
|
|
497
|
+
when its storage fails, it shows an error toast and tries again after 10 minutes. Duo and
|
|
498
|
+
human feedback then keep their comment IDs unannounced, so a restart can retry them, and
|
|
499
|
+
Copilot notifications stay pending in storage while the review still has feedback and the
|
|
500
|
+
PR remains an open target. Legacy state-only Duo pending notifications are discarded.
|
|
501
|
+
|
|
502
|
+
A session message that fails, such as when the comment text can't be read for a summary,
|
|
503
|
+
shows an error toast. The update stays in the inbox, which retries the message every
|
|
504
|
+
minute. Acknowledging the update ends those retries, and the update doesn't come back.
|
|
267
505
|
|
|
268
506
|
Every checkout's server plugin instance shares the plugin's storage, so each baseline and
|
|
269
507
|
each pending notification has its own storage key. Instances never overwrite each other's
|
|
@@ -272,6 +510,41 @@ records. A failed storage write shows an error toast once.
|
|
|
272
510
|
On GitLab, the server reads the newest 2,000 comments. On busier MRs, edits to older
|
|
273
511
|
comments don't count as activity.
|
|
274
512
|
|
|
513
|
+
### Live comment subscriptions
|
|
514
|
+
|
|
515
|
+
GitLab feedback monitoring attempts an outbound TLS WebSocket connection to the MR's host,
|
|
516
|
+
using GitLab's internal `Noteable::NotesChannel` Action Cable interface. Sessions targeting
|
|
517
|
+
the same MR share one subscription within a checkout. No webhook receiver or project
|
|
518
|
+
administration permission is required. This interface is not a public GraphQL subscription
|
|
519
|
+
and can differ across GitLab versions.
|
|
520
|
+
|
|
521
|
+
After the server confirms a subscription, comment events trigger a feedback fetch. Events
|
|
522
|
+
within a one-second window share a refresh. Quiet connections reconcile comments every
|
|
523
|
+
15 minutes, on the next scheduled status check. The five-minute settling window still
|
|
524
|
+
applies, and the watcher fetches again before delivering pending feedback. Initial connection
|
|
525
|
+
and reconnection also trigger a fetch to catch changes while disconnected.
|
|
526
|
+
|
|
527
|
+
GitHub and hosts that cannot establish a subscription keep polling. Connection failures,
|
|
528
|
+
rejections, and missing heartbeats restore normal feedback polling. Reconnection attempts
|
|
529
|
+
back off from 30 seconds to 15 minutes. Pausing feedback, removing the last session watching
|
|
530
|
+
an MR, or unloading the plugin closes its connection. Subscriptions only accelerate comment
|
|
531
|
+
monitoring; status, pipeline, and automated-review polling continue at their existing intervals.
|
|
532
|
+
|
|
533
|
+
On HTTP 429, subscription retries honor `Retry-After` and GitLab's `RateLimit-Reset` and
|
|
534
|
+
`RateLimit-ResetTime` headers, even when the wait exceeds 15 minutes. The cooldown applies
|
|
535
|
+
to all MR subscriptions on that origin within the checkout and survives removing and
|
|
536
|
+
re-adding a target during the plugin instance's lifetime. Other origins remain independent.
|
|
537
|
+
Bun's native WebSocket client hides rejected-handshake headers, so a failed handshake
|
|
538
|
+
triggers one HTTP upgrade probe with a 10-second timeout and redirects disabled. The probe
|
|
539
|
+
reads the reset headers and discards the body. A 429 without usable headers delays retries
|
|
540
|
+
by at least one minute. Normal authenticated API polling continues during this subscription
|
|
541
|
+
cooldown.
|
|
542
|
+
|
|
543
|
+
GitLab can classify bearer-authenticated `/-/cable` requests as unauthenticated for rate
|
|
544
|
+
limiting: its rate-limit authentication checks API and feed routes, while Action Cable
|
|
545
|
+
authenticates the bearer token later. A valid API token therefore does not necessarily
|
|
546
|
+
avoid the unauthenticated per-IP limit on this route.
|
|
547
|
+
|
|
275
548
|
On GitHub, the server reads three lists: conversation comments, the text of submitted
|
|
276
549
|
reviews, and the inline comments of review threads. It keeps the newest 2,000 of each.
|
|
277
550
|
It scans every thread to include recent replies in old threads, within a limit of 200 API
|
|
@@ -283,25 +556,175 @@ The baseline also records comment IDs at its timestamp, so a new comment posted
|
|
|
283
556
|
second can still be announced. Baselines stored by older plugin versions retain their
|
|
284
557
|
timestamp-only behavior until a new baseline is created.
|
|
285
558
|
|
|
559
|
+
## Merge request updates
|
|
560
|
+
|
|
561
|
+
Forgekeeper also watches each session's explicit PR/MR targets for three changes:
|
|
562
|
+
|
|
563
|
+
- **Conflicts:** a target that could merge starts to conflict with its target branch.
|
|
564
|
+
- **Failed jobs:** a job fails in a target's head pipeline, even while the pipeline still
|
|
565
|
+
runs.
|
|
566
|
+
- **Merges:** a target merges.
|
|
567
|
+
|
|
568
|
+
All come from the forge's own state, compared between two lookups, never from comments
|
|
569
|
+
such as GitLab's "can no longer be merged" note. The first lookup of a target only records
|
|
570
|
+
it, so nothing that happened before Forgekeeper first saw the target is announced. Records
|
|
571
|
+
persist across restarts, so a change that happened while OpenCode wasn't running is
|
|
572
|
+
announced at the next lookup. A
|
|
573
|
+
mergeability the forge hasn't settled yet, such as while GitLab rechecks an MR after a
|
|
574
|
+
push to it or to its target branch, keeps the last known state. A conflict that ends and
|
|
575
|
+
starts again is a new update.
|
|
576
|
+
Sessions are watched the same way as for review feedback, without visiting their tabs.
|
|
577
|
+
|
|
578
|
+
Failed jobs don't wait for the pipeline to finish, which can take much longer than the
|
|
579
|
+
job. Each lookup reads the pipeline's failed jobs:
|
|
580
|
+
|
|
581
|
+
- On GitLab, the first 10 failed jobs that aren't retried, with the MR, and no other
|
|
582
|
+
query. One that isn't allowed to fail is enough to know the pipeline fails. When all
|
|
583
|
+
10 are allowed to fail and more failed, an unread one may fail the pipeline, but none
|
|
584
|
+
counts as failing it while the pipeline runs. Once the pipeline finishes as failed,
|
|
585
|
+
which jobs allowed to fail never cause, the update reports that a job past those 10
|
|
586
|
+
failed it.
|
|
587
|
+
- On GitHub, the head commit's failed checks, which count as one pipeline.
|
|
588
|
+
|
|
589
|
+
Each job attempt counts once. A lookup that finds new failed jobs makes one update for
|
|
590
|
+
them, which names and links them, lists the jobs that failed earlier in that pipeline,
|
|
591
|
+
and says whether the pipeline still runs. That update replaces the pipeline's earlier one
|
|
592
|
+
in the inbox, and reaches the session again. A retry that fails again is a new attempt,
|
|
593
|
+
so it's a new failure. Forgekeeper remembers the newest 500 attempts it saw in a pipeline,
|
|
594
|
+
so one that newer failures push past the jobs it reads isn't new when it comes back.
|
|
595
|
+
Another pipeline that fails, such as after a push, is new too. At
|
|
596
|
+
most 10 jobs are named, and more are counted. Records stored before Forgekeeper told job
|
|
597
|
+
attempts apart only record the jobs, without an update. The update clears once the
|
|
598
|
+
pipeline no longer has failed jobs, another pipeline replaces it, or the PR/MR closes.
|
|
599
|
+
Forgekeeper only reports failed jobs: it doesn't retry or fix them.
|
|
600
|
+
|
|
601
|
+
Each change waits in the target's record until it reaches the inbox, in the same storage
|
|
602
|
+
write as the record. A merge stops polling, so waiting changes are retried every minute
|
|
603
|
+
and after a reload, without another lookup. Updates go to `/forge-feedback`, and
|
|
604
|
+
`delivery.lifecycle` sets what else they do: `"inbox"`, the default, `"passive"`, or
|
|
605
|
+
`"summary"`, as for [review feedback](#review-notifications). Each change keeps the mode
|
|
606
|
+
from when it was detected, so turning on summaries never starts turns for changes found
|
|
607
|
+
earlier. A summary turn has no tools, and gets only Forgekeeper's facts about the change,
|
|
608
|
+
never comments.
|
|
609
|
+
|
|
610
|
+
A conflict update clears once the conflict ends or the PR/MR closes. A merge update stays
|
|
611
|
+
until you acknowledge it, because the merge is the news. Updates for targets the session
|
|
612
|
+
drops clear right away. Setting `watch.lifecycle` to `false` stops all three kinds, and
|
|
613
|
+
retarget proposals too, since nothing notices merges then.
|
|
614
|
+
|
|
615
|
+
### Stacked PRs/MRs
|
|
616
|
+
|
|
617
|
+
A target **builds on** another of the session's targets when it targets that one's source
|
|
618
|
+
branch in the same project, on the same host. Forgekeeper links them only when exactly one
|
|
619
|
+
target matches, and keeps the link once the parent merges, because forges may retarget the
|
|
620
|
+
dependent PR/MR or delete the merged branch at that point. Sharing a session doesn't make
|
|
621
|
+
two PRs/MRs a stack.
|
|
622
|
+
|
|
623
|
+
When a parent merges, its update lists each dependent, and `lifecycleExecution.parentMerged`
|
|
624
|
+
sets what Forgekeeper works out about them:
|
|
625
|
+
|
|
626
|
+
| Mode | What happens |
|
|
627
|
+
| ----------- | ------------------------------------------------------------------------------------- |
|
|
628
|
+
| `"notify"` | The default. The update names the dependents and the branch they target, and no more. |
|
|
629
|
+
| `"prepare"` | Forgekeeper works out the listed actions and proposes them in the update. |
|
|
630
|
+
|
|
631
|
+
`actions` lists what `prepare` works out, so it needs at least one. `parentMerged`
|
|
632
|
+
supports `"retarget"`:
|
|
633
|
+
|
|
634
|
+
```json
|
|
635
|
+
{
|
|
636
|
+
"lifecycleExecution": { "parentMerged": { "mode": "prepare", "actions": ["retarget"] } }
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Forgekeeper proposes retargeting a dependent to the branch the parent merged into, and only
|
|
641
|
+
when it still targets the parent's source branch in the same project. It reads the
|
|
642
|
+
dependent from the forge first, so one the forge already retargeted gets no proposal, and
|
|
643
|
+
neither does one that targets something else, closed, or merged. A parent from a fork
|
|
644
|
+
gets no proposal, since its branch lives in another project. Only dependents the session
|
|
645
|
+
still targets count.
|
|
646
|
+
|
|
647
|
+
Forgekeeper never retargets anything itself, and never rebases. GitLab retargets up to
|
|
648
|
+
four dependent MRs when the merge deletes the source branch, so a proposal usually means
|
|
649
|
+
the branch was kept, which may be deliberate. Retarget the MR yourself, and rebase it if
|
|
650
|
+
the parent was squashed, since its commits stay on the dependent branch. Each event keeps
|
|
651
|
+
the policy it happened under, and the stricter of that and the current policy applies, so
|
|
652
|
+
enabling `prepare` never reads dependents again for earlier merges.
|
|
653
|
+
|
|
654
|
+
Conflicts and failed jobs only notify, so `lifecycleExecution` has no policy for them. An
|
|
655
|
+
invalid or unsupported policy, such as `"prepare"` without actions, falls back to
|
|
656
|
+
`"notify"`, and Forgekeeper shows an error toast saying which options it ignored.
|
|
657
|
+
|
|
286
658
|
## Options
|
|
287
659
|
|
|
660
|
+
### Monitoring security boundary
|
|
661
|
+
|
|
662
|
+
Monitoring uses the existing authenticated `gh` and `glab` clients for fixed read-only
|
|
663
|
+
GraphQL queries and GitLab MR REST lookups. GitLab subscriptions read the host's token with
|
|
664
|
+
`glab config get token --host` and send it in the WebSocket authorization header over TLS.
|
|
665
|
+
Tokens are not stored in plugin storage or included in URLs. Subprocesses use argument arrays
|
|
666
|
+
rather than shell interpolation, share
|
|
667
|
+
the existing concurrency limit of four, and time out after 30 seconds. Their credentials
|
|
668
|
+
and environment are those of the OpenCode service; monitoring does not provide OS-level
|
|
669
|
+
isolation or reduce those credentials' scopes.
|
|
670
|
+
|
|
671
|
+
The `inbox` mode invokes a model only when you ask for a summary with Ctrl+R in
|
|
672
|
+
`/forge-feedback`. The `passive` mode starts no turn, but when
|
|
673
|
+
something else wakes the session, its message can run as one step that sends the model
|
|
674
|
+
only the notice's title, time, and links, without tools. The `summary` mode sends review
|
|
675
|
+
text to the session's model in a request whose only possible tool reads GitLab notes that
|
|
676
|
+
the comments link to, as described in [Linked notes](#linked-notes). It reads with your
|
|
677
|
+
`glab` credentials, so it can read private notes in other projects on the same host that
|
|
678
|
+
those links name. No mode executes commands from
|
|
679
|
+
review content, edits repository files, commits, pushes, posts replies, or resolves
|
|
680
|
+
discussions.
|
|
681
|
+
|
|
682
|
+
Forgekeeper changes nothing on a forge. For stacked PRs/MRs, it only proposes retargets.
|
|
683
|
+
See [Stacked PRs/MRs](#stacked-prsmrs).
|
|
684
|
+
Comment links are opened only after user selection and must use HTTPS on the target's
|
|
685
|
+
origin without embedded credentials. Review bodies remain external data, not trusted
|
|
686
|
+
instructions. Analysis and remediation are outside this feature's scope.
|
|
687
|
+
|
|
688
|
+
Pending updates are stored locally through OpenCode's plugin storage. Conversation history
|
|
689
|
+
is inspected locally only to find the latest user-message timestamp, with at most four
|
|
690
|
+
session checks in flight. The 72-hour window limits monitoring eligibility, not access
|
|
691
|
+
rights. OpenCode's provider, integration, and organization policies still apply. This
|
|
692
|
+
implementation has not received AppSec approval.
|
|
693
|
+
|
|
288
694
|
Set options with the object form of the `plugins` entry in `opencode.json`:
|
|
289
695
|
|
|
290
696
|
```json
|
|
291
697
|
{
|
|
292
|
-
"plugins": [
|
|
698
|
+
"plugins": [
|
|
699
|
+
{ "package": "opencode-forgekeeper", "options": { "watch": { "humanReviews": false } } }
|
|
700
|
+
]
|
|
293
701
|
}
|
|
294
702
|
```
|
|
295
703
|
|
|
296
|
-
| Option | Default
|
|
297
|
-
| ------------------------ |
|
|
298
|
-
| `
|
|
299
|
-
| `hosts` | `["gitlab.com"]`
|
|
300
|
-
| `githubHosts` | `["github.com"]`
|
|
301
|
-
| `pollSeconds` | `120`
|
|
302
|
-
| `
|
|
303
|
-
| `
|
|
304
|
-
| `
|
|
704
|
+
| Option | Default | Description |
|
|
705
|
+
| ------------------------ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
706
|
+
| `status` | `true` | Show the PR/MR status and monitor the session's PRs/MRs. Set to `false` to keep only session targets and titles. |
|
|
707
|
+
| `hosts` | `["gitlab.com"]` | GitLab hosts whose remotes are recognized, besides hosts named `gitlab.*`. |
|
|
708
|
+
| `githubHosts` | `["github.com"]` | GitHub hosts, including GitHub Enterprise hosts. |
|
|
709
|
+
| `pollSeconds` | `120` | Normal polling interval. Running automated reviews poll at 30 seconds or this value, whichever is lower. |
|
|
710
|
+
| `watch.humanReviews` | `true` | Watch for new review feedback from people. |
|
|
711
|
+
| `watch.automatedReviews` | `true` | Watch for automated reviews that finish with feedback, such as GitLab Duo's and GitHub Copilot's. |
|
|
712
|
+
| `watch.reviewBots` | `true` | Watch for comments from the accounts `reviewBots` lists. |
|
|
713
|
+
| `watch.lifecycle` | `true` | Watch for targets that start to conflict, get failed jobs, or merge. See [Merge request updates](#merge-request-updates). |
|
|
714
|
+
| `reviewBots` | GitLab.com security reviewers | Bot accounts whose comments count as review feedback, by host, with exact usernames. See [Review bots](#review-notifications). |
|
|
715
|
+
| `delivery.reviews` | `"inbox"` | What a review update does besides entering the inbox: `"inbox"`, `"passive"`, or `"summary"`. See [Summary turns](#summary-turns). |
|
|
716
|
+
| `delivery.lifecycle` | `"inbox"` | What a merge request update does besides entering the inbox: `"inbox"`, `"passive"`, or `"summary"`. |
|
|
717
|
+
| `lifecycleExecution` | `"notify"` | What Forgekeeper works out when a parent PR/MR merges, in `parentMerged`. See [Stacked PRs/MRs](#stacked-prsmrs). |
|
|
718
|
+
|
|
719
|
+
`watch` and `delivery` list only what you change, and sources added in later releases start
|
|
720
|
+
on. For example, this summarizes review feedback and stops watching merge request changes:
|
|
721
|
+
|
|
722
|
+
```json
|
|
723
|
+
{ "watch": { "lifecycle": false }, "delivery": { "reviews": "summary" } }
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
An option Forgekeeper doesn't know, or a value it can't use, shows an error toast at startup
|
|
727
|
+
that names the option and what's used instead, even with `status` off.
|
|
305
728
|
|
|
306
729
|
The server entry point reads every option, so changing one takes effect when the server
|
|
307
730
|
plugin reloads. The CLI has no options of its own. Each checkout's server reads the
|
|
@@ -313,7 +736,7 @@ To keep the behavior of `opencode-forge-session-title` alone, turn off review st
|
|
|
313
736
|
|
|
314
737
|
```json
|
|
315
738
|
{
|
|
316
|
-
"plugins": [{ "package": "opencode-forgekeeper", "options": { "
|
|
739
|
+
"plugins": [{ "package": "opencode-forgekeeper", "options": { "status": false } }]
|
|
317
740
|
}
|
|
318
741
|
```
|
|
319
742
|
|
|
@@ -321,6 +744,10 @@ The server still registers `set_session_target`, adds the agent guidance, and ma
|
|
|
321
744
|
title prefix. It doesn't poll or send review notifications, and the CLI shows no footer.
|
|
322
745
|
`/forge-status` and `/forge-open` explain that the status is off.
|
|
323
746
|
|
|
747
|
+
A summary or notice that Forgekeeper queued before you turned review status off can still
|
|
748
|
+
start a step. That step still runs without tools, history, or permissions. It doesn't read
|
|
749
|
+
the review text: it only says the text is unavailable.
|
|
750
|
+
|
|
324
751
|
## RPC
|
|
325
752
|
|
|
326
753
|
The server registers an [RPC](https://opencode.ai/v2/docs/build/plugins/rpc) with the ID
|
|
@@ -335,6 +762,13 @@ The server registers an [RPC](https://opencode.ai/v2/docs/build/plugins/rpc) wit
|
|
|
335
762
|
session. `visible` is the key on screen.
|
|
336
763
|
- `release({ clientID })` drops the caller's leases.
|
|
337
764
|
- `refresh({ key })` looks the key up now and returns `{ enabled, snapshot }`.
|
|
765
|
+
- `feedback({})` returns `{ items, paused }` for this checkout's pending feedback.
|
|
766
|
+
- `acknowledgeFeedback({ id })` dismisses a local update and returns the updated inbox.
|
|
767
|
+
- `summarizeFeedback({ id })` starts a summary turn for an update in its session.
|
|
768
|
+
- `monitoringStatus({ sessionID })` reports, without changing anything, whether the
|
|
769
|
+
session is monitored, and each target's fetch health and subscription state.
|
|
770
|
+
- `pauseFeedback({ paused })` persists the checkout's review-monitoring pause state.
|
|
771
|
+
- `feedbackChanged` fires with `{ directory }` when the inbox or pause state changes.
|
|
338
772
|
- `targetChanged` fires after `set_session_target` runs, with the same fields as `target`
|
|
339
773
|
and the `sessionID`. It has only the `sessionID` in branch mode.
|
|
340
774
|
- `status` fires with `{ directory, key, snapshot }` when a key's status changes.
|
|
@@ -368,9 +802,12 @@ OpenCode scopes plugin storage by plugin ID, so nothing stored under the former
|
|
|
368
802
|
aren't announced.
|
|
369
803
|
- Callers of the RPC need the new ID, `opencode-forgekeeper`.
|
|
370
804
|
|
|
371
|
-
With default options, the
|
|
372
|
-
|
|
373
|
-
`
|
|
805
|
+
With default options, the footer and review monitoring start right away. All notifications
|
|
806
|
+
stay in the feedback inbox, and no session turn starts unless you set `delivery.reviews`
|
|
807
|
+
or `delivery.lifecycle` to `"summary"`. Set `status` to `false` to keep only titles.
|
|
808
|
+
|
|
809
|
+
If you set options for `opencode-forgekeeper` 2.x, also see
|
|
810
|
+
[Upgrade from forgekeeper 2.x](#upgrade-from-forgekeeper-2x).
|
|
374
811
|
|
|
375
812
|
## Code layout
|
|
376
813
|
|
|
@@ -390,11 +827,13 @@ notifications resume the session. Set `reviewStatus` to `false` to keep only tit
|
|
|
390
827
|
`src/tui.tsx` connects it to the RPC and renders the footer, dialog, commands, and notices.
|
|
391
828
|
`src/notice.ts` decides which notices a CLI shows and which tabs it reopens.
|
|
392
829
|
- `src/forge.ts` defines the forge-neutral types, the `Forge` adapter interface, and
|
|
393
|
-
`ForgeTraits` with its optional `automatedReview` and `
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
830
|
+
`ForgeTraits` with its optional `automatedReview`, `feedback`, and `lifecycle`
|
|
831
|
+
capabilities.
|
|
832
|
+
- `src/adapters/gitlab/feedback.ts` and `src/adapters/github/feedback.ts` read each forge's
|
|
833
|
+
comments for its `feedback` capability.
|
|
834
|
+
- `src/adapters/gitlab/adapter.ts` and `src/adapters/github/adapter.ts` provide each forge's
|
|
835
|
+
adapter and traits. Adapters own API calls, pagination, error classification, and status
|
|
836
|
+
normalization. Traits cover
|
|
398
837
|
everything else: hosts, URL parsing, reference syntax, vocabulary, and forge-specific
|
|
399
838
|
footer and dialog content.
|
|
400
839
|
- `src/forges.ts` registers each forge's traits, classifies hosts, opens adapters, and
|
|
@@ -405,6 +844,18 @@ notifications resume the session. Set `reviewStatus` to `false` to keep only tit
|
|
|
405
844
|
renders the footer and dialog.
|
|
406
845
|
- `src/automated-review-watch.ts` and `src/human-review-watch.ts` decide when to notify the
|
|
407
846
|
agent.
|
|
847
|
+
- `src/review-monitoring.ts` configures the review watchers, stores their state, and records
|
|
848
|
+
review updates in the inbox.
|
|
849
|
+
- `src/lifecycle-watch.ts` finds conflicts, failed jobs, merges, and stack links between lookups, and
|
|
850
|
+
keeps each event until it reaches the inbox. `src/lifecycle-execution.ts` proposes retargets
|
|
851
|
+
of stacked PRs/MRs.
|
|
852
|
+
- `src/lifecycle-notifications.ts` formats lifecycle updates, checks retarget proposals, and
|
|
853
|
+
connects lifecycle events to the inbox. `src/feedback-cleanup.ts` identifies resolved,
|
|
854
|
+
closed, and settled updates for removal.
|
|
855
|
+
- `src/adapters/gitlab/linked-notes.ts` finds and reads the notes review comments link to.
|
|
856
|
+
- `src/summary-delivery.ts` stores and delivers summary turns, restricts their model requests,
|
|
857
|
+
and handles linked-note access. `src/review-summary.ts` formats their content.
|
|
858
|
+
- `src/keyed-queue.ts` serializes updates by session or record while allowing other keys to progress.
|
|
408
859
|
|
|
409
860
|
Shared code never compares a forge against a specific name. It asks the forge's traits, or
|
|
410
861
|
checks for a capability, instead. To add a forge, add its kind to `ForgeKind` and
|