@elalfner/relevo 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/scripts/dev-server-guard.mjs +32 -0
  4. package/scripts/extra-server.mjs +78 -0
  5. package/scripts/init.mjs +3 -1
  6. package/scripts/lib/can-edit-issue.mjs +4 -1
  7. package/scripts/lib/config.mjs +19 -1
  8. package/scripts/lib/dev-server-guard.mjs +49 -0
  9. package/scripts/lib/doctor.mjs +136 -5
  10. package/scripts/lib/extra-server.mjs +84 -0
  11. package/scripts/lib/github.mjs +15 -5
  12. package/scripts/lib/init.mjs +63 -3
  13. package/scripts/lib/labels.mjs +4 -0
  14. package/scripts/lib/messages.mjs +109 -0
  15. package/scripts/lib/next-step.mjs +12 -0
  16. package/scripts/lib/plan.mjs +29 -6
  17. package/scripts/lib/preflight.mjs +1 -1
  18. package/scripts/lib/review-adapters.mjs +15 -2
  19. package/scripts/lib/review-brief.mjs +12 -4
  20. package/scripts/lib/review-events.mjs +6 -3
  21. package/scripts/lib/review-state.mjs +30 -4
  22. package/scripts/lib/ship-github.mjs +87 -0
  23. package/scripts/lib/ship-unattended.mjs +271 -0
  24. package/scripts/lib/ship.mjs +15 -3
  25. package/scripts/lib/toolchain-check.mjs +6 -0
  26. package/scripts/lib/web-preflight.mjs +133 -0
  27. package/scripts/lib/web-review.mjs +82 -1
  28. package/scripts/review-adapter.mjs +5 -3
  29. package/scripts/review-brief.mjs +3 -2
  30. package/scripts/review-event.mjs +10 -1
  31. package/scripts/review-state.mjs +12 -5
  32. package/scripts/ship-check.mjs +6 -4
  33. package/scripts/ship-unattended.mjs +45 -0
  34. package/scripts/toolchain-check.mjs +3 -3
  35. package/scripts/web-hook.mjs +28 -0
  36. package/scripts/web-preflight.mjs +48 -0
  37. package/scripts/web-review.mjs +80 -29
  38. package/skills/doctor/SKILL.md +3 -2
  39. package/skills/go/SKILL.md +4 -2
  40. package/skills/init/SKILL.md +7 -2
  41. package/skills/review/SKILL.md +49 -10
  42. package/skills/ship/SKILL.md +3 -1
  43. package/stacks/vue.md +2 -2
  44. package/templates/agents/antigravity-step.yml +8 -5
  45. package/templates/relevo.yml +4 -0
  46. package/templates/workflows/relevo-digest.yml +10 -1
  47. package/templates/workflows/relevo-go.yml +25 -5
  48. package/templates/workflows/relevo-queue.yml +10 -1
  49. package/templates/workflows/relevo-review.yml +10 -1
  50. package/templates/workflows/relevo-ship.yml +135 -0
  51. package/templates/workflows/relevo-sla.yml +10 -1
@@ -88,6 +88,8 @@ node ${CLAUDE_PLUGIN_ROOT}/scripts/preflight.mjs
88
88
 
89
89
  It checks the configuration, that OpenSpec runs and finds a root for this repository, and that the working tree is clean. If it exits non-zero, stop with a clear message and change nothing: show its output, which names each failed check and its fix.
90
90
 
91
+ When the failed check is `openspec` (the command line does not run), stop there. Show the install command it gives and do not go on from memory or from reading the code: never write anything under `openspec/changes/` by hand, and never imitate what `openspec` would have produced.
92
+
91
93
  Find the repository name: `$GITHUB_REPOSITORY`, or `gh repo view --json nameWithOwner -q .nameWithOwner`.
92
94
 
93
95
  ### 1. Plan and claim
@@ -110,7 +112,7 @@ This reads the queue from GitHub, applies the gates and limits, carries out labe
110
112
 
111
113
  Read the decision for the issue in the JSON it prints:
112
114
 
113
- - `start`: continue. It gives you `branch`, `change_id`, `base`, `assignee`, `title`, `qa_retries`, `run_max_turns`, `run_max_cost`, `issue_hash` (the text of the issue when it was claimed), `allow_paths` (dependency files and folders the issue lets the change touch), and the platform the issue is for: `platform` (its name), `stack`, `path` (its folder) and `targets`.
115
+ - `start`: continue. It gives you `branch`, `change_id`, `base`, `assignee`, `title`, `qa_retries`, `run_max_turns`, `run_max_cost`, `issue_hash` (the text of the issue when it was claimed), `allow_paths` (dependency files and folders the issue lets the change touch), and the platform the issue is for: `platform` (its name), `stack`, `path` (its folder) and `targets`. When a valid urgent label let the issue start over the assignee's capacity it also has `urgent` (true) and `built_waiting` (how many built issues the assignee will have waiting).
114
116
  - anything else (`needs-spec`, `block`, `queue`, `denied`, `no-milestone`, `no-assignee`, `skip`): that is a normal outcome, already applied. Report the decision and its reason in one line, and stop. Do not build.
115
117
 
116
118
  ### 2. Branch
@@ -245,7 +247,7 @@ Do not add links to this session or attribution lines to the message.
245
247
 
246
248
  1. `git push -u origin <branch>` (a normal push, never forced).
247
249
  2. Move the issue to `built`: `label-change.mjs --to built --current "..."`, applied with `gh issue edit`.
248
- 3. Post one comment with `render-message.mjs built`, mentioning the assignee. Fill: `assignee`, `issue`, `title`, `tasks_done` and `tasks_total` (`progress.complete` and `progress.total` of `openspec instructions apply --change "<change_id>" --json`), `branch`, `issue_hash`, `scenarios` (`node ${CLAUDE_PLUGIN_ROOT}/scripts/checklist.mjs openspec/changes/<change_id> --count`), `risk` if QA or the developer pointed at a riskiest area, and `checklist` (the output of `node ${CLAUDE_PLUGIN_ROOT}/scripts/checklist.mjs openspec/changes/<change_id>`).
250
+ 3. Post one comment with `render-message.mjs built`, mentioning the assignee. Fill: `assignee`, `issue`, `title`, `tasks_done` and `tasks_total` (`progress.complete` and `progress.total` of `openspec instructions apply --change "<change_id>" --json`), `branch`, `issue_hash`, `scenarios` (`node ${CLAUDE_PLUGIN_ROOT}/scripts/checklist.mjs openspec/changes/<change_id> --count`), `risk` if QA or the developer pointed at a riskiest area, `urgent` and `built_waiting` copied from the decision when it has them (they add the line that says the issue started ahead of the queue; leave both out otherwise), and `checklist` (the output of `node ${CLAUDE_PLUGIN_ROOT}/scripts/checklist.mjs openspec/changes/<change_id>`).
249
251
 
250
252
  The comment tells the assignee the next command (`/relevo:review <issue>`).
251
253
 
@@ -34,13 +34,14 @@ If `relevo.yml` already exists and is valid, skip this step. Otherwise ask, in s
34
34
  - the review mode: `none` (a solo project), `peer` or `roles`;
35
35
  - the language of the hand-off messages (`en` or `es`);
36
36
  - the platforms: for each one a name, its stack (look at the project to propose one: the profiles in `${CLAUDE_PLUGIN_ROOT}/stacks/` are the stacks Relevo ships with: `flutter`, `vue`, `laravel` and `supabase`), the folder where it lives (`.` for a repository with one platform, `apps/web` in a monorepo) and, for a stack that has targets, which ones (for Flutter: `android`, `ios`, `web`; for Vue: `web`; Laravel and Supabase have none). Several platforms in one repository are written down, but `go` builds only a repository with one for now;
