diff --git a/.agents/skills/pr-watch/SKILL.md b/.agents/skills/pr-watch/SKILL.md index c39a5254..b8e39f2f 100644 --- a/.agents/skills/pr-watch/SKILL.md +++ b/.agents/skills/pr-watch/SKILL.md @@ -5,13 +5,19 @@ description: Wait for a GitHub PR's checks to finish before merging or reporting # Waiting on a PR +"Watching a PR" means tracking **both** CI checks **and** GitHub comments/reviews. +A PR is not ready to merge just because checks are green — review feedback must be +read and answered first. + +## CI Checks + ``` -scripts/pr-watch [--expect ] [--timeout ] [--interval ] +scripts/pr-watch [--expect ] [--timeout ] [--interval ] [--reviews] ``` ``` -exit 0 every check COMPLETED, none failed, and MERGEABLE with a state of - CLEAN, UNSTABLE or HAS_HOOKS +exit 0 every check COMPLETED, none failed, MERGEABLE with a state of + CLEAN, UNSTABLE or HAS_HOOKS, and reviews fetched (if --reviews) exit 1 a check failed (under any mergeability, BLOCKED included), or the PR is not mergeable (CONFLICTING, DIRTY, BEHIND, DRAFT, or a state it does not know) @@ -21,21 +27,70 @@ exit 3 timed out with checks still running, mergeability still UNKNOWN, exit 4 usage or environment error exit 5 the PR is MERGED or CLOSED, at the head the watch pinned; it ends on the first poll that sees it closed, which may be mid-wait +exit 6 checks green/mergeable, but pr-reviews failed (fetch error, not "no reviews") ``` +**`--reviews`** (default): after checks complete, print all reviews and comments +via `scripts/pr-reviews`. Use `--no-reviews` to skip this. The reviews are shown +but do not change the exit code — answering them is your responsibility. + **Exit 0 now means mergeable, not only green.** GitHub computes mergeability lazily and reports `UNKNOWN` for a while after the base moves. It also reports `BLOCKED` for the moment before a required check that `needs:` another job has registered. The watch waits out both, so you do not need to re-check `mergeable` by hand after it. **Exit 3 is not a pass.** It may be a required -check that never ran. Why each -state counts the way it does: `context/code/tooling.md`, "pr-watch and -mergeability" (improvise-edpl). +check that never ran. Why each state counts the way it does: +`context/code/tooling.md`, "pr-watch and mergeability" (improvise-edpl). **Pass `--expect ` whenever you know which commit you are asking about.** It is the difference between "this PR is green" and "the thing I pushed is green", and a PR here was once merged at a head that predated its own fix. +## Reviews and Comments + +Reviews are a **separate API surface** from the comment thread. Codex/ChatGPT +reviews arrive as *reviews*, not comments — checking only the comment thread +misses them entirely. `pr-watch --reviews` (the default) shows all three: + +| Surface | What it is | +|---|---| +| `[REVIEW]` | The review's own body (pulls/{n}/reviews) | +| `[INLINE]` | A comment on a line of the diff (pulls/{n}/comments) | +| `[COMMENT]` | A comment on the PR thread (issues/{n}/comments) | + +To fetch reviews directly: `./scripts/pr-reviews ` — **do not pipe it**. The +script's `$?` must be read directly; piping or chaining loses the exit code. +Redirect to a file if you need to process it. + +### Review Obligations + +Before treating a PR as done or merging: + +1. **Read all review feedback** — reviews against an earlier head may still apply +2. **Answer substantive feedback with attribution** — every reply must include + the attribution line (see below) +3. **Re-check after each push** that new reviews target the current head SHA +4. **Don't wait indefinitely on Copilot/Codex** — reviews arrive as quota allows; + an absent review and an unrequested one look identical. The adversarial review + that gates a merge is dispatched by the orchestrator; external reviews are + read *in addition*, not instead. + +### GitHub Attribution (Hard Rule) + +**Every GitHub write must include an attribution line.** Posts go out under the +repository owner's account — an unattributed post reads as the human's words. + +Open the body with: `_Posted by ()_` + +Examples: +- `_Posted by Claude (Opus 4.5)_` +- `_Posted by Cursor Agent (claude-sonnet-5)_` + +A thread is not "answered" until the reply carries this attribution. + +See AGENTS.md "GitHub Attribution" for the full rule and "Shipping a wave" for +Copilot request mechanics and how to answer declined findings. + ## Do not write the loop yourself Four signals look like "finished" and are not: diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 52b0e9c2..dea08886 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -1,273 +1,62 @@ -#!/bin/bash +#!/usr/bin/env bash # Prepare a Claude Code on the web container for work on this repository. # -# Section 0 runs everywhere, cloud or not: it asserts that the `bd` on PATH -# is the one flake.nix pins. Everything after it is cloud-only setup. +# This hook delegates to the shared `scripts/agent-bootstrap.sh`, which works +# for any coding agent stack (Claude Code, Cursor, etc.). This file exists +# because Claude Code expects hooks at `.claude/hooks/session-start.sh`. # -# Three things are slow or wrong by default in a fresh cloud session: +# For local checkouts, only the bd pin assertion runs. +# For cloud sessions (CLAUDE_CODE_REMOTE=true), full bootstrap runs. # -# 1. `bd` is not installed, so the `bd prime` hook and every `bd` command -# in AGENTS.md silently do nothing. -# 2. `.beads/issues.jsonl` is a stale export. An agent that reads it picks -# work that is already done — this has happened. The authoritative -# tracker is the Dolt database on the git remote. -# 3. The Rust workspace is uncompiled, so the first `cargo test` pays a -# cold build before it reports anything. -# -# The tracker is re-synced on every session, not just when the database is -# missing: cloud containers reuse a cached workspace, so "already present" -# does not mean "still current", and a silently ageing tracker is the exact -# failure this hook exists to prevent. Installing bd and warming the build -# do check first, being genuinely one-time. -# -# Nothing here writes to a tracked file, stages anything, or creates a commit. +# See also: scripts/agent-bootstrap.sh (the stack-agnostic implementation) set -uo pipefail ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" -cd "$ROOT" || exit 0 +BOOTSTRAP="$ROOT/scripts/agent-bootstrap.sh" -log() { printf '[session-start] %s\n' "$*"; } - -# ── 0. The bd on PATH must be the bd flake.nix pins ──────────────────────── -# `flake.nix` pins beads so that one build of `bd` touches this repo. A git -# worktree does not load that pin — a fresh `.envrc` is unallowed, so a -# process there falls through to whatever `bd` the user's profile happens to -# carry. Today those agree; nothing enforces that they keep agreeing. -# -# The failure that matters is silent: writing .beads/issues.jsonl with a -# skewed build empties the dependency graph and reports success. Nobody -# notices until they ask what blocks what. So the mismatch is turned into a -# loud one here rather than trusted. Reading with a skewed bd is safe — it -# is the mutating commands that corrupt — so this blocks nothing, it shouts. -# -# The expected version is read from flake.nix, not written down twice: a -# hard-coded copy would go stale the first time the pin moves, and a stale -# assertion is worse than none. -beads_pin() { - sed -n 's|.*beads\.url[[:space:]]*=[[:space:]]*"github:[^/]*/[^/]*/v\{0,1\}\([^"]*\)".*|\1|p' \ - flake.nix 2>/dev/null | head -1 -} - -bd_running_version() { - bd version 2>/dev/null | sed -n 's/^bd version \([0-9][0-9.]*\).*/\1/p' | head -1 -} - -bd_pin_banner() { - { - printf '\n[session-start] ===== BEADS VERSION SKEW =====\n' - printf '[session-start] %s\n' "$@" - printf '[session-start] =============================\n\n' - } >&2 -} - -# Prints the banner and returns 1 when the two disagree, or when either one -# cannot be read — an unreadable answer is a rotted assertion, not a pass. -assert_bd_pin() { - command -v bd >/dev/null 2>&1 || return 0 # nothing on PATH to assert about - - pinned=$(beads_pin) - running=$(bd_running_version) - - case "$pinned" in - [0-9]*.[0-9]*.[0-9]*) ;; - *) - bd_pin_banner "cannot read the beads pin out of flake.nix" \ - "read: '${pinned:-}'" \ - "The pin moved to a ref this hook cannot parse (a branch or a" \ - "commit, perhaps). Teach beads_pin() to read it, or the check" \ - "silently protects nothing." - return 1 - ;; - esac - - if [ -z "$running" ]; then - bd_pin_banner "cannot read the version of the bd on PATH" \ - "bd: $(command -v bd)" \ - "'bd version' did not answer in the expected shape. Until it does," \ - "nothing here can tell whether that build matches the pin." - return 1 - fi - - [ "$running" = "$pinned" ] && return 0 - - bd_pin_banner "the bd on PATH is NOT the bd this repo pins" \ - "pinned (flake.nix): $pinned" \ - "running: $running ($(command -v bd))" \ - "" \ - "Reading is safe. Do NOT run a mutating bd command — create, update," \ - "close, dep, dolt push. Version skew between builds empties the" \ - "dependency graph in .beads/issues.jsonl and reports success." \ - "" \ - "Fix now: nix develop (in this directory), then re-run the" \ - " command; or 'direnv allow' here." \ - "Fix for good: worktrees are created fresh per dispatch, so allowing" \ - " them one at a time never catches the next one. Put the" \ - " repo root under [whitelist] prefix in" \ - " ~/.config/direnv/direnv.toml." - return 1 -} - -pin_status=0 -assert_bd_pin || pin_status=2 - -# Local checkouts have their own setup; this is for the web containers. -if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then - exit "$pin_status" -fi - -# Install a verified database at .beads/embeddeddolt. -# -# The staged copy lives on another filesystem, so it is brought alongside the -# live one FIRST. Every step after that is a same-filesystem rename, which -# either happens or does not — no half-finished copy. The previous database is -# removed only once the new one is in place, so no failure path leaves the repo -# without a tracker, and every move is checked: an unchecked one would let the -# new database land *inside* the old directory and still report success. -install_tracker() { - staged=$1 - live=.beads/embeddeddolt - incoming=$live.incoming - previous=$live.previous - - rm -rf "$incoming" "$previous" || return 1 - cp -rf "$staged" "$incoming" || { rm -rf "$incoming"; return 1; } - - if [ -d "$live" ] && ! mv "$live" "$previous"; then - rm -rf "$incoming" - return 1 - fi - - if mv "$incoming" "$live"; then - rm -rf "$previous" - return 0 - fi - - # Could not put the new one in place; restore what was there. - if [ -d "$previous" ] && ! mv "$previous" "$live"; then - log "ERROR: tracker left at $previous — move it back to $live by hand" - fi - rm -rf "$incoming" - return 1 -} - -# ── 1. bd on PATH ────────────────────────────────────────────────────────── -# The embedded Dolt engine needs CGO, so this builds from source (~3 min). -# The container image is cached after the hook, so it is paid once. -export PATH="$PATH:/root/go/bin:$HOME/go/bin" -# Pinned to a commit rather than `main`, and downloaded before it is run: -# piping a moving URL straight into a shell executes code nobody has seen, -# and leaves nothing to inspect afterwards. The installer stays at -# /tmp/bd-install.sh so what actually ran can be read. -# -# To take a newer beads, resolve the peeled commit of its tag: -# git ls-remote --tags https://github.com/steveyegge/beads | grep 'v1.3.0^{}' -BD_INSTALLER_REF=f45b249ce6b40ba62aecc03949e6371e8f7c79d8 # tag v1.3.0 -BD_INSTALLER_URL="https://raw.githubusercontent.com/steveyegge/beads/$BD_INSTALLER_REF/scripts/install.sh" - -if ! command -v bd >/dev/null 2>&1; then - log "installing bd (beads v1.3.0)…" - if ! curl -fsSL "$BD_INSTALLER_URL" -o /tmp/bd-install.sh 2>/tmp/bd-install.log; then - log "WARNING: could not download the bd installer; see /tmp/bd-install.log." - elif bash /tmp/bd-install.sh >>/tmp/bd-install.log 2>&1; then - log "bd installed" - else - log "WARNING: bd install failed; see /tmp/bd-install.log (the script that" - log " ran is at /tmp/bd-install.sh). Issue tracking unavailable." - fi -fi +# Forward the Claude env file variable to the generic name the bootstrap uses if [ -n "${CLAUDE_ENV_FILE:-}" ]; then - # Single quotes are deliberate: $PATH and $HOME must be written literally so - # they expand when the session sources this, not now. - # shellcheck disable=SC2016 - echo 'export PATH="$PATH:/root/go/bin:$HOME/go/bin"' >> "$CLAUDE_ENV_FILE" -fi -command -v bd >/dev/null 2>&1 && bd metrics off >/dev/null 2>&1 - -# bd may have arrived only just now, in which case §0 had nothing to check. -# Re-check, but only if §0 stayed quiet, so the banner is never printed twice. -if [ "$pin_status" -eq 0 ]; then - assert_bd_pin || pin_status=2 + export AGENT_ENV_FILE="$CLAUDE_ENV_FILE" fi -# ── 2. Sync the issue tracker from the Dolt remote ───────────────────────── -# `bd init` in-place rewrites .beads/config.yaml and commits, so the sync -# happens in a throwaway directory and only the database is moved back. -# That directory must be named after the database (see .beads/metadata.json), -# since bd derives the database name from it. -if command -v bd >/dev/null 2>&1; then - DB_NAME="$(jq -r '.dolt_database // "improvise"' .beads/metadata.json 2>/dev/null || echo improvise)" - ORIGIN="$(git remote get-url origin 2>/dev/null || true)" - case "$ORIGIN" in - git@github.com:*) ORIGIN="https://github.com/${ORIGIN#git@github.com:}" ;; - esac - ORIGIN="${ORIGIN%.git}" - - # The mktemp is part of the condition on purpose. Unchecked, a failure - # leaves TMP empty, "$TMP/$DB_NAME" becomes an absolute path at the - # filesystem root, and the cleanup at the end degrades to `rm -rf ""`. - # Short-circuiting also means no temp directory is created when there is - # no origin to sync from. - if [ -n "$ORIGIN" ] && TMP=$(mktemp -d) && [ -d "$TMP" ]; then - log "syncing issue tracker from $ORIGIN …" - STAGE="$TMP/$DB_NAME" - mkdir -p "$STAGE" - - # `bd init` exits non-zero when the remote is unreachable but STILL leaves - # a .beads/embeddeddolt behind — an empty database. Testing only for the - # directory would swap that over a good tracker, so check the status and - # confirm the result actually carries issues. An empty remote tracker is - # indistinguishable from a failed clone here; keeping what we already have - # is the safe way to be wrong. - sync_ok=0 - if ( - cd "$STAGE" && git init -q . && - BD_NON_INTERACTIVE=1 bd init --non-interactive --skip-agents --skip-hooks -q \ - --remote "git+$ORIGIN" - ) >/tmp/bd-sync.log 2>&1 && [ -d "$STAGE/.beads/embeddeddolt" ]; then - listing="$( (cd "$STAGE" && bd list --all 2>/dev/null) || true )" - case "$listing" in - "" | "No issues found."*) ;; - *) sync_ok=1 ;; - esac +# For local checkouts, only verify the bd pin (no install, no sync) +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + # Inline the pin-check only, since the full bootstrap is cloud-only + cd "$ROOT" || exit 0 + beads_pin() { + sed -n 's|.*beads\.url[[:space:]]*=[[:space:]]*"github:[^/]*/[^/]*/v\{0,1\}\([^"]*\)".*|\1|p' \ + flake.nix 2>/dev/null | head -1 + } + bd_running_version() { + bd version 2>/dev/null | sed -n 's/^bd version \([0-9][0-9.]*\).*/\1/p' | head -1 + } + if command -v bd >/dev/null 2>&1; then + pinned=$(beads_pin) + running=$(bd_running_version) + # Fail closed: if we can't parse either version, warn rather than silently pass + if [ -z "$pinned" ]; then + printf '[session-start] WARNING: cannot parse beads pin from flake.nix\n' >&2 + printf '[session-start] The pin check is skipped, but mutating bd commands may be unsafe.\n' >&2 + exit 2 fi - - if [ "$sync_ok" -eq 1 ] && install_tracker "$STAGE/.beads/embeddeddolt"; then - log "issue tracker synced — 'bd ready' now reflects the remote, not issues.jsonl" - elif [ -d .beads/embeddeddolt ]; then - log "WARNING: tracker sync failed (see /tmp/bd-sync.log); kept the previous database." - log " It may be out of date — verify any bead against the code." - else - log "WARNING: tracker sync failed; see /tmp/bd-sync.log." - log " .beads/issues.jsonl is a STALE export — verify any bead against the code." + if [ -z "$running" ]; then + printf '[session-start] WARNING: cannot parse bd version from "bd version"\n' >&2 + printf '[session-start] bd: %s\n' "$(command -v bd)" >&2 + printf '[session-start] The pin check is skipped, but mutating bd commands may be unsafe.\n' >&2 + exit 2 + fi + if [ "$running" != "$pinned" ]; then + printf '\n[session-start] ===== BEADS VERSION SKEW =====\n' >&2 + printf '[session-start] pinned (flake.nix): %s\n' "$pinned" >&2 + printf '[session-start] running: %s (%s)\n' "$running" "$(command -v bd)" >&2 + printf '[session-start] Reading is safe. Do NOT run mutating bd commands.\n' >&2 + printf '[session-start] Fix: nix develop, or direnv allow.\n' >&2 + printf '[session-start] =============================\n\n' >&2 + exit 2 fi - rm -rf "$TMP" - elif [ -n "$ORIGIN" ]; then - log "WARNING: could not create a temporary directory; skipped the tracker sync." - log " .beads/issues.jsonl is a STALE export — verify any bead against the code." fi + exit 0 fi -# ── 3. Warm the build so the first cargo test is not a cold compile ──────── -if command -v cargo >/dev/null 2>&1; then - log "warming cargo build…" - if cargo build --workspace --tests >/tmp/cargo-warm.log 2>&1; then - log "workspace built" - else - log "WARNING: cargo build failed; see /tmp/cargo-warm.log" - fi -fi - -# ── 4. Environment facts worth knowing up front ──────────────────────────── -cat <<'NOTES' -[session-start] Environment notes (see context/cloud-agents.md): - • Use the preinstalled cargo toolchain for ordinary work. `nix develop` - also works, but only after scripts/nix-seed-flake-inputs.sh — GitHub - tarballs are not served here, so the locked inputs are seeded over git. - • `bd dolt push` is blocked outbound (403). Bead status changes stay local. - • Never run a bare `bd init` in this repo: it rewrites AGENTS.md, CLAUDE.md - and .claude/settings.json, and commits. Use --skip-agents --skip-hooks. - • `bd dolt push ` ignores its argument and pushes to origin. -NOTES - -# Non-zero only when §0 found version skew; the setup above still ran. -exit "$pin_status" +# Cloud session: run the full bootstrap +exec "$BOOTSTRAP" diff --git a/.claude/skills/beads b/.claude/skills/beads new file mode 120000 index 00000000..f57add10 --- /dev/null +++ b/.claude/skills/beads @@ -0,0 +1 @@ +../../.agents/skills/beads \ No newline at end of file diff --git a/.cursor/environment.json b/.cursor/environment.json new file mode 100644 index 00000000..8f01ff31 --- /dev/null +++ b/.cursor/environment.json @@ -0,0 +1,5 @@ +{ + "name": "Improvise", + "install": "./scripts/agent-bootstrap.sh", + "start": "nix-daemon &" +} diff --git a/.cursor/skills/beads b/.cursor/skills/beads new file mode 120000 index 00000000..f57add10 --- /dev/null +++ b/.cursor/skills/beads @@ -0,0 +1 @@ +../../.agents/skills/beads \ No newline at end of file diff --git a/.cursor/skills/beads-merge b/.cursor/skills/beads-merge new file mode 120000 index 00000000..53a96ecc --- /dev/null +++ b/.cursor/skills/beads-merge @@ -0,0 +1 @@ +../../.agents/skills/beads-merge \ No newline at end of file diff --git a/.cursor/skills/code-review b/.cursor/skills/code-review new file mode 120000 index 00000000..2d88efdf --- /dev/null +++ b/.cursor/skills/code-review @@ -0,0 +1 @@ +../../.agents/skills/code-review \ No newline at end of file diff --git a/.cursor/skills/kb b/.cursor/skills/kb new file mode 120000 index 00000000..3ee2d209 --- /dev/null +++ b/.cursor/skills/kb @@ -0,0 +1 @@ +../../.agents/skills/kb \ No newline at end of file diff --git a/.cursor/skills/pr-watch b/.cursor/skills/pr-watch new file mode 120000 index 00000000..3ef371ce --- /dev/null +++ b/.cursor/skills/pr-watch @@ -0,0 +1 @@ +../../.agents/skills/pr-watch \ No newline at end of file diff --git a/.cursor/skills/wave b/.cursor/skills/wave new file mode 120000 index 00000000..0c2044da --- /dev/null +++ b/.cursor/skills/wave @@ -0,0 +1 @@ +../../.agents/skills/wave \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index a476b158..b5c5e18a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,8 +13,10 @@ force-push. The rule, the message convention and the incidents are `.agents/skills/wave/agent-rules.md`, rule 8. - always start responses with bananaS! -- there is a prolog MCP available: load a session from `kb` at the root of the repository at the beginning of - every session, and query it as you work. **Reading only** — the MCP must never write `kb.pl`. +- **Knowledge base: use `scripts/kb`, not Prolog MCP.** The Prolog MCP is optional and not + available in all agent stacks (Cursor cloud agents do not have it). All agents must use + `scripts/kb add|replace|rm|query|check` for KB operations — see the `kb` skill at + `.agents/skills/kb/SKILL.md`. Reading `prolog-sessions/kb.pl` directly is fine for queries. - save structured facts as you go — work done, interesting locations, important decisions — with `scripts/kb add` (see the `kb` skill). Write a fact when you learn it; there is no reason to batch. - query the knowledge base as needed and use Prolog reasoning when it makes sense. @@ -25,6 +27,87 @@ This project uses **bd** (beads) for issue tracking. Run `bd prime` for full workflow context. +## Multi-Stack Agent Support + +This repo supports multiple coding-agent stacks: Claude Code, Cursor (IDE and cloud), and +potentially others. All stacks share the same operational setup: + +| Component | Location | Notes | +|---|---|---| +| Toolchain | `nix develop` / `direnv` | **Required.** Rust from flake, wasm target, beads pin, trealla | +| Skills | `.agents/skills/` | Canonical source; `.claude/skills/` and `.cursor/skills/` symlink here | +| Session bootstrap | `scripts/agent-bootstrap.sh` | Seeds nix inputs, installs bd, syncs tracker, warms cargo | +| Knowledge base | `scripts/kb` | **All stacks use this**, not Prolog MCP | +| Issue tracker | `bd` CLI | Synced from Dolt remote at session start | + +### Toolchain Requirement + +**Agents must work inside `nix develop` (or direnv) for this repo.** Bare system cargo that +fails `edition2024` or missing wasm target is a misconfigured session. The bootstrap seeds +nix flake inputs automatically (GitHub tarballs are blocked in cloud), so `nix develop` +should work after install completes. + +Run commands inside the dev shell: +```bash +nix develop --command cargo test +nix develop --command cargo build --workspace +``` + +Or enter an interactive shell: `nix develop` + +**Claude Code**: `.claude/hooks/session-start.sh` calls the bootstrap automatically. + +**Cursor cloud**: `.cursor/environment.json` runs the bootstrap via `install`. Skills are +discovered from `.cursor/skills/` (symlinks to `.agents/skills/`). + +**Local agents**: Run `scripts/agent-bootstrap.sh` manually, or use `direnv allow` for +automatic dev shell activation. + +See `context/cloud-agents.md` for environment-specific notes (nix, network, etc.). + +### GitHub Attribution (Hard Rule) + +**Every GitHub write (PR, comment, review reply) must include an attribution line identifying +the AI agent.** Posts go out under the repository owner's account, and an unattributed post +reads as the human owner's words to everyone who sees it. + +**Format:** Open the body with an italicized attribution line: + +``` +_Posted by ()_ +``` + +Examples: +- `_Posted by Claude (Opus 4.5)_` +- `_Posted by Cursor Agent (claude-sonnet-5)_` +- `_Posted by Claude Code_` + +For PR bodies, the `🤖 Generated with …` footer that some tools add covers this. For +comments and review replies, add the line yourself — there is no automatic footer. + +**This applies to:** PR creation, PR updates, issue comments, review comments, and review +replies. A thread is not "answered" until the reply carries this attribution. + +### PR Tracking: Checks AND Reviews + +For any PR an agent owns or is working on, **track both CI checks and GitHub comments/reviews**. +The `pr-watch` skill (`.agents/skills/pr-watch/SKILL.md`) is the agent-facing recipe. + +**`scripts/pr-watch`** now prints reviews after checks are green (use `--no-reviews` to skip). +Reviews are a separate API surface from comments — Codex/ChatGPT reviews arrive as *reviews*, +not comments, and are easy to miss if you only read the comment thread. + +1. **Run `scripts/pr-watch `** — waits for checks, then prints all reviews/comments +2. **Read all review feedback** — reviews against an earlier head may still apply +3. **Answer substantive feedback with attribution** before merging (see format above) +4. **Re-check after each push** that new reviews target the current head SHA +5. **Don't wait indefinitely on Copilot/Codex** — reviews arrive as quota allows + +To fetch reviews directly: `./scripts/pr-reviews ` — **do not pipe it** (exit code trap). + +See the `pr-watch` skill for the full recipe, and "Shipping a wave" below for Copilot +request mechanics and how to answer declined findings. + ## Quick Reference ```bash diff --git a/context/cloud-agents.md b/context/cloud-agents.md index 9fb343b4..fa6bf2f1 100644 --- a/context/cloud-agents.md +++ b/context/cloud-agents.md @@ -1,11 +1,30 @@ # Cloud Agents (LLM Reference) -Environment notes for agents running in Claude Code on the web. Local -checkouts are unaffected — the session hook exits early unless +Environment notes for agents running in cloud containers — Claude Code on the web, +Cursor cloud agents, and similar setups. Local checkouts are unaffected. + +## Session Bootstrap + +All agent stacks use the same bootstrap script: `scripts/agent-bootstrap.sh`. It: + +1. **Seeds nix flake inputs** (GitHub tarballs are blocked; seeds over git instead) +2. Verifies the `bd` version matches the flake.nix pin +3. Installs `bd` if missing +4. Syncs the Dolt issue tracker from the git remote +5. Warms the cargo build **inside `nix develop`** + +**After bootstrap, `nix develop` works.** This is the required toolchain — bare system +cargo that fails `edition2024` or missing wasm target is a misconfigured session. + +**Claude Code**: `.claude/hooks/session-start.sh` calls the bootstrap automatically when `CLAUDE_CODE_REMOTE=true`. -`.claude/hooks/session-start.sh` handles the setup automatically. This doc -is the "why", and the record of what bites when it doesn't run. +**Cursor cloud**: `.cursor/environment.json` runs the bootstrap via `install`. Skills are +in `.cursor/skills/` (symlinks to `.agents/skills/`). + +**Other stacks**: Run `scripts/agent-bootstrap.sh` manually after checkout. + +This doc is the "why", and the record of what bites when setup doesn't run. --- @@ -60,7 +79,8 @@ same if you need to re-sync by hand; never run it in the working tree. | Thing | Status | Use instead | |---|---|---| -| `nix develop` **unseeded** | Fails: flake inputs are GitHub **tarballs**, which are not served here | Run `scripts/nix-seed-flake-inputs.sh` first — see below. Or just use the preinstalled `cargo`/`rustc`, the same stable toolchain the flake pins | +| `nix develop` **without bootstrap** | Fails: flake inputs are GitHub **tarballs**, which are blocked | Bootstrap seeds inputs automatically; if it failed, run `scripts/nix-seed-flake-inputs.sh` manually | +| Bare system cargo | Fails: `edition2024`, missing wasm target | **Always use `nix develop`** — this is the required toolchain | | `bd dolt push` | Fails: 403 outbound | Nothing; bead status changes stay local. Say so rather than implying the tracker was updated | | `git+ssh://` Dolt remote in `config.yaml` | No ssh client in the container | `git+https://`, which the hook passes explicitly | | `git push`, GitHub API | **Work normally** | — | @@ -70,13 +90,13 @@ starting by hand). Reads of public GitHub repos over the git protocol work — it is specifically the `/archive/*.tar.gz` and `codeload` endpoints that are refused. -### Making `nix develop` work +### How nix develop works after bootstrap -`scripts/nix-seed-flake-inputs.sh` makes the devShell work **without touching -`flake.nix` or `flake.lock`**. Verified end to end: `rustc 1.98.1`, `cargo`, -`crate2nix` in the shell, `git status` clean. +The bootstrap runs `scripts/nix-seed-flake-inputs.sh` automatically, which seeds +the flake inputs over git **without touching `flake.nix` or `flake.lock`**. +After bootstrap, `nix develop` should work. If it fails, check `/tmp/nix-seed.log`. -Why it is needed, and why the obvious fixes are not available: +Why the seeding is needed: - Nix's `github:` fetcher wants a tarball. GitHub traffic **bypasses the environment's domain allowlist entirely** and goes through a proxy that @@ -105,9 +125,9 @@ around it — a mismatch fails loudly. Do not instead rewrite the lock's nodes to `git` type: carrying the old `narHash` onto a different fetcher is exactly the claim the lock exists to make. -It is not in the session-start hook because it is expensive (minutes, plus -several GB of store once the devShell builds) and most work does not need the -devShell. Run it when you do. +The bootstrap runs this automatically. It takes a few minutes and several GB of +store once the devShell builds. If bootstrap reported a seed failure, check +`/tmp/nix-seed.log` and re-run `scripts/nix-seed-flake-inputs.sh` manually. Two alternatives if per-container seeding is unattractive: push the inputs to a binary cache with `nix flake archive` and point sessions at it with @@ -143,3 +163,52 @@ environment, where egress leaves through your own network boundary. reorder or fold commits (`.agents/skills/wave/agent-rules.md`, rule 8). - Keep `context/repo-map.md` and `context/design-principles.md` current — they are the two docs agents are expected to maintain by hand. + +--- + +## PR Tracking + +For any PR you own, **track both CI checks and GitHub comments/reviews**: + +- **`scripts/pr-watch `**: Waits for checks, then prints all reviews/comments (default). + Reviews are a separate API surface — Codex/ChatGPT reviews arrive as *reviews* not comments. +- **Answer feedback with attribution** before marking work done (see below). +- **Re-check after push** that reviews target the current head SHA. + +The `pr-watch` skill (`.agents/skills/pr-watch/SKILL.md`) is the agent-facing recipe. +See AGENTS.md "PR Tracking: Checks AND Reviews" and "Shipping a wave" for the full workflow. + +--- + +## GitHub Attribution (Hard Rule) + +**Every GitHub write (PR, comment, review reply) must include an attribution line.** +Posts go out under the repository owner's account — an unattributed post reads as the +human's words. + +Open the body with: `_Posted by ()_` + +Examples: +- `_Posted by Claude (Opus 4.5)_` +- `_Posted by Cursor Agent (claude-sonnet-5)_` + +A thread is not "answered" until the reply carries this attribution. See AGENTS.md +"GitHub Attribution" for the full rule. + +--- + +## Knowledge Base + +**Use `scripts/kb`, not Prolog MCP.** The Prolog MCP is not available in all agent stacks +(Cursor cloud agents do not have it). All agents must use `scripts/kb` for KB operations: + +```bash +scripts/kb check # kb_consistent, exactly what CI runs +scripts/kb query '' # run a goal; exit 1 if it fails +scripts/kb add # add clauses; refuses if a key already exists +scripts/kb replace # add, overwriting existing keys +scripts/kb rm # remove by key; refuses if a key is absent +``` + +Reading `prolog-sessions/kb.pl` directly for queries is fine. See the `kb` skill at +`.agents/skills/kb/SKILL.md` for the full interface. diff --git a/scripts/agent-bootstrap.sh b/scripts/agent-bootstrap.sh new file mode 100755 index 00000000..35e9be8d --- /dev/null +++ b/scripts/agent-bootstrap.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +# Stack-agnostic agent bootstrap for this repository. +# +# This script prepares any coding agent (Claude Code, Cursor, etc.) to work +# on this repo. It handles: +# 1. Nix flake input seeding (so `nix develop` works in cloud environments) +# 2. bd (beads) installation and pin verification +# 3. Issue tracker sync from the Dolt remote +# 4. Optional cargo workspace warming (inside nix develop) +# +# Usage: +# scripts/agent-bootstrap.sh [--skip-cargo-warm] [--skip-nix-seed] +# +# The script is idempotent and safe to re-run. Nothing here writes to tracked +# files, stages anything, or creates a commit. +# +# IMPORTANT: This repo requires `nix develop` (or direnv) for builds/tests. +# Bare system cargo that fails edition2024 is a misconfigured session. +# +# See also: `.claude/hooks/session-start.sh` (Claude Code wrapper), +# `.cursor/environment.json` (Cursor cloud config) +set -uo pipefail + +SKIP_CARGO_WARM=0 +SKIP_NIX_SEED=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-cargo-warm) SKIP_CARGO_WARM=1; shift ;; + --skip-nix-seed) SKIP_NIX_SEED=1; shift ;; + *) shift ;; + esac +done + +ROOT="${AGENT_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}}" +cd "$ROOT" || exit 0 + +log() { printf '[agent-bootstrap] %s\n' "$*"; } + +# ── 0. Seed nix flake inputs (required for cloud environments) ───────────── +# GitHub tarball endpoints are blocked in cloud containers. This seeds the +# flake inputs over git so `nix develop` can build the dev shell. +if [ "$SKIP_NIX_SEED" -eq 0 ] && command -v nix >/dev/null 2>&1; then + # Set SSL cert for proxy environments + if [ -z "${NIX_SSL_CERT_FILE:-}" ] && [ -r /root/.ccr/ca-bundle.crt ]; then + export NIX_SSL_CERT_FILE=/root/.ccr/ca-bundle.crt + fi + + if [ -f "$ROOT/scripts/nix-seed-flake-inputs.sh" ]; then + log "seeding nix flake inputs (GitHub tarballs blocked in cloud)…" + if "$ROOT/scripts/nix-seed-flake-inputs.sh" >/tmp/nix-seed.log 2>&1; then + log "flake inputs seeded" + else + log "WARNING: nix-seed-flake-inputs.sh failed; see /tmp/nix-seed.log" + log " 'nix develop' may fail. Check network/proxy settings." + fi + fi +fi + +# ── 1. The bd on PATH must be the bd flake.nix pins ──────────────────────── +beads_pin() { + sed -n 's|.*beads\.url[[:space:]]*=[[:space:]]*"github:[^/]*/[^/]*/v\{0,1\}\([^"]*\)".*|\1|p' \ + flake.nix 2>/dev/null | head -1 +} + +bd_running_version() { + bd version 2>/dev/null | sed -n 's/^bd version \([0-9][0-9.]*\).*/\1/p' | head -1 +} + +bd_pin_banner() { + { + printf '\n[agent-bootstrap] ===== BEADS VERSION SKEW =====\n' + printf '[agent-bootstrap] %s\n' "$@" + printf '[agent-bootstrap] =============================\n\n' + } >&2 +} + +assert_bd_pin() { + command -v bd >/dev/null 2>&1 || return 0 + + pinned=$(beads_pin) + running=$(bd_running_version) + + case "$pinned" in + [0-9]*.[0-9]*.[0-9]*) ;; + *) + bd_pin_banner "cannot read the beads pin out of flake.nix" \ + "read: '${pinned:-}'" \ + "The pin moved to a ref this hook cannot parse (a branch or a" \ + "commit, perhaps). Teach beads_pin() to read it, or the check" \ + "silently protects nothing." + return 1 + ;; + esac + + if [ -z "$running" ]; then + bd_pin_banner "cannot read the version of the bd on PATH" \ + "bd: $(command -v bd)" \ + "'bd version' did not answer in the expected shape. Until it does," \ + "nothing here can tell whether that build matches the pin." + return 1 + fi + + [ "$running" = "$pinned" ] && return 0 + + bd_pin_banner "the bd on PATH is NOT the bd this repo pins" \ + "pinned (flake.nix): $pinned" \ + "running: $running ($(command -v bd))" \ + "" \ + "Reading is safe. Do NOT run a mutating bd command — create, update," \ + "close, dep, dolt push. Version skew between builds empties the" \ + "dependency graph in .beads/issues.jsonl and reports success." \ + "" \ + "Fix now: nix develop (in this directory), then re-run the" \ + " command; or 'direnv allow' here." \ + "Fix for good: worktrees are created fresh per dispatch, so allowing" \ + " them one at a time never catches the next one. Put the" \ + " repo root under [whitelist] prefix in" \ + " ~/.config/direnv/direnv.toml." + return 1 +} + +pin_status=0 +assert_bd_pin || pin_status=2 + +# Install a verified database at .beads/embeddeddolt. +install_tracker() { + staged=$1 + live=.beads/embeddeddolt + incoming=$live.incoming + previous=$live.previous + + rm -rf "$incoming" "$previous" || return 1 + cp -rf "$staged" "$incoming" || { rm -rf "$incoming"; return 1; } + + if [ -d "$live" ] && ! mv -f "$live" "$previous"; then + rm -rf "$incoming" + return 1 + fi + + if mv -f "$incoming" "$live"; then + rm -rf "$previous" + return 0 + fi + + if [ -d "$previous" ] && ! mv -f "$previous" "$live"; then + log "ERROR: tracker left at $previous — move it back to $live by hand" + fi + rm -rf "$incoming" + return 1 +} + +# ── 2. bd on PATH ────────────────────────────────────────────────────────── +export PATH="$PATH:/root/go/bin:$HOME/go/bin" + +BD_INSTALLER_REF=f45b249ce6b40ba62aecc03949e6371e8f7c79d8 # tag v1.3.0 +BD_INSTALLER_URL="https://raw.githubusercontent.com/steveyegge/beads/$BD_INSTALLER_REF/scripts/install.sh" + +if ! command -v bd >/dev/null 2>&1; then + log "installing bd (beads v1.3.0)…" + if ! curl -fsSL "$BD_INSTALLER_URL" -o /tmp/bd-install.sh 2>/tmp/bd-install.log; then + log "WARNING: could not download the bd installer; see /tmp/bd-install.log." + elif bash /tmp/bd-install.sh >>/tmp/bd-install.log 2>&1; then + log "bd installed" + else + log "WARNING: bd install failed; see /tmp/bd-install.log (the script that" + log " ran is at /tmp/bd-install.sh). Issue tracking unavailable." + fi +fi + +# Export PATH for the calling environment if AGENT_ENV_FILE is set +if [ -n "${AGENT_ENV_FILE:-${CLAUDE_ENV_FILE:-}}" ]; then + env_file="${AGENT_ENV_FILE:-${CLAUDE_ENV_FILE}}" + echo 'export PATH="$PATH:/root/go/bin:$HOME/go/bin"' >> "$env_file" +fi + +command -v bd >/dev/null 2>&1 && bd metrics off >/dev/null 2>&1 + +if [ "$pin_status" -eq 0 ]; then + assert_bd_pin || pin_status=2 +fi + +# ── 3. Sync the issue tracker from the Dolt remote ───────────────────────── +if command -v bd >/dev/null 2>&1; then + DB_NAME="$(jq -r '.dolt_database // "improvise"' .beads/metadata.json 2>/dev/null || echo improvise)" + ORIGIN="$(git remote get-url origin 2>/dev/null || true)" + case "$ORIGIN" in + git@github.com:*) ORIGIN="https://github.com/${ORIGIN#git@github.com:}" ;; + esac + ORIGIN="${ORIGIN%.git}" + + if [ -n "$ORIGIN" ] && TMP=$(mktemp -d) && [ -d "$TMP" ]; then + log "syncing issue tracker from $ORIGIN …" + STAGE="$TMP/$DB_NAME" + mkdir -p "$STAGE" + + sync_ok=0 + if ( + cd "$STAGE" && git init -q . && + BD_NON_INTERACTIVE=1 bd init --non-interactive --skip-agents --skip-hooks -q \ + --remote "git+$ORIGIN" + ) >/tmp/bd-sync.log 2>&1 && [ -d "$STAGE/.beads/embeddeddolt" ]; then + listing="$( (cd "$STAGE" && bd list --all 2>/dev/null) || true )" + case "$listing" in + "" | "No issues found."*) ;; + *) sync_ok=1 ;; + esac + fi + + if [ "$sync_ok" -eq 1 ] && install_tracker "$STAGE/.beads/embeddeddolt"; then + log "issue tracker synced — 'bd ready' now reflects the remote, not issues.jsonl" + elif [ -d .beads/embeddeddolt ]; then + log "WARNING: tracker sync failed (see /tmp/bd-sync.log); kept the previous database." + log " It may be out of date — verify any bead against the code." + else + log "WARNING: tracker sync failed; see /tmp/bd-sync.log." + log " .beads/issues.jsonl is a STALE export — verify any bead against the code." + fi + rm -rf "$TMP" + elif [ -n "$ORIGIN" ]; then + log "WARNING: could not create a temporary directory; skipped the tracker sync." + log " .beads/issues.jsonl is a STALE export — verify any bead against the code." + fi +fi + +# ── 4. Warm the build inside nix develop (optional) ──────────────────────── +# Uses nix develop to ensure the correct toolchain (rust from flake, wasm target, etc.) +if [ "$SKIP_CARGO_WARM" -eq 0 ] && command -v nix >/dev/null 2>&1; then + log "warming cargo build inside nix develop…" + NIX_FLAGS=(--extra-experimental-features 'nix-command flakes') + if nix "${NIX_FLAGS[@]}" develop --command cargo build --workspace --tests >/tmp/cargo-warm.log 2>&1; then + log "workspace built (nix develop toolchain)" + else + log "WARNING: cargo build failed; see /tmp/cargo-warm.log" + log " This may indicate nix-seed failed or flake inputs are missing." + fi +elif [ "$SKIP_CARGO_WARM" -eq 0 ]; then + log "WARNING: nix not available; skipping cargo warm" + log " Install nix or use a container with nix for this repo." +fi + +# ── 5. Environment notes ─────────────────────────────────────────────────── +cat <<'NOTES' +[agent-bootstrap] Environment notes (see context/cloud-agents.md): + • This repo REQUIRES `nix develop` (or direnv) for builds and tests. + Bare system cargo that fails edition2024 is a misconfigured session. + • The bootstrap seeded flake inputs over git (GitHub tarballs are blocked + in cloud). If nix develop still fails, check /tmp/nix-seed.log. + • `bd dolt push` is blocked outbound (403). Bead status changes stay local. + • Never run a bare `bd init` in this repo: it rewrites AGENTS.md, CLAUDE.md + and .claude/settings.json, and commits. Use --skip-agents --skip-hooks. + • Use `scripts/kb` for knowledge base operations, not Prolog MCP. + • Run commands inside `nix develop` or prefix with `nix develop --command`. +NOTES + +exit "$pin_status" diff --git a/scripts/pr-watch b/scripts/pr-watch index 67222362..10844e1e 100755 --- a/scripts/pr-watch +++ b/scripts/pr-watch @@ -5,36 +5,47 @@ set -euo pipefail usage() { cat >&2 <<'EOF' -usage: pr-watch [--expect ] [--timeout ] [--interval ] +usage: pr-watch [--expect ] [--timeout ] [--interval ] [--reviews|--no-reviews] --expect fail if the PR head is not this (prefix ok) at any poll --timeout give up after this long (default 1800) --interval seconds between polls (default 20) + --reviews after checks complete, print reviews/comments (default) + --no-reviews skip the reviews/comments printout exit 0 every check COMPLETED, none failed, and GitHub says MERGEABLE with a - state of CLEAN, UNSTABLE or HAS_HOOKS + state of CLEAN, UNSTABLE or HAS_HOOKS (and reviews fetched if --reviews) exit 1 a check failed, or the PR is not mergeable (any other known state) exit 2 the head moved during the wait, or is not --expect exit 3 timed out with checks still running, or mergeability still UNKNOWN, or still BLOCKED with every registered check green exit 4 usage or environment error exit 5 the PR is MERGED or CLOSED, so there is nothing left to wait for +exit 6 checks green/mergeable, but pr-reviews failed (fetch error, not "no reviews") + +After exit 0, reviews and comments are printed (unless --no-reviews). Reviews are +a separate API surface from comments; Codex/ChatGPT reviews arrive as *reviews* +and are easy to miss if you only read the comment thread. EOF exit 4 } -PR=""; EXPECT=""; TIMEOUT=1800; INTERVAL=20 +PR=""; EXPECT=""; TIMEOUT=1800; INTERVAL=20; SHOW_REVIEWS=1 while [ $# -gt 0 ]; do case "$1" in - --expect) EXPECT="${2:-}"; shift 2 ;; - --timeout) TIMEOUT="${2:-}"; shift 2 ;; - --interval) INTERVAL="${2:-}"; shift 2 ;; - -h|--help) usage ;; - *) [ -z "$PR" ] || usage; PR="$1"; shift ;; + --expect) EXPECT="${2:-}"; shift 2 ;; + --timeout) TIMEOUT="${2:-}"; shift 2 ;; + --interval) INTERVAL="${2:-}"; shift 2 ;; + --reviews) SHOW_REVIEWS=1; shift ;; + --no-reviews) SHOW_REVIEWS=0; shift ;; + -h|--help) usage ;; + *) [ -z "$PR" ] || usage; PR="$1"; shift ;; esac done [ -n "$PR" ] || usage +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + # `gh` is shell-aliased to a different tool in this repo's shells, so the name # is never trusted: a real gh is located explicitly. fetch() { @@ -130,4 +141,30 @@ echo "pr-watch: head=$head $mergeable/$state $total checks, $failed failed" # An allowlist, so a state GitHub adds later fails closed. DIRTY and BLOCKED # can wear a MERGEABLE flag: context/code/tooling.md, "pr-watch and mergeability". [ "$mergeable" = MERGEABLE ] || exit 1 -case "$state" in CLEAN|UNSTABLE|HAS_HOOKS) exit 0 ;; *) exit 1 ;; esac +case "$state" in CLEAN|UNSTABLE|HAS_HOOKS) ;; *) exit 1 ;; esac + +# ── Reviews and comments ─────────────────────────────────────────────────── +# Checks are green and mergeable. Show reviews/comments so the agent sees any +# feedback that needs answering before merge. This does not change the exit +# code for success -- answering reviews is your responsibility. However, if +# pr-reviews itself fails (usage error, network, etc.), we warn and exit 6. +if [ "$SHOW_REVIEWS" -eq 1 ]; then + echo "" + echo "pr-watch: fetching reviews and comments (--no-reviews to skip)…" + reviews_exit=0 + if [ -n "${PR_WATCH_FIXTURE_DIR:-}" ]; then + PR_REVIEWS_FIXTURE_DIR="$PR_WATCH_FIXTURE_DIR" "$SCRIPT_DIR/pr-reviews" "$PR" || reviews_exit=$? + else + "$SCRIPT_DIR/pr-reviews" "$PR" || reviews_exit=$? + fi + echo "" + if [ "$reviews_exit" -eq 0 ]; then + echo "pr-watch: reviews shown above. Answer substantive feedback before merging." + else + echo "pr-watch: WARNING: pr-reviews failed (exit $reviews_exit). Reviews may not be shown." >&2 + echo "pr-watch: Run './scripts/pr-reviews $PR' manually to fetch reviews." >&2 + exit 6 + fi +fi + +exit 0 diff --git a/scripts/pr-watch-test.sh b/scripts/pr-watch-test.sh index 59dd6531..5293388e 100755 --- a/scripts/pr-watch-test.sh +++ b/scripts/pr-watch-test.sh @@ -20,8 +20,39 @@ run_case() { [ "$got" = "$want" ] && ok "$name" || bad "$name" "$got" "$want" } +# Like run_case but writes output to a caller-provided file. +# Caller must NOT use command substitution (subshell loses ok/bad counters). +# $1 name, $2 expected exit, $3 fixture dir, $4 output file, rest: extra args +run_case_capture() { + local name="$1" want="$2" dir="$3" outfile="$4"; shift 4 + PR_WATCH_FIXTURE_DIR="$dir" "$WATCH" 1 --interval 0 --timeout 1 "$@" >"$outfile" 2>&1 + local got=$? + [ "$got" = "$want" ] && ok "$name" || bad "$name" "$got" "$want" +} + mk() { mkdir -p "$1"; cat > "$1/$2"; } +# Create empty pr-reviews fixtures so pr-watch can call pr-reviews in fixture mode. +# $1 dir, $2 head (must match the pr-watch fixture's headRefOid) +mk_reviews_empty() { + local dir="$1" head="$2" + mkdir -p "$dir" + printf '{"headRefOid":"%s","reviews":[]}\n' "$head" > "$dir/last.pr.json" + echo '[]' > "$dir/last.inline.json" + echo '[]' > "$dir/last.issue.json" +} + +# Create pr-reviews fixtures with a review. $1 dir, $2 head, $3 review commit +mk_reviews_with_review() { + local dir="$1" head="$2" review_commit="$3" + mkdir -p "$dir" + cat > "$dir/last.pr.json" < "$dir/last.inline.json" + echo '[]' > "$dir/last.issue.json" +} + # The state a fresh push is in: the PR exists, no check has registered yet. # A watcher keyed on "nothing pending" calls this finished and green. mk "$work/none" last.json <<'EOF' @@ -43,8 +74,43 @@ mk "$work/green" last.json <<'EOF' {"name":"build","status":"COMPLETED","conclusion":"SUCCESS"}, {"name":"skipped","status":"COMPLETED","conclusion":"SKIPPED"}]} EOF +mk_reviews_empty "$work/green" "aaaaaaaaaaaa" run_case "all completed and green" 0 "$work/green" +# ── Reviews integration ──────────────────────────────────────────────────── +# pr-watch now calls pr-reviews after checks are green. Test that it works. +mk "$work/green-reviews" last.json <<'EOF' +{"headRefOid":"aaaaaaaaaaaa","mergeable":"MERGEABLE","mergeStateStatus":"CLEAN","statusCheckRollup":[ + {"name":"build","status":"COMPLETED","conclusion":"SUCCESS"}]} +EOF +mk_reviews_with_review "$work/green-reviews" "aaaaaaaaaaaa" "aaaaaaaaaaaa" +run_case_capture "green with reviews shows review output" 0 "$work/green-reviews" "$work/out1.txt" +if grep -q "pr-reviews:" "$work/out1.txt"; then + ok "green with reviews prints pr-reviews output" +else + bad "green with reviews prints pr-reviews output" "no pr-reviews output" "pr-reviews: in output" +fi + +# --no-reviews skips the review printout +run_case_capture "--no-reviews skips review output" 0 "$work/green-reviews" "$work/out2.txt" --no-reviews +if grep -q "pr-reviews:" "$work/out2.txt"; then + bad "--no-reviews skips review output" "pr-reviews in output" "no pr-reviews" +else + ok "--no-reviews actually skips review output" +fi + +# pr-reviews failure: if pr-reviews fails, pr-watch exits 6 (not 0) +# Create a green fixture with MALFORMED review fixtures so pr-reviews fails +mk "$work/green-bad-reviews" last.json <<'EOF' +{"headRefOid":"aaaaaaaaaaaa","mergeable":"MERGEABLE","mergeStateStatus":"CLEAN","statusCheckRollup":[ + {"name":"build","status":"COMPLETED","conclusion":"SUCCESS"}]} +EOF +# Create malformed JSON that will cause jq to fail in pr-reviews +echo 'NOT VALID JSON' > "$work/green-bad-reviews/last.pr.json" +echo '[]' > "$work/green-bad-reviews/last.inline.json" +echo '[]' > "$work/green-bad-reviews/last.issue.json" +run_case "pr-reviews failure exits 6" 6 "$work/green-bad-reviews" + mk "$work/red" last.json <<'EOF' {"headRefOid":"aaaaaaaaaaaa","mergeable":"MERGEABLE","mergeStateStatus":"UNSTABLE","statusCheckRollup":[ {"name":"build","status":"COMPLETED","conclusion":"FAILURE"}]} @@ -112,6 +178,10 @@ for m in MERGEABLE CONFLICTING UNKNOWN; do fi sv="\"$s\""; [ "$s" = null ] && sv=null state "$work/table-$m-$s" last.json "\"$m\"" "$sv" "$green_rollup" + # Add review fixtures for cases that exit 0 (pr-watch calls pr-reviews) + if [ "$want" = 0 ]; then + mk_reviews_empty "$work/table-$m-$s" "aaaaaaaaaaaa" + fi run_case "table: $m/$s" "$want" "$work/table-$m-$s" --timeout 0 done done @@ -148,6 +218,7 @@ gap_state "$work/gap" 1.json MERGEABLE BLOCKED "$not_yet | $build_running" gap_state "$work/gap" 2.json MERGEABLE BLOCKED "$not_yet" gap_state "$work/gap" 3.json MERGEABLE BLOCKED "$harness_running" gap_state "$work/gap" last.json MERGEABLE CLEAN '.' +mk_reviews_empty "$work/gap" "aaaaaaaaaaaa" run_case "the needs-gap before the required check registers is not a failure" 0 "$work/gap" --timeout 60 gap_state "$work/gap-never" last.json MERGEABLE BLOCKED "$not_yet"