@elalfner/relevo 0.0.0-stage → 0.3.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 (139) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +51 -0
  4. package/README.es.md +90 -0
  5. package/README.md +89 -2
  6. package/agents/dev.md +101 -0
  7. package/agents/qa.md +112 -0
  8. package/package.json +42 -4
  9. package/scripts/add-corrections.mjs +21 -0
  10. package/scripts/app-id.mjs +19 -0
  11. package/scripts/archive-state.mjs +31 -0
  12. package/scripts/branch-state.mjs +31 -0
  13. package/scripts/branch-work.mjs +25 -0
  14. package/scripts/can-edit-issue.mjs +45 -0
  15. package/scripts/capture.mjs +22 -0
  16. package/scripts/check-archive.mjs +19 -0
  17. package/scripts/check-issue.mjs +41 -0
  18. package/scripts/checklist.mjs +29 -0
  19. package/scripts/cli.mjs +28 -0
  20. package/scripts/commit-change.mjs +50 -0
  21. package/scripts/conflict-scope.mjs +19 -0
  22. package/scripts/design-fetch.mjs +103 -0
  23. package/scripts/device.mjs +12 -0
  24. package/scripts/digest.mjs +50 -0
  25. package/scripts/doctor.mjs +47 -0
  26. package/scripts/fix-guard.mjs +38 -0
  27. package/scripts/fix-stash.mjs +57 -0
  28. package/scripts/fix-state.mjs +28 -0
  29. package/scripts/fix-tasks.mjs +22 -0
  30. package/scripts/go-local.mjs +49 -0
  31. package/scripts/go-plan.mjs +82 -0
  32. package/scripts/init.mjs +109 -0
  33. package/scripts/intent-to-add.mjs +20 -0
  34. package/scripts/issue-coverage.mjs +30 -0
  35. package/scripts/issue-hash.mjs +44 -0
  36. package/scripts/issue-sections.mjs +42 -0
  37. package/scripts/label-change.mjs +35 -0
  38. package/scripts/lib/appid.mjs +12 -0
  39. package/scripts/lib/archive-check.mjs +29 -0
  40. package/scripts/lib/archive-state.mjs +15 -0
  41. package/scripts/lib/branch-work.mjs +13 -0
  42. package/scripts/lib/branch.mjs +43 -0
  43. package/scripts/lib/can-edit-issue.mjs +34 -0
  44. package/scripts/lib/checklist.mjs +68 -0
  45. package/scripts/lib/cli.mjs +82 -0
  46. package/scripts/lib/commit-change.mjs +57 -0
  47. package/scripts/lib/config.mjs +296 -0
  48. package/scripts/lib/design-fetch.mjs +156 -0
  49. package/scripts/lib/device.mjs +67 -0
  50. package/scripts/lib/doctor.mjs +614 -0
  51. package/scripts/lib/execution-mode.mjs +24 -0
  52. package/scripts/lib/fix-guard.mjs +23 -0
  53. package/scripts/lib/fix-stash.mjs +17 -0
  54. package/scripts/lib/fix.mjs +31 -0
  55. package/scripts/lib/github.mjs +161 -0
  56. package/scripts/lib/go-local.mjs +102 -0
  57. package/scripts/lib/init.mjs +324 -0
  58. package/scripts/lib/intent-to-add.mjs +32 -0
  59. package/scripts/lib/issue-check.mjs +63 -0
  60. package/scripts/lib/issue-coverage.mjs +47 -0
  61. package/scripts/lib/issue-hash.mjs +25 -0
  62. package/scripts/lib/issue-parse.mjs +194 -0
  63. package/scripts/lib/issue-sections.mjs +43 -0
  64. package/scripts/lib/labels.mjs +65 -0
  65. package/scripts/lib/language.mjs +9 -0
  66. package/scripts/lib/logaudit.mjs +123 -0
  67. package/scripts/lib/messages.mjs +328 -0
  68. package/scripts/lib/next-step.mjs +144 -0
  69. package/scripts/lib/openspec-commands.mjs +44 -0
  70. package/scripts/lib/openspec-status.mjs +16 -0
  71. package/scripts/lib/plan.mjs +256 -0
  72. package/scripts/lib/platform-of.mjs +32 -0
  73. package/scripts/lib/platform-scope.mjs +32 -0
  74. package/scripts/lib/platforms.mjs +90 -0
  75. package/scripts/lib/preflight.mjs +71 -0
  76. package/scripts/lib/protected-paths.mjs +102 -0
  77. package/scripts/lib/requeue.mjs +23 -0
  78. package/scripts/lib/review-adapters.mjs +37 -0
  79. package/scripts/lib/review-brief.mjs +43 -0
  80. package/scripts/lib/review-events.mjs +56 -0
  81. package/scripts/lib/review-state.mjs +157 -0
  82. package/scripts/lib/reviewers.mjs +39 -0
  83. package/scripts/lib/role-skills.mjs +112 -0
  84. package/scripts/lib/run-flags.mjs +16 -0
  85. package/scripts/lib/run-usage.mjs +65 -0
  86. package/scripts/lib/scan.mjs +73 -0
  87. package/scripts/lib/ship.mjs +161 -0
  88. package/scripts/lib/sla.mjs +90 -0
  89. package/scripts/lib/stack-profile.mjs +169 -0
  90. package/scripts/lib/staging.mjs +21 -0
  91. package/scripts/lib/status-data.mjs +109 -0
  92. package/scripts/lib/status.mjs +199 -0
  93. package/scripts/lib/tasks-file.mjs +81 -0
  94. package/scripts/lib/web-review.mjs +98 -0
  95. package/scripts/lib/yaml-lite.mjs +157 -0
  96. package/scripts/needs-reviewer.mjs +41 -0
  97. package/scripts/next-step.mjs +39 -0
  98. package/scripts/pick-reviewer.mjs +42 -0
  99. package/scripts/platform-of.mjs +49 -0
  100. package/scripts/preflight.mjs +32 -0
  101. package/scripts/render-message.mjs +35 -0
  102. package/scripts/requeue.mjs +53 -0
  103. package/scripts/review-adapter.mjs +32 -0
  104. package/scripts/review-brief.mjs +29 -0
  105. package/scripts/review-event.mjs +70 -0
  106. package/scripts/review-sla.mjs +41 -0
  107. package/scripts/review-state.mjs +171 -0
  108. package/scripts/review-who.mjs +21 -0
  109. package/scripts/role-skills.mjs +53 -0
  110. package/scripts/run-usage.mjs +22 -0
  111. package/scripts/scan-diff.mjs +16 -0
  112. package/scripts/ship-check.mjs +54 -0
  113. package/scripts/spec-ready.mjs +40 -0
  114. package/scripts/stack-profile.mjs +67 -0
  115. package/scripts/stage.mjs +14 -0
  116. package/scripts/status.mjs +47 -0
  117. package/scripts/usage-limit.mjs +17 -0
  118. package/scripts/validate-config.mjs +44 -0
  119. package/scripts/web-review.mjs +134 -0
  120. package/skills/doctor/SKILL.md +28 -0
  121. package/skills/fix/SKILL.md +128 -0
  122. package/skills/go/SKILL.md +242 -0
  123. package/skills/init/SKILL.md +72 -0
  124. package/skills/issue/SKILL.md +128 -0
  125. package/skills/issue/references/update.md +15 -0
  126. package/skills/review/SKILL.md +157 -0
  127. package/skills/ship/SKILL.md +128 -0
  128. package/skills/status/SKILL.md +27 -0
  129. package/skills/think/SKILL.md +76 -0
  130. package/stacks/flutter.md +63 -0
  131. package/stacks/laravel.md +65 -0
  132. package/stacks/vue.md +70 -0
  133. package/templates/issue.md +99 -0
  134. package/templates/relevo.yml +59 -0
  135. package/templates/workflows/relevo-digest.yml +54 -0
  136. package/templates/workflows/relevo-go.yml +263 -0
  137. package/templates/workflows/relevo-queue.yml +72 -0
  138. package/templates/workflows/relevo-review.yml +60 -0
  139. package/templates/workflows/relevo-sla.yml +53 -0
