Skip to content

Commit d4d1eb9

Browse files
NickSeagullNickSeagullBotclaude
authored
Pipeline Phase 6 — release tail + learning loop (#722)
* feat(pipeline): Phase 6a — telemetry schema v3 + failure→asset-delta enforcement Foundation for Phase 6 (release tail + learning loop, ADR-0068). The failure→asset-delta protocol was prose-only in the pipeline skill; this mechanizes it and lays the schema the miner/weekly-review (PR-C) import. - telemetry schema v2 → **v3**: new run fields `asset_delta` (the class-fix a non-ok run ships) and `assets_consulted` (usage accounting). Readers tolerate older versions; the emitter writes v3. - `telemetry.py finish --outcome {failed,parked}` now **requires** `--asset-delta <type>:<dest>` from a closed delta-type taxonomy v1 (`alias|extension-point|phrasebook|hot-card|hlint-rule|hook|cli-utility| skill-edit|telemetry-label|PRUNE|none`); `none:<reason>` is the honest escape. - `telemetry.py consult --asset <kind>:<name>` — usage accounting so the miner can PRUNE never-consulted assets. - `telemetry.py --self-test` (new; wired into `./dev doctor`) covers the v3 fields, the enforcement, and a **cross-file label-taxonomy parity** check (telemetry.py FAILURE_LABELS ≡ pipeline-state LABELS) — closes the duplicate- enum drift risk. - ADR-0068 formalizes the protocol, the recommendation contract, the notify-only kill switch, and the deferred archive-sunset checklist. - SCHEMA.md documents the taxonomy, the `recommendations.jsonl` record, and the decided golden-retention policy (local/gitignored, prune after 4 weeks). - pipeline SKILL.md + AGENTS.md: the failure policy now names the enforced `--asset-delta` close and the `consult` usage logging. Part of #715 (Phase 6). PR-A of the A/B/C sequence; PR-B (release tail) and PR-C (learning loop) build on this schema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(pipeline): Phase 6 release tail + learning loop (DoD, kill switch, changelog, miner) Completes Phase 6 on top of the schema-v3 spine (ADR-0068). Everything is self-tested on fixtures via `./dev doctor`; the data-dependent activations (first weekly review, first miner report, archive sunset) are deferred until real runs accumulate. Release tail: - **DoD gate**: `spec-check --criteria <spec>` emits the criteria table as JSON; a tier lint rejects an `acceptance` criterion that names no `.hurl` test (and a `unit` one that does). `dod.yml` (workflow_run on `Test`) flags a suite failure on `main` as a **revert-candidate** — notify-only, never auto-reverts. - **Kill switch**: `scripts/revert` + `./dev revert <commit>` opens a revert PR; `revert.yml` runs it on a maintainer `/revert` comment (OWNER/MEMBER-gated, distinct from claude.yml's `@claude` so they never double-fire). - **Changelog**: `scripts/changelog` derives `CHANGELOG.md` from contract-delta specs; breaking = a removed signature line ⇒ mandatory migration note; `changelog --check` gates it at PR-ready (checks.yml). - **`./dev testbed`**: the acceptance suite gets a verb (parity — was CI-only). Learning loop: - **`scripts/retrospect`**: the deterministic weekly digest (stage times, failure labels, invented-API trend vs the first-5 baseline, waiting-on-human, asset usage) + the recommendation-contract validator (≥2 runs, in-taxonomy, measured cost) writing `telemetry/recommendations.jsonl`. - **`neohaskell-retrospective-miner` skill**: the frontier-model procedure that turns recurring friction into ≤5 contract-validated recommendations. Governance: 4 new `./dev` verbs (testbed/changelog/revert/retrospect) + 3 new self-tests wired into doctor (now 23 verbs, 7 skills); AGENTS.md gains a Phase 6 section. All 6 workflows parse. Part of #715 (Phase 6). Consolidated into this PR per request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pipeline): Phase 6 review hardening — anti-rot parity gates, DoD honesty, ledger safety Closes the 45 findings from the Phase 6 PR review. Every doc claim corrected here is now backed by a gate (doctor/self-test), not just edited, so it can't re-rot on the next change. - Parity gates (telemetry.py self-test): schema-version (docs == CURRENT_SCHEMA) and delta-type prose (SCHEMA.md + miner skill == DELTA_TYPES); fix stale v2 strings. - ./dev telemetry verb (drop doctor exemption); all docs invoke it, not bare telemetry.py. - Parallel-worktree ledger safety: runs/recommendations.jsonl merge=union + run_id collision guard in `telemetry start`. - changelog migration gate rejects placeholder impact (TBD/N/A/-/TODO/pending). - DoD gate watches "Test" AND "Test macOS"; resolves PR via commits->pulls; honest comment. - Kill switch: revert.yml and claude.yml provably disjoint (exact /revert match); conflicting `git revert` aborts cleanly before any push. - retrospect: golden pruner + inventory, `--set` lifecycle (gated status/validated), week/<=5-per-week validation, schema-v1 dummy + malformed-line skip, PRUNE zero-saving. - telemetry asset-delta: `none:<reason>` keeps colons (not split into ref). - DoD honesty: new `spec-check --criteria-tests` existence gate wired into CI (doctor asserts it has a caller); AGENTS.md de-claims the post-merge per-level checker. - consult logging wired into pipeline skill localize/implement steps. - Cleanups: tier-lint "hurl" substring, ADR 0067/0068 Accepted, SCHEMA header reword, CHANGELOG promotion note, pipeline-bootstrap dogfood-exemption note, revert OWNER/MEMBER rationale. Verified: ./dev doctor green; negative probes confirm the schema-version and delta-type gates fail on drift; all workflow YAML valid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(pipeline): address CodeRabbit on Phase 6 — revert hardening, ledger + gate safety Five findings from CodeRabbit's review of the Phase 6 PR, all valid: - revert.yml: add `concurrency` keyed on the PR number (cancel-in-progress) so two quick `/revert` comments can't race on the revert branch; set `persist-credentials: false` and authenticate the push via an explicit tokenized remote instead of leaving the token in .git/config for later steps. - scripts/revert: `gh pr create` is now checked — a push that succeeds but a PR that fails no longer reports success (exits with recovery guidance); `--no-pr` no longer falsely prints "opened revert PR". - scripts/retrospect: `_load_recs()` tolerates a malformed recommendations.jsonl line (warn + skip, mirroring load_runs) so one bad hand-edit can't crash the weekly review; self-test covers it. - scripts/spec-check: `_module_exists` uses an exact stem match (like `_hurl_exists`) — a substring match let a criterion naming `FooSpec` be "satisfied" by an unrelated `FooBarSpec`, weakening the DoD gate. ./dev doctor green (24 verbs, 7 skills, all self-tests); revert.yml parses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(test): job-level path gating so ci-gate is a safe required check Enforce "the project compiles AND tests pass" without deadlocking tooling-only PRs. A required status check that never runs stays pending forever and blocks the merge; Test.yml's workflow-level `paths:` filter did exactly that for any PR not touching compiled paths. - Remove the `pull_request` `paths:` filter — Test now always triggers on PRs (push keeps its paths filter; post-merge/dod.yml behavior unchanged). - New cheap `changes` job detects whether a PR touched compiled surface (core/testbed/integrations/nix/workflows/cabal.project/flake); heavy jobs skip when it didn't (build+doctest gated; test-*/codemap-sync cascade via needs). - `ci-gate` (always runs, real conclusion) now enforces build+doctest+all test suites, accepting skips ONLY when there is nothing to test (draft or no compiled change) and failing closed if path-detection itself failed. This makes `ci-gate` sound to require in branch protection: green on tooling PRs (nothing to build), red whenever a compiled PR fails to build or test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(test): codemap-sync runs last and is enforced via ci-gate codemap-sync now depends on build + every test suite, so it's the final job — the last confirmation right before merge — and the codemap regen isn't spent on a PR that's already failing. Folded into ci-gate's checked results (last, so a real test failure surfaces before codemap-sync's transitive skip), so the already-required ci-gate now enforces "committed codemap matches the code" too. No branch-protection change needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(test): keep codemap-sync parallel (enforced, no wall-clock cost) Reverts the serial-last ordering — codemap-sync runs alongside the test suites again (needs only build's artifact) on its own runner, so it adds no PR latency, while staying in ci-gate's checked results so it remains a merge gate. The "don't spend the regen if tests fail" saving was a runner-minute, not worth lengthening every compiled PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: NickSeagullBot <bot@nickseagull.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent b547cb5 commit d4d1eb9

22 files changed

Lines changed: 1830 additions & 72 deletions

File tree

.claude/skills/neohaskell-pipeline/SKILL.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ description: Orchestrate a NeoHaskell change end-to-end through the spec-gated p
88
Exactly **two human gates**: spec approval (draft PR) and final PR review.
99
Everything between them is mechanical or agent-run, resumable from
1010
`.pipeline/state.json`, and telemetered. Stage names below are the telemetry
11-
schema v2 canon (`telemetry/SCHEMA.md`) — state, telemetry lines, and this
11+
schema v3 canon (`telemetry/SCHEMA.md`) — state, telemetry lines, and this
1212
skill share one vocabulary.
1313

1414
## Stage flow
@@ -19,13 +19,16 @@ intake ─ localize ─ spec ─▶ DRAFT PR ══ GATE 1 (maintainer) ══
1919
```
2020

2121
1. **intake** — `./dev pipeline init --run-id YYYY-MM-DD-NNN --request issue#N
22-
--branch <branch>`; `scripts/telemetry.py` run start. Restate the request;
22+
--branch <branch>`; `./dev telemetry start`. Restate the request;
2323
ambiguity that changes the contract → one clarifying question NOW (cheap
2424
here, a wrong PR later).
2525
2. **localize**`neohaskell-localizer` skill. Output: capability IDs +
2626
`touches:`/`files:`/`uses:` lists → `./dev pipeline set plan.touches …`
2727
etc. The plan is now BINDING: resume never re-plans; a wrong plan parks
2828
the run (`wrong-localization`) and re-enters from intake, visibly.
29+
As you consult each aid (alias, capability, signature), log it:
30+
`./dev telemetry consult --asset <kind>:<name>` (e.g. `alias:http-transport`)
31+
— this feeds the miner's PRUNE of never-consulted assets.
2932
3. **spec** — copy `docs/changes/TEMPLATE.md``NNN-slug.md` (next 3-digit
3033
number). Contract delta in signatures vocabulary; criteria C1…Cn each
3134
naming its proving test AND level (`unit|integration|acceptance` — a
@@ -56,7 +59,9 @@ intake ─ localize ─ spec ─▶ DRAFT PR ══ GATE 1 (maintainer) ══
5659
hspec-discovered).
5760
8. **implement**`neohaskell-implementer` skill; repair loop via
5861
`./dev check` (never `cabal build` in the loop); max 2 repair rounds per
59-
error, then the failure policy below.
62+
error, then the failure policy below. Log the aids you actually use
63+
(`./dev telemetry consult --asset hot-card:Text.toLower`,
64+
`phrasebook:task-validation`, …) so never-consulted assets surface as PRUNE.
6065
9. **verify** — in order, no skipping:
6166
a. criteria tests green at their DECLARED levels (`./dev test "<pattern>" <suite>`)
6267
b. targeted regression: `./dev spec-check --plan <spec>`
@@ -69,7 +74,7 @@ intake ─ localize ─ spec ─▶ DRAFT PR ══ GATE 1 (maintainer) ══
6974
11. **ci** — watch checks; bot comments triaged (fix real findings; push
7075
back with evidence on wrong ones). Merge is the maintainer's.
7176

72-
After merge: `scripts/telemetry.py` finish (outcome `ok`), golden archive
77+
After merge: `./dev telemetry finish` (outcome `ok`), golden archive
7378
(`telemetry/golden/<run_id>/`: request.md, spec.md, final.diff, verdict.md,
7479
transcript.md).
7580

@@ -91,10 +96,19 @@ pipeline succeeding at honesty, not failing at work. Labels are the closed
9196
taxonomy (SCHEMA.md); `other` requires `failure_note` and a weekly-review
9297
reclassification.
9398

99+
**Closing a non-`ok` run carries the class-fix (Phase 6, enforced).**
100+
`./dev telemetry finish --outcome {parked,failed} --failure-label <l>
101+
--asset-delta <type>:<destination>` — the delta is the fix for the *class*
102+
(a new alias, phrasebook entry, hlint rule, hook, hot-card line…) that ships
103+
alongside the retry. `none:<reason>` is the honest escape when no asset applies;
104+
the emitter refuses to close a failed/parked run without one.
105+
94106
## Resume contract
95107

96108
`./dev pipeline status` → resume at the recorded stage with the recorded
97109
plan. Never re-derive `touches:`/`files:`/`uses:` on resume. If reality
98110
contradicts the plan (file moved, API changed under you), park with
99111
`wrong-localization` — the asset fix (alias, capability, extension point)
100-
ships with the retry, per the failure→asset-delta protocol (Phase 6).
112+
ships with the retry, per the failure→asset-delta protocol (ADR-0068). Log the
113+
aids you consult while working (`./dev telemetry consult --asset
114+
<kind>:<name>`) so the weekly miner can PRUNE what nothing uses.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
name: neohaskell-retrospective-miner
3+
description: Weekly retrospective mining of pipeline telemetry into ≤5 validated asset-delta recommendations. Use at the weekly telemetry review, once real runs have accumulated in telemetry/runs.jsonl. Reads the deterministic digest, proposes recommendations under a closed contract, and records the ones that pass.
4+
---
5+
6+
# Retrospective miner (weekly learning loop)
7+
8+
The judgement half of Phase 6's learning loop (ADR-0068). The deterministic half —
9+
gathering runs, computing the digest, and enforcing the recommendation contract —
10+
is `./dev retrospect`; this skill decides *what to recommend*. **Activation
11+
gate:** run this only once `telemetry/runs.jsonl` holds real runs (it has one
12+
dummy line today); with no data there is nothing to mine.
13+
14+
## Procedure
15+
16+
1. **Digest.** `./dev retrospect --since <YYYY-MM-DD of the week>` → the markdown
17+
digest (outcomes, failure labels, stage times, invented-API trend vs the
18+
first-5-runs baseline, waiting-on-human, assets-consulted frequency, bench
19+
pointer). This is your evidence base — do not recall, transcribe.
20+
2. **Find friction.** From the digest + the week's golden stage summaries
21+
(`telemetry/golden/<run_id>/`), name concrete friction: an invented API that
22+
recurred, a stage repeatedly over its time-box, a failure label clustering, an
23+
asset nothing consulted.
24+
3. **Propose ≤5 recommendations**, each under the closed contract:
25+
`{week, friction, evidence:[run:stage,…], delta_type, destination, estimated_saving_s}`.
26+
4. **Record the survivors.** `./dev retrospect --record '<json>'` per recommendation
27+
— the harness re-checks the contract and appends to
28+
`telemetry/recommendations.jsonl`, or rejects with the failed rule. Do not
29+
hand-edit that file.
30+
31+
## The contract (the harness enforces all of this — internalize it, don't fight it)
32+
33+
- **Cited friction + measured cost (rule a):** `friction` is a real observation;
34+
`estimated_saving_s` is a positive, measured number. No counterfactual "this
35+
might help" — those are hypotheses, not findings, and are discarded.
36+
- **≥2 independent runs (rule b):** `evidence` must cite the same friction across
37+
**two or more distinct run_ids**. One-off friction is noise; act on recurrence.
38+
- **Closed taxonomy (rule c):** `delta_type` ∈ `alias | extension-point |
39+
phrasebook | hot-card | hlint-rule | hook | cli-utility | skill-edit |
40+
telemetry-label | PRUNE | none`. A proposal that needs a *new* asset kind or a
41+
new CLI utility is an ontology change — surface it to the maintainer in the
42+
review, never `--record` it.
43+
- **Validation over time (rule d):** an implemented delta is `validated` against
44+
its claimed metric over subsequent weeks; no movement makes it a `PRUNE`
45+
candidate in turn. The weekly review flips `status`/`validated` with
46+
`./dev retrospect --set '<week>' '<friction-substr>' --status implemented|pruned
47+
[--validated true|false]` — the sanctioned rewriter, so the file stays
48+
append-plus-gated-edit, never hand-edited.
49+
- **Removal is first-class (rule e):** the digest's "assets consulted" frequency
50+
plus the asset inventory surface never-consulted assets — recommend `PRUNE`.
51+
52+
## Destinations (delta_type → the real file the fix lands in)
53+
54+
`alias``codemap/capabilities.yaml` · `extension-point``codemap/extension-points.yaml`
55+
· `phrasebook``codemap/phrasebook.md` · `hot-card``codemap/api-hot.md`
56+
· `hlint-rule``.hlint.yaml` (skill: `neohaskell-dialect-rules`) · `hook`
57+
`.claude/hooks/*` · `cli-utility``scripts/*` (+ a `./dev` verb) · `skill-edit`
58+
`.claude/skills/*/SKILL.md` · `telemetry-label``telemetry/SCHEMA.md` (a
59+
schema bump) · `PRUNE` → delete the unused asset · `none` → the `destination`
60+
field carries the justification.
61+
62+
## Output of the weekly review
63+
64+
The review picks **1–2** recorded recommendations to implement that week (the
65+
maintainer's call); the rest stay `proposed` for re-evaluation. The instance-fix
66+
and the class-fix ship together (`./dev telemetry finish --asset-delta` already
67+
recorded the class-fix for each failed run this week — the miner generalizes
68+
across runs, it does not replace the per-run delta).

.gitattributes

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11

22
# Use bd merge for beads JSONL files
33
.beads/issues.jsonl merge=beads
4+
5+
# Append-only pipeline ledgers: parallel worktree pipelines append concurrently,
6+
# so union-merge (a git built-in — no driver install needed) keeps both sides'
7+
# lines instead of conflicting. run_id uniqueness is guarded in telemetry.py start.
8+
telemetry/runs.jsonl merge=union
9+
telemetry/recommendations.jsonl merge=union

.github/workflows/checks.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ jobs:
8282
env:
8383
BASE_REF: ${{ github.base_ref }}
8484
run: ./dev spec-check --reviews-pr "origin/${BASE_REF}"
85+
# Changelog gate: at PR-ready, every changed spec must have a CHANGELOG.md
86+
# entry (breaking ones with a migration note) — the changelog is generated,
87+
# not hand-written (Phase 6, task 3).
88+
- name: Changelog entries present (PR-ready only)
89+
if: github.event_name == 'pull_request' && github.event.pull_request.draft == false
90+
env:
91+
BASE_REF: ${{ github.base_ref }}
92+
run: ./dev changelog --check "origin/${BASE_REF}"
93+
# DoD criterion-test gate: at PR-ready, every criterion in a changed spec
94+
# must name a proving test that RESOLVES (a real .hurl or *.hs spec module)
95+
# — a criterion can't cite a test that was never written (Phase 6, task 1).
96+
- name: Criterion proving-tests resolve (PR-ready only)
97+
if: github.event_name == 'pull_request' && github.event.pull_request.draft == false
98+
env:
99+
BASE_REF: ${{ github.base_ref }}
100+
run: ./dev spec-check --criteria-tests "origin/${BASE_REF}"
85101

86102
expectations:
87103
name: expectation guard (test-contract diff)

.github/workflows/claude.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ jobs:
1818
# author-association check makes that signal unforgeable: comments from
1919
# anyone who is not an org OWNER/MEMBER are ignored entirely (which also
2020
# closes the standing hole of any passer-by invoking the action).
21+
# A `/revert` comment is the kill switch (revert.yml), NOT a @claude trigger —
22+
# excluded here so the two workflows stay disjoint even for `/revert @claude`.
2123
if: |
2224
(
23-
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
25+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && !startsWith(github.event.comment.body, '/revert')) ||
2426
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
2527
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
2628
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))

