Files
boshu2__agentops/scripts/check-epic-children-closed.sh
T
Bo eba83ad11b feat(ci): committed reconcile-pr + no-epic-close-with-open-child gate (ag-9gac #reconcile-helper-epic-gate) (#669)
Promotes the ephemeral `/tmp` bead-crank reconcile engine (ran ~20x this
session) to committed, **tested, enforced** scripts. Turns S3's advisory
prose into executable gates.

## What lands
- **`scripts/reconcile-pr.sh <pr> <bead> [--epic <epic>]`** — polls
checks until terminal, reruns the lone `correctness (ubuntu-latest)`
tar-cache flake **once**, treats only substantive non-`claude-review`
fails as blocking, `gh pr merge --squash --admin` when clean, confirms
`gh pr view --json state == MERGED`, then `bd update <bead> --status
closed` (bd close has a dolt blocker-query glitch). `--epic` mode calls
the gate before closing the epic.
- **`scripts/check-epic-children-closed.sh <epic>`** — the
**no-epic-close-with-open-child** gate: exit 1 naming any
open/in_progress child, exit 0 when all closed.
- **15 hermetic bats** (`gh`+`bd` stubbed via PATH shims) — prove
merge+close on green, BLOCK (exit 2) on a substantive fail,
rerun-once-then-merge on the lone flake, no-close when state != MERGED,
gate rejects+names an open child, passes when all closed. shellcheck -S
warning clean.
- **crank/evolve SKILL.md** prose now points to the committed scripts
(tiny edits).

## Exit-code contract (asserted in tests)
`0` merged+closed · `1` epic-gate fail (open child) · `2` blocked
(substantive fail) · `3` merge attempted but state != MERGED · `4`
usage/dep error.

## Gates
- shellcheck -S warning: CLEAN (both scripts)
- `bats tests/scripts/{reconcile-pr,check-epic-children-closed}.bats`:
15/15 pass
- `scripts/regen-all.sh --check --skills crank,evolve`: ALL GATES GREEN
- Diff scope-only: 2 scripts + 2 bats + 2 skill pointers + 3 codex hash
files (source_hash bumps). Pre-existing `docs/cli-surface.*` main drift
reverted, not swept in.

Orchestrator helpers (not necessarily CI jobs) — wired as enforcement
for the crank/evolve close discipline.

Closes-scenario: ag-9gac#reconcile-helper-epic-gate
Bounded-context: BC3-Loop
Evidence: scripts/check-epic-children-closed.sh
2026-05-31 17:31:08 -04:00

101 lines
3.3 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# check-epic-children-closed.sh — the no-epic-close-with-open-child GATE.
#
# Enumerates the children of an epic (parent-child dependents) and FAILS if any
# child is still open or in_progress. This is the committed, tested form of the
# advisory "never close an epic with an open child" prose from the crank/evolve
# skills; reconcile-pr.sh --epic calls it before closing an epic.
#
# Usage:
# scripts/check-epic-children-closed.sh <epic-id>
#
# Exit codes (documented contract — tests assert these exactly):
# 0 all children are closed (or the epic has no children)
# 1 at least one child is open/in_progress (each offending child printed)
# 4 usage / missing-dependency / bad-input error
#
# Dependencies: bd, jq (stubbed via PATH in the hermetic bats suite).
#
# Child enumeration uses the canonical bd dependents query:
# bd dep list <epic> --direction=up -t parent-child --json
# We pull the child id from whichever id-bearing field the record carries
# (id / from_id / issue_id / dependent_id) so the gate survives bd schema drift.
set -uo pipefail
usage() {
cat >&2 <<'USAGE'
Usage: scripts/check-epic-children-closed.sh <epic-id>
Fails (exit 1) if any parent-child child of <epic-id> is still open or
in_progress, naming each offender. Exits 0 when every child is closed.
Exit codes: 0 all-closed · 1 open-child(ren) · 4 usage.
USAGE
}
die() { echo "ERROR: $*" >&2; exit 4; }
EPIC=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--*) usage; die "unknown flag: $1" ;;
*) [[ -z "$EPIC" ]] || { usage; die "only one epic-id allowed"; }; EPIC="$1"; shift ;;
esac
done
[[ -n "$EPIC" ]] || { usage; die "need an epic-id"; }
command -v bd >/dev/null 2>&1 || die "bd CLI not on PATH"
command -v jq >/dev/null 2>&1 || die "jq not on PATH"
# Enumerate child ids (parent-child dependents of the epic).
children_json="$(bd dep list "$EPIC" --direction=up -t parent-child --json 2>/dev/null || true)"
# Guard: if the query errored or returned a non-array, treat as no children
# only when it's clearly empty; otherwise surface the error.
if echo "$children_json" | jq -e 'type=="object" and has("error")' >/dev/null 2>&1; then
die "bd dep list failed: $(echo "$children_json" | jq -r '.error')"
fi
# Pull the child id from whichever id-bearing field exists.
child_ids="$(echo "$children_json" | jq -r '
(if type=="array" then . else [] end)
| .[]
| (.id // .from_id // .issue_id // .dependent_id // empty)
' 2>/dev/null || true)"
if [[ -z "$child_ids" ]]; then
echo "OK: epic $EPIC has no open children (no children found)" >&2
exit 0
fi
offenders=0
while IFS= read -r child; do
[[ -n "$child" ]] || continue
status="$(bd show "$child" --json 2>/dev/null | jq -r '.[0].status // .status // empty' 2>/dev/null)"
case "$status" in
open|in_progress)
echo "OPEN-CHILD: $child status=$status" >&2
offenders=$((offenders+1))
;;
"")
echo "WARN: could not read status for child $child (treating as offender)" >&2
offenders=$((offenders+1))
;;
*)
: # closed / done / cancelled — fine
;;
esac
done <<< "$child_ids"
if [[ "$offenders" -gt 0 ]]; then
echo "EPIC-GATE FAIL: $EPIC has $offenders open/in_progress child(ren)" >&2
exit 1
fi
echo "OK: all children of $EPIC are closed" >&2
exit 0