@@ -0,0 +1,99 @@
1
+ ## Summary
2
+
3
+ <!-- What this does, who it is for and why, in two or three sentences. -->
4
+
5
+ ## Outcome
6
+
7
+ <!-- Optional. The observable result that shows the issue is done, in one or two sentences. -->
8
+
9
+ ## Scenarios
10
+
11
+ - **WHEN** <the situation>
12
+ - **THEN** <the result a person can observe>
13
+
14
+ ## Edge cases
15
+
16
+ - <unusual input or state>: <what must happen>
17
+
18
+ ## Out of scope
19
+
20
+ - <what is deliberately left out, with the issue that covers it when there is one>
21
+
22
+ <!-- Optional sections: delete what does not apply. Headings stay in English; write the contents in the language of the repository. -->
23
+
24
+ ## Decisions
25
+
26
+ <!-- Optional. One block per decision the agreement settled, with an id. Words that were rejected go under Discarded. -->
27
+
28
+ ### D1. <decision title>
29
+ Decided: <what was decided>
30
+ Why: <the reason>
31
+ Discarded: <the alternative> (<why not>)
32
+
33
+ ## Constraints
34
+
35
+ <!-- Optional. A concrete limit per line, with an id: security, performance, data, integration, business rule or accessibility. -->
36
+
37
+ - C1: <a concrete limit>
38
+
39
+ ## Code pointers
40
+
41
+ <!-- Optional. What the exploration found in the code; the builder checks it before trusting it. -->
42
+
43
+ - `<path/to/file>`: <why it matters>
44
+
45
+ ## Verification
46
+
47
+ <!-- Optional. What only a person can check on a device or in a browser. Test data by name, never secrets. -->
48
+
49
+ - <what only a person can check, and with which data>
50
+
51
+ ## Assumptions
52
+
53
+ <!-- Optional. What the person approved without being sure. -->
54
+
55
+ - <an assumption>
56
+
57
+ ## Open questions
58
+
59
+ <!-- Anything written here sends the issue back to its writer: answer it first, then delete the section. -->
60
+
61
+ ## Screens and behaviour
62
+
63
+ <!-- Where it appears, what it looks like, when it shows or hides, animation and feedback, loading and error states, reference image. -->
64
+
65
+ ## Data contract
66
+
67
+ <!-- For each endpoint or data source: method and path, how the caller authenticates, request fields, a realistic response with types, and which values may be null. -->
68
+
69
+ ## Design reference
70
+
71
+ <!-- Optional. What the screen must look like: one bullet per reference, saying what must match (structure, spacing, typography, colour, states). A file of this repository, a picture pasted into this issue, a file of a linked repository written owner/repo:path, or example code in a fenced block. At most five. -->
72
+
73
+ - `<path/to/reference>`: <what must match>
74
+
75
+ ## Documentation
76
+
77
+ <!-- Optional. Only when the agreement settled terms or decisions to record; leave it out otherwise. Exact text, nothing but glossary entries and decision records. -->
78
+
79
+ - Add these entries to `GLOSSARY.md`, under its existing heading, after the existing ones, exactly as written. They are vocabulary only; nothing else in that file changes.
80
+
81
+ **<term>**:
82
+ <definition>
83
+ _Avoid_: <words to avoid>
84
+
85
+ <!-- Optional. Only when relevo.yml has several platforms: the one platform this issue is for. A feature that needs two platforms is two issues linked with Depends-on. -->
86
+
87
+ Platform: <name of a platform in relevo.yml>
88
+
89
+ <!-- Optional. Only when the change needs a new dependency: the manifest and lock files it may touch. Without this line an unattended run refuses to change them. -->
90
+
91
+ Allow-paths: <dependency files the change may touch, for example pubspec.yaml, pubspec.lock>
92
+
93
+ <!-- Optional. Related issues of the same idea; it only informs. -->
94
+
95
+ Related: #<number>
96
+
97
+ <!-- Optional. #<number> is an issue of this repository; owner/repo#<number> is an issue of a repository listed in links of relevo.yml. -->
98
+
99
+ Depends-on: #<number>
@@ -0,0 +1,59 @@
1
+ # relevo.yml: one per repository. /relevo:init creates and updates this file.
2
+ # Validate it with: node scripts/validate-config.mjs relevo.yml
3
+ # Never put API keys or tokens here; they belong in the repository secret store.
4
+
5
+ branches:
6
+ base: dev # integration branch: feature pull requests target it
7
+ release: main # promoted from base in a separate pull request
8
+
9
+ tracker: github # github (more later)
10
+ git_provider: github # github (more later)
11
+ platforms: # what is built and where; one entry per stack (see docs/stacks.md)
12
+ app:
13
+ stack: flutter # the profile in stacks/<stack>.md of the plugin, or in .relevo/stacks/
14
+ path: . # folder of the repository where this platform lives
15
+ targets: [android] # flutter: android | ios | web
16
+ # checks: # replace a verification command of the profile, for example:
17
+ # test: fvm flutter test
18
+
19
+ links: [] # repositories an issue may depend on (Depends-on: owner/repo#N); private ones need the secret RELEVO_LINKS_TOKEN
20
+
21
+ team:
22
+ members: [] # GitHub usernames of the people who build and review
23
+ spec_writer: null # writes the specs; never reviews or merges
24
+ approvers: [] # only used when review.mode is "roles"
25
+
26
+ review:
27
+ mode: peer # none (solo) | peer | roles
28
+ merger: approver # author | approver: who runs /relevo:ship
29
+ autopilot: off # off | precheck | verify: how much of the on-device review the agent may drive (the person always closes it)
30
+ allowed_origins: [] # web origins a driven review may visit besides localhost, e.g. https://staging.example.com
31
+
32
+ limits:
33
+ built_unreviewed_per_person: 2 # built issues one person may have waiting
34
+ concurrent_agents: 3 # unattended runs at the same time
35
+ review_sla_hours: 8 # reminder, then escalation after the same time again
36
+ run_max_turns: 100 # per unattended run
37
+ building_alert_hours: 3 # hours without activity before an issue on `building` is reported as stuck
38
+ run_max_cost: 5 # REQUIRED: US dollars per unattended run; choose your own
39
+
40
+ qa:
41
+ retries: 1 # developer attempts after QA rejects
42
+ rules_from: CLAUDE.md # project rules QA reads at every run
43
+
44
+ evidence:
45
+ policy: attach_to_pr # attach_to_pr | local_only (use for sensitive data)
46
+
47
+ digest:
48
+ issue: null # number of the issue where the daily digest is posted as a comment; subscribe your chat tool to it
49
+
50
+ design:
51
+ images: false # true: a picture pasted into an issue is downloaded and shown to the model (it may hold customer data); files in the repository are always available
52
+
53
+ language: en # language of hand-off messages (en, es)
54
+ trust: private-team # private-team | public
55
+
56
+ # Rename any label here; keys are fixed, names are yours.
57
+ # labels:
58
+ # ready: relevo:ready
59
+ # built: relevo:built
@@ -0,0 +1,54 @@
1
+ # Relevo: posts one digest per working day, as a comment on the issue named by digest.issue in relevo.yml.
2
+ # It lists who is waiting on what and the alerts, and posts nothing when nothing is waiting.
3
+ # Subscribe your chat tool to that issue to receive it. Copy to .github/workflows/relevo-digest.yml.
4
+ # Change the time below to suit your team (cron is in UTC).
5
+ # The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
6
+
7
+ name: relevo-digest
8
+
9
+ # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
10
+ # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
11
+ env:
12
+ RELEVO_VERSION: '0.3.0'
13
+
14
+ on:
15
+ schedule:
16
+ - cron: '0 13 * * 1-5'
17
+ workflow_dispatch:
18
+
19
+ concurrency:
20
+ group: relevo-digest
21
+ cancel-in-progress: false
22
+
23
+ permissions:
24
+ contents: read
25
+ issues: write
26
+ pull-requests: read
27
+
28
+ jobs:
29
+ digest:
30
+ runs-on: ubuntu-latest
31
+ timeout-minutes: 10
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - uses: actions/setup-node@v4
36
+ with:
37
+ node-version: 22
38
+
39
+ - name: Install the Relevo scripts
40
+ # The published package at the version pinned above; no repository is cloned for them. A package without the
41
+ # script this job runs stops the run here, before anything is changed.
42
+ run: |
43
+ mkdir -p .relevo-plugin
44
+ npm pack "@elalfner/relevo@$RELEVO_VERSION" --silent --pack-destination "$RUNNER_TEMP" > /dev/null
45
+ tar -xzf "$RUNNER_TEMP"/elalfner-relevo-*.tgz --strip-components=1 -C .relevo-plugin
46
+ for f in scripts/digest.mjs; do
47
+ [ -f ".relevo-plugin/$f" ] || { echo "::error::The Relevo package $RELEVO_VERSION lacks $f"; exit 1; }
48
+ done
49
+
50
+ - name: Post the digest
51
+ env:
52
+ GH_TOKEN: ${{ github.token }}
53
+ GITHUB_REPOSITORY: ${{ github.repository }}
54
+ run: node .relevo-plugin/scripts/digest.mjs --repo "$GITHUB_REPOSITORY"
@@ -0,0 +1,263 @@
1
+ # Relevo: builds an issue when someone with write access labels it `relevo:ready`.
2
+ # Copy to .github/workflows/relevo-go.yml. If you renamed the ready label in relevo.yml, change it below too.
3
+ #
4
+ # Two jobs. `decide` is light (Node and the plugin's scripts, about half a minute): it runs the gates, claims the
5
+ # issue and comments when there is nothing to build. Only when the decision is `start` does `build` install the
6
+ # toolchain and run the agent, so an issue that is blocked, queued or incomplete never pays for that setup.
7
+ #
8
+ # Needs: a model credential secret (ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN), and a relevo.yml at the
9
+ # repository root.
10
+ # Keep the numbers in claude_args in line with limits.run_max_turns and limits.run_max_cost.
11
+ # Where the numbers disagree the lower one wins, so this file can only make a run cheaper than relevo.yml says.
12
+
13
+ name: relevo-go
14
+
15
+ # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
16
+ # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
17
+ env:
18
+ RELEVO_VERSION: '0.3.0'
19
+
20
+ on:
21
+ issues:
22
+ types: [labeled]
23
+ # The queue workflow (relevo-queue.yml) starts waiting issues through this trigger.
24
+ workflow_dispatch:
25
+ inputs:
26
+ issue:
27
+ description: Issue number to build
28
+ required: true
29
+
30
+ # One run per issue: a repeated label event waits for the first run and then finds the issue already building.
31
+ concurrency:
32
+ group: relevo-go-${{ github.event.issue.number || inputs.issue }}
33
+ cancel-in-progress: false
34
+
35
+ permissions:
36
+ contents: read
37
+ issues: write
38
+ pull-requests: read
39
+
40
+ jobs:
41
+ decide:
42
+ # The repository variable RELEVO_CLOUD_GO set to `off` pauses the cloud: a ready label starts nothing and the issue
43
+ # stays ready for a local run (/relevo:go <n>). It is read at run time, so changing it needs no pull request.
44
+ if: vars.RELEVO_CLOUD_GO != 'off' && (github.event_name == 'workflow_dispatch' || (github.event.label.name == 'relevo:ready' && github.event.sender.type == 'User'))
45
+ runs-on: ubuntu-latest
46
+ timeout-minutes: 10
47
+ outputs:
48
+ action: ${{ steps.plan.outputs.action }}
49
+ decision_b64: ${{ steps.plan.outputs.decision_b64 }}
50
+ # The platform the issue is for (empty unless the decision is `start`): the build job installs only its toolchain.
51
+ platform: ${{ steps.plan.outputs.platform }}
52
+ stack: ${{ steps.plan.outputs.stack }}
53
+ path: ${{ steps.plan.outputs.path }}
54
+ steps:
55
+ # The repository holds relevo.yml; the plugin holds the scripts that apply the gates.
56
+ - uses: actions/checkout@v4
57
+
58
+ - uses: actions/setup-node@v4
59
+ with:
60
+ node-version: 22
61
+
62
+ - name: Install the Relevo scripts
63
+ # The published package at the version pinned above; no repository is cloned for them. A package without the
64
+ # script this job runs stops the run here, before anything is changed.
65
+ run: |
66
+ mkdir -p .relevo-plugin
67
+ npm pack "@elalfner/relevo@$RELEVO_VERSION" --silent --pack-destination "$RUNNER_TEMP" > /dev/null
68
+ tar -xzf "$RUNNER_TEMP"/elalfner-relevo-*.tgz --strip-components=1 -C .relevo-plugin
69
+ for f in scripts/go-plan.mjs; do
70
+ [ -f ".relevo-plugin/$f" ] || { echo "::error::The Relevo package $RELEVO_VERSION lacks $f"; exit 1; }
71
+ done
72
+
73
+ # Applies the gates and, when the decision is `start`, claims the issue (relevo:building). Any other decision
74
+ # (needs-spec, blocked, queued, denied, no milestone, no assignee) is applied here with its comment.
75
+ - name: Gates and claim
76
+ id: plan
77
+ env:
78
+ GH_TOKEN: ${{ github.token }}
79
+ # Only needed when `links` in relevo.yml lists a private repository: a read-only token (issues and pull
80
+ # requests) stored as the secret RELEVO_LINKS_TOKEN. It is used only to read the issues the dependencies
81
+ # name, never to write, and an empty value is fine (public repositories are read without it).
82
+ GH_LINKS_TOKEN: ${{ secrets.RELEVO_LINKS_TOKEN }}
83
+ GITHUB_REPOSITORY: ${{ github.repository }}
84
+ ISSUE: ${{ github.event.issue.number || inputs.issue }}
85
+ # Without a model credential the cloud cannot build: the issue stays ready and a comment says to run it locally.
86
+ HAS_CREDENTIAL: ${{ secrets.ANTHROPIC_API_KEY != '' || secrets.CLAUDE_CODE_OAUTH_TOKEN != '' }}
87
+ run: node .relevo-plugin/scripts/go-plan.mjs --repo "$GITHUB_REPOSITORY" --only "$ISSUE" --apply --claim --credential "$HAS_CREDENTIAL" --github-output "$GITHUB_OUTPUT"
88
+
89
+ # See the same step in `build`. A failure before the issue is claimed (the plugin cannot be cloned, the
90
+ # scripts fail) leaves it ready or queued with nobody told.
91
+ - name: Report a run that ended without an outcome
92
+ if: always()
93
+ env:
94
+ GH_TOKEN: ${{ github.token }}
95
+ ISSUE: ${{ github.event.issue.number || inputs.issue }}
96
+ JOB_STATUS: ${{ job.status }}
97
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
98
+ run: |
99
+ labels=$(gh issue view "$ISSUE" --json labels --jq '.labels[].name')
100
+ if [ "$JOB_STATUS" = "success" ]; then
101
+ exit 0
102
+ fi
103
+ if ! printf '%s\n' "$labels" | grep -qxE 'relevo:(ready|queued|building)'; then
104
+ exit 0
105
+ fi
106
+ assignee=$(gh issue view "$ISSUE" --json assignees --jq '.assignees[0].login // empty')
107
+ gh issue edit "$ISSUE" --remove-label relevo:ready --remove-label relevo:queued --remove-label relevo:building --add-label relevo:qa-failed
108
+ 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."
109
+
110
+ build:
111
+ needs: decide
112
+ if: needs.decide.outputs.action == 'start'
113
+ runs-on: ubuntu-latest
114
+ timeout-minutes: 60
115
+ permissions:
116
+ contents: write
117
+ issues: write
118
+ pull-requests: read
119
+ steps:
120
+ - uses: actions/checkout@v4
121
+ with:
122
+ fetch-depth: 0
123
+
124
+ - uses: actions/setup-node@v4
125
+ with:
126
+ node-version: 22
127
+
128
+ # The agent runs the project's own checks (QA runs them), so install here the toolchain of the stack of the
129
+ # platform of the issue. The `decide` job tells which one (`needs.decide.outputs.stack`) and where it lives
130
+ # (`needs.decide.outputs.path`), so an issue for the front does not install the back's tools. Uncomment the
131
+ # examples your stacks need and adjust versions and caching; the names are the `stack` of relevo.yml:
132
+ # - if: needs.decide.outputs.stack == 'flutter'
133
+ # uses: subosito/flutter-action@v2
134
+ # with:
135
+ # channel: stable
136
+ # cache: true
137
+ # - if: needs.decide.outputs.stack == 'vue'
138
+ # uses: actions/setup-node@v4
139
+ # with:
140
+ # node-version: 22
141
+ # cache: npm
142
+ # cache-dependency-path: ${{ needs.decide.outputs.path }}/package-lock.json
143
+ # - if: needs.decide.outputs.stack == 'laravel'
144
+ # uses: shivammathur/setup-php@v2
145
+ # with:
146
+ # php-version: '8.3'
147
+ # tools: composer
148
+ # Install the dependencies of the platform from its folder, for example:
149
+ # - if: needs.decide.outputs.stack == 'vue'
150
+ # working-directory: ${{ needs.decide.outputs.path }}
151
+ # run: npm ci --ignore-scripts
152
+ # Install dependencies without package scripts where the tool offers it, so a dependency cannot run code
153
+ # while it is installed: `npm ci --ignore-scripts`, `pnpm install --frozen-lockfile --ignore-scripts`,
154
+ # `composer install --no-scripts`. `flutter pub get` and `dart pub get` run no install-time scripts of
155
+ # the packages, so they need no such option.
156
+
157
+ - name: Install OpenSpec
158
+ run: npm install --global @fission-ai/openspec
159
+ env:
160
+ OPENSPEC_TELEMETRY: '0'
161
+
162
+ - id: agent
163
+ uses: anthropics/claude-code-action@v1
164
+ env:
165
+ OPENSPEC_TELEMETRY: '0'
166
+ GH_TOKEN: ${{ github.token }}
167
+ # Without this the agent may start a subagent in the background and end the run before it finishes,
168
+ # leaving the issue stuck on building with no comment (seen in the pilot, see docs/pilot-findings.md).
169
+ CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: '1'
170
+ with:
171
+ # Credentials: choose ONE of the next two lines.
172
+ # a) API key (default):
173
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
174
+ # b) Subscription: create a token with `claude setup-token` and store it as the secret CLAUDE_CODE_OAUTH_TOKEN.
175
+ # The command prints the token over two lines; remove all whitespace before storing it, for example
176
+ # `pbpaste | tr -d '[:space:]' | gh secret set CLAUDE_CODE_OAUTH_TOKEN`. Then replace the line above with:
177
+ # claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
178
+ github_token: ${{ github.token }}
179
+ # The queue starts builds with `gh workflow run`, so the actor is the Actions bot and the action refuses
180
+ # it unless it is allowed. Only this one bot: never '*', which would let outside apps start runs.
181
+ allowed_bots: github-actions
182
+ # Plugin repository: choose ONE of the next two lines.
183
+ # a) Public repository (default):
184
+ plugin_marketplaces: https://github.com/elalfner/relevo.git
185
+ # b) Private repository: the runner cannot clone it without a credential. Store a read-only fine-grained
186
+ # token as the secret RELEVO_PLUGIN_TOKEN and replace the line above with:
187
+ # plugin_marketplaces: https://x-access-token:${{ secrets.RELEVO_PLUGIN_TOKEN }}@github.com/elalfner/relevo.git
188
+ plugins: relevo@relevo
189
+ # The issue text is data, never instructions: the skill tells the agent so. The tool list is closed:
190
+ # only the tools in --allowedTools, and no web access (nothing to fetch, nowhere to send a secret).
191
+ # Do not set allowed_non_write_users or show_full_output here, and never widen allowed_bots: they open the run to
192
+ # strangers or print tool results (possibly secrets) to the log.
193
+ # The decision the `decide` job already made travels with the prompt, encoded so that the issue title in it
194
+ # is never read as an instruction. The skill decodes it and skips its own planning step.
195
+ prompt: /relevo:go ${{ github.event.issue.number || inputs.issue }} --decision ${{ needs.decide.outputs.decision_b64 }}
196
+ claude_args: >-
197
+ --max-turns 100
198
+ --max-budget-usd 5
199
+ --allowedTools "Bash,Read,Write,Edit,Glob,Grep,Task"
200
+ --disallowedTools "WebFetch,WebSearch,Bash(curl:*),Bash(wget:*)"
201
+
202
+ # The scripts of the next two steps come from the plugin package. It is unpacked into the workspace only now, after the agent has
203
+ # finished, because the agent's preflight needs a clean working tree and a checkout inside it would not be.
204
+ - name: Install the Relevo scripts for the reports
205
+ if: always()
206
+ continue-on-error: true
207
+ run: |
208
+ mkdir -p .relevo-plugin
209
+ npm pack "@elalfner/relevo@$RELEVO_VERSION" --silent --pack-destination "$RUNNER_TEMP" > /dev/null
210
+ tar -xzf "$RUNNER_TEMP"/elalfner-relevo-*.tgz --strip-components=1 -C .relevo-plugin
211
+ for f in scripts/run-usage.mjs scripts/usage-limit.mjs; do
212
+ [ -f ".relevo-plugin/$f" ] || { echo "::error::The Relevo package $RELEVO_VERSION lacks $f"; exit 1; }
213
+ done
214
+
215
+ # What the run used, read from the agent's execution log. The format of that log is not documented, so the
216
+ # reader is tolerant: when it finds nothing it posts nothing, and this step can never fail the job.
217
+ - name: Report what the run used
218
+ if: always()
219
+ continue-on-error: true
220
+ env:
221
+ GH_TOKEN: ${{ github.token }}
222
+ ISSUE: ${{ github.event.issue.number || inputs.issue }}
223
+ EXECUTION_FILE: ${{ steps.agent.outputs.execution_file }}
224
+ # With a subscription token and no API key the cost is notional.
225
+ ONLY_SUBSCRIPTION: ${{ secrets.ANTHROPIC_API_KEY == '' && secrets.CLAUDE_CODE_OAUTH_TOKEN != '' }}
226
+ run: |
227
+ [ -n "$EXECUTION_FILE" ] || exit 0
228
+ flag=""; [ "$ONLY_SUBSCRIPTION" = "true" ] && flag="--subscription"
229
+ line=$(node .relevo-plugin/scripts/run-usage.mjs --file "$EXECUTION_FILE" $flag)
230
+ [ -n "$line" ] && gh issue comment "$ISSUE" --body "$line"
231
+ exit 0
232
+
233
+ # A run can end without telling anyone: the plugin cannot be cloned, the login fails, preflight stops, the
234
+ # run reached --max-turns or --max-budget-usd (the runner cuts it; the agent cannot see those counters and
235
+ # does not stop itself), or the agent ends its session early (all seen in the pilot, see docs/pilot-findings.md). If the issue is still
236
+ # ready or building after the agent step, no outcome was recorded, so say so on the issue.
237
+ # If you renamed the labels in relevo.yml, change the names below too.
238
+ - name: Report a run that ended without an outcome
239
+ if: always()
240
+ env:
241
+ GH_TOKEN: ${{ github.token }}
242
+ ISSUE: ${{ github.event.issue.number || inputs.issue }}
243
+ JOB_STATUS: ${{ job.status }}
244
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
245
+ EXECUTION_FILE: ${{ steps.agent.outputs.execution_file }}
246
+ run: |
247
+ labels=$(gh issue view "$ISSUE" --json labels --jq '.labels[].name')
248
+ # A run the queue dispatched that fails before it claims the issue leaves it `queued`: report that
249
+ # too, or the queue would dispatch it again every 30 minutes. A run that ends normally leaves it queued
250
+ # on purpose (no capacity), so that case is only reported when the job failed.
251
+ if ! printf '%s\n' "$labels" | grep -qxE 'relevo:(ready|building)'; then
252
+ if ! { [ "$JOB_STATUS" != "success" ] && printf '%s\n' "$labels" | grep -qx 'relevo:queued'; }; then
253
+ exit 0
254
+ fi
255
+ fi
256
+ assignee=$(gh issue view "$ISSUE" --json assignees --jq '.assignees[0].login // empty')
257
+ gh issue edit "$ISSUE" --remove-label relevo:ready --remove-label relevo:queued --remove-label relevo:building --add-label relevo:qa-failed
258
+ # A usage limit, a quota or an empty credit balance is not a failure of the code: say so.
259
+ if [ -n "$EXECUTION_FILE" ] && node .relevo-plugin/scripts/usage-limit.mjs --file "$EXECUTION_FILE"; then
260
+ gh issue comment "$ISSUE" --body "${assignee:+@$assignee }The run stopped because the usage limit (or the credit) of the model account was reached, so nothing was built. Nothing is wrong with the code. Label the issue \`relevo:ready\` again later, after removing \`relevo:qa-failed\`: $RUN_URL"
261
+ else
262
+ 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."
263
+ fi
@@ -0,0 +1,72 @@
1
+ # Relevo: re-evaluates the queue and starts the issues that are now eligible.
2
+ # Copy to .github/workflows/relevo-queue.yml next to relevo-go.yml (it starts builds through that workflow).
3
+ #
4
+ # Runs when an issue is closed or its labels change (a merge, a built issue leaving that state) and on a
5
+ # schedule, so a lost event does not stall the queue. Needs a relevo.yml at the repository root.
6
+ # The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
7
+
8
+ name: relevo-queue
9
+
10
+ # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
11
+ # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
12
+ env:
13
+ RELEVO_VERSION: '0.3.0'
14
+
15
+ on:
16
+ issues:
17
+ types: [closed, labeled, unlabeled]
18
+ schedule:
19
+ - cron: '*/30 * * * *'
20
+ workflow_dispatch:
21
+
22
+ # One evaluation at a time; a second one waits and then sees the first one's labels.
23
+ concurrency:
24
+ group: relevo-queue
25
+ cancel-in-progress: false
26
+
27
+ permissions:
28
+ contents: read
29
+ issues: write
30
+ pull-requests: read
31
+ actions: write
32
+
33
+ jobs:
34
+ requeue:
35
+ # Paused together with the go workflow by the repository variable RELEVO_CLOUD_GO set to `off`.
36
+ if: vars.RELEVO_CLOUD_GO != 'off'
37
+ runs-on: ubuntu-latest
38
+ timeout-minutes: 10
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+
42
+ - uses: actions/setup-node@v4
43
+ with:
44
+ node-version: 22
45
+
46
+ - name: Install the Relevo scripts
47
+ # The published package at the version pinned above; no repository is cloned for them. A package without the
48
+ # script this job runs stops the run here, before anything is changed.
49
+ run: |
50
+ mkdir -p .relevo-plugin
51
+ npm pack "@elalfner/relevo@$RELEVO_VERSION" --silent --pack-destination "$RUNNER_TEMP" > /dev/null
52
+ tar -xzf "$RUNNER_TEMP"/elalfner-relevo-*.tgz --strip-components=1 -C .relevo-plugin
53
+ for f in scripts/requeue.mjs; do
54
+ [ -f ".relevo-plugin/$f" ] || { echo "::error::The Relevo package $RELEVO_VERSION lacks $f"; exit 1; }
55
+ done
56
+
57
+ - name: Evaluate the queue and dispatch builds
58
+ env:
59
+ GH_TOKEN: ${{ github.token }}
60
+ # Only needed when `links` in relevo.yml lists a private repository: a read-only token (issues and pull
61
+ # requests) stored as the secret RELEVO_LINKS_TOKEN. It is used only to read the issues the dependencies
62
+ # name, never to write, and an empty value is fine (public repositories are read without it).
63
+ GH_LINKS_TOKEN: ${{ secrets.RELEVO_LINKS_TOKEN }}
64
+ GITHUB_REPOSITORY: ${{ github.repository }}
65
+ # Without a model credential nothing is dispatched.
66
+ HAS_CREDENTIAL: ${{ secrets.ANTHROPIC_API_KEY != '' || secrets.CLAUDE_CODE_OAUTH_TOKEN != '' }}
67
+ run: |
68
+ result=$(node .relevo-plugin/scripts/requeue.mjs --repo "$GITHUB_REPOSITORY" --credential "$HAS_CREDENTIAL")
69
+ echo "$result"
70
+ for issue in $(echo "$result" | node -e 'JSON.parse(require("fs").readFileSync(0,"utf8")).start.forEach(n=>console.log(n))'); do
71
+ gh workflow run relevo-go.yml --repo "$GITHUB_REPOSITORY" -f issue="$issue"
72
+ done
@@ -0,0 +1,60 @@
1
+ # Relevo: keeps the issue label in step with its pull request.
2
+ # A review that requests changes sets `relevo:changes-requested` and mentions the author; a new push returns the
3
+ # issue to `relevo:pr-open`; an approval mentions the person who will run /relevo:ship.
4
+ # Copy to .github/workflows/relevo-review.yml. Needs a relevo.yml at the repository root.
5
+ # Pull requests from forks are skipped explicitly (and would only get a read-only token anyway).
6
+ # The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
7
+
8
+ name: relevo-review
9
+
10
+ # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
11
+ # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
12
+ env:
13
+ RELEVO_VERSION: '0.3.0'
14
+
15
+ on:
16
+ pull_request_review:
17
+ types: [submitted]
18
+ pull_request:
19
+ types: [synchronize]
20
+
21
+ concurrency:
22
+ group: relevo-review-${{ github.event.pull_request.number }}
23
+ cancel-in-progress: false
24
+
25
+ permissions:
26
+ contents: read
27
+ issues: write
28
+ pull-requests: read
29
+
30
+ jobs:
31
+ review:
32
+ if: startsWith(github.event.pull_request.head.ref, 'feature/') && github.event.pull_request.head.repo.full_name == github.repository
33
+ runs-on: ubuntu-latest
34
+ timeout-minutes: 5
35
+ steps:
36
+ # The base branch holds relevo.yml; the pull request code is never checked out or run.
37
+ - uses: actions/checkout@v4
38
+ with:
39
+ ref: ${{ github.event.pull_request.base.ref }}
40
+
41
+ - uses: actions/setup-node@v4
42
+ with:
43
+ node-version: 22
44
+
45
+ - name: Install the Relevo scripts
46
+ # The published package at the version pinned above; no repository is cloned for them. A package without the
47
+ # script this job runs stops the run here, before anything is changed.
48
+ run: |
49
+ mkdir -p .relevo-plugin
50
+ npm pack "@elalfner/relevo@$RELEVO_VERSION" --silent --pack-destination "$RUNNER_TEMP" > /dev/null
51
+ tar -xzf "$RUNNER_TEMP"/elalfner-relevo-*.tgz --strip-components=1 -C .relevo-plugin
52
+ for f in scripts/review-event.mjs; do
53
+ [ -f ".relevo-plugin/$f" ] || { echo "::error::The Relevo package $RELEVO_VERSION lacks $f"; exit 1; }
54
+ done
55
+
56
+ - name: Update the label
57
+ env:
58
+ GH_TOKEN: ${{ github.token }}
59
+ GITHUB_REPOSITORY: ${{ github.repository }}
60
+ run: node .relevo-plugin/scripts/review-event.mjs --repo "$GITHUB_REPOSITORY"
@@ -0,0 +1,53 @@
1
+ # Relevo: reminds the reviewer when a pull request waits longer than limits.review_sla_hours, and escalates
2
+ # to the spec writer (adding the needs-reviewer label) after the same time again.
3
+ # Copy to .github/workflows/relevo-sla.yml. Needs a relevo.yml at the repository root.
4
+ # The scripts come from the published package @elalfner/relevo, at the version pinned in RELEVO_VERSION.
5
+
6
+ name: relevo-sla
7
+
8
+ # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
9
+ # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
10
+ env:
11
+ RELEVO_VERSION: '0.3.0'
12
+
13
+ on:
14
+ schedule:
15
+ - cron: '17 * * * *'
16
+ workflow_dispatch:
17
+
18
+ concurrency:
19
+ group: relevo-sla
20
+ cancel-in-progress: false
21
+
22
+ permissions:
23
+ contents: read
24
+ issues: write
25
+ pull-requests: write
26
+
27
+ jobs:
28
+ sla:
29
+ runs-on: ubuntu-latest
30
+ timeout-minutes: 10
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+
34
+ - uses: actions/setup-node@v4
35
+ with:
36
+ node-version: 22
37
+
38
+ - name: Install the Relevo scripts
39
+ # The published package at the version pinned above; no repository is cloned for them. A package without the
40
+ # script this job runs stops the run here, before anything is changed.
41
+ run: |
42
+ mkdir -p .relevo-plugin
43
+ npm pack "@elalfner/relevo@$RELEVO_VERSION" --silent --pack-destination "$RUNNER_TEMP" > /dev/null
44
+ tar -xzf "$RUNNER_TEMP"/elalfner-relevo-*.tgz --strip-components=1 -C .relevo-plugin
45
+ for f in scripts/review-sla.mjs; do
46
+ [ -f ".relevo-plugin/$f" ] || { echo "::error::The Relevo package $RELEVO_VERSION lacks $f"; exit 1; }
47
+ done
48
+
49
+ - name: Remind and escalate
50
+ env:
51
+ GH_TOKEN: ${{ github.token }}
52
+ GITHUB_REPOSITORY: ${{ github.repository }}
53
+ run: node .relevo-plugin/scripts/review-sla.mjs --repo "$GITHUB_REPOSITORY"