.github/workflows/dod.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Definition-of-done gate (pipeline Phase 6, task 1) — POST-MERGE, NOTIFY-ONLY.
2+
#
3+
# Watches the post-merge suites on `main`: both "Test" (Linux: doctest + all
4+
# nhcore suites incl. the acceptance test-hurl job) and "Test macOS" (Darwin
5+
# subset). If EITHER concludes `failure` on a push to `main`, the merge is
6+
# flagged as a **revert-candidate** and a notification is posted. NB: both
7+
# workflows carry a `paths:` filter, so a pure-tooling merge (only scripts/,
8+
# docs/, telemetry/, .claude/) triggers neither and this gate correctly no-ops
9+
# for it — there is no compiled surface to regress.
10+
# It NEVER auto-reverts (ADR-0068); the kill switch (`/revert`, revert.yml) is a
11+
# separate, deliberate maintainer action.
12+
name: dod
13+
14+
on:
15+
workflow_run:
16+
workflows: ["Test", "Test macOS"]
17+
types: [completed]
18+
19+
permissions:
20+
contents: read
21+
pull-requests: write
22+
issues: write
23+
24+
jobs:
25+
flag:
26+
if: >-
27+
github.event.workflow_run.event == 'push' &&
28+
github.event.workflow_run.head_branch == 'main' &&
29+
github.event.workflow_run.conclusion == 'failure'
30+
runs-on: ubuntu-latest
31+
timeout-minutes: 5
32+
steps:
33+
- name: Flag the merge as a revert-candidate
34+
env:
35+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+
REPO: ${{ github.repository }}
37+
SHA: ${{ github.event.workflow_run.head_sha }}
38+
WORKFLOW: ${{ github.event.workflow_run.name }}
39+
RUN_URL: ${{ github.event.workflow_run.html_url }}
40+
run: |
41+
set -euo pipefail
42+
# Resolve the PR that produced this commit from the commit→PRs association
43+
# (exact), falling back to the last "#NNN" in the squash subject.
44+
NUM=$(gh api "repos/${REPO}/commits/${SHA}/pulls" -q '.[0].number' 2>/dev/null || true)
45+
if [ -z "${NUM}" ]; then
46+
MSG=$(gh api "repos/${REPO}/commits/${SHA}" -q '.commit.message' | head -1)
47+
NUM=$(printf '%s' "$MSG" | grep -oE '#[0-9]+' | tail -1 | tr -d '#' || true)
48+
fi
49+
BODY=$(printf '%s' "⚠️ **DoD gate**: the \`${WORKFLOW}\` suite failed on \`main\` after this merged (\`${SHA}\`) — the change is not green at its declared levels. Marked **revert-candidate**. Verify the criteria, or trigger the kill switch by commenting \`/revert\`. Notify-only — nothing was reverted. Run: ${RUN_URL}")
50+
if [ -n "${NUM}" ]; then
51+
gh label create revert-candidate --color B60205 --description "Post-merge DoD suite failure; candidate for /revert" 2>/dev/null || true
52+
gh pr edit "${NUM}" --add-label revert-candidate || echo "note: could not label PR #${NUM}"
53+
gh pr comment "${NUM}" --body "${BODY}"
54+
else
55+
gh issue create --title "DoD gate failed on main (${SHA})" --body "${BODY}"
56+
fi

