@elalfner/relevo 0.7.0 → 0.9.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/.claude-plugin/plugin.json +1 -1
- package/README.es.md +1 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/scripts/doctor.mjs +5 -4
- package/scripts/init.mjs +25 -4
- package/scripts/lib/can-edit-issue.mjs +4 -1
- package/scripts/lib/cli.mjs +6 -4
- package/scripts/lib/config.mjs +19 -1
- package/scripts/lib/doctor.mjs +264 -25
- package/scripts/lib/github.mjs +15 -5
- package/scripts/lib/init.mjs +89 -6
- package/scripts/lib/labels.mjs +4 -0
- package/scripts/lib/messages.mjs +109 -0
- package/scripts/lib/next-step.mjs +12 -0
- package/scripts/lib/plan.mjs +29 -6
- package/scripts/lib/preflight.mjs +1 -1
- package/scripts/lib/review-events.mjs +6 -3
- package/scripts/lib/ship-github.mjs +87 -0
- package/scripts/lib/ship-unattended.mjs +271 -0
- package/scripts/lib/ship.mjs +15 -3
- package/scripts/lib/toolchain-check.mjs +6 -0
- package/scripts/lib/web-preflight.mjs +50 -4
- package/scripts/lib/workflow-upgrade.mjs +198 -0
- package/scripts/review-event.mjs +10 -1
- package/scripts/ship-check.mjs +6 -4
- package/scripts/ship-unattended.mjs +45 -0
- package/scripts/toolchain-check.mjs +3 -3
- package/scripts/web-hook.mjs +28 -0
- package/scripts/web-preflight.mjs +6 -5
- package/scripts/web-review.mjs +1 -1
- package/skills/doctor/SKILL.md +6 -5
- package/skills/go/SKILL.md +4 -2
- package/skills/init/SKILL.md +10 -4
- package/skills/review/SKILL.md +4 -3
- package/skills/ship/SKILL.md +3 -1
- package/stacks/vue.md +2 -2
- package/templates/agents/antigravity-step.yml +8 -5
- package/templates/relevo.yml +4 -0
- package/templates/workflows/relevo-digest.yml +11 -1
- package/templates/workflows/relevo-go.yml +33 -14
- package/templates/workflows/relevo-queue.yml +11 -1
- package/templates/workflows/relevo-review.yml +11 -1
- package/templates/workflows/relevo-ship.yml +136 -0
- package/templates/workflows/relevo-sla.yml +11 -1
package/scripts/ship-check.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { resolve } from 'node:path';
|
|
|
11
11
|
import { loadConfig } from './lib/config.mjs';
|
|
12
12
|
import { realGh } from './lib/github.mjs';
|
|
13
13
|
import { shipDecision } from './lib/ship.mjs';
|
|
14
|
+
import { baselineChecks, loadPr } from './lib/ship-github.mjs';
|
|
14
15
|
|
|
15
16
|
const arg = (name) => {
|
|
16
17
|
const i = process.argv.indexOf(`--${name}`);
|
|
@@ -26,9 +27,9 @@ if (!existsSync(configPath)) fail(`relevo.yml not found at ${configPath}. Run /r
|
|
|
26
27
|
const loaded = loadConfig(readFileSync(configPath, 'utf8'));
|
|
27
28
|
if (!loaded.ok) fail(loaded.errors.map((e) => `error: ${e.message}`).join('\n'));
|
|
28
29
|
|
|
29
|
-
const FIELDS = 'number,state,isDraft,mergeable,title,author,reviews,statusCheckRollup,headRefName,headRefOid,baseRefName';
|
|
30
30
|
let pr;
|
|
31
31
|
let invoker = arg('invoker');
|
|
32
|
+
let carried;
|
|
32
33
|
|
|
33
34
|
if (arg('input')) {
|
|
34
35
|
pr = JSON.parse(readFileSync(arg('input'), 'utf8'));
|
|
@@ -36,10 +37,10 @@ if (arg('input')) {
|
|
|
36
37
|
const repo = arg('repo') ?? process.env.GITHUB_REPOSITORY;
|
|
37
38
|
const branch = arg('branch');
|
|
38
39
|
if (!repo || !branch) fail('usage: ship-check.mjs --repo owner/name --branch <branch> [--invoker login] [--config relevo.yml]');
|
|
39
|
-
|
|
40
|
-
const open = found.find((p) => p.state === 'OPEN') ?? found[0];
|
|
41
|
-
pr = open ? JSON.parse(realGh(['pr', 'view', String(open.number), '--repo', repo, '--json', FIELDS])) : null;
|
|
40
|
+
pr = loadPr(realGh, repo, branch);
|
|
42
41
|
invoker ??= realGh(['api', 'user', '--jq', '.login']).trim();
|
|
42
|
+
// The archive commit may have no checks of its own; the approved head's count when only openspec/ changed since.
|
|
43
|
+
if (pr && arg('baseline')) carried = baselineChecks(realGh, repo, arg('baseline'), pr.headRefOid);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
const decision = shipDecision({
|
|
@@ -49,6 +50,7 @@ const decision = shipDecision({
|
|
|
49
50
|
baseline: arg('baseline'),
|
|
50
51
|
labels: arg('labels') === undefined ? undefined : arg('labels').split(',').map((l) => l.trim()).filter(Boolean),
|
|
51
52
|
issue: arg('issue'),
|
|
53
|
+
baselineChecks: carried,
|
|
52
54
|
});
|
|
53
55
|
console.log(JSON.stringify(decision, null, 2));
|
|
54
56
|
process.exit(decision.allowed ? 0 : 1);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Ships an issue on GitHub, without a model, when a person applied the `relevo:ship` label. Usage (in the workflow):
|
|
3
|
+
// node scripts/ship-unattended.mjs --repo owner/name --issue 31 --actor login --actor-type User --work <checkout>
|
|
4
|
+
// Environment: GH_TOKEN (the workflow token), GH_PUSH_TOKEN (the App token, or the workflow token), HAS_APP (true|false).
|
|
5
|
+
// Prints the outcome as JSON. exit 0: merged, stopped with a comment, or ignored exit 2: bad arguments
|
|
6
|
+
// Any other failure exits non-zero, and the workflow's last step reports it on the issue.
|
|
7
|
+
|
|
8
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
9
|
+
import { dirname, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { runUnattendedShip } from './lib/ship-unattended.mjs';
|
|
12
|
+
|
|
13
|
+
const arg = (name) => {
|
|
14
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
15
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
16
|
+
};
|
|
17
|
+
const [repo, issue, actor, actorType, work] = ['repo', 'issue', 'actor', 'actor-type', 'work'].map(arg);
|
|
18
|
+
if (!repo || !/^\d+$/.test(issue ?? '') || !actor || !actorType || !work) {
|
|
19
|
+
console.error('usage: ship-unattended.mjs --repo owner/name --issue <n> --actor <login> --actor-type <User|Bot> --work <checkout>');
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ghWith = (token) => (args) => execFileSync('gh', args, {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
26
|
+
env: { ...process.env, GH_TOKEN: token ?? process.env.GH_TOKEN },
|
|
27
|
+
});
|
|
28
|
+
const sh = (cmd, args, { cwd, input, env } = {}) => {
|
|
29
|
+
const r = spawnSync(cmd, args, { cwd, input, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: { ...process.env, ...env } });
|
|
30
|
+
return { status: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? String(r.error ?? '') };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const outcome = await runUnattendedShip({
|
|
34
|
+
repo,
|
|
35
|
+
issue,
|
|
36
|
+
actor,
|
|
37
|
+
actorType,
|
|
38
|
+
hasApp: process.env.HAS_APP === 'true',
|
|
39
|
+
workDir: resolve(work),
|
|
40
|
+
pluginRoot: resolve(dirname(fileURLToPath(import.meta.url)), '..'),
|
|
41
|
+
sh,
|
|
42
|
+
gh: ghWith(process.env.GH_TOKEN),
|
|
43
|
+
pushGh: ghWith(process.env.GH_PUSH_TOKEN || process.env.GH_TOKEN),
|
|
44
|
+
});
|
|
45
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Checks that the tools which the profile of a platform lists are on the path of this machine. Usage:
|
|
3
3
|
// node scripts/toolchain-check.mjs --platform <name> [--config relevo.yml] [--plugin-root <dir>]
|
|
4
|
-
// It prints { platform, stack, tools, missing } as JSON. A tool is there when `<tool> --version` starts. The cloud build
|
|
4
|
+
// It prints { platform, stack, tools, missing } as JSON; `missing` also covers git, gh, jq and node, which every job needs. A tool is there when `<tool> --version` starts. The cloud build
|
|
5
5
|
// runs it before the agent, so that a runner without the SDK stops in seconds and not after a full run.
|
|
6
6
|
// exit 0: every tool is there (or the profile lists none) exit 1: a tool is missing, or the profile cannot be resolved
|
|
7
7
|
// exit 2: bad usage or an invalid relevo.yml
|
|
@@ -12,7 +12,7 @@ import { dirname, resolve } from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { loadConfig } from './lib/config.mjs';
|
|
14
14
|
import { resolveProfile } from './lib/stack-profile.mjs';
|
|
15
|
-
import { missingTools } from './lib/toolchain-check.mjs';
|
|
15
|
+
import { missingTools, RUNNER_TOOLS } from './lib/toolchain-check.mjs';
|
|
16
16
|
|
|
17
17
|
const arg = (name) => {
|
|
18
18
|
const i = process.argv.indexOf(`--${name}`);
|
|
@@ -44,6 +44,6 @@ const resolved = resolveProfile({ platform, pluginRoot, read });
|
|
|
44
44
|
if (!resolved.ok) fail(resolved.errors.join('\n'), 1);
|
|
45
45
|
|
|
46
46
|
const probe = (tool) => spawnSync(tool, ['--version'], { stdio: 'ignore' }).error === undefined;
|
|
47
|
-
const missing = missingTools(resolved.profile.tools, probe);
|
|
47
|
+
const missing = missingTools([...RUNNER_TOOLS, ...(resolved.profile.tools ?? [])], probe);
|
|
48
48
|
console.log(JSON.stringify({ platform: name, stack: resolved.profile.stack, tools: resolved.profile.tools, missing }));
|
|
49
49
|
process.exit(missing.length > 0 ? 1 : 0);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Adds the SessionStart hook that installs OpenSpec in web sessions to .claude/settings.json. Usage:
|
|
3
|
+
// node scripts/web-hook.mjs [--settings .claude/settings.json]
|
|
4
|
+
// Run by /relevo:init only after the person agreed to the hook. Every other key and hook of the file is kept, and a
|
|
5
|
+
// file that is not a JSON object is left as it is.
|
|
6
|
+
// exit 0: written, or already there exit 2: the file could not be merged
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
import { mergeWebHook } from './lib/init.mjs';
|
|
11
|
+
|
|
12
|
+
const i = process.argv.indexOf('--settings');
|
|
13
|
+
const path = i === -1 ? '.claude/settings.json' : process.argv[i + 1];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const before = existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
17
|
+
const after = mergeWebHook(before);
|
|
18
|
+
if (after === before) {
|
|
19
|
+
console.log(`${path} already installs OpenSpec at the start of a web session; nothing changed`);
|
|
20
|
+
} else {
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
22
|
+
writeFileSync(path, after);
|
|
23
|
+
console.log(`${path} now installs OpenSpec at the start of a web session; the file is left for you to commit`);
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error(`could not add the hook to ${path}: ${err.message}`);
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// under review with the module scripts its page loads. Usage:
|
|
4
4
|
// node scripts/web-preflight.mjs --url http://localhost:5173 [--timeout 5000] [--wait 30000]
|
|
5
5
|
// With --wait it is also the readiness probe of a server the agent started: the address is retried until it answers.
|
|
6
|
-
// Prints { ok, check, detail, start, env } as JSON: `start` is the command that starts the app, `env` the
|
|
7
|
-
// names of .env.example (the real environment files are never read)
|
|
8
|
-
//
|
|
9
|
-
// exit
|
|
10
|
-
// exit
|
|
6
|
+
// Prints { ok, check, detail, start, env, install } as JSON: `start` is the command that starts the app, `env` the
|
|
7
|
+
// variable names of .env.example (the real environment files are never read), `install` the frozen install command of
|
|
8
|
+
// the lock file (null without one).
|
|
9
|
+
// exit 0: ready exit 3: no node_modules exit 7: node_modules does not match package-lock.json
|
|
10
|
+
// exit 4: Playwright does not resolve from the project exit 5: the Chromium of Playwright is missing
|
|
11
|
+
// exit 6: the address or one of its modules does not answer below 400 exit 2: bad arguments
|
|
11
12
|
|
|
12
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
13
14
|
import { loadPlaywright } from './lib/web-review.mjs';
|
package/scripts/web-review.mjs
CHANGED
|
@@ -73,7 +73,7 @@ const secrets = Object.fromEntries(secretNames.filter((n) => process.env[n]).map
|
|
|
73
73
|
const hide = (text) => redact(text, secrets);
|
|
74
74
|
|
|
75
75
|
const playwright = loadPlaywright();
|
|
76
|
-
if (!playwright) stop(1, 'Playwright cannot be resolved from this project:
|
|
76
|
+
if (!playwright) stop(1, 'Playwright cannot be resolved from this project: it belongs in its development dependencies; run /relevo:init, or "npm install -D playwright" and "npx playwright install chromium" in the folder of the platform, and commit the change on the base branch');
|
|
77
77
|
|
|
78
78
|
if (arg('out') === undefined && existsSync(join(out, 'web-log.txt'))) {
|
|
79
79
|
tell(`warning: ${join(out, 'web-log.txt')} is replaced; a run against another server uses an --out of its own (evidence/run-<port>)`);
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doctor
|
|
3
|
-
description: Checks that this machine, the repository and GitHub are ready for Relevo and reports
|
|
4
|
-
argument-hint: ""
|
|
3
|
+
description: Checks that this machine, the repository and GitHub are ready for Relevo and reports what needs action first (failures, then warnings, then notes, with the fix next to each failure and warning) and counts the checks that passed; --all lists them too. It covers the required tools (OpenSpec, git, the GitHub CLI, the toolchain and the targets of each platform's stack profile), the skills that /relevo:think invokes, the extra skills of each role, relevo.yml, labels and branches, branch protection and approvals, the automation secret and workflows, what the ship workflow needs, in a web session whether the repository installs OpenSpec at its start, and a CI check. Read-only: it never changes anything. Use when asked to check, diagnose or verify the Relevo setup.
|
|
4
|
+
argument-hint: "[--all]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Relevo doctor
|
|
@@ -18,10 +18,11 @@ Scripts live in `${CLAUDE_PLUGIN_ROOT}/scripts/`. Run them with `node`. They hol
|
|
|
18
18
|
|
|
19
19
|
## Steps
|
|
20
20
|
|
|
21
|
-
1. Run `node ${CLAUDE_PLUGIN_ROOT}/scripts/doctor.mjs` from the repository root. It finds the repository itself; pass `--repo owner/name` only when it cannot.
|
|
22
|
-
2. Show its output as it is
|
|
21
|
+
1. Run `node ${CLAUDE_PLUGIN_ROOT}/scripts/doctor.mjs` from the repository root. It finds the repository itself; pass `--repo owner/name` only when it cannot. Add `--all` when the person asked for it, to list the checks that passed.
|
|
22
|
+
2. Show its output as it is. The first line says what was judged. Then come the `FAILURES`, the `WARNINGS` and the `NOTES`, each with its count; every `[FAIL]` and `[warn]` line has its fix on the next line. A `[note]` asks for nothing: do not present it as a problem. The checks that passed are only counted, and `--all` lists them as `[pass]`.
|
|
23
23
|
3. If there are failures, say which command fixes most of them: `/relevo:init` for labels, branches, protection, workflows and the ignore line; the install commands shown for tools and skills; adding the model credential to the repository secrets by hand.
|
|
24
|
-
4. If the
|
|
24
|
+
4. If a `ship.app` check fails, say that the `relevo:ship` label cannot ship in this repository until a GitHub App and its two secrets exist, because the base branch requires checks, and that `/relevo:ship` still works meanwhile.
|
|
25
|
+
5. If the output says branch protection is not available, say plainly that approving and merging are then enforced only by the written confirmation `ship` asks for, not by the platform.
|
|
25
26
|
|
|
26
27
|
## Exit status
|
|
27
28
|
|
package/skills/go/SKILL.md
CHANGED
|
@@ -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
|
|
package/skills/init/SKILL.md
CHANGED
|
@@ -15,7 +15,8 @@ Scripts live in `${CLAUDE_PLUGIN_ROOT}/scripts/`. Run them with `node`. They hol
|
|
|
15
15
|
|
|
16
16
|
- Never read, write, print or ask for the model API key or any token. Tell the person to add it to the repository secrets themselves.
|
|
17
17
|
- Never apply a change before the person says yes to the plan, and never install anything before they confirm that exact command.
|
|
18
|
-
- Never overwrite
|
|
18
|
+
- Never overwrite `relevo.yml` that already exists and differs; report it. A `relevo.yml` with the older `platform: flutter-android` line stays as it is.
|
|
19
|
+
- A workflow that already exists and differs is reported, unless the plan offers to update it: that line (`Update .github/workflows/<file> from A to B, keeping your own changes`) means a three-way merge with the published templates came out clean, and it is written only with the person's yes to the plan. A workflow whose merge conflicts or has no close published version is left as it is; never edit it to resolve the conflict yourself.
|
|
19
20
|
- Never create files under `openspec/` by hand. If there is no OpenSpec root, tell the person to run `openspec init`.
|
|
20
21
|
- Never commit or push. The new files are left for the person to commit.
|
|
21
22
|
- Never put credentials in `relevo.yml` or in the answers file.
|
|
@@ -34,13 +35,14 @@ If `relevo.yml` already exists and is valid, skip this step. Otherwise ask, in s
|
|
|
34
35
|
- the review mode: `none` (a solo project), `peer` or `roles`;
|
|
35
36
|
- the language of the hand-off messages (`en` or `es`);
|
|
36
37
|
- 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;
|
|
38
|
+
- 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;
|
|
39
|
+
- 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 `linux-build`): 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
40
|
- 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
41
|
- 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
42
|
- 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
43
|
- 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
44
|
|
|
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.
|
|
45
|
+
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
46
|
|
|
45
47
|
### 3. Show the plan
|
|
46
48
|
|
|
@@ -48,7 +50,7 @@ Write the answers as JSON to a temporary file **outside the repository**, with t
|
|
|
48
50
|
node ${CLAUDE_PLUGIN_ROOT}/scripts/init.mjs plan --answers <answers file>
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
Show it as it is. Each line says whether it is already `ok`, will be done, `differs` (exists, left alone) or must be done `by hand`. Then ask one direct yes/no question: apply the changes marked "will do"? Wait for the answer.
|
|
53
|
+
Show it as it is. Each line says whether it is already `ok`, will be done, `differs` (exists, left alone) or must be done `by hand`. A workflow update shows its difference under the line, and a conflict shows both versions. When a line says that no published version is close to a workflow, ask which version of Relevo it was copied from and run the plan again with `--from <version>`. Then ask one direct yes/no question: apply the changes marked "will do"? Wait for the answer.
|
|
52
54
|
|
|
53
55
|
### 4. Offer the installs, one at a time
|
|
54
56
|
|
|
@@ -56,6 +58,10 @@ For each line in the plan that is an install, show its exact command and ask for
|
|
|
56
58
|
|
|
57
59
|
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
60
|
|
|
61
|
+
`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.
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
59
65
|
### 5. Apply
|
|
60
66
|
|
|
61
67
|
```bash
|
package/skills/review/SKILL.md
CHANGED
|
@@ -79,7 +79,7 @@ If `evidence/review-state.json` exists for this issue, resume: show the confirme
|
|
|
79
79
|
|
|
80
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.
|
|
81
81
|
|
|
82
|
-
For a `browser` adapter there is no `adb` and no device log: after `init`, run the preflight against the address under review (the one the person gives; for Vue, `http://localhost:5173` when they give none):
|
|
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
83
|
|
|
84
84
|
```bash
|
|
85
85
|
node ${CLAUDE_PLUGIN_ROOT}/scripts/web-preflight.mjs --url <address>
|
|
@@ -87,8 +87,9 @@ node ${CLAUDE_PLUGIN_ROOT}/scripts/web-preflight.mjs --url <address>
|
|
|
87
87
|
|
|
88
88
|
It prints JSON and exits by what is missing. Tell the person in their language:
|
|
89
89
|
|
|
90
|
-
- `3
|
|
91
|
-
- `4
|
|
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.
|
|
92
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.
|
|
93
94
|
- `0`: ready.
|
|
94
95
|
|
package/skills/ship/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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 "$
|
|
36
|
-
install -m 0755 "$RUNNER_TEMP/antigravity" "$
|
|
37
|
-
echo "$
|
|
38
|
-
|
|
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
|
package/templates/relevo.yml
CHANGED
|
@@ -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, linux-build] # 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
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
# relevo-template: 0.9.0
|
|
1
2
|
# Relevo: posts one digest per working day, as a comment on the issue named by digest.issue in relevo.yml.
|
|
2
3
|
# It lists who is waiting on what and the alerts, and posts nothing when nothing is waiting.
|
|
3
4
|
# Subscribe your chat tool to that issue to receive it. Copy to .github/workflows/relevo-digest.yml.
|
|
4
5
|
# Change the time below to suit your team (cron is in UTC).
|
|
5
6
|
# The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
|
|
6
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
|
+
|
|
7
12
|
name: relevo-digest
|
|
8
13
|
|
|
9
14
|
# The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
|
|
10
15
|
# were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
|
|
11
16
|
env:
|
|
12
|
-
RELEVO_VERSION: '0.
|
|
17
|
+
RELEVO_VERSION: '0.9.0'
|
|
13
18
|
|
|
14
19
|
on:
|
|
15
20
|
schedule:
|
|
@@ -52,3 +57,8 @@ jobs:
|
|
|
52
57
|
GH_TOKEN: ${{ github.token }}
|
|
53
58
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
54
59
|
run: node .relevo-plugin/scripts/digest.mjs --repo "$GITHUB_REPOSITORY"
|
|
60
|
+
|
|
61
|
+
# A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
|
|
62
|
+
- name: Remove what this job added
|
|
63
|
+
if: always()
|
|
64
|
+
run: rm -rf .relevo-plugin
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# relevo-template: 0.9.0
|
|
1
2
|
# Relevo: builds an issue when someone with write access labels it `relevo:ready`.
|
|
2
3
|
# Copy to .github/workflows/relevo-go.yml. If you renamed the ready label in relevo.yml, change it below too.
|
|
3
4
|
#
|
|
@@ -10,12 +11,18 @@
|
|
|
10
11
|
# Keep the numbers in claude_args in line with limits.run_max_turns and limits.run_max_cost.
|
|
11
12
|
# Where the numbers disagree the lower one wins, so this file can only make a run cheaper than relevo.yml says.
|
|
12
13
|
|
|
14
|
+
# Runner: `runs-on` comes from `runner.labels` in relevo.yml (`/relevo:init` writes it into this file, `/relevo:doctor` checks it).
|
|
15
|
+
# 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
|
|
16
|
+
# persist between jobs, so the job puts its tools under $RUNNER_TEMP and removes what it adds to the workspace.
|
|
17
|
+
|
|
13
18
|
name: relevo-go
|
|
14
19
|
|
|
15
20
|
# The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
|
|
16
21
|
# were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
|
|
17
22
|
env:
|
|
18
|
-
RELEVO_VERSION: '0.
|
|
23
|
+
RELEVO_VERSION: '0.9.0'
|
|
24
|
+
# The version of OpenSpec that the job installs for itself.
|
|
25
|
+
OPENSPEC_VERSION: '1.14.3'
|
|
19
26
|
|
|
20
27
|
on:
|
|
21
28
|
issues:
|
|
@@ -109,6 +116,11 @@ jobs:
|
|
|
109
116
|
gh issue edit "$ISSUE" --remove-label relevo:ready --remove-label relevo:queued --remove-label relevo:building --add-label relevo:qa-failed
|
|
110
117
|
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
118
|
|
|
119
|
+
# A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
|
|
120
|
+
- name: Remove what this job added
|
|
121
|
+
if: always()
|
|
122
|
+
run: rm -rf .relevo-plugin
|
|
123
|
+
|
|
112
124
|
build:
|
|
113
125
|
needs: decide
|
|
114
126
|
if: needs.decide.outputs.action == 'start'
|
|
@@ -162,14 +174,18 @@ jobs:
|
|
|
162
174
|
# the packages, so they need no such option.
|
|
163
175
|
|
|
164
176
|
- name: Install OpenSpec
|
|
165
|
-
|
|
177
|
+
# Under the temporary folder of the job, at the pinned version: nothing global is left on a runner that persists.
|
|
178
|
+
run: |
|
|
179
|
+
npm install --prefix "$RUNNER_TEMP/openspec" "@fission-ai/openspec@$OPENSPEC_VERSION" --no-fund --no-audit
|
|
180
|
+
echo "$RUNNER_TEMP/openspec/node_modules/.bin" >> "$GITHUB_PATH"
|
|
166
181
|
env:
|
|
167
182
|
OPENSPEC_TELEMETRY: '0'
|
|
168
183
|
|
|
169
184
|
# Stops before the agent, without spending a model turn, when the runner lacks a tool that the profile of the
|
|
170
185
|
# platform lists (the Flutter SDK, PHP and Composer, ...): otherwise the developer writes the code and QA only
|
|
171
186
|
# finds out at the end, after a full run. The package is unpacked outside the workspace on purpose: the agent needs
|
|
172
|
-
# a clean working tree.
|
|
187
|
+
# a clean working tree. The agent loads its plugin from this same folder (--plugin-dir), so a package without
|
|
188
|
+
# .claude-plugin/plugin.json stops the run here too. If you renamed the labels in relevo.yml, change the names below too.
|
|
173
189
|
- name: Check the toolchain
|
|
174
190
|
env:
|
|
175
191
|
GH_TOKEN: ${{ github.token }}
|
|
@@ -179,18 +195,19 @@ jobs:
|
|
|
179
195
|
mkdir -p "$RUNNER_TEMP/relevo"
|
|
180
196
|
npm pack "@elalfner/relevo@$RELEVO_VERSION" --silent --pack-destination "$RUNNER_TEMP" > /dev/null
|
|
181
197
|
tar -xzf "$RUNNER_TEMP"/elalfner-relevo-*.tgz --strip-components=1 -C "$RUNNER_TEMP/relevo"
|
|
182
|
-
for f in scripts/toolchain-check.mjs scripts/render-message.mjs; do
|
|
198
|
+
for f in scripts/toolchain-check.mjs scripts/render-message.mjs .claude-plugin/plugin.json; do
|
|
183
199
|
[ -f "$RUNNER_TEMP/relevo/$f" ] || { echo "::error::The Relevo package $RELEVO_VERSION lacks $f"; exit 1; }
|
|
184
200
|
done
|
|
185
201
|
if result=$(node "$RUNNER_TEMP/relevo/scripts/toolchain-check.mjs" --platform "$PLATFORM" --plugin-root "$RUNNER_TEMP/relevo"); then
|
|
186
202
|
exit 0
|
|
187
203
|
fi
|
|
188
|
-
|
|
204
|
+
# No jq here: the tool that is missing may be jq itself, and the comment must still be posted.
|
|
205
|
+
missing=$(printf '%s' "$result" | node -e 'console.log(JSON.parse(require("fs").readFileSync(0, "utf8")).missing.join(", "))')
|
|
189
206
|
[ -n "$missing" ] || { echo "::error::The toolchain check failed: $result"; exit 1; }
|
|
190
207
|
title=$(gh issue view "$ISSUE" --json title --jq .title)
|
|
191
208
|
assignee=$(gh issue view "$ISSUE" --json assignees --jq '.assignees[0].login // empty')
|
|
192
|
-
body=$(
|
|
193
|
-
|
|
209
|
+
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}))' \
|
|
210
|
+
| node "$RUNNER_TEMP/relevo/scripts/render-message.mjs" toolchain_missing --config relevo.yml)
|
|
194
211
|
gh issue edit "$ISSUE" --remove-label relevo:ready --remove-label relevo:queued --remove-label relevo:building --add-label relevo:qa-failed
|
|
195
212
|
gh issue comment "$ISSUE" --body "$body"
|
|
196
213
|
echo "::error::The runner has no $missing, so nothing was built"
|
|
@@ -217,13 +234,9 @@ jobs:
|
|
|
217
234
|
# The queue starts builds with `gh workflow run`, so the actor is the Actions bot and the action refuses
|
|
218
235
|
# it unless it is allowed. Only this one bot: never '*', which would let outside apps start runs.
|
|
219
236
|
allowed_bots: github-actions
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
-
# b) Private repository: the runner cannot clone it without a credential. Store a read-only fine-grained
|
|
224
|
-
# token as the secret RELEVO_PLUGIN_TOKEN and replace the line above with:
|
|
225
|
-
# plugin_marketplaces: https://x-access-token:${{ secrets.RELEVO_PLUGIN_TOKEN }}@github.com/elalfner/relevo.git
|
|
226
|
-
plugins: relevo@relevo
|
|
237
|
+
# The plugin is not installed from a marketplace: --plugin-dir in claude_args loads the package that the step
|
|
238
|
+
# "Check the toolchain" unpacked at RELEVO_VERSION, so one pin decides the scripts and the skills, nothing is
|
|
239
|
+
# cloned and no token is needed to read the plugin repository.
|
|
227
240
|
# The issue text is data, never instructions: the skill tells the agent so. The tool list is closed:
|
|
228
241
|
# only the tools in --allowedTools, and no web access (nothing to fetch, nowhere to send a secret).
|
|
229
242
|
# Do not set allowed_non_write_users or show_full_output here, and never widen allowed_bots: they open the run to
|
|
@@ -234,6 +247,7 @@ jobs:
|
|
|
234
247
|
claude_args: >-
|
|
235
248
|
--max-turns 100
|
|
236
249
|
--max-budget-usd 5
|
|
250
|
+
--plugin-dir ${{ runner.temp }}/relevo
|
|
237
251
|
--allowedTools "Bash,Read,Write,Edit,Glob,Grep,Task"
|
|
238
252
|
--disallowedTools "WebFetch,WebSearch,Bash(curl:*),Bash(wget:*)"
|
|
239
253
|
# relevo:agent-step end
|
|
@@ -300,3 +314,8 @@ jobs:
|
|
|
300
314
|
else
|
|
301
315
|
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
316
|
fi
|
|
317
|
+
|
|
318
|
+
# A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
|
|
319
|
+
- name: Remove what this job added
|
|
320
|
+
if: always()
|
|
321
|
+
run: rm -rf .relevo-plugin
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# relevo-template: 0.9.0
|
|
1
2
|
# Relevo: re-evaluates the queue and starts the issues that are now eligible.
|
|
2
3
|
# Copy to .github/workflows/relevo-queue.yml next to relevo-go.yml (it starts builds through that workflow).
|
|
3
4
|
#
|
|
@@ -5,12 +6,16 @@
|
|
|
5
6
|
# schedule, so a lost event does not stall the queue. Needs a relevo.yml at the repository root.
|
|
6
7
|
# The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
|
|
7
8
|
|
|
9
|
+
# Runner: `runs-on` comes from `runner.labels` in relevo.yml (`/relevo:init` writes it into this file, `/relevo:doctor` checks it).
|
|
10
|
+
# 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
|
|
11
|
+
# persist between jobs, so the job puts its tools under $RUNNER_TEMP and removes what it adds to the workspace.
|
|
12
|
+
|
|
8
13
|
name: relevo-queue
|
|
9
14
|
|
|
10
15
|
# The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
|
|
11
16
|
# were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
|
|
12
17
|
env:
|
|
13
|
-
RELEVO_VERSION: '0.
|
|
18
|
+
RELEVO_VERSION: '0.9.0'
|
|
14
19
|
|
|
15
20
|
on:
|
|
16
21
|
issues:
|
|
@@ -72,3 +77,8 @@ jobs:
|
|
|
72
77
|
for issue in $(echo "$result" | node -e 'JSON.parse(require("fs").readFileSync(0,"utf8")).start.forEach(n=>console.log(n))'); do
|
|
73
78
|
gh workflow run relevo-go.yml --repo "$GITHUB_REPOSITORY" -f issue="$issue"
|
|
74
79
|
done
|
|
80
|
+
|
|
81
|
+
# A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
|
|
82
|
+
- name: Remove what this job added
|
|
83
|
+
if: always()
|
|
84
|
+
run: rm -rf .relevo-plugin
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# relevo-template: 0.9.0
|
|
1
2
|
# Relevo: keeps the issue label in step with its pull request.
|
|
2
3
|
# A review that requests changes sets `relevo:changes-requested` and mentions the author; a new push returns the
|
|
3
4
|
# issue to `relevo:pr-open`; an approval mentions the person who will run /relevo:ship.
|
|
@@ -5,12 +6,16 @@
|
|
|
5
6
|
# Pull requests from forks are skipped explicitly (and would only get a read-only token anyway).
|
|
6
7
|
# The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
|
|
7
8
|
|
|
9
|
+
# Runner: `runs-on` comes from `runner.labels` in relevo.yml (`/relevo:init` writes it into this file, `/relevo:doctor` checks it).
|
|
10
|
+
# 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
|
|
11
|
+
# persist between jobs, so the job puts its tools under $RUNNER_TEMP and removes what it adds to the workspace.
|
|
12
|
+
|
|
8
13
|
name: relevo-review
|
|
9
14
|
|
|
10
15
|
# The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
|
|
11
16
|
# were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
|
|
12
17
|
env:
|
|
13
|
-
RELEVO_VERSION: '0.
|
|
18
|
+
RELEVO_VERSION: '0.9.0'
|
|
14
19
|
|
|
15
20
|
on:
|
|
16
21
|
pull_request_review:
|
|
@@ -58,3 +63,8 @@ jobs:
|
|
|
58
63
|
GH_TOKEN: ${{ github.token }}
|
|
59
64
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
60
65
|
run: node .relevo-plugin/scripts/review-event.mjs --repo "$GITHUB_REPOSITORY"
|
|
66
|
+
|
|
67
|
+
# A runner may keep its workspace between jobs (a self-hosted one does): remove what this job added to it.
|
|
68
|
+
- name: Remove what this job added
|
|
69
|
+
if: always()
|
|
70
|
+
run: rm -rf .relevo-plugin
|