From 4e5bfcef816303e219bcd1dcf717f2333af5b6c2 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 23:32:21 +0100 Subject: [PATCH 1/7] docs: design spec for --notify-cmd run-summary notifications (v0.7.0) --- .../specs/2026-06-12-notify-cmd-design.md | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-12-notify-cmd-design.md diff --git a/docs/superpowers/specs/2026-06-12-notify-cmd-design.md b/docs/superpowers/specs/2026-06-12-notify-cmd-design.md new file mode 100644 index 0000000..c34fb15 --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-notify-cmd-design.md @@ -0,0 +1,131 @@ +# Design: `--notify-cmd` run-summary notifications — v0.7.0 + +Date: 2026-06-12. Status: approved (user chose: notifications via an +operator-trusted command hook; the resume-mode roadmap item is retired in favor +of documentation, since the label system already provides resume semantics). + +## Problem + +fixbuddy runs take 30 minutes to hours. Operators running unattended batches +(cron, long queues) currently learn the outcome only by reading the log +directory. There is no way to get a push notification, chat message, or email +when a run finishes or aborts. + +## Decision + +A single mechanism, consistent with the project's dependency-light philosophy: + +- **`--notify-cmd `** (repeatable) and the **additive config key + `notify_cmd`** — exactly the trust and parsing model of `--check-cmd`: + operator-trusted strings, run via the shell, combinable from config and CLI, + not removable from the CLI once set in config. +- No built-in webhook. `--notify-cmd 'curl -s -d @- https://...'` covers it. + +## Behavior + +**When it fires:** once per run, immediately after the Summary block prints. +This includes the crash-abort path (the abort `break`s out of the issue loop +and falls through to the summary). It does NOT fire: + +- under `--dry-run` (a preview is contractually read-only and side-effect-free), +- when the run exits early because no issues matched (nothing happened), +- on Ctrl-C/SIGTERM (interactive abort; the interrupt trap stays minimal). + +**Where it runs:** in fixbuddy's launch directory (CWD), like config loading — +NOT in `$PROJECT`. Notifications are about the run, not the checkout. + +**What it receives:** + +1. Environment variables: + +| Variable | Value | +| --- | --- | +| `FIXBUDDY_REPO` | target `owner/repo` | +| `FIXBUDDY_PROCESSED` | issues processed this run | +| `FIXBUDDY_MERGED` | PRs confirmed merged | +| `FIXBUDDY_PR_OPENED` | PRs opened (not yet merged) | +| `FIXBUDDY_FALSE_POSITIVES` | issues closed as false positives | +| `FIXBUDDY_BLOCKED` | issues blocked (crash/timeout/needs-human) | +| `FIXBUDDY_REJECTED` | issues whose fixes were rejected | +| `FIXBUDDY_ABORTED` | `true` when the batch hit `--crash-abort`, else `false` | +| `FIXBUDDY_LOG_DIR` | the run's log directory | +| `FIXBUDDY_VERSION` | fixbuddy version | + +2. stdin: a human-readable multi-line summary (repo, counts, log dir, abort + note when applicable), so `ntfy publish t`, `mail -s ...`, or a Slack + `curl -d @-` work without any argument plumbing. + +**Failure handling:** each command's stdout/stderr is appended to +`$log_root/notify.log`. A non-zero exit warns (`notify command failed (exit N)`) +but never changes fixbuddy's exit code, and the remaining notify commands still +run. Notify commands get no watchdog (same as `--check-cmd`; operator-trusted). + +## Implementation sketch + +- New global `NOTIFY_CMDS=()`; parse `--notify-cmd` flag and `notify_cmd` + config key (additive, mirroring `check_cmd`). +- New `aborted` flag set to `true` in the crash-abort branch before `break`. +- New function `run_notifications` called after the Summary block: + +```bash +run_notifications() { + [ "${#NOTIFY_CMDS[@]}" -gt 0 ] || return 0 + local summary cmd rc + summary="fixbuddy v$VERSION run on $REPO +Processed: $processed | Merged: $merged | PRs opened: $opened +False positives: $fp | Blocked: $blocked | Rejected: $rejected +${aborted:+Batch ABORTED after consecutive agent crashes. +}Logs: $log_root" + for cmd in "${NOTIFY_CMDS[@]}"; do + printf '%s\n' "$summary" | ( + export FIXBUDDY_REPO="$REPO" FIXBUDDY_PROCESSED="$processed" \ + FIXBUDDY_MERGED="$merged" FIXBUDDY_PR_OPENED="$opened" \ + FIXBUDDY_FALSE_POSITIVES="$fp" FIXBUDDY_BLOCKED="$blocked" \ + FIXBUDDY_REJECTED="$rejected" FIXBUDDY_ABORTED="$aborted" \ + FIXBUDDY_LOG_DIR="$log_root" FIXBUDDY_VERSION="$VERSION" + eval "$cmd" + ) >>"$log_root/notify.log" 2>&1 + rc=$? + [ "$rc" -ne 0 ] && warn "notify command failed (exit $rc): $cmd" + done + return 0 +} +``` + +(The final form follows the existing `run_checks` style; `aborted` is a plain +`true`/`false` string, so the summary line uses an explicit `if` rather than +the `${aborted:+...}` shorthand if that reads better.) + +- Help header: new option line(s) added to the top comment — the `--help` + handler prints `sed -n '2,47p' "$0"`, so the range must be extended to match + the new header length. + +## Docs + +- README: Options table row, config-key table row, an Examples entry + (`ntfy`/Slack-curl/macOS `osascript`), and a note on the trust model. +- README Roadmap: remove both remaining items — notifications ships here, and + resume mode is retired because the label system already provides resume + (interrupted issues stay in the queue; `fix:blocked` auto-requeues; + `fix:pr-open` deduplicates). Add an FAQ entry: "What happens if I interrupt + a run?" documenting exactly that. +- Wizard and action.yml: unchanged (advanced flag; CI users add their own + notification steps after the action). + +## Testing (extend tests/integration.sh) + +1. Happy path with TWO `--notify-cmd` entries writing env vars and stdin to + files — assert exact values (`FIXBUDDY_PR_OPENED=1`, `FIXBUDDY_MERGED=0`, + `FIXBUDDY_ABORTED=false`, …) and the stdin text, proving both commands ran. +2. Failing notify command (`exit 7`) → run still exits 0, warning in run log, + and a SECOND notify command still runs. +3. Crash scenario with notify → `FIXBUDDY_BLOCKED=1`. +4. `notify_cmd` via `.fixbuddy.conf` in the launch dir → fires (additive with + CLI). +5. `--dry-run` with `--notify-cmd` → does NOT fire. + +## Release + +v0.7.0: version bumps (fixbuddy.sh, wizard, install.sh `DEFAULT_REF`, README +one-liners), CHANGELOG entry, fresh `SHA256SUMS`, full DoD review gate, PR. +Tagging/pushing only with explicit user approval. From 41352a2f245271860654922d6e074fc2377ec591 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 23:34:46 +0100 Subject: [PATCH 2/7] docs: implementation plan for --notify-cmd notifications --- .../plans/2026-06-12-notify-cmd.md | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-12-notify-cmd.md diff --git a/docs/superpowers/plans/2026-06-12-notify-cmd.md b/docs/superpowers/plans/2026-06-12-notify-cmd.md new file mode 100644 index 0000000..9ead4a9 --- /dev/null +++ b/docs/superpowers/plans/2026-06-12-notify-cmd.md @@ -0,0 +1,326 @@ +# `--notify-cmd` Notifications (v0.7.0) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Operator-trusted notification hook (`--notify-cmd` / config `notify_cmd`) fired once with the run summary, for unattended runs. + +**Architecture:** Mirrors the existing `--check-cmd` pattern (additive flag+config key, shell-eval, operator trust). A new `run_notifications` function is called right after the Summary block; it pipes a text summary to each command with `FIXBUDDY_*` env vars exported. TDD against the existing offline integration harness — no new stubs needed, notify commands are plain shell. + +**Tech Stack:** Bash 3.2-compatible, no new dependencies. + +Spec: `docs/superpowers/specs/2026-06-12-notify-cmd-design.md` + +--- + +### Task 1: Failing integration tests + +**Files:** +- Modify: `tests/integration.sh` (five new test functions before the Runner section; extend `TESTS`) + +- [ ] **Step 1: Add the test functions** + +Insert before `# ---------------- Runner ----------------`: + +```bash +test_notify_cmd_receives_summary() { + # Notify commands run in the LAUNCH directory ($TMP) and get the summary as + # FIXBUDDY_* env vars plus human-readable text on stdin. Both commands run. + SCENARIO=happy; make_fixture + run_fixbuddy --auto-merge \ + --notify-cmd 'env | grep ^FIXBUDDY_ | sort > notify-env.txt; cat > notify-stdin.txt' \ + --notify-cmd 'echo second > notify-second.txt' + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_REPO=acme/app" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_PROCESSED=1" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_PR_OPENED=1" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_MERGED=0" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_BLOCKED=0" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_ABORTED=false" + assert_substr "$TMP/notify-stdin.txt" "PRs opened: 1" + [ -f "$TMP/notify-second.txt" ] || fail "second notify command did not run" +} + +test_notify_failure_does_not_break_run() { + SCENARIO=happy; make_fixture + run_fixbuddy --auto-merge --notify-cmd 'exit 7' \ + --notify-cmd 'echo ran > notify-after-fail.txt' + [ "$RC" -eq 0 ] || fail "notify failure changed the exit code (rc=$RC)" + assert_grep "$RUNLOG" 'notify command failed \(exit 7\)' + [ -f "$TMP/notify-after-fail.txt" ] || fail "subsequent notify command did not run" +} + +test_notify_reports_blocked() { + # One crash (below the abort threshold): BLOCKED=1, ABORTED=false. + SCENARIO=crash; make_fixture + run_fixbuddy --auto-merge --notify-cmd 'env | grep ^FIXBUDDY_ > notify-env.txt' + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_BLOCKED=1" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_ABORTED=false" +} + +test_notify_cmd_from_config() { + # notify_cmd is an additive config key, read from the launch dir like the + # other config keys. + SCENARIO=happy; make_fixture + printf 'notify_cmd = echo config-notify > notify-config.txt\n' > "$TMP/.fixbuddy.conf" + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + [ -f "$TMP/notify-config.txt" ] || fail "config notify_cmd did not run" +} + +test_notify_skipped_on_dry_run() { + SCENARIO=happy; make_fixture + run_fixbuddy --dry-run --notify-cmd 'echo nope > notify-dry.txt' + [ "$RC" -eq 0 ] || fail "exit code $RC" + [ ! -f "$TMP/notify-dry.txt" ] || fail "notify fired during --dry-run" +} +``` + +- [ ] **Step 2: Extend the test list** + +```bash +TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate + test_dry_run_read_only test_crash_labels_blocked + test_agy_full_pipeline test_gemini_rejected_with_migration_hint + test_agy_internal_timeout_is_blocked + test_verify_residue_is_stashed test_verify_commit_is_discarded + test_verify_residue_cleaned_on_early_return + test_reviewer_commit_is_discarded test_reviewer_residue_cleaned_before_retry + test_notify_cmd_receives_summary test_notify_failure_does_not_break_run + test_notify_reports_blocked test_notify_cmd_from_config + test_notify_skipped_on_dry_run) +``` + +- [ ] **Step 3: Run to verify the new tests fail** + +Run: `tests/integration.sh 2>&1 | grep -E "^(FAIL|[0-9]+ passed)"` +Expected: 14 passed; the four non-dry-run notify tests fail with "Unknown arg: --notify-cmd" effects (exit 2) or missing files; `test_notify_skipped_on_dry_run` also fails (exit 2 on the unknown flag). + +- [ ] **Step 4: Commit** + +```bash +git add tests/integration.sh +git commit -m "test: notify-cmd scenarios (env/stdin, failure isolation, config, dry-run)" +``` + +--- + +### Task 2: Implement `--notify-cmd` in fixbuddy.sh + +**Files:** +- Modify: `fixbuddy.sh` — header comment (~line 30-47), help sed range (line 174), defaults (line 57), config parser (line 144), arg parsing (line 164), after `run_checks` (~line 630), counters init (~line 870), crash-abort branch (~line 1303), after the Summary block (~line 1325) + +- [ ] **Step 1: Header comment + help range** + +After the 4-line `--check-cmd` block in the header, insert: + +```text +# --notify-cmd Run-summary notification hook (repeatable). Runs in the +# LAUNCH directory after the final summary (also after a +# crash-abort); gets FIXBUDDY_* env vars + a text summary +# on stdin. OPERATOR-TRUSTED and run via the shell. +``` + +Update the config comment line to mention the new additive key: +`...label and check_cmd are` → `...label, check_cmd, and notify_cmd are`. + +The header grew by 4 lines: change line 174 `-h|--help) sed -n '2,47p' "$0"; exit 0 ;;` → `sed -n '2,51p'`. + +- [ ] **Step 2: Globals, config key, flag** + +- Line 57 area, after `CHECK_CMDS=()`: add `NOTIFY_CMDS=()`. +- Config parser, after `check_cmd) CHECK_CMDS+=("$value") ;;`: add + `notify_cmd) NOTIFY_CMDS+=("$value") ;;` +- Arg parsing, after `--check-cmd) ...`: add + `--notify-cmd) NOTIFY_CMDS+=("$2"); shift 2 ;;` + +- [ ] **Step 3: `run_notifications` function** + +Insert directly after the `run_checks` function: + +```bash +# Run the operator-supplied --notify-cmd hook(s) with the final run summary. +# Commands are trusted (CLI/config level, like --check-cmd) and run via the +# shell in the LAUNCH directory — not $PROJECT; notifications are about the +# run, not the checkout. Each command gets the summary as FIXBUDDY_* env vars +# plus a human-readable text on stdin. A failure warns and never changes +# fixbuddy's exit code; the remaining commands still run. Output is appended +# to $log_root/notify.log. +run_notifications() { + [ "${#NOTIFY_CMDS[@]}" -gt 0 ] || return 0 + local summary cmd rc abort_note="" + if [ "$aborted" = "true" ]; then + abort_note="Batch ABORTED after consecutive agent crashes. +" + fi + summary="fixbuddy v$VERSION run on $REPO +Processed: $processed | Merged: $merged | PRs opened: $opened +False positives: $fp | Blocked: $blocked | Rejected: $rejected +${abort_note}Logs: $log_root" + for cmd in "${NOTIFY_CMDS[@]}"; do + printf '%s\n' "$summary" | ( + export FIXBUDDY_REPO="$REPO" FIXBUDDY_PROCESSED="$processed" \ + FIXBUDDY_MERGED="$merged" FIXBUDDY_PR_OPENED="$opened" \ + FIXBUDDY_FALSE_POSITIVES="$fp" FIXBUDDY_BLOCKED="$blocked" \ + FIXBUDDY_REJECTED="$rejected" FIXBUDDY_ABORTED="$aborted" \ + FIXBUDDY_LOG_DIR="$log_root" FIXBUDDY_VERSION="$VERSION" + eval "$cmd" + ) >>"$log_root/notify.log" 2>&1 + rc=$? + [ "$rc" -ne 0 ] && warn "notify command failed (exit $rc): $cmd" + done + return 0 +} +``` + +- [ ] **Step 4: `aborted` state** + +- Counters init block (`processed=0 ... rejected=0` before `process_issue`): add `aborted=false`. +- In the crash-abort branch (the `if [ "$CONSECUTIVE_CRASHES" -ge ...` block), add `aborted=true` immediately before `break`. + +- [ ] **Step 5: Fire after the Summary** + +After the final `info "Logs: $log_root"` of the Summary block, append: + +```bash +run_notifications +``` + +(Dry-run, empty-queue, and Ctrl-C paths exit before this line, so they never notify — by design.) + +- [ ] **Step 6: Run tests** + +Run: `bash -n fixbuddy.sh && shellcheck fixbuddy.sh tests/integration.sh && tests/integration.sh 2>&1 | grep -E "^(FAIL|[0-9]+ passed)"` +Expected: `19 passed, 0 failed`. + +- [ ] **Step 7: Commit** + +```bash +git add fixbuddy.sh +git commit -m "feat: --notify-cmd run-summary notification hook" +``` + +--- + +### Task 3: Documentation + +**Files:** +- Modify: `README.md` (options table, config tables + example, Examples, FAQ, Roadmap), `CHANGELOG.md` + +- [ ] **Step 1: README options table** + +After the `--check-cmd` row: + +```markdown +| `--notify-cmd ` | Run-summary notification hook. Repeatable. Runs in the **launch** directory after the final summary (also after a crash-abort), receiving `FIXBUDDY_*` env vars (counts, `FIXBUDDY_ABORTED`, `FIXBUDDY_LOG_DIR`) and a human-readable summary on stdin. A failure warns but never changes the exit code. Not fired for `--dry-run`, empty queues, or Ctrl-C. Operator-trusted (same trust level as CLI flags) | none | +``` + +- [ ] **Step 2: README config docs** + +- Allowlist table, after the `check_cmd` row: `| notify_cmd | --notify-cmd | additive (see below) |` +- Additive-keys paragraph: `**Additive keys** (\`label\`, \`check_cmd\`)` → `**Additive keys** (\`label\`, \`check_cmd\`, \`notify_cmd\`)` +- Format example block: add `notify_cmd = curl -s -d @- ntfy.sh/my-topic` after the `check_cmd` lines. + +- [ ] **Step 3: README example + FAQ + roadmap** + +Examples section, new entry: + +```markdown +Get a push notification when an unattended batch finishes (anything that reads stdin works — ntfy, Slack webhook, `mail`): + +​```bash +./fixbuddy.sh --repo owner/repo --project ~/code/repo --max 10 \ + --notify-cmd 'curl -s -d @- ntfy.sh/my-fixbuddy-topic' +​``` +``` + +FAQ, after the "What happens if CI fails?" entry: + +```markdown +**What happens if I interrupt a run (Ctrl-C)?** +The in-flight agent is stopped and the local branch is cleaned up; no label is set, so the issue simply stays in the queue. There is no separate resume mode because the labels already provide it: the next run picks up where the last one stopped (`fix:blocked` re-queues automatically, `fix:pr-open` prevents duplicate PRs). +``` + +Delete the `## Roadmap` section (both items are resolved: notifications ship here; resume mode is covered by the FAQ above). + +- [ ] **Step 4: CHANGELOG** + +Insert above the `## [0.6.0]` entry: + +```markdown +## [0.7.0] - 2026-06-12 + +### Added +- **`--notify-cmd `** (repeatable) and additive config key `notify_cmd` — + a run-summary notification hook for unattended runs. Commands run in the + launch directory after the final summary (including after a crash-abort), + receive `FIXBUDDY_*` env vars (counts, `FIXBUDDY_ABORTED`, log dir) plus a + human-readable summary on stdin, and are operator-trusted (same model as + `--check-cmd`). A failing command warns but never changes fixbuddy's exit + code. Not fired for `--dry-run`, empty queues, or Ctrl-C. + +### Changed +- README Roadmap retired: notifications shipped here, and explicit resume mode + is intentionally not built — the label system already resumes interrupted + runs (documented in a new FAQ entry). +``` + +And add the compare link above the 0.6.0 link: +`[0.7.0]: https://github.com/Codevena/fixbuddy/compare/v0.6.0...v0.7.0` + +- [ ] **Step 5: Verify and commit** + +Run: `grep -n "notify" README.md | head; tests/integration.sh >/dev/null && echo OK` +Expected: rows present; suite still green. + +```bash +git add README.md CHANGELOG.md +git commit -m "docs: document --notify-cmd, retire roadmap (resume covered by FAQ)" +``` + +--- + +### Task 4: Release housekeeping (v0.7.0) + +**Files:** +- Modify: `fixbuddy.sh:2,50`, `fixbuddy-wizard.sh:2,35`, `install.sh` (5×), `README.md` (one-liners + pinned-version sentence), `SHA256SUMS`, `NEXT_SESSION.md` + +- [ ] **Step 1: Version bumps v0.6.0 → v0.7.0** + +```bash +sed -i '' 's/fixbuddy v0\.6\.0 — two-agent pipeline/fixbuddy v0.7.0 — two-agent pipeline/; s/^VERSION="0\.6\.0"/VERSION="0.7.0"/' fixbuddy.sh +sed -i '' 's/fixbuddy-wizard\.sh v0\.6\.0/fixbuddy-wizard.sh v0.7.0/; s/fixbuddy wizard v0\.6\.0/fixbuddy wizard v0.7.0/' fixbuddy-wizard.sh +sed -i '' 's/v0\.6\.0/v0.7.0/g' install.sh +``` + +README: replace both `raw.githubusercontent.com/Codevena/fixbuddy/v0.6.0/install.sh` URLs and the "pinned `v0.6.0` scripts" sentence with v0.7.0. + +- [ ] **Step 2: SHA256SUMS + NEXT_SESSION** + +```bash +shasum -a 256 fixbuddy.sh fixbuddy-wizard.sh > SHA256SUMS +``` + +Update `NEXT_SESSION.md`: status = v0.7.0 on branch awaiting merge/tag; release checklist unchanged; "What's next" = roadmap is empty — next session picks new goals (ideas: agy in the GitHub Action docs, more agents, log retention). + +- [ ] **Step 3: Full verification + commit** + +Run: `bash -n fixbuddy.sh && bash -n fixbuddy-wizard.sh && bash -n install.sh && shellcheck fixbuddy.sh fixbuddy-wizard.sh install.sh tests/integration.sh tests/stubs/agent tests/stubs/gh && tests/integration.sh 2>&1 | tail -2` +Expected: all clean, `19 passed, 0 failed`. + +```bash +git add -A +git commit -m "release: v0.7.0 (version bumps, SHA256SUMS, housekeeping)" +``` + +--- + +### Task 5: Definition-of-Done review pipeline + +Per `~/.claude/CLAUDE.md`; reviewers review the branch diff (`git diff main...HEAD`). + +- [ ] **Step 1: Static checks green** (done in Task 4 Step 3). +- [ ] **Step 2: Codex Agent A** — prompt via `.review/codex-prompt.txt`; run `codex exec "$(<.review/codex-prompt.txt)" Date: Fri, 12 Jun 2026 23:35:31 +0100 Subject: [PATCH 3/7] test: notify-cmd scenarios (env/stdin, failure isolation, config, dry-run) --- tests/integration.sh | 58 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/tests/integration.sh b/tests/integration.sh index e272d4c..d729c57 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -224,6 +224,59 @@ test_reviewer_residue_cleaned_before_retry() { [ "$(grep -c '^claude:fix$' "$STAGELOG")" -eq 2 ] || fail "expected 2 fix attempts" } +test_notify_cmd_receives_summary() { + # Notify commands run in the LAUNCH directory ($TMP) and get the summary as + # FIXBUDDY_* env vars plus human-readable text on stdin. Both commands run. + SCENARIO=happy; make_fixture + run_fixbuddy --auto-merge \ + --notify-cmd 'env | grep ^FIXBUDDY_ | sort > notify-env.txt; cat > notify-stdin.txt' \ + --notify-cmd 'echo second > notify-second.txt' + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_REPO=acme/app" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_PROCESSED=1" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_PR_OPENED=1" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_MERGED=0" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_BLOCKED=0" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_ABORTED=false" + assert_substr "$TMP/notify-stdin.txt" "PRs opened: 1" + [ -f "$TMP/notify-second.txt" ] || fail "second notify command did not run" +} + +test_notify_failure_does_not_break_run() { + SCENARIO=happy; make_fixture + run_fixbuddy --auto-merge --notify-cmd 'exit 7' \ + --notify-cmd 'echo ran > notify-after-fail.txt' + [ "$RC" -eq 0 ] || fail "notify failure changed the exit code (rc=$RC)" + assert_grep "$RUNLOG" 'notify command failed \(exit 7\)' + [ -f "$TMP/notify-after-fail.txt" ] || fail "subsequent notify command did not run" +} + +test_notify_reports_blocked() { + # One crash (below the abort threshold): BLOCKED=1, ABORTED=false. + SCENARIO=crash; make_fixture + run_fixbuddy --auto-merge --notify-cmd 'env | grep ^FIXBUDDY_ > notify-env.txt' + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_BLOCKED=1" + assert_substr "$TMP/notify-env.txt" "FIXBUDDY_ABORTED=false" +} + +test_notify_cmd_from_config() { + # notify_cmd is an additive config key, read from the launch dir like the + # other config keys. + SCENARIO=happy; make_fixture + printf 'notify_cmd = echo config-notify > notify-config.txt\n' > "$TMP/.fixbuddy.conf" + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + [ -f "$TMP/notify-config.txt" ] || fail "config notify_cmd did not run" +} + +test_notify_skipped_on_dry_run() { + SCENARIO=happy; make_fixture + run_fixbuddy --dry-run --notify-cmd 'echo nope > notify-dry.txt' + [ "$RC" -eq 0 ] || fail "exit code $RC" + [ ! -f "$TMP/notify-dry.txt" ] || fail "notify fired during --dry-run" +} + # ---------------- Runner ---------------- TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate @@ -232,7 +285,10 @@ TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate test_agy_internal_timeout_is_blocked test_verify_residue_is_stashed test_verify_commit_is_discarded test_verify_residue_cleaned_on_early_return - test_reviewer_commit_is_discarded test_reviewer_residue_cleaned_before_retry) + test_reviewer_commit_is_discarded test_reviewer_residue_cleaned_before_retry + test_notify_cmd_receives_summary test_notify_failure_does_not_break_run + test_notify_reports_blocked test_notify_cmd_from_config + test_notify_skipped_on_dry_run) for t in "${TESTS[@]}"; do CURRENT="$t" From b2083e80bee64f7088d678d0bb774240ba2015df Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 23:37:14 +0100 Subject: [PATCH 4/7] feat: --notify-cmd run-summary notification hook --- fixbuddy.sh | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/fixbuddy.sh b/fixbuddy.sh index f488d03..74bb0d9 100755 --- a/fixbuddy.sh +++ b/fixbuddy.sh @@ -31,6 +31,10 @@ # after the fix commit and before review; a non-zero exit is # treated like a review rejection (retried, then fix:rejected). # Commands are OPERATOR-TRUSTED and run via the shell. +# --notify-cmd Run-summary notification hook (repeatable). Runs in the +# LAUNCH directory after the final summary (also after a +# crash-abort); gets FIXBUDDY_* env vars + a text summary +# on stdin. OPERATOR-TRUSTED and run via the shell. # --max-retries Fix retries after review rejection (default: 1 → 2 total attempts) # --agent-timeout Wall-clock timeout per agent invocation (default: 1200 = 20min) # --crash-abort Abort batch after N consecutive agent crashes (default: 3) @@ -42,9 +46,9 @@ # --yes, -y Skip confirmation # # Config files (key = value, parsed without eval; CLI flags override): -# ~/.fixbuddy/config (global), then ./.fixbuddy.conf (cwd). label and check_cmd are -# ADDITIVE: config and CLI entries combine (labels become an AND filter), and a -# config-provided label/check cannot be removed from the CLI. +# ~/.fixbuddy/config (global), then ./.fixbuddy.conf (cwd). label, check_cmd, and +# notify_cmd are ADDITIVE: config and CLI entries combine (labels become an AND +# filter), and a config-provided label/check/notify cannot be removed from the CLI. set -uo pipefail VERSION="0.6.0" @@ -55,6 +59,7 @@ PROJECT="" LABELS=() ISSUES=() CHECK_CMDS=() +NOTIFY_CMDS=() SEVERITY="" MAX="" FIX_AGENT="claude" @@ -142,6 +147,7 @@ load_config() { esac ;; label) LABELS+=("$value") ;; check_cmd) CHECK_CMDS+=("$value") ;; + notify_cmd) NOTIFY_CMDS+=("$value") ;; *) warn "unknown config key '$key' in $file (ignored)" ;; esac done < "$file" @@ -162,6 +168,7 @@ while [ $# -gt 0 ]; do --fix-agent) FIX_AGENT="$2"; shift 2 ;; --review-agent) REVIEW_AGENT="$2"; shift 2 ;; --check-cmd) CHECK_CMDS+=("$2"); shift 2 ;; + --notify-cmd) NOTIFY_CMDS+=("$2"); shift 2 ;; --max-retries) MAX_RETRIES="$2"; shift 2 ;; --agent-timeout) AGENT_TIMEOUT="$2"; shift 2 ;; --crash-abort) CRASH_ABORT_THRESHOLD="$2"; shift 2 ;; @@ -171,7 +178,7 @@ while [ $# -gt 0 ]; do --skip-label) SKIP_LABEL="$2"; shift 2 ;; --dry-run) DRY_RUN=true; shift ;; -y|--yes) AUTO_YES=true; shift ;; - -h|--help) sed -n '2,47p' "$0"; exit 0 ;; + -h|--help) sed -n '2,51p' "$0"; exit 0 ;; --version) echo "fixbuddy $VERSION"; exit 0 ;; *) err "Unknown arg: $1"; exit 2 ;; esac @@ -624,6 +631,39 @@ run_checks() { return 0 } +# Run the operator-supplied --notify-cmd hook(s) with the final run summary. +# Commands are trusted (CLI/config level, like --check-cmd) and run via the +# shell in the LAUNCH directory — not $PROJECT; notifications are about the +# run, not the checkout. Each command gets the summary as FIXBUDDY_* env vars +# plus a human-readable text on stdin. A failure warns and never changes +# fixbuddy's exit code; the remaining commands still run. Output is appended +# to $log_root/notify.log. +run_notifications() { + [ "${#NOTIFY_CMDS[@]}" -gt 0 ] || return 0 + local summary cmd rc abort_note="" + if [ "$aborted" = "true" ]; then + abort_note="Batch ABORTED after consecutive agent crashes. +" + fi + summary="fixbuddy v$VERSION run on $REPO +Processed: $processed | Merged: $merged | PRs opened: $opened +False positives: $fp | Blocked: $blocked | Rejected: $rejected +${abort_note}Logs: $log_root" + for cmd in "${NOTIFY_CMDS[@]}"; do + printf '%s\n' "$summary" | ( + export FIXBUDDY_REPO="$REPO" FIXBUDDY_PROCESSED="$processed" \ + FIXBUDDY_MERGED="$merged" FIXBUDDY_PR_OPENED="$opened" \ + FIXBUDDY_FALSE_POSITIVES="$fp" FIXBUDDY_BLOCKED="$blocked" \ + FIXBUDDY_REJECTED="$rejected" FIXBUDDY_ABORTED="$aborted" \ + FIXBUDDY_LOG_DIR="$log_root" FIXBUDDY_VERSION="$VERSION" + eval "$cmd" + ) >>"$log_root/notify.log" 2>&1 + rc=$? + [ "$rc" -ne 0 ] && warn "notify command failed (exit $rc): $cmd" + done + return 0 +} + # Interrupt handler — on Ctrl-C/SIGTERM, stop the in-flight agent and leave the LOCAL repo # in a clean state so the next run resumes. No label is set: an interrupted issue simply # stays in the queue. cleanup_branch is local-only (stash + checkout base + delete LOCAL @@ -907,6 +947,7 @@ opened=0 fp=0 blocked=0 rejected=0 +aborted=false process_issue() { local num="$1" title="$2" body="$3" @@ -1306,6 +1347,7 @@ while IFS= read -r issue; do err "Next steps:" err " • Wait for recovery and rerun — issues marked fix:blocked auto-requeue." err " • Or rerun with --review-agent $([ "$REVIEW_AGENT" = codex ] && echo claude || echo codex) as a fallback." + aborted=true break fi done < <(echo "$filtered" | jq -c '.[]') @@ -1323,3 +1365,5 @@ ok "False positives: $fp" warn "Blocked: $blocked" err "Rejected: $rejected" info "Logs: $log_root" + +run_notifications From 871e16aae299d3cca055583fb137fdb96bb01905 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 23:38:20 +0100 Subject: [PATCH 5/7] docs: document --notify-cmd, retire roadmap (resume covered by FAQ) --- CHANGELOG.md | 17 +++++++++++++++++ README.md | 22 +++++++++++++++------- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a83f70b..682c1b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to fixbuddy are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project aims to follow [Semantic Versioning](https://semver.org/). +## [0.7.0] - 2026-06-12 + +### Added +- **`--notify-cmd `** (repeatable) and additive config key `notify_cmd` — + a run-summary notification hook for unattended runs. Commands run in the + launch directory after the final summary (including after a crash-abort), + receive `FIXBUDDY_*` env vars (counts, `FIXBUDDY_ABORTED`, log dir) plus a + human-readable summary on stdin, and are operator-trusted (same model as + `--check-cmd`). A failing command warns but never changes fixbuddy's exit + code. Not fired for `--dry-run`, empty queues, or Ctrl-C. + +### Changed +- README Roadmap retired: notifications shipped here, and explicit resume mode + is intentionally not built — the label system already resumes interrupted + runs (documented in a new FAQ entry). + ## [0.6.0] - 2026-06-12 Google retires the Gemini CLI on 2026-06-18; its successor is the Antigravity @@ -81,5 +97,6 @@ to existing flags. Predate this changelog. See the git history and the `v0.4.0` / `v0.3.2` tags. +[0.7.0]: https://github.com/Codevena/fixbuddy/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/Codevena/fixbuddy/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/Codevena/fixbuddy/compare/v0.4.0...v0.5.0 diff --git a/README.md b/README.md index e375cb6..e835d7a 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ These agent invocations are intentionally powerful. Run fixbuddy only against re | `--base ` | PR base branch | auto-detect | | `--issue ` | Process only this issue number. Repeatable; dedup filters and `--label`/`--severity` still apply. Warns for requested numbers that are not found, closed, or already labeled non-actionable | none | | `--check-cmd ` | Shell command to run as a test gate after each fix commit and before review. Repeatable. A non-zero exit is treated as a review rejection: output is fed back to the fix agent and the attempt is retried; if the retry budget is exhausted the issue is labeled `fix:rejected`. Because review and PR are only reached after all checks pass, checks also gate auto-merge. Commands run in `$PROJECT` and are operator-trusted (same trust level as CLI flags) | none | +| `--notify-cmd ` | Run-summary notification hook. Repeatable. Runs in the **launch** directory after the final summary (also after a crash-abort), receiving `FIXBUDDY_*` env vars (counts, `FIXBUDDY_ABORTED`, `FIXBUDDY_LOG_DIR`) and a human-readable summary on stdin. A failure warns but never changes the exit code. Not fired for `--dry-run`, empty queues, or Ctrl-C. Operator-trusted (same trust level as CLI flags) | none | | `--auto-merge` | Enable auto-merge, overriding a config `auto_merge = false` | off | | `--no-auto-merge` | Open PRs without requesting auto-merge | off | | `--skip-label