.github/workflows/revert.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Kill switch (pipeline Phase 6, task 2; ADR-0068) — MAINTAINER-TRIGGERED.
2+
#
3+
# A maintainer comments `/revert` on a MERGED PR → this opens a revert PR (it
4+
# never merges it). The author-association gate mirrors claude.yml: a comment
5+
# from anyone who is not an org OWNER/MEMBER is ignored entirely, so the ability
6+
# to trigger a revert is unforgeable. NeoHaskell is org-owned, so OWNER/MEMBER is
7+
# the maintainer set (COLLABORATOR = outside collaborators, deliberately excluded).
8+
# The trigger is an exact `/revert` (or `/revert <args>`) — not a `/revert…`
9+
# prefix — and claude.yml excludes `/revert` comments from its `@claude` handler,
10+
# so the two workflows are disjoint and never double-fire on one comment.
11+
name: revert
12+
13+
on:
14+
issue_comment:
15+
types: [created]
16+
17+
permissions:
18+
contents: write
19+
pull-requests: write
20+
21+
# One revert run per PR at a time — two quick `/revert` comments must not race
22+
# on creating the same revert branch.
23+
concurrency:
24+
group: revert-${{ github.event.issue.number }}
25+
cancel-in-progress: true
26+
27+
jobs:
28+
revert:
29+
if: >-
30+
github.event.issue.pull_request &&
31+
(github.event.comment.body == '/revert' || startsWith(github.event.comment.body, '/revert ')) &&
32+
contains(fromJSON('["OWNER", "MEMBER"]'), github.event.comment.author_association)
33+
runs-on: ubuntu-latest
34+
timeout-minutes: 10
35+
steps:
36+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
37+
with:
38+
# full history so `git revert` can resolve the merge commit
39+
fetch-depth: 0
40+
# don't leave the token in .git/config for every later step; the push
41+
# below authenticates via an explicit tokenized remote instead
42+
persist-credentials: false
43+
- name: Open a revert PR for the merged change
44+
env:
45+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46+
REPO: ${{ github.repository }}
47+
PR: ${{ github.event.issue.number }}
48+
BASE: ${{ github.event.repository.default_branch }}
49+
run: |
50+
set -euo pipefail
51+
SHA=$(gh pr view "${PR}" --json mergeCommit -q '.mergeCommit.oid')
52+
if [ -z "${SHA}" ] || [ "${SHA}" = "null" ]; then
53+
gh pr comment "${PR}" --body "revert: PR #${PR} is not merged — nothing to revert."
54+
exit 0
55+
fi
56+
# explicit push auth (persist-credentials:false above) — scoped to this job
57+
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"
58+
git config user.name "github-actions[bot]"
59+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
60+
./dev revert "${SHA}" --base "${BASE}"

0 commit comments

Comments
 (0)