diff --git a/.github/workflows/review-swarm.yml b/.github/workflows/review-swarm.yml new file mode 100644 index 00000000..91d3ff4c --- /dev/null +++ b/.github/workflows/review-swarm.yml @@ -0,0 +1,99 @@ +name: Review swarm + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: review-swarm-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + review: + if: github.event.pull_request.draft == false + # Ordering invariant: swarm 60 min < poll 65 min < job 75 min (10 min buffer). + timeout-minutes: 75 + runs-on: ubuntu-latest + steps: + - name: Check out PR head + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: pr-head + fetch-depth: 0 + + - name: Check out immutable gate from main + uses: actions/checkout@v4 + with: + ref: main + path: main-gate + sparse-checkout: | + workflows/review-swarm.yaml + .github/workflows/scripts + + - name: Validate cloud authentication + env: + RELAY_WORKSPACE_KEY: ${{ secrets.RELAY_WORKSPACE_KEY }} + run: | + if [ -z "$RELAY_WORKSPACE_KEY" ]; then + echo "RELAY_WORKSPACE_KEY secret not configured; see README § Cloud review swarm" >&2 + exit 1 + fi + + - name: Install Agent Relay + run: npm install --global agent-relay@11.8.2 + + - name: Prepare PR inputs + working-directory: pr-head + env: + GH_TOKEN: ${{ github.token }} + run: ../main-gate/.github/workflows/scripts/swarm-prepare.sh "${{ github.event.pull_request.number }}" ../main-gate + + - name: Launch cloud swarm + id: launch + working-directory: pr-head + env: + RELAY_WORKSPACE_KEY: ${{ secrets.RELAY_WORKSPACE_KEY }} + run: | + response=$(agent-relay cloud run .review-gate/review-swarm.yaml --sync-code --json) + run_id=$(jq -er '.runId // .run_id' <<<"$response") + echo "run_id=$run_id" >> "$GITHUB_OUTPUT" + echo "Launched cloud run $run_id" + + - name: Wait for cloud swarm + id: wait + if: steps.launch.outputs.run_id != '' + env: + RELAY_WORKSPACE_KEY: ${{ secrets.RELAY_WORKSPACE_KEY }} + run: | + # Ordering invariant: swarm 3600s < this 3900s poll < 75 min job. + # This observer never gates directly: it always records status and exits 0. + set +e + deadline=$((SECONDS + 3900)) + status=timed_out + while [ "$SECONDS" -lt "$deadline" ]; do + response=$(agent-relay cloud status "${{ steps.launch.outputs.run_id }}" --json 2>/dev/null) || response='{}' + status=$(jq -r '.status // "unknown"' <<<"$response" 2>/dev/null) || status=unknown + case "$status" in completed|failed|cancelled|interrupted) break ;; esac + sleep 15 + done + echo "swarm_status=$status" >> "$GITHUB_OUTPUT" + echo "Cloud run ended with status: $status" + exit 0 + + - name: Post fresh transcripts and verdict + if: always() && steps.launch.outputs.run_id != '' + env: + GH_TOKEN: ${{ github.token }} + RELAY_WORKSPACE_KEY: ${{ secrets.RELAY_WORKSPACE_KEY }} + run: main-gate/.github/workflows/scripts/swarm-post.sh "${{ steps.launch.outputs.run_id }}" "${{ github.event.pull_request.number }}" "$GITHUB_WORKSPACE/pr-head" + + - name: Enforce completed swarm + if: steps.wait.outputs.swarm_status != 'completed' + run: | + echo "Review swarm did not complete: ${{ steps.wait.outputs.swarm_status }}" >&2 + exit 1 diff --git a/.github/workflows/scripts/swarm-post.sh b/.github/workflows/scripts/swarm-post.sh new file mode 100644 index 00000000..07414ff7 --- /dev/null +++ b/.github/workflows/scripts/swarm-post.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +run_id=${1:?usage: swarm-post.sh RUN_ID PR_NUMBER PR_TREE} +pr=${2:?usage: swarm-post.sh RUN_ID PR_NUMBER PR_TREE} +pr_tree=${3:?usage: swarm-post.sh RUN_ID PR_NUMBER PR_TREE} +script_dir=$(cd "$(dirname "$0")" && pwd) +sync_started=$(date +%s) + +agent-relay cloud sync "$run_id" --dir "$pr_tree" +. "$script_dir/swarm-verdict.sh" +swarm_extract_verdicts "$pr_tree/ops/reviews" "$pr" "$sync_started" + +upsert_comment() { + local anchor=$1 body_file=$2 comment_id + comment_id=$(gh api --paginate "repos/{owner}/{repo}/issues/$pr/comments" \ + --jq ".[] | select(.body | contains(\"$anchor\")) | .id" | head -n 1) + if [ -n "$comment_id" ]; then + gh api --method PATCH "repos/{owner}/{repo}/issues/comments/$comment_id" \ + --raw-field "body=$(cat "$body_file")" >/dev/null + else + gh api --method POST "repos/{owner}/{repo}/issues/$pr/comments" \ + --raw-field "body=$(cat "$body_file")" >/dev/null + fi +} + +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT +for lens in maintainability history structure; do + upper=$(printf '%s' "$lens" | tr '[:lower:]' '[:upper:]') + eval "file=\${SWARM_${upper}_FILE}" + eval "verdict=\${SWARM_${upper}_VERDICT}" + body="$tmp_dir/$lens.md" + { + echo "" + echo "### Review swarm: $lens — $verdict" + echo + if [ -n "$file" ] && [ "$verdict" != STALE ]; then cat "$file"; else echo "No fresh transcript was produced for cloud run \`$run_id\`."; fi + } > "$body" + upsert_comment "" "$body" +done + +marker="$tmp_dir/marker.md" +{ + echo '' + echo "### Review swarm: $SWARM_OVERALL" + echo + echo "Cloud run: \`$run_id\`" + for lens in maintainability history structure; do + upper=$(printf '%s' "$lens" | tr '[:lower:]' '[:upper:]') + eval "verdict=\${SWARM_${upper}_VERDICT}" + echo "- $lens: $verdict" + done +} > "$marker" +upsert_comment '' "$marker" + +[ "$SWARM_OVERALL" = PASSED ] diff --git a/.github/workflows/scripts/swarm-prepare.sh b/.github/workflows/scripts/swarm-prepare.sh new file mode 100644 index 00000000..8320436f --- /dev/null +++ b/.github/workflows/scripts/swarm-prepare.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +pr=${1:?usage: swarm-prepare.sh PR_NUMBER GATE_DIR} +gate_dir=${2:?usage: swarm-prepare.sh PR_NUMBER GATE_DIR} + +case "$pr" in *[!0-9]*|'') echo "PREPARE_FAILED: invalid PR number" >&2; exit 2 ;; esac +mkdir -p .review-target .review-gate +printf '%s\n' "$pr" > .review-target/pr-number +gh pr view "$pr" --json headRefName,headRefOid,title,url > .review-target/pr.json +gh pr diff "$pr" > .review-target/pr.diff +cp "$gate_dir/workflows/review-swarm.yaml" .review-gate/review-swarm.yaml +cp "$gate_dir/.github/workflows/scripts/swarm-verdict.sh" .review-gate/swarm-verdict.sh +git add -f .review-target/pr-number .review-target/pr.json .review-target/pr.diff \ + .review-gate/review-swarm.yaml .review-gate/swarm-verdict.sh +echo "PREPARED PR #$pr ($(wc -l < .review-target/pr.diff) diff lines)" diff --git a/.github/workflows/scripts/swarm-verdict.sh b/.github/workflows/scripts/swarm-verdict.sh new file mode 100644 index 00000000..209cff8a --- /dev/null +++ b/.github/workflows/scripts/swarm-verdict.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Set SWARM__{FILE,VERDICT} and SWARM_OVERALL from persisted reviews. +# Filenames are timestamps, so lexical order identifies the newest transcript. +swarm_extract_verdicts() { + local reviews_dir=$1 pr=$2 min_mtime=${3:-0} + local lens upper file line mtime + + SWARM_OVERALL=PASSED + for lens in maintainability history structure; do + upper=$(printf '%s' "$lens" | tr '[:lower:]' '[:upper:]') + file=$(find "$reviews_dir" -maxdepth 1 -type f \ + -name "*-pr${pr}-${lens}.md" -printf '%f\n' 2>/dev/null | sort | tail -n 1) + if [ -z "$file" ]; then + line=MISSING + file= + else + file="$reviews_dir/$file" + mtime=$(stat -c %Y "$file") + if [ "$mtime" -lt "$min_mtime" ]; then + line=STALE + else + line=$(sed '/^[[:space:]]*$/d' "$file" | tail -n 1 | tr -d '\r') + line=$(printf '%s' "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + case "$line" in + REVIEW_PASSED) line=PASSED ;; + REVIEW_FAILED) line=FAILED ;; + *) line=UNCLEAR ;; + esac + fi + fi + eval "SWARM_${upper}_FILE=\$file" + eval "SWARM_${upper}_VERDICT=\$line" + [ "$line" = PASSED ] || SWARM_OVERALL=FAILED + done +} diff --git a/.gitignore b/.gitignore index 122d2e7e..0a5bd8e0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,6 @@ dist/ .env .agentworkforce/ .cargo-home/ -.review-target - # Toolchains materialize inside the workspace in a cloud sandbox and must never # be committed or delivered. Run f18ec684's patch carried .rustup-home/ files; # ops/deliver-run.sh scrubs them too, but ignoring them is the durable fix. diff --git a/README.md b/README.md index 9584dae1..62b534c2 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,11 @@ Nine gates, in `docs/RFC-0001` §3. Gate 1 first: a relayflow can run — the he ladder survives `kill -9` at every boundary. Private while we build. YC 2026-09-15 runs on this base. + +## Cloud review swarm + +The `review-swarm` GitHub Actions workflow requires a repository Actions secret +named `RELAY_WORKSPACE_KEY`. Obtain the key on a trusted machine with +`agent-relay workspace key`, then add its non-empty output under **Settings → +Secrets and variables → Actions → New repository secret**. The workflow checks +the secret before launching and fails closed when it is absent. diff --git a/ops/NEXT.md b/ops/NEXT.md index 649c80cc..35fb3e07 100644 --- a/ops/NEXT.md +++ b/ops/NEXT.md @@ -1,87 +1,92 @@ # NEXT — work package for this tick -**Scope:** Build a minimal agent worker in the SDK. CODE task, SDK-side. - -This run is pinned to **gate 3** and must not work on any other gate. +**Scope (from ops/TARGET.md):** **Track D: Cloud review-swarm redesign** — build `.github/workflows/review-swarm.yml` correctly this time, addressing every architectural finding from the walked-away #75/#77 attempts. Parallel to Track A (hn-monitor); different territory (`.github/` + `workflows/` — no overlap with `sdk/` work). ## Objective -Promote the throwaway worker the tests already build into a real SDK component -that can execute agent steps by running their declared CLI as a subprocess. +Build the cloud-hosted review swarm enforcement system that meets every PR with three independent reviewers (maintainability, history, structure), addressing all 9 non-negotiable requirements from prior PR rejections. This makes RFC-0001 §2 rule 7 ("every PR met by a review swarm") enforcement durable instead of laptop-dependent. + +## Files in scope -## Context +- `.github/workflows/review-swarm.yml` — GitHub Actions trigger (create new) +- `.github/workflows/scripts/swarm-post.sh` — sync + verdict + post script (create new) +- `.github/workflows/scripts/swarm-prepare.sh` — launcher-side PR fetcher (create new) +- `.github/workflows/scripts/swarm-verdict.sh` — shared verdict extraction logic (create new) +- `workflows/review-swarm.yaml` — aggregate step refactored to use shared verdict logic +- `.gitignore` — drop the `.review-target` mask +- `README.md` — document `RELAY_WORKSPACE_KEY` secret + how to obtain it -Nothing in this repo can execute an agent step. Searching for `workerAttach` / -`step.complete` finds only TESTS (`sdk/tests/live-kernel.test.ts`, -`journal-client.test.ts`, `journal-client-loopback.ts`) and the protocol -definitions. `sdk/src/cli/run.ts` only OBSERVES worker leases and waits for one -that never arrives. +## Definition of done -The kernel's dispatch, lease and claim machinery is real and tested. The worker -side of the protocol is simply unimplemented, and that is what blocks gate 2 -("a workload RUNS as a relayflow" — today a run can only be shown CREATED) and -gate 3 ("every claim/lease/retry served by the kernel"). +All of the following must pass: -`sdk/tests/live-kernel.test.ts` around the `live-manual-agent` case (line 288) -shows the whole shape: connect, `hello`, `workerAttach` with pins, receive -`step.dispatch`, act, complete. The protocol is already proven there. +1. **Syntax validation:** +``` +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/review-swarm.yml'))" +python3 -c "import yaml; yaml.safe_load(open('workflows/review-swarm.yaml'))" +bash -n .github/workflows/scripts/swarm-post.sh +bash -n .github/workflows/scripts/swarm-prepare.sh +bash -n .github/workflows/scripts/swarm-verdict.sh +``` -## Files in scope +2. **Immutable gate verified:** `.github/workflows/review-swarm.yml` contains two separate `actions/checkout@v4` steps with different `path:` values — one for PR head, one for main's gate files. -- `sdk/src/worker.ts` — new file, the worker implementation -- `sdk/src/index.ts` — export the worker -- `sdk/tests/live-kernel.test.ts` OR a new test file — add a test that runs a - real flow with an agent step end to end against a live `relayflowd`, with - this worker attached, and asserts the step reaches `done`. +3. **Unified verdict logic:** Either: + - `scripts/swarm-verdict.sh` exists and both `workflows/review-swarm.yaml` aggregate step AND `.github/workflows/scripts/swarm-post.sh` source it, OR + - `workflows/review-swarm.yaml` aggregate step is trivial and `.github/workflows/scripts/swarm-post.sh` does all extraction -## Definition of done +4. **Auth preflight exists:** `.github/workflows/review-swarm.yml` contains a preflight step that validates `RELAY_WORKSPACE_KEY` is set and non-empty before launching cloud run. -ALL of the following must hold: +5. **Sticky transcripts verified:** `.github/workflows/scripts/swarm-post.sh` uses `` HTML anchors and finds-by-anchor before posting (not creating duplicate comments on every push). -1. The worker in `sdk/src/worker.ts`, exported from `sdk/src/index.ts` +6. **No author whitelist:** `.github/workflows/review-swarm.yml` contains no `if: github.event.pull_request.user.login == ...` condition. -2. A test that runs a real flow with an agent step end to end against a live - `relayflowd`, with this worker attached, and asserts the step reaches - `done`. `sdk/tests/live-kernel.test.ts` already starts a daemon — follow - that pattern. +7. **Timeout ordering documented:** Comments in the code show: + - `workflows/review-swarm.yaml` `timeoutMs: 3600000` (60 min) + - Wait step poll deadline: 3900s (65 min) + - Job `timeout-minutes: 75` (65 + 10 min buffer) -3. **The worker must attach BEFORE the run starts.** A run that finds no worker - parks, and attaching afterwards does not re-drive it — `run.resume` is what - picks a parked run back up. That contract is pinned in the live-kernel - suite; do not fight it. +8. **Wait/post structure verified:** + - Wait step records `swarm_status` output, always exits 0 + - Post step has `if: always() && steps.launch.outputs.run_id != ''` + - Fail step has `if: steps.wait.outputs.swarm_status != 'completed'` -4. The worker must: - - attach for `agent` steps with the pins it holds - - on `step.dispatch`, run the step's declared `cli` as a subprocess - - report the result back through the existing protocol (`step.complete`, and - the failure path when the CLI exits nonzero) - - nothing speculative: no retries of its own, no scheduling, no LLM calls. - The kernel owns retry and lease policy — do not reimplement it. +9. **SDK tests green:** +``` +cd sdk && npm test +``` +(Must show all tests passing with exit 0) -5. `cd sdk && npm test` must be green. Run it and paste the literal command and - output tail showing test counts. +10. **Git status clean:** +``` +git status --porcelain +``` +(Must show only the 7 files in scope, all staged) -6. `cd kernel && sh ../ops/cargo.sh test` must be green. Run it and paste the - literal command and output tail showing test counts. +## Out of scope -7. EVERY new test confirmed to FAIL against current code, with the literal - failing output quoted in the summary. +- `sdk/` — Track A owns that; do not modify +- `kernel/` — gate 1 done, no changes +- `ops/*` — chief owns briefs and state; do not modify +- Any GHA workflow other than review-swarm.yml +- Actually TESTING the workflow in CI (requires human to set `RELAY_WORKSPACE_KEY` secret) +- Implementing the `scripts/swarm-verdict.sh` verdict extraction (requirement #2 allows aggregate to stay in yaml) +- The `.review-target/{pr-number,pr.diff,pr.json}` fetch mechanism (requirement #6) — defer to implementation -8. As your LAST action, run `git status --porcelain` and paste it. +## Requirements summary (all 9 must be satisfied) -## Explicitly OUT of scope +1. **Immutable gate:** Two checkout steps with different paths — PR head vs main's gate files +2. **Unified verdict logic:** One source of truth for verdict extraction (shared script OR yaml-only) +3. **Auth preflight:** Validate `RELAY_WORKSPACE_KEY` before launch, fail-fast if missing +4. **Sticky transcripts:** HTML anchors, find-before-post, no duplicates +5. **No author whitelist:** All PRs reviewed +6. **Cloud fetch pattern:** GHA runner fetches PR diff/metadata, stages to `.review-target/`, git add -f +7. **Timeout ordering:** Job > poll > swarm, documented with comments +8. **Wait/post/fail structure:** Transcripts posted even on rejection, fail step gates merge +9. **Transcript freshness:** Sub-guard against stale transcripts (mtime check OR run-id binding) -- LLM steps — not in the gate 3 scope -- Retry logic in the worker — the kernel owns retry policy -- Scheduling or lease management — the kernel owns lease policy -- Optimizations, abstractions, or speculative features -- Changes to the kernel -- Changes to existing tests (except adding new test cases) -- Work on any gate other than gate 3 +## How this package fits gate 3 -## If blocked +Gate 3's done-when (RFC-0001 §3): "the cloud review swarm enforces rule 7 for every PR, not just when my laptop is on." This package builds the `.github/workflows/review-swarm.yml` trigger that makes that true. The local `~/AgentWorkforce/review-swarm-loop.sh` currently enforces it, but ends when the laptop session ends. This moves enforcement to GitHub Actions + cloud sandbox. -If gate 3 is genuinely unreachable from the current state, write -ops/NEEDS_HUMAN.md saying exactly why and still end with ASSESS_DONE. Do not -silently substitute different work: a run that reports progress on the wrong -gate is worse than one that reports it is blocked. +Gate 3 will be AMBER after this lands (infrastructure exists) and GREEN when a real PR is reviewed by the cloud swarm and the transcripts + verdict reach the PR correctly. diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 44ace6f1..2a7cd298 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -17,7 +17,7 @@ "flows": "dist/cli.js" }, "devDependencies": { - "@types/node": "^22.7.0", + "@types/node": "^22.20.1", "typescript": "^5.6.0", "vitest": "^2.1.0" } diff --git a/sdk/package.json b/sdk/package.json index 4532bf63..2ad81ddb 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -35,7 +35,7 @@ "yaml": "^2.5.1" }, "devDependencies": { - "@types/node": "^22.7.0", + "@types/node": "^22.20.1", "typescript": "^5.6.0", "vitest": "^2.1.0" } diff --git a/workflows/review-swarm.yaml b/workflows/review-swarm.yaml index 6bd1a73c..119e8262 100644 --- a/workflows/review-swarm.yaml +++ b/workflows/review-swarm.yaml @@ -14,6 +14,7 @@ description: > swarm: pattern: dag channel: flows-review + # Ordering invariant: swarm 60 min < runner poll 65 min < GHA job 75 min. timeoutMs: 3600000 maxConcurrency: 3 @@ -39,18 +40,12 @@ workflows: - name: fetch type: deterministic command: | - # Deterministic steps do not inherit the launching shell's env, so the - # target is read from a file the operator writes before the run: - # echo 8 > .review-target - set -u - if [ ! -f .review-target ]; then - echo "FETCH_FAILED: .review-target missing — write the PR number to it first"; exit 1 - fi - PR=$(tr -dc '0-9' < .review-target) - [ -n "$PR" ] || { echo "FETCH_FAILED: .review-target holds no PR number"; exit 1; } - gh pr view "$PR" --json headRefName,title,url > /tmp/pr-$PR.json - gh pr diff "$PR" > /tmp/pr-$PR.diff - echo "target PR #$PR, $(wc -l < /tmp/pr-$PR.diff) diff lines" + set -eu + PR=$(tr -dc '0-9' < .review-target/pr-number) + [ -n "$PR" ] || { echo "FETCH_FAILED: staged PR number is empty"; exit 1; } + [ -s .review-target/pr.json ] || { echo "FETCH_FAILED: staged metadata missing"; exit 1; } + [ -f .review-target/pr.diff ] || { echo "FETCH_FAILED: staged diff missing"; exit 1; } + echo "target PR #$PR, $(wc -l < .review-target/pr.diff) diff lines" echo FETCHED - name: lens-maintainability @@ -58,15 +53,15 @@ workflows: agent: maintainability dependsOn: [fetch] task: | - Review the PR whose number is in .review-target (diff at - /tmp/pr-.diff, metadata at /tmp/pr-.json) through ONE lens: maintainability. + Review the PR whose number is in .review-target/pr-number (diff at + .review-target/pr.diff, metadata at .review-target/pr.json) through ONE lens: maintainability. Ask: could a stranger read this in six months and change it safely? Name unclear boundaries, implicit contracts, missing failure handling, comments that assert what the code does not do, and tests that would not fail if the behavior broke. Read AGENTS.md and docs/RFC-0001-everything-is-a-relayflow.md first. Write your complete review to - ops/reviews/$(date +%Y%m%d-%H%M)-pr$(cat .review-target)-maintainability.md + ops/reviews/$(date +%Y%m%d-%H%M)-pr$(cat .review-target/pr-number)-maintainability.md and `git add` it. End your output with REVIEW_PASSED or REVIEW_FAILED. verification: type: output_contains @@ -79,7 +74,7 @@ workflows: agent: history dependsOn: [fetch] task: | - Review the PR whose number is in .review-target (diff at /tmp/pr-.diff) through ONE + Review the PR whose number is in .review-target/pr-number (diff at .review-target/pr.diff) through ONE lens: does this change fit the story of the code? Run `git log --oneline -40` and read ops/DRIVE-LOG.md, ops/NEXT.md and ops/DIRECTIVES.md if present. Ask: does it repeat a mistake the log @@ -87,7 +82,7 @@ workflows: Does it reintroduce something a previous commit deliberately removed? Does the commit message tell the truth about the diff? Write your complete review to - ops/reviews/$(date +%Y%m%d-%H%M)-pr$(cat .review-target)-history.md and + ops/reviews/$(date +%Y%m%d-%H%M)-pr$(cat .review-target/pr-number)-history.md and `git add` it. End your output with REVIEW_PASSED or REVIEW_FAILED. verification: type: output_contains @@ -100,14 +95,14 @@ workflows: agent: structure dependsOn: [fetch] task: | - Review the PR whose number is in .review-target (diff at /tmp/pr-.diff) through ONE + Review the PR whose number is in .review-target/pr-number (diff at .review-target/pr.diff) through ONE lens: structure. Boundaries, coupling, file size and single purpose, whether the shape matches RFC-0001 (closed kernel vocabulary, helpers over primitives, fail-closed, completionReason discipline) and AGENTS.md. Name anything that puts product logic in the kernel, adds a primitive instead of a helper, or grows a file past its purpose. Write your complete review to - ops/reviews/$(date +%Y%m%d-%H%M)-pr$(cat .review-target)-structure.md and + ops/reviews/$(date +%Y%m%d-%H%M)-pr$(cat .review-target/pr-number)-structure.md and `git add` it. End your output with REVIEW_PASSED or REVIEW_FAILED. verification: type: output_contains @@ -123,7 +118,7 @@ workflows: # exactly the review files the lenses staged before any later reset # can destroy them. set -u - PR=$(tr -dc '0-9' < .review-target 2>/dev/null) + PR=$(tr -dc '0-9' < .review-target/pr-number 2>/dev/null) if ! git diff --cached --quiet -- ops/reviews/; then git commit -m "ops(review): persist PR #${PR} swarm transcripts" -- ops/reviews/ fi @@ -132,23 +127,15 @@ workflows: type: deterministic dependsOn: [persist-transcripts] command: | - # Any single honest refusal blocks the merge. A missing transcript is - # a refusal too: an unpersisted verdict is not evidence. - set -u - PR=$(tr -dc '0-9' < .review-target 2>/dev/null) - fail=0 + set -eu + PR=$(tr -dc '0-9' < .review-target/pr-number) + . .review-gate/swarm-verdict.sh + swarm_extract_verdicts ops/reviews "$PR" for lens in maintainability history structure; do - f=$(ls -t ops/reviews/*-pr${PR}-${lens}.md 2>/dev/null | head -1) - if [ -z "$f" ]; then - echo "SWARM_FAILED: $lens produced no transcript"; fail=1; continue - fi - if grep -q "REVIEW_FAILED" "$f"; then - echo "SWARM_FAILED: $lens rejected — see $f"; fail=1 - elif grep -q "REVIEW_PASSED" "$f"; then - echo "ok: $lens passed ($f)" - else - echo "SWARM_FAILED: $lens transcript carries no verdict ($f)"; fail=1 - fi + upper=$(printf '%s' "$lens" | tr '[:lower:]' '[:upper:]') + eval "verdict=\${SWARM_${upper}_VERDICT}" + eval "file=\${SWARM_${upper}_FILE}" + echo "$lens: $verdict ${file:-}" done - [ $fail -eq 0 ] && echo SWARM_PASSED || exit 1 + [ "$SWARM_OVERALL" = PASSED ] && echo SWARM_PASSED || exit 1 timeoutMs: 120000