37
- - where `go` builds: in the cloud, on each person's machine, or both (the default), and, for the cloud, with an API key from the Anthropic Console (the default) or with a subscription token. A subscription's quota is shared by every run and by the interactive use of the account, so `init` sets `limits.concurrent_agents` to 1 for it. Nothing about this goes in `relevo.yml` apart from that number: the installed workflows, the secret that exists and the repository variable `RELEVO_CLOUD_GO` (`off` pauses the cloud at once, with no pull request) decide it;
37
+ - where `go` builds: in the cloud, on each person's machine, or both (the default), and, for the cloud, with an API key from the Anthropic Console (the default) or with a subscription token. A subscription's quota is shared by every run and by the interactive use of the account, so `init` sets `limits.concurrent_agents` to 1 for it. Nothing about this goes in `relevo.yml` apart from that number: the installed workflows, the secret that exists and the repository variable `RELEVO_CLOUD_GO` (`off` pauses the cloud at once, with no pull request) decide it. Whatever the answer, the ship workflow (`relevo-ship.yml`) is added: it ships an approved issue when a person applies the `relevo:ship` label, runs no model and needs no model credential, and the repository variable `RELEVO_CLOUD_SHIP` (`off`) pauses it;
38
+ - whether the repository runs its GitHub Actions on a self-hosted runner. Ask nothing more when the answer is no: the workflows run on `ubuntu-latest` and `relevo.yml` gets no `runner` key. On a yes, ask for the runner's labels (for example `self-hosted` and `Front`): they become `runner.labels` in `relevo.yml` and the `runs-on` of every workflow `init` adds. Say that Relevo assumes a self-hosted runner keeps its files between jobs, whatever the person says about it, and that the agent's code runs with the permissions of the runner's user; do not ask whether the runner is ephemeral;
38
39
  - the repositories this one depends on, if any (`owner/repo`, for example the back end of a front end): they are written to `links`, the closed list that `Depends-on: owner/repo#N` is checked against;
39
40
  - whether to create a commented `.relevo/stacks/<stack>.md` for each stack, where the project can add its own hygiene to the plugin's profile (optional; `init` creates it but does not commit it: it is left for the person to commit, because the cloud and teammates read it from git);
40
41
  - which agent builds in the cloud, only when the person says they use Antigravity: `claude` (the default) or `antigravity`. With `antigravity` the model key is `GEMINI_API_KEY`, a key made only for Relevo in its own Google Cloud project with a quota, and the limit asked below is a number of tokens (`limits.run_max_tokens`) because that agent has no cost limit; `docs/security.md` says what else it cannot enforce;
41
42
  - the cost limit per unattended run in US dollars: the template's 5 is a placeholder the person must accept or change (with `antigravity`, the token limit instead).
42
43
 
43
- Write the answers as JSON to a temporary file **outside the repository**, with the keys `members`, `specWriter`, `reviewMode`, `language`, `platforms` (a map of name to `{ stack, path, targets }`), `links` (a list of `owner/repo`, possibly none), `runs` (`cloud`, `local` or `both`), `credential` (`api_key` or `subscription`), `stackProfiles` (the stacks that get a profile file, possibly none) and, if given, `runMaxCost`. Never put a secret in it.
44
+ Write the answers as JSON to a temporary file **outside the repository**, with the keys `members`, `specWriter`, `reviewMode`, `language`, `platforms` (a map of name to `{ stack, path, targets }`), `links` (a list of `owner/repo`, possibly none), `runs` (`cloud`, `local` or `both`), `credential` (`api_key` or `subscription`), `runnerLabels` (a list of runner labels, only for a self-hosted runner), `stackProfiles` (the stacks that get a profile file, possibly none) and, if given, `runMaxCost`. Never put a secret in it.
44
45
 
45
46
  ### 3. Show the plan
46
47
 
@@ -56,6 +57,10 @@ For each line in the plan that is an install, show its exact command and ask for
56
57
 
57
58
  One install is not a dependency: `install.plugin-project` declares the Relevo plugin in the project, so that a teammate who clones the repository is offered it, even when the plugin is already installed for the machine. Show its exact command (the two `claude plugin` commands at the project scope) and run it only on a yes. If it ran, `.claude/settings.json` is new or changed: say that it is left for the person to commit. If the person declines, nothing else changes, and `/relevo:doctor` keeps warning about it. When the plan says that file is not valid JSON, do not touch it: tell the person to fix it.
58
59
 
60
+ `install.web-hook` adds a `SessionStart` hook to `.claude/settings.json` that installs OpenSpec at the start of a session of Claude Code on the web, where every new container lacks it. Show the hook the plan names (it runs nothing outside a remote session and nothing when `openspec` already runs) and ask for a yes to it alone. On a yes, run `node ${CLAUDE_PLUGIN_ROOT}/scripts/web-hook.mjs` from the repository root: it adds the hook next to the other settings and hooks of the file and changes nothing else, and it writes nothing when the file is not valid JSON. Say that `.claude/settings.json` is left for the person to commit. If the person declines, write nothing for it: `/relevo:doctor` keeps warning about it in a web session. When the plan says the file is not valid JSON, do not touch it.
61
+
62
+ For a Vue platform reviewed in a browser, the plan has two installs, in this order: `install.playwright.<platform>` adds Playwright to the platform's development dependencies, and `install.chromium.<platform>` downloads its Chromium. Run each from the repository root exactly as shown (the command enters the platform folder itself). If Playwright was added, `package.json` and the lock file of that platform changed: say that they are left for the person to commit with the other new files, on the base branch, so that every review finds Playwright. If the person declines either one, continue and say that a browser review of that platform stops until it is installed, and that `/relevo:doctor` keeps warning about it. A `review.autopilot` line among the manual steps means the existing `relevo.yml` keeps the autopilot `off`: say that a browser review needs `precheck` or `verify`, and leave the file as it is.
63
+
59
64
  ### 5. Apply
60
65
 
