@elalfner/relevo 0.4.0 → 0.5.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.
@@ -8,12 +8,14 @@
8
8
  import { loadConfig } from './config.mjs';
9
9
  import { LEGACY_PLATFORMS } from './platforms.mjs';
10
10
  import { labelDefinitions } from './labels.mjs';
11
+ import { agentWorkflow } from './agent-workflow.mjs';
11
12
  import { OPENSPEC_INSTALL, REQUIRED_SKILLS, SKILL_INSTALL, WORKFLOW_FILES } from './doctor.mjs';
12
13
 
13
14
  export const REVIEW_MODES = ['none', 'peer', 'roles'];
14
15
 
15
16
  export const RUNS = ['cloud', 'local', 'both'];
16
17
  export const CREDENTIALS = ['api_key', 'subscription'];
18
+ export const AGENT_BUILDS = ['claude', 'antigravity'];
17
19
  const API_KEY_LINE = ' anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}';
18
20
  const TOKEN_LINE = ' # claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}';
19
21
 
@@ -87,7 +89,12 @@ export function renderConfig(template, answers = {}) {
87
89
  if (answers.specWriter !== undefined) out = set(out, 'spec_writer', answers.specWriter ?? 'null');
88
90
  if (answers.reviewMode) out = set(out, 'mode', answers.reviewMode);
89
91
  if (answers.merger) out = set(out, 'merger', answers.merger);
90
- if (answers.runMaxCost !== undefined) out = set(out, 'run_max_cost', answers.runMaxCost);
92
+ if (answers.agentBuild === 'antigravity') {
93
+ // The agent has no cost limit: the limit of the run is a number of tokens, and the agent is named in its own group.
94
+ out = out.replace(/^(\s*)run_max_cost:[^\n]*$/m, '$1run_max_tokens: 2000000 # REQUIRED with agent.build antigravity: tokens per unattended run; choose your own');
95
+ out = `${out.trimEnd()}\n\n# Which agent builds in the cloud: claude (the default) or antigravity.\nagent:\n build: antigravity\n`;
96
+ if (answers.runMaxTokens !== undefined) out = set(out, 'run_max_tokens', answers.runMaxTokens);
97
+ } else if (answers.runMaxCost !== undefined) out = set(out, 'run_max_cost', answers.runMaxCost);
91
98
  if (answers.language) out = set(out, 'language', answers.language);
92
99
  const check = loadConfig(out);
93
100
  if (!check.ok) throw new Error(`the answers produce an invalid relevo.yml: ${check.errors.map((e) => e.message).join('; ')}`);
@@ -178,6 +185,20 @@ export function planInit({ facts, answers, templates, current, admin }) {
178
185
  if (!facts.openspec) items.push(item('install.openspec', 'todo', 'Install the OpenSpec command-line tool', { install: true, command: 'npm install --global @fission-ai/openspec', detail: OPENSPEC_INSTALL }));
179
186
  REQUIRED_SKILLS.filter((n) => !facts.skills[n]).forEach((n) => items.push(item(`install.skill.${n}`, 'todo', `Install the ${n} skill`, { install: true, command: n === 'openspec-explore' ? 'openspec init' : 'npx skills add mattpocock/skills', detail: SKILL_INSTALL[n] })));
180
187
 
188
+ const configuredPlatforms = facts.config?.platforms ?? answers.platforms ?? (answers.platform ? { app: LEGACY_PLATFORMS[answers.platform] } : {});
189
+ const hasSupabase = Object.values(configuredPlatforms).some((p) => p?.stack === 'supabase');
190
+ if (hasSupabase) {
191
+ ['supabase', 'supabase-postgres-best-practices'].forEach((skill) => {
192
+ if (facts.skills?.[skill] === false) {
193
+ items.push(item(`install.skill.${skill}`, 'todo', `Install the ${skill} skill`, {
194
+ install: true,
195
+ command: `npx skills add supabase/agent-skills --skill ${skill}`,
196
+ detail: SKILL_INSTALL[skill] ?? `run \`npx skills add supabase/agent-skills --skill ${skill}\``,
197
+ }));
198
+ }
199
+ });
200
+ }
201
+
181
202
  // the plugin, declared in the project so that a teammate who clones the repository is offered it
182
203
  if (facts.pluginDeclared?.declared) items.push(item('install.plugin-project', 'ok', 'The repository declares the Relevo plugin (.claude/settings.json)'));
183
204
  else if (facts.pluginDeclared?.unreadable) items.push(item('install.plugin-project', 'manual', '.claude/settings.json is not valid JSON: fix it, then run init again to declare the Relevo plugin in the project'));
@@ -225,10 +246,13 @@ export function planInit({ facts, answers, templates, current, admin }) {
225
246
  const credential = answers.credential ?? 'api_key';
226
247
  if (!RUNS.includes(runs)) throw new Error(`runs must be one of ${RUNS.join(', ')} (got "${runs}")`);
227
248
  if (!CREDENTIALS.includes(credential)) throw new Error(`credential must be one of ${CREDENTIALS.join(', ')} (got "${credential}")`);
249
+ const build = answers.agentBuild ?? 'claude';
250
+ if (!AGENT_BUILDS.includes(build)) throw new Error(`agentBuild must be one of ${AGENT_BUILDS.join(', ')} (got "${build}")`);
228
251
  const cloudFiles = ['relevo-go.yml', 'relevo-queue.yml'];
229
252
  WORKFLOW_FILES.forEach((file) => {
230
253
  if (runs === 'local' && cloudFiles.includes(file)) return;
231
- const wanted = file === 'relevo-go.yml' ? credentialLine(templates.workflows[file], credential) : templates.workflows[file];
254
+ const shared = file === 'relevo-go.yml' ? credentialLine(templates.workflows[file], credential) : templates.workflows[file];
255
+ const wanted = agentWorkflow(shared, build, templates.agentBlocks);
232
256
  const have = current.workflows[file] ?? null;
233
257
  if (have === null) items.push(item(`workflow.${file}`, 'todo', `Add .github/workflows/${file}`, { path: `.github/workflows/${file}`, content: wanted }));
234
258
  else if (have === wanted) items.push(item(`workflow.${file}`, 'ok', `.github/workflows/${file} is up to date`));
@@ -262,6 +286,9 @@ export function planInit({ facts, answers, templates, current, admin }) {
262
286
  if ((facts.secrets ?? []).includes('RELEVO_LINKS_TOKEN')) items.push(item('manual.links-token', 'ok', 'The RELEVO_LINKS_TOKEN secret exists (init never reads it)'));
263
287
  else items.push(item('manual.links-token', 'manual', `If ${links.join(', ')} ${links.length > 1 ? 'are' : 'is'} private, create a fine-grained token with read access to issues and pull requests and store it yourself as the secret RELEVO_LINKS_TOKEN (init never reads or writes it; Relevo only reads with it, and public repositories need none)`));
264
288
  }
289
+ if (hasSupabase) {
290
+ items.push(item('manual.supabase-mcp', 'manual', 'To explore your Supabase database in Claude Code or Antigravity during /relevo:think, consider connecting the Supabase MCP server (see https://supabase.com/docs/guides/ai-tools)'));
291
+ }
265
292
  items.push(item('manual.push', 'manual', 'Commit and push the new files to the default branch: a workflow only runs from there'));
266
293
 
267
294
  return items;
@@ -2,10 +2,15 @@
2
2
  //
3
3
  // Two tiers:
4
4
  // hard never allowed, whatever the issue says: workflows, the Relevo configuration, the folders of skills and
5
- // the file with the project rules (a defect is fixed in the code, not by rewriting the rule).
5
+ // the file with the project rules (a defect is fixed in the code, not by rewriting the rule), and what carries
6
+ // the instructions or the permissions of another agent (`.agents/`, `.gemini/`, `AGENTS.md`, `GEMINI.md`):
7
+ // a run that could edit them could rewrite its own instructions.
6
8
  // soft dependency manifests and lock files: allowed only when the issue lists them in an `Allow-paths:` line.
7
9
 
8
- export const HARD_PATTERNS = ['.github/', 'relevo.yml', '.relevo/', '.claude/skills/', '.agents/skills/', 'skills-lock.json'];
10
+ export const HARD_PATTERNS = ['.github/', 'relevo.yml', '.relevo/', '.claude/skills/', '.agents/', '.gemini/', 'skills-lock.json'];
11
+
12
+ // Files that an agent reads from the folder of whatever it edits, so they are protected at any depth.
13
+ export const HARD_ANY_DEPTH = ['AGENTS.md', 'GEMINI.md'];
9
14
 
10
15
  export const SOFT_FILES = [
11
16
  'pubspec.yaml', 'pubspec.lock',
@@ -60,7 +65,8 @@ const matchesHard = (path, pattern) => {
60
65
 
61
66
  /** The hard rule a path breaks, or null. */
62
67
  export function hardRule(path, { rulesFile = null } = {}) {
63
- return hardPatterns(rulesFile).find((pattern) => matchesHard(path, pattern)) ?? null;
68
+ const name = clean(path).split('/').pop();
69
+ return hardPatterns(rulesFile).find((pattern) => matchesHard(path, pattern)) ?? HARD_ANY_DEPTH.find((f) => f === name) ?? null;
64
70
  }
65
71
 
66
72
  /** Is the file a dependency manifest or lock file (soft tier)? */
@@ -87,7 +93,7 @@ export function checkProtected(paths, { allow = [], rulesFile = null } = {}) {
87
93
 
88
94
  /** Entries of an `Allow-paths:` list that name (or would cover) a hard-protected path: they can never be allowed. */
89
95
  export function hardPathsInAllow(allow, { rulesFile = null } = {}) {
90
- const probes = [...HARD_PATTERNS.map((pattern) => (pattern.endsWith('/') ? `${pattern}x` : pattern)), ...ruleFiles(rulesFile)].filter(Boolean);
96
+ const probes = [...HARD_PATTERNS.map((pattern) => (pattern.endsWith('/') ? `${pattern}x` : pattern)), ...HARD_ANY_DEPTH, ...ruleFiles(rulesFile)].filter(Boolean);
91
97
  return (allow ?? []).filter((entry) => hardRule(entry, { rulesFile }) || probes.some((probe) => matchesGlob(probe, entry)));
92
98
  }
93
99
 
@@ -57,7 +57,7 @@ export function usageLine(usage, { subscription = false } = {}) {
57
57
  // Messages the Claude CLI and the API document for a usage limit, a quota or an empty credit balance. These patterns
58
58
  // are not from a failed run seen here (see docs/pilot-findings.md); a wrong guess only changes the wording of a
59
59
  // comment on a run that already failed.
60
- const LIMIT_RE = /usage limit|limit reached|rate[ _-]?limit|credit balance is too low|exceeded your (current )?quota|insufficient[ _]quota/i;
60
+ const LIMIT_RE = /usage limit|limit reached|rate[ _-]?limit|credit balance is too low|exceeded your (current )?quota|quota exceeded|resource[ _]exhausted|insufficient[ _]quota/i;
61
61
 
62
62
  /** Does the log show that a usage limit, a quota or the credit was reached? */
63
63
  export function reachedUsageLimit(text) {
@@ -58,3 +58,61 @@ export function select({ title, choices, defaultIndex = 0, input, output, hint =
58
58
  draw();
59
59
  });
60
60
  }
61
+
62
+ /**
63
+ * Like `select`, but several choices can be ticked: Space (or the digit of a choice) toggles, Enter confirms.
64
+ * choices: [{ value, label }]; initial: the values ticked at the start. Resolves the array of ticked values in the
65
+ * order of `choices` (possibly empty), or null when the person cancels.
66
+ */
67
+ export function selectMany({ title, choices, initial = [], input, output, hint = 'Arrows to move, Space or a number to tick, Enter to confirm, Esc to cancel' }) {
68
+ if (!input?.isTTY || typeof input.setRawMode !== 'function') return Promise.reject(new Error('the list needs a terminal'));
69
+ if (!Array.isArray(choices) || choices.length === 0) return Promise.reject(new Error('the list has no choices'));
70
+ return new Promise((resolve) => {
71
+ const ticked = new Set(initial);
72
+ let index = 0;
73
+ let drawn = 0;
74
+ const draw = () => {
75
+ const lines = [title, ...choices.map((c, i) => `${i === index ? '>' : ' '} [${ticked.has(c.value) ? 'x' : ' '}] ${i + 1}. ${c.label}`), hint];
76
+ output.write(`${drawn ? UP(drawn) : ''}\r${CLEAR}${lines.join('\n')}\n`);
77
+ drawn = lines.length;
78
+ };
79
+ const finish = (value) => {
80
+ input.removeListener('keypress', onKey);
81
+ try {
82
+ input.setRawMode(false);
83
+ } finally {
84
+ input.pause?.();
85
+ output.write(SHOW);
86
+ }
87
+ resolve(value);
88
+ };
89
+ const toggle = (i) => {
90
+ const v = choices[i].value;
91
+ if (ticked.has(v)) ticked.delete(v);
92
+ else ticked.add(v);
93
+ };
94
+ function onKey(str, key = {}) {
95
+ try {
96
+ if (key.ctrl && (key.name === 'c' || key.name === 'd')) return finish(null);
97
+ if (key.name === 'escape' || str === 'q') return finish(null);
98
+ if (key.name === 'return' || key.name === 'enter') return finish(choices.map((c) => c.value).filter((v) => ticked.has(v)));
99
+ if (key.name === 'up' || str === 'k') index = (index + choices.length - 1) % choices.length;
100
+ else if (key.name === 'down' || str === 'j') index = (index + 1) % choices.length;
101
+ else if (key.name === 'space' || str === ' ') toggle(index);
102
+ else if (/^[1-9]$/.test(str ?? '') && Number(str) <= choices.length) toggle(Number(str) - 1);
103
+ else return undefined;
104
+ draw();
105
+ } catch (err) {
106
+ finish(null);
107
+ throw err;
108
+ }
109
+ return undefined;
110
+ }
111
+ emitKeypressEvents(input);
112
+ input.setRawMode(true);
113
+ input.resume?.();
114
+ input.on('keypress', onKey);
115
+ output.write(HIDE);
116
+ draw();
117
+ });
118
+ }
@@ -33,11 +33,12 @@ If `relevo.yml` already exists and is valid, skip this step. Otherwise ask, in s
33
33
  - who builds and reviews (GitHub usernames) and who writes the specs;
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
- - 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` and `laravel`), 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 has none). Several platforms in one repository are written down, but `go` builds only a repository with one for now;
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
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
38
  - 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
39
  - 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; it is not committed);
40
- - the cost limit per unattended run in US dollars: the template's 5 is a placeholder the person must accept or change.
40
+ - 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
+ - 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).
41
42
 
42
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.
43
44
 
@@ -152,6 +152,16 @@ Only when `decide` exited 0.
152
152
  - `evidence.policy: local_only`: delete the `evidence/` folder now. Nothing is attached anywhere.
153
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.
154
154
 
155
+ ### 9. Return to base branch
156
+
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.
158
+
159
+ ```bash
160
+ [ "$(git rev-parse --git-dir)" = "$(git rev-parse --git-common-dir)" ] && ! git worktree list --porcelain | grep -qx "branch refs/heads/<base>" && git switch <base>
161
+ ```
162
+
163
+ If the condition does not hold (a separate worktree, or the base branch is checked out elsewhere), do not switch: say which branch the working tree stays on. If `git switch` fails, say so; it never changes the verdict or the state of the issue, because the pull request already exists.
164
+
155
165
  ## Report
156
166
 
157
167
  Finish with one short block: the issue, the review verdict, the checklist result, the pull request link (or why none was opened), and the issue's new state.
@@ -0,0 +1,63 @@
1
+ ---
2
+ stack: supabase
3
+ commands:
4
+ analyze: supabase db lint
5
+ test: supabase test db
6
+ format: npx prettier --write supabase/
7
+ generated: []
8
+ tools: [supabase, docker]
9
+ requires_skills:
10
+ - supabase
11
+ - supabase-postgres-best-practices
12
+ skills:
13
+ dev:
14
+ - supabase
15
+ - supabase-postgres-best-practices
16
+ qa:
17
+ - supabase
18
+ - supabase-postgres-best-practices
19
+ issue:
20
+ - supabase
21
+ - supabase-postgres-best-practices
22
+ review: []
23
+ ---
24
+
25
+ # Supabase
26
+
27
+ ## Verification
28
+
29
+ Run the verification commands from the platform directory: `analyze` (`supabase db lint`) and `test` (`supabase test db`). These commands require the Supabase CLI and a running Docker daemon.
30
+
31
+ Do not claim the database schema is valid, clean, or passes tests unless you ran the commands and saw the results. All tests run hermetically against local Docker containers; never point tests at a remote Supabase project.
32
+
33
+ ## Questions for the issue writer
34
+
35
+ - **Authorization & RLS**: Who is permitted to select, insert, update, and delete rows in these tables? Which Postgres roles (`anon`, `authenticated`) should have access?
36
+ - **Relationships & Deletes**: If a parent row or user is deleted, what is the expected cascade behavior (`CASCADE`, `SET NULL`, or `RESTRICT`)?
37
+ - **Storage & Files**: If files are uploaded, what are the allowed MIME types, max file sizes, and who can overwrite or delete objects?
38
+ - **Query Performance & Filters**: Which columns will be filtered in `WHERE` clauses, sorted, or referenced in RLS policies (`auth.uid() = user_id`)?
39
+
40
+ ## Hygiene for the developer
41
+
42
+ - Follow the official guidelines from `supabase` and `supabase-postgres-best-practices`.
43
+ - Enable Row Level Security on every public table: `ALTER TABLE <table_name> ENABLE ROW LEVEL SECURITY;`.
44
+ - Avoid using `auth.role()` in policies; use the `TO` clause instead (e.g. `TO authenticated` or `TO anon`).
45
+ - Combine `TO authenticated` with an explicit ownership predicate (e.g. `(select auth.uid()) = user_id`) to prevent broken object-level authorization (BOLA / IDOR).
46
+ - Always include both `USING` and `WITH CHECK` clauses on `UPDATE` policies so users cannot reassign ownership columns.
47
+ - Ensure all views on tables with RLS specify `WITH (security_invoker = true)` (Postgres 15+).
48
+ - Avoid `SECURITY DEFINER` functions in the `public` schema; if required, keep them in a private schema and validate caller identity explicitly.
49
+ - Create an index on every foreign key column and on every column evaluated in RLS policies.
50
+ - Structure all schema changes as versioned, timestamped migrations in `supabase/migrations/`.
51
+ - Maintain test and sample data in `supabase/seed.sql` using idempotent statements (`INSERT ... ON CONFLICT DO NOTHING;`).
52
+ - Write pgTAP test suites in `supabase/tests/` verifying that unauthorized or anonymous callers cannot read or modify protected rows.
53
+
54
+ ## What the auditor looks for in the diff
55
+
56
+ - **Missing RLS**: Any table created in an exposed schema without `ENABLE ROW LEVEL SECURITY`.
57
+ - **Incomplete Update Policies**: An `UPDATE` policy that defines `USING` but omits `WITH CHECK`.
58
+ - **Permissive Policies**: Policies using `USING (true)` or checking only `TO authenticated` without row-ownership verification on private user data.
59
+ - **Unprotected Views**: Views defined without `security_invoker = true`.
60
+ - **Privilege Escalation**: `SECURITY DEFINER` functions placed in `public` or callable without `auth.uid()` checks.
61
+ - **Missing Indexes**: Foreign key constraints or RLS filter columns without an index.
62
+ - **Destructive Migrations**: `DROP TABLE`, `DROP COLUMN`, or `TRUNCATE` operations without explicit migration plans or rollback safety.
63
+ - **Exposed Secrets**: Hardcoded `service_role` keys, API tokens, or service passwords in code, migrations, or seed files.
@@ -0,0 +1,70 @@
1
+ # What `relevo-go.yml` and `relevo-queue.yml` run when `agent.build` in relevo.yml is `antigravity`. This file is not a
2
+ # workflow: each block below replaces, in the shared workflow, the lines between the markers with the same name, and
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).
5
+ #
6
+ # What Antigravity CLI cannot do on its own, and what stands in for it (docs/security.md):
7
+ # turn and token limits the run guard (scripts/agent-guard.mjs) reads the event stream and stops the agent; the numbers
8
+ # are those of relevo.yml (limits.run_max_turns, limits.run_max_tokens)
9
+ # cost limit none: the quota and the budget of the Google Cloud project that owns the key are the ceiling.
10
+ # Use a key made for Relevo, in its own project, with a quota
11
+ # refused tools the agent exits 0 when a tool is refused; the guard reads the refusal and fails the run
12
+ # closed tool list the settings file written below: the web is denied and only listed commands are allowed. It is
13
+ # a prefix list, not an operating-system boundary: `node` can run any script, as Bash can for Claude
14
+ # the key GEMINI_API_KEY is an environment variable of the agent's own commands, so the agent can read it
15
+ # the binary pinned by version and sha512 below; to update it, copy the url and the checksum from
16
+ # https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/linux_amd64.json
17
+
18
+ # relevo:credential begin
19
+ # Without a model credential the cloud cannot build: the issue stays ready and a comment says to run it locally.
20
+ HAS_CREDENTIAL: ${{ secrets.GEMINI_API_KEY != '' }}
21
+ # relevo:credential end
22
+
23
+ # relevo:agent-step begin
24
+ # Antigravity CLI, at the version and with the checksum written here. The installer script of the vendor takes
25
+ # the latest version; this takes only the one that was tried.
26
+ - name: Install the Antigravity CLI
27
+ env:
28
+ AGY_VERSION: '1.4.4'
29
+ AGY_URL: https://storage.googleapis.com/antigravity-public/antigravity-cli/1.4.4-4783783166214144/linux-x64/cli_linux_x64.tar.gz
30
+ AGY_SHA512: 7ce149d350344c1046c7b59b6659bcc5d659f351bf3a07c6c085887d93427ceb89d927752e4ccd0844b53ffb91d33d6a49f72835ceb276c7cffdd13cfb8023b8
31
+ run: |
32
+ curl -fsSL -o "$RUNNER_TEMP/agy.tar.gz" "$AGY_URL"
33
+ echo "$AGY_SHA512 $RUNNER_TEMP/agy.tar.gz" | sha512sum --check --strict -
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; }
39
+
40
+ # The Relevo plugin is rendered from the package unpacked in the step "Check the toolchain" and installed for the
41
+ # machine, so nothing is added to the working tree (the agent needs it clean). The settings of the run go to the
42
+ # user-level file of the CLI, outside the repository: a pull request cannot loosen them.
43
+ - name: Set up the Antigravity run
44
+ env:
45
+ PLATFORM: ${{ needs.decide.outputs.platform }}
46
+ run: |
47
+ for f in scripts/cli.mjs scripts/agent-guard.mjs; do
48
+ [ -f "$RUNNER_TEMP/relevo/$f" ] || { echo "::error::The Relevo package $RELEVO_VERSION lacks $f"; exit 1; }
49
+ done
50
+ node "$RUNNER_TEMP/relevo/scripts/cli.mjs" init --tools antigravity --scope machine --yes
51
+ node "$RUNNER_TEMP/relevo/scripts/agent-guard.mjs" settings --platform "$PLATFORM" --plugin-root "$RUNNER_TEMP/relevo" --out "$HOME/.gemini/antigravity-cli/settings.json"
52
+ git config user.name "github-actions[bot]"
53
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
54
+
55
+ # The issue text is data, never instructions: the skill tells the agent so. The decision the `decide` job already
56
+ # made travels with the prompt, encoded so that the issue title in it is never read as an instruction. Both
57
+ # values reach the shell as variables, never pasted into the script.
58
+ - id: agent
59
+ env:
60
+ OPENSPEC_TELEMETRY: '0'
61
+ GH_TOKEN: ${{ github.token }}
62
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
63
+ ISSUE: ${{ github.event.issue.number || inputs.issue }}
64
+ DECISION: ${{ needs.decide.outputs.decision_b64 }}
65
+ run: |
66
+ node "$RUNNER_TEMP/relevo/scripts/agent-guard.mjs" run \
67
+ --prompt "/relevo:go $ISSUE --decision $DECISION" \
68
+ --config relevo.yml --timeout-minutes 50 \
69
+ --summary "$RUNNER_TEMP/relevo-guard-summary.json" --github-output "$GITHUB_OUTPUT"
70
+ # relevo:agent-step end
@@ -0,0 +1,75 @@
1
+ name: Deploy Supabase Migrations
2
+
3
+ # Automatically applies migrations to Supabase Cloud after Relevo pull requests are merged.
4
+ # - Push to base branch ('dev'): deploys to Staging.
5
+ # - Push to release branch ('main'): deploys to Production (under GitHub Environment protection).
6
+
7
+ on:
8
+ push:
9
+ branches:
10
+ - dev # Relevo base branch (Staging)
11
+ - main # Relevo release branch (Production)
12
+ paths:
13
+ - 'supabase/migrations/**'
14
+ - 'supabase/functions/**'
15
+ - 'supabase/config.toml'
16
+ workflow_dispatch:
17
+
18
+ jobs:
19
+ deploy-staging:
20
+ name: Deploy to Staging
21
+ if: github.ref_name == 'dev'
22
+ runs-on: ubuntu-latest
23
+ environment: staging
24
+ steps:
25
+ - name: Checkout code
26
+ uses: actions/checkout@v4
27
+
28
+ - name: Setup Supabase CLI
29
+ uses: supabase/setup-cli@v1
30
+ with:
31
+ version: latest
32
+
33
+ - name: Link and Push Migrations to Staging
34
+ env:
35
+ SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
36
+ run: |
37
+ supabase link --project-ref ${{ secrets.SUPABASE_PROJECT_ID_STAGING }}
38
+ supabase db push
39
+
40
+ - name: Deploy Edge Functions (if present)
41
+ if: hashFiles('supabase/functions/**') != ''
42
+ env:
43
+ SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
44
+ run: |
45
+ supabase functions deploy
46
+
47
+ deploy-production:
48
+ name: Deploy to Production
49
+ if: github.ref_name == 'main'
50
+ runs-on: ubuntu-latest
51
+ # Configure the 'production' environment in GitHub Settings > Environments with required reviewers
52
+ # so human approval is mandatory before this job executes.
53
+ environment: production
54
+ steps:
55
+ - name: Checkout code
56
+ uses: actions/checkout@v4
57
+
58
+ - name: Setup Supabase CLI
59
+ uses: supabase/setup-cli@v1
60
+ with:
61
+ version: latest
62
+
63
+ - name: Link and Push Migrations to Production
64
+ env:
65
+ SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
66
+ run: |
67
+ supabase link --project-ref ${{ secrets.SUPABASE_PROJECT_ID_PROD }}
68
+ supabase db push
69
+
70
+ - name: Deploy Edge Functions (if present)
71
+ if: hashFiles('supabase/functions/**') != ''
72
+ env:
73
+ SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
74
+ run: |
75
+ supabase functions deploy
@@ -9,7 +9,7 @@ name: relevo-digest
9
9
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
10
10
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
11
11
  env:
12
- RELEVO_VERSION: '0.4.0'
12
+ RELEVO_VERSION: '0.5.0'
13
13
 
14
14
  on:
15
15
  schedule:
@@ -15,7 +15,7 @@ name: relevo-go
15
15
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
16
16
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
17
17
  env:
18
- RELEVO_VERSION: '0.4.0'
18
+ RELEVO_VERSION: '0.5.0'
19
19
 
20
20
  on:
21
21
  issues:
@@ -82,8 +82,10 @@ jobs:
82
82
  GH_LINKS_TOKEN: ${{ secrets.RELEVO_LINKS_TOKEN }}
83
83
  GITHUB_REPOSITORY: ${{ github.repository }}
84
84
  ISSUE: ${{ github.event.issue.number || inputs.issue }}
85
+ # relevo:credential begin
85
86
  # Without a model credential the cloud cannot build: the issue stays ready and a comment says to run it locally.
86
87
  HAS_CREDENTIAL: ${{ secrets.ANTHROPIC_API_KEY != '' || secrets.CLAUDE_CODE_OAUTH_TOKEN != '' }}
88
+ # relevo:credential end
87
89
  run: node .relevo-plugin/scripts/go-plan.mjs --repo "$GITHUB_REPOSITORY" --only "$ISSUE" --apply --claim --credential "$HAS_CREDENTIAL" --github-output "$GITHUB_OUTPUT"
88
90
 
89
91
  # See the same step in `build`. A failure before the issue is claimed (the plugin cannot be cloned, the
@@ -194,6 +196,7 @@ jobs:
194
196
  echo "::error::The runner has no $missing, so nothing was built"
195
197
  exit 1
196
198
 
199
+ # relevo:agent-step begin
197
200
  - id: agent
198
201
  uses: anthropics/claude-code-action@v1
199
202
  env:
@@ -233,6 +236,7 @@ jobs:
233
236
  --max-budget-usd 5
234
237
  --allowedTools "Bash,Read,Write,Edit,Glob,Grep,Task"
235
238
  --disallowedTools "WebFetch,WebSearch,Bash(curl:*),Bash(wget:*)"
239
+ # relevo:agent-step end
236
240
 
237
241
  # The scripts of the next two steps come from the plugin package. It is unpacked into the workspace only now, after the agent has
238
242
  # finished, because the agent's preflight needs a clean working tree and a checkout inside it would not be.
@@ -10,7 +10,7 @@ name: relevo-queue
10
10
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
11
11
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
12
12
  env:
13
- RELEVO_VERSION: '0.4.0'
13
+ RELEVO_VERSION: '0.5.0'
14
14
 
15
15
  on:
16
16
  issues:
@@ -62,8 +62,10 @@ jobs:
62
62
  # name, never to write, and an empty value is fine (public repositories are read without it).
63
63
  GH_LINKS_TOKEN: ${{ secrets.RELEVO_LINKS_TOKEN }}
64
64
  GITHUB_REPOSITORY: ${{ github.repository }}
65
+ # relevo:credential begin
65
66
  # Without a model credential nothing is dispatched.
66
67
  HAS_CREDENTIAL: ${{ secrets.ANTHROPIC_API_KEY != '' || secrets.CLAUDE_CODE_OAUTH_TOKEN != '' }}
68
+ # relevo:credential end
67
69
  run: |
68
70
  result=$(node .relevo-plugin/scripts/requeue.mjs --repo "$GITHUB_REPOSITORY" --credential "$HAS_CREDENTIAL")
69
71
  echo "$result"
@@ -10,7 +10,7 @@ name: relevo-review
10
10
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
11
11
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
12
12
  env:
13
- RELEVO_VERSION: '0.4.0'
13
+ RELEVO_VERSION: '0.5.0'
14
14
 
15
15
  on:
16
16
  pull_request_review:
@@ -8,7 +8,7 @@ name: relevo-sla
8
8
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
9
9
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
10
10
  env:
11
- RELEVO_VERSION: '0.4.0'
11
+ RELEVO_VERSION: '0.5.0'
12
12
 
13
13
  on:
14
14
  schedule: