Merge request notes (ADR 0032): one uniform review object, published at once, no draft review.
Threads (one level, resolution on the root), line ranges on one side, author edit and delete.
Notes and descriptions rendered as safe Markdown (no raw HTML, link policy), ^N links to note N.
Optional verdict on a root note (approve, changes requested), needs the new mr:review scope; informative only; latest per author; approvals stale after a push; changes requested stay until the author's next verdict; immutable.
RPC, CLI (mr comment --reply/--line A-B/--approve/--request-changes, mr edit-comment, mr delete-comment, show and watch), web (threads, quote, Shift+click ranges with a no-JS fallback, verdict list), docs and the ermite skill.
Depends on the ermite-api MR (feat/mr-notes). Until ermite-api v0.8.0 is tagged this branch builds only with a go.work that uses that branch; a last commit will require v0.8.0.
Upgrade: mr:review is a new scope, so a token that held every scope loses the owner's rights until it is recreated (--preset cli) with the root token or from an owner web session.
Checks (with the API branch in a go.work): build, go test -race, go test ./internal/app, golangci-lint --new-from-rev=main, hexcheck, templ drift, buf lint/breaking/generate, mocks drift, node tests (145). Reviewed by three read-only reviewers plus a targeted re-review; all findings fixed.
Not verified yet: manual browser check of the merge request page.
// Verdict is the immutable review decision of a note.
68
+
enum Verdict {
69
+
VERDICT_UNSPECIFIED = 0;
70
+
VERDICT_APPROVE = 1;
71
+
VERDICT_CHANGES_REQUESTED = 2;
72
+
}
73
+
67
74
// Comment is stored under the key "mrc/<repo>/<number:010d>/<id:010d>".
68
75
messageComment {
69
76
int64 id = 1;
@@ -75,6 +82,11 @@ message Comment {
75
82
string text = 7;
76
83
bool resolved = 8;
77
84
google.protobuf.Timestamp created_at = 9;
85
+
int64 reply_to = 10;
86
+
int32 start_line = 11;
87
+
Verdict verdict = 12;
88
+
google.protobuf.Timestamp edited_at = 13;
89
+
bool deleted = 14;
78
90
}
79
91
80
92
// Sequence is stored under the key "mrseq/<repo>": the last merge request
@@ -64,6 +64,13 @@ message MergeRequest {
64
int64 comment_seq = 14;
64
int64 comment_seq = 14;
65
}
65
}
66
66
67
+
// Verdict is the immutable review decision of a note.
68
+
enum Verdict {
69
+
VERDICT_UNSPECIFIED = 0;
70
+
VERDICT_APPROVE = 1;
71
+
VERDICT_CHANGES_REQUESTED = 2;
72
+
}
73
+
67
// Comment is stored under the key "mrc/<repo>/<number:010d>/<id:010d>".
74
// Comment is stored under the key "mrc/<repo>/<number:010d>/<id:010d>".
68
messageComment {
75
messageComment {
69
int64 id = 1;
76
int64 id = 1;
@@ -75,6 +82,11 @@ message Comment {
75
string text = 7;
82
string text = 7;
76
bool resolved = 8;
83
bool resolved = 8;
77
google.protobuf.Timestamp created_at = 9;
84
google.protobuf.Timestamp created_at = 9;
85
+
int64 reply_to = 10;
86
+
int32 start_line = 11;
87
+
Verdict verdict = 12;
88
+
google.protobuf.Timestamp edited_at = 13;
89
+
bool deleted = 14;
78
}
90
}
79
91
80
// Sequence is stored under the key "mrseq/<repo>": the last merge request
92
// Sequence is stored under the key "mrseq/<repo>": the last merge request
Mskills/ermite/CLI.md+11-6
@@ -28,7 +28,7 @@ Presets add up with `--scope` (repeatable or comma-separated):
28
28
29
29
| Preset | Scopes |
30
30
|---|---|
31
-
| `cli` | every scope, for the owner's daily use |
31
+
| `cli` | every scope (including `mr:review`), for the owner's daily use; a token created before `mr:review` existed lacks it and loses owner rights: recreate it with `--preset cli` using the root token or from a signed-in owner web session (an old token lacking `mr:review` cannot grant it), then replace the stored CLI secret and revoke the old token |
| `mr:write` | open, comment on, resolve, close, mark read |
42
+
| `mr:write` | open, comment on, reply, edit and delete own notes, resolve, close, mark read |
43
+
| `mr:review` | give a verdict (`--approve`, `--request-changes`) |
43
44
| `mr:merge` | merge, and delete the merged source branch with `--delete-branch` |
44
45
| `token:admin` | create, list and revoke tokens |
45
46
| `ci:write`, `secrets:write`, `runner:register` | reserved for CI |
@@ -55,8 +56,10 @@ A token can never grant more than the token that creates it. `--repo` (repeatabl
55
56
| `mr create <repo> --source B --title T [--target B] [--description TEXT\|-]` | `mr:write` | target defaults to the default branch; title of 120 characters max; `-` reads standard input |
| `mr show <repo> <n> [--diff] [--since-last-read] [--mark-read] [--json]` | `mr:write` for `--mark-read` | commits, changed files and comments; `--diff` adds hunks with line numbers |
58
-
| `mr comment <repo> <n> -m TEXT\|- [--path P --line N --side old\|new]` | `mr:write` | the line must be in the current diff; a deleted file uses its old path |
59
-
| `mr resolve <repo> <n> <comment-id> [--undo]` | `mr:write` | doing it twice is not an error |
59
+
| `mr comment <repo> <n> -m TEXT\|- [--path P --line N\|A-B --side old\|new] [--reply ID] [--approve\|--request-changes]` | `mr:write`; verdicts `mr:review` | Markdown note (8000 characters max); the lines must be in the current diff on one side; a deleted file uses its old path; `--reply` takes a root note (`12`, `#12` or `^12`), inherits its anchor and cannot carry a verdict; a verdict note may have no message |
60
+
| `mr edit-comment <repo> <n> <comment-id> -m TEXT\|-` | `mr:write` | author only, open merge request; the verdict is unchanged |
61
+
| `mr delete-comment <repo> <n> <comment-id>` | `mr:write` | author only; the text is lost, the verdict is kept |
62
+
| `mr resolve <repo> <n> <comment-id> [--undo]` | `mr:write` | on the thread root, covers its replies; doing it twice is not an error |
60
63
| `mr merge <repo> <n> --head <full-id> [--squash] [--delete-branch]` | `mr:merge` | refused if the branch moved or the target is not an ancestor of the head; `--delete-branch` deletes the source after the merge (refused for the default branch or a branch another open request uses; JSON adds `source_branch_deleted`) |
61
64
| `mr close <repo> <n>` | `mr:write` | a closed merge request cannot be reopened |
62
65
@@ -64,7 +67,9 @@ A token can never grant more than the token that creates it. `--repo` (repeatabl
64
67
merge request web URL on its own line. With `--json`, merge request objects
65
68
(create, show, list, close and merge) include `url` alongside the other fields.
Comment JSON: `id`, `author`, `path`, `line`, `side`, `commit_id`, `text`, `resolved`, `created_at`, `reply_to` (empty for roots), `start_line` (0 for a single line), `verdict` (`""`, `approve`, `changes_requested`), `edited_at`, `deleted`. Verdict JSON (latest per author): `author`, `verdict`, `comment_id`, `commit_id`, `created_at`, `stale` (an approval given on another head; changes requested are never stale). Verdicts are informative and never block a merge. In notes, `^N` refers to note N.
| `comment_added`, `comment_edited`, `comment_deleted`, `comment_resolved` | `repo`, `number`, `comment` (a verdict note has `verdict` set) |
90
95
| `gap` | none: events were lost, reload the state |
91
96
92
97
## Server and web
@@ -28,7 +28,7 @@ Presets add up with `--scope` (repeatable or comma-separated):
28
28
29
| Preset | Scopes |
29
| Preset | Scopes |
30
|---|---|
30
|---|---|
31
-
| `cli` | every scope, for the owner's daily use |
31
+
| `cli` | every scope (including `mr:review`), for the owner's daily use; a token created before `mr:review` existed lacks it and loses owner rights: recreate it with `--preset cli` using the root token or from a signed-in owner web session (an old token lacking `mr:review` cannot grant it), then replace the stored CLI secret and revoke the old token |
| `mr:write` | open, comment on, resolve, close, mark read |
42
+
| `mr:write` | open, comment on, reply, edit and delete own notes, resolve, close, mark read |
43
+
| `mr:review` | give a verdict (`--approve`, `--request-changes`) |
43
| `mr:merge` | merge, and delete the merged source branch with `--delete-branch` |
44
| `mr:merge` | merge, and delete the merged source branch with `--delete-branch` |
44
| `token:admin` | create, list and revoke tokens |
45
| `token:admin` | create, list and revoke tokens |
45
| `ci:write`, `secrets:write`, `runner:register` | reserved for CI |
46
| `ci:write`, `secrets:write`, `runner:register` | reserved for CI |
@@ -55,8 +56,10 @@ A token can never grant more than the token that creates it. `--repo` (repeatabl
55
| `mr create <repo> --source B --title T [--target B] [--description TEXT\|-]` | `mr:write` | target defaults to the default branch; title of 120 characters max; `-` reads standard input |
56
| `mr create <repo> --source B --title T [--target B] [--description TEXT\|-]` | `mr:write` | target defaults to the default branch; title of 120 characters max; `-` reads standard input |
| `mr show <repo> <n> [--diff] [--since-last-read] [--mark-read] [--json]` | `mr:write` for `--mark-read` | commits, changed files and comments; `--diff` adds hunks with line numbers |
58
| `mr show <repo> <n> [--diff] [--since-last-read] [--mark-read] [--json]` | `mr:write` for `--mark-read` | commits, changed files and comments; `--diff` adds hunks with line numbers |
58
-
| `mr comment <repo> <n> -m TEXT\|- [--path P --line N --side old\|new]` | `mr:write` | the line must be in the current diff; a deleted file uses its old path |
59
+
| `mr comment <repo> <n> -m TEXT\|- [--path P --line N\|A-B --side old\|new] [--reply ID] [--approve\|--request-changes]` | `mr:write`; verdicts `mr:review` | Markdown note (8000 characters max); the lines must be in the current diff on one side; a deleted file uses its old path; `--reply` takes a root note (`12`, `#12` or `^12`), inherits its anchor and cannot carry a verdict; a verdict note may have no message |
59
-
| `mr resolve <repo> <n> <comment-id> [--undo]` | `mr:write` | doing it twice is not an error |
60
+
| `mr edit-comment <repo> <n> <comment-id> -m TEXT\|-` | `mr:write` | author only, open merge request; the verdict is unchanged |
61
+
| `mr delete-comment <repo> <n> <comment-id>` | `mr:write` | author only; the text is lost, the verdict is kept |
62
+
| `mr resolve <repo> <n> <comment-id> [--undo]` | `mr:write` | on the thread root, covers its replies; doing it twice is not an error |
60
| `mr merge <repo> <n> --head <full-id> [--squash] [--delete-branch]` | `mr:merge` | refused if the branch moved or the target is not an ancestor of the head; `--delete-branch` deletes the source after the merge (refused for the default branch or a branch another open request uses; JSON adds `source_branch_deleted`) |
63
| `mr merge <repo> <n> --head <full-id> [--squash] [--delete-branch]` | `mr:merge` | refused if the branch moved or the target is not an ancestor of the head; `--delete-branch` deletes the source after the merge (refused for the default branch or a branch another open request uses; JSON adds `source_branch_deleted`) |
61
| `mr close <repo> <n>` | `mr:write` | a closed merge request cannot be reopened |
64
| `mr close <repo> <n>` | `mr:write` | a closed merge request cannot be reopened |
62
65
@@ -64,7 +67,9 @@ A token can never grant more than the token that creates it. `--repo` (repeatabl
64
merge request web URL on its own line. With `--json`, merge request objects
67
merge request web URL on its own line. With `--json`, merge request objects
65
(create, show, list, close and merge) include `url` alongside the other fields.
68
(create, show, list, close and merge) include `url` alongside the other fields.
Comment JSON: `id`, `author`, `path`, `line`, `side`, `commit_id`, `text`, `resolved`, `created_at`, `reply_to` (empty for roots), `start_line` (0 for a single line), `verdict` (`""`, `approve`, `changes_requested`), `edited_at`, `deleted`. Verdict JSON (latest per author): `author`, `verdict`, `comment_id`, `commit_id`, `created_at`, `stale` (an approval given on another head; changes requested are never stale). Verdicts are informative and never block a merge. In notes, `^N` refers to note N.
2. Read: `ermite mr show <repo> <n> --diff --json`. Keep `head_id`; take line numbers from the hunks.
38
-
3. Comment on a line: `ermite mr comment <repo> <n> -m "text" --path <file> --line <n> --side new`. Without `--path` the comment is general. The line must appear in the diff.
39
-
4. Merge only when asked: `ermite mr merge <repo> <n> --head <head_id>`, with `--squash` for one commit signed by the server and `--delete-branch` to delete the source branch afterwards.
38
+
3. Comment on a line: `ermite mr comment <repo> <n> -m "text" --path <file> --line <n> --side new`; `--line 10-14` covers a range on one side. Without `--path` the comment is general. The line must appear in the diff. Notes are Markdown; `^N` refers to note N.
39
+
4. End a review with a verdict (needs `mr:review`): `--approve` or `--request-changes`, alone or with a message. Verdicts are informative and cannot be edited; a new verdict replaces the previous one.
40
+
5. Merge only when asked: `ermite mr merge <repo> <n> --head <head_id>`, with `--squash` for one commit signed by the server and `--delete-branch` to delete the source branch afterwards.
40
41
41
42
The merge is refused when the branch moved since `head_id` (review again) or when the target is not an ancestor of the head (rebase and push again). The repository may allow only one merge mode.
42
43
44
+
### Act on a review
45
+
46
+
Notes are visible as soon as they are written, so a review is not finished until its verdict. Act on a verdict, not on each note (`ermite watch --json` shows `verdict` on the `comment_added` event).
47
+
48
+
On `changes_requested`: run `ermite mr show <repo> <n> --json`, read the open threads (roots with `resolved` false and their replies), fix the code, push, then answer each thread with `ermite mr comment <repo> <n> --reply <id> -m "Fixed in ^N"`. Leave resolving to the reviewer: do not run `mr resolve` on their threads. Edit or delete only your own notes (`mr edit-comment`, `mr delete-comment`).
49
+
50
+
An approval is stale after a push; changes requested stay until the reviewer gives a new verdict.
51
+
43
52
`ermite mr create` and `ermite mr close` print a confirmation followed by the full
44
53
merge request web URL on its own line. With `--json`, merge request objects
45
54
(create, show, list, close and merge) include `url` alongside the other fields.
@@ -52,11 +61,11 @@ Open one: `ermite mr create <repo> --source <branch> --title "..."`. After new p
The secret is printed once. Presets: `agent` (push, open and comment on merge requests), `ci` (read), `cli` (everything). Add `--scope mr:merge` to let an agent merge. A token can never grant more than the token that creates it. Full scope list: [CLI.md](CLI.md).
64
+
The secret is printed once. Presets: `agent` (push, open and comment on merge requests; no verdicts), `ci` (read), `cli` (everything). Add `--scope mr:review` to let an agent give verdicts, `--scope mr:merge` to let it merge. A token can never grant more than the token that creates it. Full scope list: [CLI.md](CLI.md).
56
65
57
66
## Follow events
58
67
59
-
`ermite watch [repo] --json` prints one JSON object per event (pushes, repositories, merge requests, comments). It reconnects and resumes on its own; a `gap` event means some events were lost, so reload the state.
68
+
`ermite watch [repo] --json` prints one JSON object per event (pushes, repositories, merge requests, notes: added, edited, deleted, resolved). It reconnects and resumes on its own; a `gap` event means some events were lost, so reload the state.
2. Read: `ermite mr show <repo> <n> --diff --json`. Keep `head_id`; take line numbers from the hunks.
37
2. Read: `ermite mr show <repo> <n> --diff --json`. Keep `head_id`; take line numbers from the hunks.
38
-
3. Comment on a line: `ermite mr comment <repo> <n> -m "text" --path <file> --line <n> --side new`. Without `--path` the comment is general. The line must appear in the diff.
38
+
3. Comment on a line: `ermite mr comment <repo> <n> -m "text" --path <file> --line <n> --side new`; `--line 10-14` covers a range on one side. Without `--path` the comment is general. The line must appear in the diff. Notes are Markdown; `^N` refers to note N.
39
-
4. Merge only when asked: `ermite mr merge <repo> <n> --head <head_id>`, with `--squash` for one commit signed by the server and `--delete-branch` to delete the source branch afterwards.
39
+
4. End a review with a verdict (needs `mr:review`): `--approve` or `--request-changes`, alone or with a message. Verdicts are informative and cannot be edited; a new verdict replaces the previous one.
40
+
5. Merge only when asked: `ermite mr merge <repo> <n> --head <head_id>`, with `--squash` for one commit signed by the server and `--delete-branch` to delete the source branch afterwards.
40
41
41
The merge is refused when the branch moved since `head_id` (review again) or when the target is not an ancestor of the head (rebase and push again). The repository may allow only one merge mode.
42
The merge is refused when the branch moved since `head_id` (review again) or when the target is not an ancestor of the head (rebase and push again). The repository may allow only one merge mode.
42
43
44
+
### Act on a review
45
+
46
+
Notes are visible as soon as they are written, so a review is not finished until its verdict. Act on a verdict, not on each note (`ermite watch --json` shows `verdict` on the `comment_added` event).
47
+
48
+
On `changes_requested`: run `ermite mr show <repo> <n> --json`, read the open threads (roots with `resolved` false and their replies), fix the code, push, then answer each thread with `ermite mr comment <repo> <n> --reply <id> -m "Fixed in ^N"`. Leave resolving to the reviewer: do not run `mr resolve` on their threads. Edit or delete only your own notes (`mr edit-comment`, `mr delete-comment`).
49
+
50
+
An approval is stale after a push; changes requested stay until the reviewer gives a new verdict.
51
+
43
`ermite mr create` and `ermite mr close` print a confirmation followed by the full
52
`ermite mr create` and `ermite mr close` print a confirmation followed by the full
44
merge request web URL on its own line. With `--json`, merge request objects
53
merge request web URL on its own line. With `--json`, merge request objects
45
(create, show, list, close and merge) include `url` alongside the other fields.
54
(create, show, list, close and merge) include `url` alongside the other fields.
@@ -52,11 +61,11 @@ Open one: `ermite mr create <repo> --source <branch> --title "..."`. After new p
The secret is printed once. Presets: `agent` (push, open and comment on merge requests), `ci` (read), `cli` (everything). Add `--scope mr:merge` to let an agent merge. A token can never grant more than the token that creates it. Full scope list: [CLI.md](CLI.md).
64
+
The secret is printed once. Presets: `agent` (push, open and comment on merge requests; no verdicts), `ci` (read), `cli` (everything). Add `--scope mr:review` to let an agent give verdicts, `--scope mr:merge` to let it merge. A token can never grant more than the token that creates it. Full scope list: [CLI.md](CLI.md).
56
65
57
## Follow events
66
## Follow events
58
67
59
-
`ermite watch [repo] --json` prints one JSON object per event (pushes, repositories, merge requests, comments). It reconnects and resumes on its own; a `gap` event means some events were lost, so reload the state.
68
+
`ermite watch [repo] --json` prints one JSON object per event (pushes, repositories, merge requests, notes: added, edited, deleted, resolved). It reconnects and resumes on its own; a `gap` event means some events were lost, so reload the state.