61
66
  ```bash
@@ -19,6 +19,7 @@ Scripts live in `${CLAUDE_PLUGIN_ROOT}/scripts/`. Run them with `node`. They hol
19
19
  - Never patch code during the review. A defect found here goes through `/relevo:fix`.
20
20
  - Never waive a finding, and never suggest or hint that one could be waived: a waiver is the person's decision, written by them as `waive <number> <reason>`.
21
21
  - Never commit the `evidence/` folder, and never force-push.
22
+ - Never read the project's environment files (`.env`, `.env.local` and the like); only `.env.example` and the specification give variable names. Never stop, restart or signal a server you did not start with `extra-server.mjs`, above all the one the person started.
22
23
 
23
24
  The only `adb` commands you may run are: list devices, clear or read the log (`logcat`), and the screenshot taken by `capture.mjs`.
24
25
 
@@ -31,7 +32,8 @@ Stop with a clear message, changing nothing, if any of these fails:
31
32
  - `node ${CLAUDE_PLUGIN_ROOT}/scripts/validate-config.mjs relevo.yml` exits non-zero. Show its output.
32
33
  - `git check-ignore -q evidence/` fails: the evidence folder is not ignored by git. Tell the person to add `evidence/` to `.gitignore`.
33
34
  - The issue is not `built` (or `in-review`, to resume). Say which state it is in and what to do (`/relevo:go`, `/relevo:fix`).
34
- - `node ${CLAUDE_PLUGIN_ROOT}/scripts/review-adapter.mjs --platform <platform>` (the platform of the issue, from `platform-of.mjs`) exits 1: no review is possible for that platform and target; show the `reason` and stop, changing nothing. If it prints `needs_autopilot: true` and `review.autopilot` is `off`, stop and tell the person to set `review.autopilot` to `precheck` or `verify` in `relevo.yml`: a browser has no manual review.
35
+ - `node ${CLAUDE_PLUGIN_ROOT}/scripts/review-adapter.mjs --platform <platform>` (the platform of the issue, from `platform-of.mjs`) exits 1: no review is possible for that platform and target; show the `reason` and stop, changing nothing. If it prints `needs_autopilot: true` and `review.autopilot` is `off`, stop and show its `advice` as it is: it recommends `verify`, names `precheck` as the cautious option, and is already in the language of `relevo.yml`.
36
+ - Not a stop, a warning: if `git status --porcelain -- relevo.yml` prints anything, tell the person that `relevo.yml` has uncommitted changes, that this review uses them as they are, and that they belong on the base branch through a commit of their own. Never stage or commit `relevo.yml` in this review.
35
37
  - For an adapter whose kind is `device`: `node ${CLAUDE_PLUGIN_ROOT}/scripts/device.mjs` exits non-zero. Show its message: it says what to connect or authorise. Keep the serial it prints as `<serial>`. A `browser` adapter needs no device.
36
38
 
37
39
  Find the repository (`$GITHUB_REPOSITORY`, or `gh repo view --json nameWithOwner -q .nameWithOwner`), the issue's branch (`git branch -r --list "origin/feature/$ARGUMENTS-*"`) and its change folder: the one `openspec list --json` names that starts with `$ARGUMENTS-` (`openspec/changes/<name>`).
@@ -77,14 +79,29 @@ If `evidence/review-state.json` exists for this issue, resume: show the confirme
77
79
 
78
80
  Otherwise: `node ${CLAUDE_PLUGIN_ROOT}/scripts/review-state.mjs init openspec/changes/<change folder> --autopilot <review.autopilot>`, then clear the log so the audit only sees this session: `adb -s <serial> logcat -c`. Then ask the person to start the app, or restart it if it was already open, so that the log names its process and the audit can ignore other apps on the device.
79
81
 
82
+ For a `browser` adapter there is no `adb` and no device log: after `init`, run the preflight from the folder of the platform, against the address under review (the one the person gives; for Vue, `http://localhost:5173` when they give none):
83
+
84
+ ```bash
85
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/web-preflight.mjs --url <address>
86
+ ```
87
+
88
+ It prints JSON and exits by what is missing. Tell the person in their language:
89
+
90
+ - `3` (there is no `node_modules`) or `7` (the installed dependencies do not match `package-lock.json`, for example after the merge changed it): when `install` is `null` there is no lock file to install from; ask the person to install the dependencies themselves and stop until they say it is done. Otherwise show the `install` command exactly as printed and ask for a yes to that command alone; never compose another one. Only on a yes run it from the folder of the platform, then `git status --porcelain -- package.json <the lock file>`: if either changed, restore it with `git restore --` and say which. Then run the preflight again. If the install fails, show its error and stop: do not retry it with package scripts or another command, and do not drive. If the person says no, stop without driving; the issue stays as it is. After an install, if the person's dev server is already running, tell them to restart it before you check the address again; never restart it yourself.
91
+ - `4`: Playwright does not resolve from the project. Install nothing: say that Playwright is missing from the development dependencies of the project, that `/relevo:init` adds it once for everyone (or `npm install -D playwright` in the folder of the platform, committed on the base branch), and stop without driving.
92
+ - `5`: the Chromium of Playwright is missing. It lives outside the project: ask for permission, and only on a yes run `npx playwright install chromium` from the folder of the platform, then run the preflight again.
93
+ - `6`: the app does not answer, or one of its modules answers 400 or more (`detail` names it). Keep its `start` and `env` for step 3: the person starts the app there. If the app was already running and a module fails, tell them to restart their server; never do it yourself.
94
+ - `0`: ready.
95
+
80
96
  ### 3. Brief the person, then stop
81
97
 
82
- Tell the person to start the app on the device themselves (for example `flutter run -d <serial>` in another terminal). Then show:
98
+ Tell the person to start the app on the device themselves (for example `flutter run -d <serial>` in another terminal). For a `browser` adapter, tell them the exact command to start it in their own terminal: the `start` of the preflight, with the variable names of its `env` and the address under review. Then show:
83
99
 
84
- 0. The **brief**, computed and not written by you: save the issue body and the `built` comment outside the working tree (`gh issue view $ARGUMENTS --json title,body,comments`), then `node ${CLAUDE_PLUGIN_ROOT}/scripts/review-brief.mjs --issue $ARGUMENTS --title "<title>" --body-file <body> --built-file <the built comment> --language <language>`. It prints about five lines: what was asked, the outcome, what to check first and how many lines there are to check. Show them as they are.
85
- 1. A **shot list**: two or three screenshots worth taking, named like `01_initial`, `02_action`, `03_result`, taken from the scenarios. For a change with nothing visible, say there are no screenshots to take and the log will be audited anyway.
100
+ 0. The **brief**, computed and not written by you: save the issue body and the `built` comment outside the working tree (`gh issue view $ARGUMENTS --json title,body,comments`), then `node ${CLAUDE_PLUGIN_ROOT}/scripts/review-brief.mjs --issue $ARGUMENTS --title "<title>" --body-file <body> --built-file <the built comment> --language <language> --autopilot <review.autopilot>`. It prints about five lines: what was asked, the outcome, what to check first and how many lines there are to check, and, with `precheck` or `verify`, a fixed note that the person confirms every line. Show them as they are.
101
+ 1. A **shot list**: two or three screenshots worth taking, named like `01_initial`, `02_action`, `03_result`, taken from the scenarios. For a change with nothing visible, say there are no screenshots to take and the log will be audited anyway. In a browser review, each shot also names what proves the page shows that state and no other shot's (the text of the alert, the heading, the URL): it becomes the check of its capture.
86
102
  2. The **checklist**: `node ${CLAUDE_PLUGIN_ROOT}/scripts/checklist.mjs openspec/changes/<change folder>`, one numbered line per scenario, edge cases included. Point out any step the person must do outside the app, because you will not do it.
87
- 3. What they can write: `capture <name>`, `ok <numbers>` (or `ok all`), `fail <number> <what went wrong>`, `retest <numbers>`, `done`. They may write in any language; understand the intent.
103
+ 3. With `review.autopilot` `precheck` or `verify`, the **plan of the walk**, so the person does not have to ask: one line per checklist line you walk yourself, `<n>. <method>: <how>`, where the method is `browser` (a Playwright assertion), `command` (an npm script of `package.json`, such as typecheck, lint, test or build), `read` (code, CI or docs) or `server <port>` (a run against a server you start with another configuration: say which variables are set or removed). Then the lines left to the person. Every configuration you will start is listed here, before the person confirms the data (Autopilot, step 1), so that confirmation covers them too.
104
+ 4. What they can write: `capture <name>`, `ok <numbers>` (or `ok all`), `fail <number> <what went wrong>`, `retest <numbers>`, `done`. They may write in any language; understand the intent.
88
105
 
89
106
  **Then end your turn.** Make no further tool calls until the person writes. Do not advance on your own and do not run the log audit early.
90
107
 
@@ -92,7 +109,7 @@ Tell the person to start the app on the device themselves (for example `flutter
92
109
 
93
110
  Handle each message as it comes:
94
111
 
95
- - `capture <name>`: `node ${CLAUDE_PLUGIN_ROOT}/scripts/capture.mjs <name>`, then record it with `review-state.mjs capture <path>`. Reply with the saved path only. If the script rejects the name, show its message and ask for another.
112
+ - `capture <name>`: `node ${CLAUDE_PLUGIN_ROOT}/scripts/capture.mjs <name>`, then record it with `review-state.mjs capture <path>`. Reply with the saved path only. If the script rejects the name, show its message and ask for another. In a browser review there is no device to capture: add the shot, with its check, to the shot list and to the review script, which then needs QA again (Autopilot, step 4).
96
113
  - `ok <numbers>`: `review-state.mjs set <n> ok` for each. `fail <n> <note>`: `review-state.mjs set <n> fail <note>`. `retest <numbers>`: `review-state.mjs set <n> pending` for each, so the line must be confirmed again (useful after a fix).
97
114
  - A bug or something that looks wrong: do not edit anything. Record it as a `fail` and say the fix goes through `/relevo:fix $ARGUMENTS`; the review can resume afterwards.
98
115
  - `waive <number> <reason>`: the person's decision, available once the audit has findings; run `review-state.mjs waive <number> <reason>`. It refuses an empty reason and an inconclusive audit; show its message.
@@ -102,14 +119,21 @@ After **each** message of the person, keep the device log from being lost to its
102
119
 
103
120
  Never go to step 5 without an explicit `done` (or the equivalent in the person's language).
104
121
 
122
+ Captures belong to this step: the shot list is taken while the person tests, before the agreement, and `review-state.mjs capture` refuses after `agree`. If the person asks for screenshots after agreeing, point to the files already in `evidence/` and do not drive again.
123
+
105
124
  ### Autopilot (only when `review.autopilot` is `precheck` or `verify`)
106
125
 
107
126
  When it is `off` skip this section: the person does everything. The person always confirms the lines and gives the agreement; nothing here replaces that.
108
127
 
109
128
  1. **Confirm the data.** Before any driven step, ask the person to confirm in writing that the app points to data that is not production. Record it only after they say so: `node ${CLAUDE_PLUGIN_ROOT}/scripts/review-state.mjs confirm-env`. Without it `precheck` and `auto` are refused.
110
- 2. **Walk the lines.** `precheck`: after you walk a line, record your own result with `review-state.mjs precheck <n> ok|fail <note>`; the line stays pending until the person writes `ok`. `verify`: in addition, when the `THEN` of a line was proved by an assertion, a semantic finder or the log, record `review-state.mjs auto <n> <what proved it>`; the person confirms the other lines. A line about appearance, animation or fidelity to an image is never marked automatically.
129
+ 2. **Walk the lines.** `precheck`: after you walk a line, record your own result with `review-state.mjs precheck <n> ok|fail <note>`; the line stays pending until the person writes `ok`. `verify`: in addition, when the `THEN` of a line was proved by an assertion, a semantic finder or the log, or by the exit code of a project command, record `review-state.mjs auto <n> <what proved it>`; the person confirms the other lines. A line about appearance, animation or fidelity to an image is never marked automatically. A line checked only by reading code, CI or docs is recorded with `precheck` and what you found, never with `auto`.
130
+ - **Project commands.** Run the npm script of `package.json` that the line names (typecheck, lint, test, build), never a script that fixes or formats files. Run `git status --porcelain` before and after: if a tracked file changed, restore it with `git restore -- <file>` and tell the person which file changed. Record the command and its exit code as what proved the line.
111
131
  3. **Flutter on a device.** Drive through the Dart and Flutter MCP tools: `launch_app`, `connect_dart_tooling_daemon`, then `flutter_driver` (`get_health`, `waitFor`, `tap`, `enter_text`, `scroll`, `screenshot`), `get_runtime_errors`, `get_app_logs`, and `hot_reload` after a micro-fix. It needs `lib/main_driver.dart` with `enableFlutterDriverExtension()`. Rules: never pass `timeout` to `flutter_driver` (it breaks the MCP server); find widgets only by semantic finders (text, key, tooltip, type), never by coordinates; if a finder fails twice, stop driving and go back to manual for that line; the log of the MCP server is the source of the audit, and if it does not answer or is empty the audit is inconclusive.
112
- 4. **A browser (Vue, or Flutter on the web).** Write a Playwright script from the scenarios to `evidence/review-web.mjs`: it exports a default async function that receives `{ page, baseUrl, line, credentials }`, uses `node:assert/strict`, and calls `line(<checklist number>, "<what is asserted>", async () => { ... })` once per line it can check. Every `THEN` it covers must have an assertion that would fail if the feature did not exist (text, URL, response, absence of errors), not a screenshot. A test login is read with `credentials("NAME")` where `NAME` is an environment variable the person names; you never read, print or store its value. Then launch `relevo:qa` on the script, read-only. Only if it answers `QA PASSED`, run `node ${CLAUDE_PLUGIN_ROOT}/scripts/web-review.mjs --script evidence/review-web.mjs --url <the address of the app> --allow-origins <review.allowed_origins> --secret-env <the names>`. The browser is its own, with a clean profile, and can reach only `localhost`, the address under review and `review.allowed_origins`; a refused script is not run in any other way. Its `auto-ok` lines are recorded as in step 2, its `auto-fail` lines as `precheck <n> fail` with the message, and `evidence/web-log.txt` is the log of step 5 (audit with `--package web`). The script checks are a tripwire, not a sandbox: if it touches anything sensitive, show it to the person before it runs.
132
+ 4. **A browser (Vue, or Flutter on the web).** Write a Playwright script from the scenarios to `evidence/review-web.mjs`: it exports a default async function that receives `{ page, baseUrl, line, credentials, capture }`, uses `node:assert/strict`, and calls `line(<checklist number>, "<what is asserted>", async () => { ... })` once per line it can check. Every `THEN` it covers must have an assertion that would fail if the feature did not exist (text, URL, response, absence of errors), not a screenshot. A test login is read with `credentials("NAME")` where `NAME` is an environment variable the person names; you never read, print or store its value.
133
+ - **Screenshots.** Turn the shot list into `capture("<name>", async () => { <the check of that shot> }, { fullPage: true })` calls. The check runs first, and when it fails no screenshot is taken and the capture is reported as failed. `capture` is the only way to take one: the runner refuses `page.screenshot`, any file path, and anything the script prints goes to `script-console.txt`, never to you. You never open the PNG files; their paths are all you see.
134
+ - **QA.** Then launch `relevo:qa` on the script, read-only, with the shot list: it rejects a `capture` name that is not on the shot list, and a check that would also hold for another shot (for example only `[role="alert"]` for two different errors). Only if it answers `QA PASSED`, record that exact script with `node ${CLAUDE_PLUGIN_ROOT}/scripts/review-state.mjs qa-pass evidence/review-web.mjs`. Any change to the script afterwards, even one port, needs `relevo:qa` and `qa-pass` again: the runner refuses a script whose content QA did not pass.
135
+ - **The normal run.** Run `node ${CLAUDE_PLUGIN_ROOT}/scripts/web-preflight.mjs --url <the address of the app>` again: it must exit 0. Then run `node ${CLAUDE_PLUGIN_ROOT}/scripts/web-review.mjs --script evidence/review-web.mjs --url <the address of the app> --allow-origins <review.allowed_origins> --secret-env <the names> --out evidence`. The runner itself refuses before any browser starts when the state has no confirmation of the data, the autopilot is `off` or the review is already agreed. The browser is its own, with a clean profile, and can reach only `localhost`, the address under review and `review.allowed_origins`; a refused script is not run in any other way. Its `auto-ok` lines are recorded as in step 2, its `auto-fail` lines as `precheck <n> fail` with the message, its captures are already recorded in the review state, and `evidence/web-log.txt` is the log of step 5 (audit with `--package web`). When it says `the page itself failed`, that is the cause to tell the person, not the selector timeout after it. The script checks are a tripwire, not a sandbox: if it touches anything sensitive, show it to the person before it runs.
136
+ - **Runs with another configuration** (the `server <port>` lines of the plan). Start each server yourself on a free localhost port with `node ${CLAUDE_PLUGIN_ROOT}/scripts/extra-server.mjs start --port <port> [--set NAME=value] [--unset NAME] -- npx vite --port <port> --strictPort`. It refuses a busy port and refuses `--unset` while an environment file exists (the dev server loads it by itself): then that line is pre-checked by reading the code or left for the person. Wait until it answers with `web-preflight.mjs --url http://localhost:<port> --wait 30000`, confirm it with `extra-server.mjs check --port <port>`, and run the same script with `--url http://localhost:<port> --out evidence/run-<port>`: each run has an output folder of its own, and these logs are never audited, because they show errors on purpose. Stop each one afterwards with `extra-server.mjs stop --port <port>`. The server the person started is never stopped or restarted.
113
137
  5. **Never** drive in the person's own browser, on an origin that is not allowed, with coordinates, with `adb shell input`, Appium or Maestro, or send a message or make a call in another app.
114
138
 
115
139
  ### 5. Audit the log
@@ -119,6 +143,12 @@ package=$(node ${CLAUDE_PLUGIN_ROOT}/scripts/app-id.mjs)
119
143
  adb -s <serial> logcat -d -v threadtime | node ${CLAUDE_PLUGIN_ROOT}/scripts/review-state.mjs audit --accumulated --package "$package"
120
144
  ```
121
145
 
146
+ For a `browser` adapter there is no device: audit only the log of the normal run, never one of `evidence/run-<port>/`:
147
+
148
+ ```bash
149
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/review-state.mjs audit evidence/web-log.txt --package web
150
+ ```
151
+
122
152
  `app-id.mjs` prints the application id of the project. The audit then looks only at the lines written by that app's processes, so other apps on the device are neither audited nor quoted. If `app-id.mjs` finds no id, run the audit without `--package`.
123
153
 
124
154
  It prints `REVIEW PASSED`, `REVIEW REJECTED` with the numbered findings, or `REVIEW INCONCLUSIVE`, and writes `evidence/review-report.md`. **Inconclusive** means the log was empty or had no line of the app: it proves nothing, it is not a pass and it cannot be waived. Ask the person to start the app again, repeat the steps, and audit once more.
@@ -150,11 +180,20 @@ Only when `decide` exited 0.
150
180
  ### 8. Evidence
151
181
 
152
182
  - `evidence.policy: local_only`: delete the `evidence/` folder now. Nothing is attached anywhere.
153
- - `evidence.policy: attach_to_pr`: GitHub has no API for attaching images, so you cannot upload them. List the files in `evidence/` and ask the person to drag them into the pull request description. Keep the folder until they say they did, then delete it.
183
+ - `evidence.policy: attach_to_pr`: GitHub has no API for attaching images, so you cannot upload them. List only the images, in every run folder (`find evidence -name '*.png'`), and ask the person to drag them into the pull request description; the logs, results and scripts repeat what the body already says. Keep the folder until they say they did, then delete it. When there is no image, delete the folder without asking and say there was nothing to attach.
154
184
 
155
185
  ### 9. Return to base branch
156
186
 
157
- Restore the working tree to the base branch so the workspace remains clean on the base branch for other tasks or subsequent reviews, but only where that is possible: in the main checkout, and when no other worktree holds the base branch.
187
+ Restore the working tree to the base branch so the workspace remains clean on the base branch for other tasks or subsequent reviews, but only where that is possible: in the main checkout, when no other worktree holds the base branch, and when no dev server of the address under review runs from the working tree (switching would remove its files under it and break it until it is restarted).
188
+
189
+ For a `browser` adapter, first stop the servers you started, then ask the guard:
190
+
191
+ ```bash
192
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/extra-server.mjs stop --all
193
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/dev-server-guard.mjs --url <the address of the app>
194
+ ```
195
+
196
+ Switch only when the guard exits 0. On 1 a server runs from the working tree; on 3 the guard cannot tell. In both cases do not switch: say why, and that the person can stop their server and run `git switch <base>` themselves.
158
197
 
159
198
  ```bash
160
199
  [ "$(git rev-parse --git-dir)" = "$(git rev-parse --git-common-dir)" ] && ! git worktree list --porcelain | grep -qx "branch refs/heads/<base>" && git switch <base>
@@ -99,7 +99,7 @@ Do not add links to this session or attribution lines to the commit.
99
99
 
100
100
  ### 4. Check again
101
101
 
102
- The push may start new checks or dismiss an approval. Wait for any running checks (`gh pr checks <number> --repo <owner/name> --watch`), then run `ship-check.mjs` again, exactly as in step 1 and adding `--baseline <baseline>`, so an approval given on the head before the archive still counts. If it now says the pull request has conflicts, see below. If it says anything else to stop, show the reasons and stop. The most common one is an approval dismissed by the push: tell the person that the repository must not dismiss approvals on new commits (`/relevo:doctor` checks it).
102
+ The push may start new checks or dismiss an approval. Wait for any running checks (`gh pr checks <number> --repo <owner/name> --watch`), then run `ship-check.mjs` again, exactly as in step 1 and adding `--baseline <baseline>`, so an approval given on the head before the archive still counts. When the archive commit has no checks of its own and changes only `openspec/`, `ship-check` judges the checks of `<baseline>` instead, and its summary says `checksFrom: baseline`; do not ask about `no-ci` then. If it now says the pull request has conflicts, see below. If it says anything else to stop, show the reasons and stop. The most common one is an approval dismissed by the push: tell the person that the repository must not dismiss approvals on new commits (`/relevo:doctor` checks it).
103
103
 
104
104
  **The archive left conflicts.** If the only reason is `CONFLICTING`, the base branch changed the specifications the archive wrote. Recover with normal pushes only, never a force-push:
105
105
 
@@ -126,3 +126,5 @@ The subject is the pull request title, which already ends with the issue number.
126
126
  Finish with one short block: the issue, the pull request, the merge commit, that the issue is closed and the labels removed.
127
127
 
128
128
  End with this reminder: promoting `<base>` to `<release>` is a separate pull request, with a version tag. `ship` does not do it.
129
+
130
+ When the repository has the ship workflow (`.github/workflows/relevo-ship.yml`), add one line: next time, the person who ships can apply the `relevo:ship` label to the issue instead, and GitHub ships it with the same checks, without a local copy.
package/stacks/vue.md CHANGED
@@ -20,7 +20,7 @@ targets:
20
20
 
21
21
  ## Verification
22
22
 
23
- Run the commands of this profile from the folder of the platform: `analyze` (type check, then linter), `test` and `format`. They use the versions pinned in the project (`npx --no-install`), so the dependencies must be installed; if they are not, say so instead of installing anything new.
23
+ Run the commands of this profile from the folder of the platform: `analyze` (type check, then linter), `test` and `format`. They use the versions pinned in the project (`npx --no-install`), so the dependencies must be installed; if they are not, say so instead of installing anything new. Only the review may install them: it offers the frozen install of the project's lock file (`npm ci --ignore-scripts`) and runs it on a yes.
24
24
 
25
25
  Do not claim the code type-checks, lints or passes tests unless you ran the commands and saw the result. The commands written here are read-only on purpose: the `lint` script that `npm create vue` generates uses `--fix` and rewrites files, so do not run `npm run lint` as a check. Only `format` writes.
26
26
 
@@ -57,7 +57,7 @@ Do not spend time re-checking what `analyze` already enforces: read its output.
57
57
 
58
58
  ## Targets
59
59
 
60
- - `web`: needs a browser; no extra tool. The review of a web target is always driven by the agent (`review.autopilot` must be `precheck` or `verify`) with a Playwright script that `qa` reads first; Playwright is a dependency of the project, installed with `npm install -D playwright` and `npx playwright install chromium`.
60
+ - `web`: needs a browser; no extra tool. The review of a web target is always driven by the agent (`review.autopilot` must be `precheck` or `verify`) with a Playwright script that `qa` reads first; Playwright is a development dependency of the project: `/relevo:init` offers to add it (`npm install -D playwright`) and its Chromium (`npx playwright install chromium`), and the review never installs Playwright itself.
61
61
 
62
62
  ## Questions for the issue writer
63
63
 
@@ -1,7 +1,7 @@
1
1
  # What `relevo-go.yml` and `relevo-queue.yml` run when `agent.build` in relevo.yml is `antigravity`. This file is not a
2
2
  # workflow: each block below replaces, in the shared workflow, the lines between the markers with the same name, and
3
3
  # nothing else of the workflow changes (a test checks it). Edit the shared workflow for anything that is not about the
4
- # agent. Written for ubuntu-latest (x86-64).
4
+ # agent. Written for an x86-64 Linux runner (the binary below is linux-x64).
5
5
  #
6
6
  # What Antigravity CLI cannot do on its own, and what stands in for it (docs/security.md):
7
7
  # turn and token limits the run guard (scripts/agent-guard.mjs) reads the event stream and stops the agent; the numbers
@@ -32,10 +32,13 @@
32
32
  curl -fsSL -o "$RUNNER_TEMP/agy.tar.gz" "$AGY_URL"
33
33
  echo "$AGY_SHA512 $RUNNER_TEMP/agy.tar.gz" | sha512sum --check --strict -
34
34
  tar -xzf "$RUNNER_TEMP/agy.tar.gz" -C "$RUNNER_TEMP" antigravity
35
- mkdir -p "$HOME/.local/bin"
36
- install -m 0755 "$RUNNER_TEMP/antigravity" "$HOME/.local/bin/agy"
37
- echo "$HOME/.local/bin" >> "$GITHUB_PATH"
38
- [ "$("$HOME/.local/bin/agy" --version)" = "$AGY_VERSION" ] || { echo "::error::agy is not version $AGY_VERSION"; exit 1; }
35
+ mkdir -p "$RUNNER_TEMP/bin" "$RUNNER_TEMP/home"
36
+ install -m 0755 "$RUNNER_TEMP/antigravity" "$RUNNER_TEMP/bin/agy"
37
+ echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
38
+ # The CLI, the plugin of the machine and the settings of the run live in a home folder of this job, so a runner
39
+ # that persists keeps none of them. Every later step of the job sees this HOME.
40
+ echo "HOME=$RUNNER_TEMP/home" >> "$GITHUB_ENV"
41
+ [ "$("$RUNNER_TEMP/bin/agy" --version)" = "$AGY_VERSION" ] || { echo "::error::agy is not version $AGY_VERSION"; exit 1; }
39
42
 
40
43
  # The Relevo plugin is rendered from the package unpacked in the step "Check the toolchain" and installed for the
41
44
  # machine, so nothing is added to the working tree (the agent needs it clean). The settings of the run go to the
@@ -28,6 +28,7 @@ review:
28
28
  merger: approver # author | approver: who runs /relevo:ship
29
29
  autopilot: off # off | precheck | verify: how much of the on-device review the agent may drive (the person always closes it)
30
30
  allowed_origins: [] # web origins a driven review may visit besides localhost, e.g. https://staging.example.com
31
+ ship_without_ci: false # true: the relevo:ship label may merge when no check ran (/relevo:ship asks instead)
31
32
 
32
33
  limits:
33
34
  built_unreviewed_per_person: 2 # built issues one person may have waiting
@@ -37,6 +38,9 @@ limits:
37
38
  building_alert_hours: 3 # hours without activity before an issue on `building` is reported as stuck
38
39
  run_max_cost: 5 # REQUIRED: US dollars per unattended run; choose your own
39
40
 
41
+ # runner: # where the Relevo workflows run; leave it out for GitHub-hosted (ubuntu-latest)
42
+ # labels: [self-hosted, Front] # a self-hosted runner is assumed to persist between jobs (docs/execution.md)
43
+
40
44
  qa:
41
45
  retries: 1 # developer attempts after QA rejects
42
46
  rules_from: CLAUDE.md # project rules QA reads at every run
@@ -4,12 +4,16 @@
4
4
  # Change the time below to suit your team (cron is in UTC).
5
5
  # The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
6
6
 
7
+ # Runner: `runs-on` comes from `runner.labels` in relevo.yml (`/relevo:init` writes it into this file, `/relevo:doctor` checks it).
8
+ # The runner needs git, gh and jq; Node and the tools of the job are installed per job. A self-hosted runner is assumed to
9
+ # persist between jobs, so the job puts its tools under $RUNNER_TEMP and removes what it adds to the workspace.
10
+
7
11
  name: relevo-digest
8
12
 
9
13
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
10
14
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
11
15
  env:
12
- RELEVO_VERSION: '0.6.0'
16
+ RELEVO_VERSION: '0.8.0'
13
17
 
14
18
  on:
15
19
  schedule:
@@ -52,3 +56,8 @@ jobs:
52
56
  GH_TOKEN: ${{ github.token }}
53
57
  GITHUB_REPOSITORY: ${{ github.repository }}
54
58
  run: node .relevo-plugin/scripts/digest.mjs --repo "$GITHUB_REPOSITORY"
59
+
60
+ # A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
61
+ - name: Remove what this job added
62
+ if: always()
63
+ run: rm -rf .relevo-plugin
@@ -10,12 +10,18 @@
10
10
  # Keep the numbers in claude_args in line with limits.run_max_turns and limits.run_max_cost.
11
11
  # Where the numbers disagree the lower one wins, so this file can only make a run cheaper than relevo.yml says.
12
12
 
13
+ # Runner: `runs-on` comes from `runner.labels` in relevo.yml (`/relevo:init` writes it into this file, `/relevo:doctor` checks it).
14
+ # The runner needs git, gh and jq; Node and the tools of the job are installed per job. A self-hosted runner is assumed to
15
+ # persist between jobs, so the job puts its tools under $RUNNER_TEMP and removes what it adds to the workspace.
16
+
13
17
  name: relevo-go
14
18
 
15
19
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
16
20
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
17
21
  env:
18
- RELEVO_VERSION: '0.6.0'
22
+ RELEVO_VERSION: '0.8.0'
23
+ # The version of OpenSpec that the job installs for itself.
24
+ OPENSPEC_VERSION: '1.14.3'
19
25
 
20
26
  on:
21
27
  issues:
@@ -109,6 +115,11 @@ jobs:
109
115
  gh issue edit "$ISSUE" --remove-label relevo:ready --remove-label relevo:queued --remove-label relevo:building --add-label relevo:qa-failed
110
116
  gh issue comment "$ISSUE" --body "${assignee:+@$assignee }The run failed before it could decide what to do (job status: $JOB_STATUS), so nothing was built. Read the log to see why: $RUN_URL. When the cause is fixed, remove \`relevo:qa-failed\` and apply \`relevo:ready\` again."
111
117
 
118
+ # A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
119
+ - name: Remove what this job added
120
+ if: always()
121
+ run: rm -rf .relevo-plugin
122
+
112
123
  build:
113
124
  needs: decide
114
125
  if: needs.decide.outputs.action == 'start'
@@ -162,7 +173,10 @@ jobs:
162
173
  # the packages, so they need no such option.
163
174
 
164
175
  - name: Install OpenSpec
165
- run: npm install --global @fission-ai/openspec
176
+ # Under the temporary folder of the job, at the pinned version: nothing global is left on a runner that persists.
177
+ run: |
178
+ npm install --prefix "$RUNNER_TEMP/openspec" "@fission-ai/openspec@$OPENSPEC_VERSION" --no-fund --no-audit
179
+ echo "$RUNNER_TEMP/openspec/node_modules/.bin" >> "$GITHUB_PATH"
166
180
  env:
167
181
  OPENSPEC_TELEMETRY: '0'
168
182
 
@@ -185,12 +199,13 @@ jobs:
185
199
  if result=$(node "$RUNNER_TEMP/relevo/scripts/toolchain-check.mjs" --platform "$PLATFORM" --plugin-root "$RUNNER_TEMP/relevo"); then
186
200
  exit 0
187
201
  fi
188
- missing=$(printf '%s' "$result" | jq -r '.missing | join(", ")')
202
+ # No jq here: the tool that is missing may be jq itself, and the comment must still be posted.
203
+ missing=$(printf '%s' "$result" | node -e 'console.log(JSON.parse(require("fs").readFileSync(0, "utf8")).missing.join(", "))')
189
204
  [ -n "$missing" ] || { echo "::error::The toolchain check failed: $result"; exit 1; }
190
205
  title=$(gh issue view "$ISSUE" --json title --jq .title)
191
206
  assignee=$(gh issue view "$ISSUE" --json assignees --jq '.assignees[0].login // empty')
192
- body=$(jq -n --arg assignee "$assignee" --arg issue "$ISSUE" --arg title "$title" --arg tools "$missing" --arg platform "$PLATFORM" \
193
- '{assignee: $assignee, issue: $issue, title: $title, tools: $tools, platform: $platform}' | node "$RUNNER_TEMP/relevo/scripts/render-message.mjs" toolchain_missing --config relevo.yml)
207
+ body=$(ASSIGNEE="$assignee" TITLE="$title" TOOLS="$missing" node -e 'const e = process.env; console.log(JSON.stringify({assignee: e.ASSIGNEE, issue: e.ISSUE, title: e.TITLE, tools: e.TOOLS, platform: e.PLATFORM}))' \
208
+ | node "$RUNNER_TEMP/relevo/scripts/render-message.mjs" toolchain_missing --config relevo.yml)
194
209
  gh issue edit "$ISSUE" --remove-label relevo:ready --remove-label relevo:queued --remove-label relevo:building --add-label relevo:qa-failed
195
210
  gh issue comment "$ISSUE" --body "$body"
196
211
  echo "::error::The runner has no $missing, so nothing was built"
@@ -300,3 +315,8 @@ jobs:
300
315
  else
301
316
  gh issue comment "$ISSUE" --body "${assignee:+@$assignee }The run ended without a result (job status: $JOB_STATUS), so nothing was built or reported. Read the log to see why: $RUN_URL. When the cause is fixed, remove \`relevo:qa-failed\` and apply \`relevo:ready\` again."
302
317
  fi
318
+
319
+ # A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
320
+ - name: Remove what this job added
321
+ if: always()
322
+ run: rm -rf .relevo-plugin
@@ -5,12 +5,16 @@
5
5
  # schedule, so a lost event does not stall the queue. Needs a relevo.yml at the repository root.
6
6
  # The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
7
7
 
8
+ # Runner: `runs-on` comes from `runner.labels` in relevo.yml (`/relevo:init` writes it into this file, `/relevo:doctor` checks it).
9
+ # The runner needs git, gh and jq; Node and the tools of the job are installed per job. A self-hosted runner is assumed to
10
+ # persist between jobs, so the job puts its tools under $RUNNER_TEMP and removes what it adds to the workspace.
11
+
8
12
  name: relevo-queue
9
13
 
10
14
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
11
15
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
12
16
  env:
13
- RELEVO_VERSION: '0.6.0'
17
+ RELEVO_VERSION: '0.8.0'
14
18
 
15
19
  on:
16
20
  issues:
@@ -72,3 +76,8 @@ jobs:
72
76
  for issue in $(echo "$result" | node -e 'JSON.parse(require("fs").readFileSync(0,"utf8")).start.forEach(n=>console.log(n))'); do
73
77
  gh workflow run relevo-go.yml --repo "$GITHUB_REPOSITORY" -f issue="$issue"
74
78
  done
79
+
80
+ # A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
81
+ - name: Remove what this job added
82
+ if: always()
83
+ run: rm -rf .relevo-plugin
@@ -5,12 +5,16 @@
5
5
  # Pull requests from forks are skipped explicitly (and would only get a read-only token anyway).
6
6
  # The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
7
7
 
8
+ # Runner: `runs-on` comes from `runner.labels` in relevo.yml (`/relevo:init` writes it into this file, `/relevo:doctor` checks it).
9
+ # The runner needs git, gh and jq; Node and the tools of the job are installed per job. A self-hosted runner is assumed to
10
+ # persist between jobs, so the job puts its tools under $RUNNER_TEMP and removes what it adds to the workspace.
11
+
8
12
  name: relevo-review
9
13
 
10
14
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
11
15
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
12
16
  env:
13
- RELEVO_VERSION: '0.6.0'
17
+ RELEVO_VERSION: '0.8.0'
14
18
 
15
19
  on:
16
20
  pull_request_review:
@@ -58,3 +62,8 @@ jobs:
58
62
  GH_TOKEN: ${{ github.token }}
59
63
  GITHUB_REPOSITORY: ${{ github.repository }}
60
64
  run: node .relevo-plugin/scripts/review-event.mjs --repo "$GITHUB_REPOSITORY"
65
+
66
+ # A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
67
+ - name: Remove what this job added
68
+ if: always()
69
+ run: rm -rf .relevo-plugin
@@ -0,0 +1,135 @@
1
+ # Relevo: ships an issue when a person applies the `relevo:ship` label to it.
2
+ # Copy to .github/workflows/relevo-ship.yml. If you renamed the ship label in relevo.yml, change it below too.
3
+ #
4
+ # It runs the steps of /relevo:ship with the same rules, and no model: it needs no model credential. The person who
5
+ # applied the label is the one judged (they must be the one who ships under review.merger), and the label is their
6
+ # written confirmation. It archives the specification on the branch, squash-merges the pull request, closes the issue
7
+ # and removes its labels. When anything stops it, it comments why on the issue and removes the label. Nothing from the
8
+ # pull request's branch is run: the scripts come from the published package and relevo.yml from the base branch.
9
+ #
10
+ # Checks: a push with the workflow token starts no workflows, so the archive commit gets no checks of its own. The
11
+ # checks of the approved head count for it, because it changes only openspec/. If the base branch REQUIRES status
12
+ # checks, GitHub refuses that merge: create a GitHub App for this repository (contents, pull requests and issues:
13
+ # read and write), store its id as the secret RELEVO_SHIP_APP_ID and its private key as RELEVO_SHIP_APP_KEY. The run
14
+ # then pushes, merges and closes with the App's token, so your CI runs on the archive commit, and waits for it. The App
15
+ # is also worth it when you rely on workflows that run on pushes to the base branch. Never add it to a bypass list.
16
+ #
17
+ # The repository variable RELEVO_CLOUD_SHIP set to `off` pauses it: the label then does nothing.
18
+ # This workflow runs from the default branch; it works only once it is there.
19
+
20
+ # Runner: `runs-on` comes from `runner.labels` in relevo.yml (`/relevo:init` writes it into this file, `/relevo:doctor` checks it).
21
+ # The runner needs git, gh and jq; Node and the tools of the job are installed per job. A self-hosted runner is assumed to
22
+ # persist between jobs, so the job puts its tools under $RUNNER_TEMP and removes what it adds to the workspace.
23
+
24
+ name: relevo-ship
25
+
26
+ # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
27
+ # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
28
+ env:
29
+ RELEVO_VERSION: '0.8.0'
30
+ # The version of OpenSpec that the job installs for itself.
31
+ OPENSPEC_VERSION: '1.14.3'
32
+
33
+ on:
34
+ issues:
35
+ types: [labeled]
36
+
37
+ # One ship per issue: a second label event waits for the first run and then finds the label gone or the issue closed.
38
+ concurrency:
39
+ group: relevo-ship-${{ github.event.issue.number }}
40
+ cancel-in-progress: false
41
+
42
+ permissions:
43
+ contents: write
44
+ issues: write
45
+ pull-requests: write
46
+ # To start the queue (relevo-queue.yml) after the close, which the workflow token's close does not start by itself.
47
+ actions: write
48
+
49
+ jobs:
50
+ ship:
51
+ # Any sender: a label applied by a bot is removed by the script without shipping anything.
52
+ if: vars.RELEVO_CLOUD_SHIP != 'off' && github.event.label.name == 'relevo:ship' && !github.event.issue.pull_request
53
+ runs-on: ubuntu-latest
54
+ # The run may wait up to 30 minutes for the checks of the archive commit.
55
+ timeout-minutes: 45
56
+ env:
57
+ HAS_APP: ${{ secrets.RELEVO_SHIP_APP_ID != '' && secrets.RELEVO_SHIP_APP_KEY != '' }}
58
+ steps:
59
+ # Only with the App: a token for this repository, used to push, merge and close.
60
+ - name: App token
61
+ id: app
62
+ if: env.HAS_APP == 'true'
63
+ uses: actions/create-github-app-token@v1
64
+ with:
65
+ app-id: ${{ secrets.RELEVO_SHIP_APP_ID }}
66
+ private-key: ${{ secrets.RELEVO_SHIP_APP_KEY }}
67
+
68
+ # The default branch, which holds this workflow and names the base branch. The script switches to the feature
69
+ # branch in this checkout only to run git and OpenSpec on it; it never installs or runs anything from it.
70
+ - uses: actions/checkout@v4
71
+ with:
72
+ path: work
73
+ fetch-depth: 0
74
+ token: ${{ steps.app.outputs.token || github.token }}
75
+
76
+ - uses: actions/setup-node@v4
77
+ with:
78
+ node-version: 22
79
+
80
+ - name: Install the Relevo scripts
81
+ # The published package at the version pinned above, unpacked outside the checkout so that the archive sees
82
+ # a clean working tree. A package without a script this job runs stops the run here, before anything changes.
83
+ run: |
84
+ mkdir -p "$RUNNER_TEMP/relevo"
85
+ npm pack "@elalfner/relevo@$RELEVO_VERSION" --silent --pack-destination "$RUNNER_TEMP" > /dev/null
86
+ tar -xzf "$RUNNER_TEMP"/elalfner-relevo-*.tgz --strip-components=1 -C "$RUNNER_TEMP/relevo"
87
+ for f in scripts/ship-unattended.mjs scripts/scan-diff.mjs scripts/check-archive.mjs scripts/stage.mjs scripts/fix-tasks.mjs scripts/conflict-scope.mjs scripts/render-message.mjs; do
88
+ [ -f "$RUNNER_TEMP/relevo/$f" ] || { echo "::error::The Relevo package $RELEVO_VERSION lacks $f"; exit 1; }
89
+ done
90
+
91
+ - name: Install OpenSpec
92
+ # Under the temporary folder of the job, at the pinned version: nothing global is left on a runner that persists.
93
+ run: |
94
+ npm install --prefix "$RUNNER_TEMP/openspec" "@fission-ai/openspec@$OPENSPEC_VERSION" --no-fund --no-audit
95
+ echo "$RUNNER_TEMP/openspec/node_modules/.bin" >> "$GITHUB_PATH"
96
+ env:
97
+ OPENSPEC_TELEMETRY: '0'
98
+
99
+ - name: Ship
100
+ working-directory: work
101
+ env:
102
+ GH_TOKEN: ${{ github.token }}
103
+ GH_PUSH_TOKEN: ${{ steps.app.outputs.token || github.token }}
104
+ OPENSPEC_TELEMETRY: '0'
105
+ GITHUB_REPOSITORY: ${{ github.repository }}
106
+ ISSUE: ${{ github.event.issue.number }}
107
+ ACTOR: ${{ github.event.sender.login }}
108
+ ACTOR_TYPE: ${{ github.event.sender.type }}
109
+ run: |
110
+ git config user.name 'github-actions[bot]'
111
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
112
+ node "$RUNNER_TEMP/relevo/scripts/ship-unattended.mjs" --repo "$GITHUB_REPOSITORY" --issue "$ISSUE" --actor "$ACTOR" --actor-type "$ACTOR_TYPE" --work .
113
+
114
+ # A run that fails before it could comment (the package cannot be installed, a git or GitHub error) leaves the
115
+ # label on the issue with nobody told. If the label is still there, remove it and say so.
116
+ # If you renamed the ship label in relevo.yml, change the name below too.
117
+ - name: Report a run that ended without an outcome
118
+ if: always()
119
+ env:
120
+ GH_TOKEN: ${{ github.token }}
121
+ ISSUE: ${{ github.event.issue.number }}
122
+ ACTOR: ${{ github.event.sender.login }}
123
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
124
+ run: |
125
+ labels=$(gh issue view "$ISSUE" --json labels --jq '.labels[].name')
126
+ if ! printf '%s\n' "$labels" | grep -qx 'relevo:ship'; then
127
+ exit 0
128
+ fi
129
+ gh issue edit "$ISSUE" --remove-label relevo:ship
130
+ body=""
131
+ if [ -f "$RUNNER_TEMP/relevo/scripts/render-message.mjs" ]; then
132
+ body=$(jq -n --arg issue "$ISSUE" --arg run "$RUN_URL" --arg actor "$ACTOR" '{issue: $issue, run: $run, actor: $actor}' | node "$RUNNER_TEMP/relevo/scripts/render-message.mjs" ship_failed --config work/relevo.yml) || body=""
133
+ fi
134
+ [ -n "$body" ] || body="@$ACTOR #$ISSUE was not shipped: the run ended without an outcome. Read the log to see why: $RUN_URL"
135
+ gh issue comment "$ISSUE" --body "$body"
@@ -3,12 +3,16 @@
3
3
  # Copy to .github/workflows/relevo-sla.yml. Needs a relevo.yml at the repository root.
4
4
  # The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
5
5
 
6
+ # Runner: `runs-on` comes from `runner.labels` in relevo.yml (`/relevo:init` writes it into this file, `/relevo:doctor` checks it).
7
+ # The runner needs git, gh and jq; Node and the tools of the job are installed per job. A self-hosted runner is assumed to
8
+ # persist between jobs, so the job puts its tools under $RUNNER_TEMP and removes what it adds to the workspace.
9
+
6
10
  name: relevo-sla
7
11
 
8
12
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
9
13
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
10
14
  env:
11
- RELEVO_VERSION: '0.6.0'
15
+ RELEVO_VERSION: '0.8.0'
12
16
 
13
17
  on:
14
18
  schedule:
@@ -51,3 +55,8 @@ jobs:
51
55
  GH_TOKEN: ${{ github.token }}
52
56
  GITHUB_REPOSITORY: ${{ github.repository }}
53
57
  run: node .relevo-plugin/scripts/review-sla.mjs --repo "$GITHUB_REPOSITORY"
58
+
59
+ # A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
60
+ - name: Remove what this job added
61
+ if: always()
62
+ run: rm -rf .relevo-plugin