From 5e283ec09ee7ac2282785e6ef06d75d440af30a1 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:21:52 +0000 Subject: [PATCH 1/3] docs: define cross-repo discovery guards --- BUILDER.md | 23 +++++++++++++++++++---- CHANGELOG.md | 1 + FLEET.md | 15 ++++++++++++--- REVIEWER.md | 23 ++++++++++++++++++++--- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/BUILDER.md b/BUILDER.md index 6667fb2..185afe7 100644 --- a/BUILDER.md +++ b/BUILDER.md @@ -26,8 +26,14 @@ triage bug, and the move is to say so on the issue, not to guess. ## Building - Branch per issue; open the PR **as a draft early**, `Closes #N` in the - body. Drafts are invisible to the reviewer panel on purpose — the draft - phase is yours. + body. `Closes #N` does not cross repos: when the PR is in a different repo + from its authorizing issue, use `Part of /#N` instead, and + comment on that issue with the draft PR link as soon as the draft opens. + Triage closes the authorizing issue by hand when its acceptance criteria + are met; the cross-repo merge never closes it. This codifies the linkage + builders already used on rig#112 and ceremony #13/#16 rather than adding a + new review obligation. Drafts are invisible to the reviewer panel on + purpose — the draft phase is yours. - **The issue's acceptance criteria are your definition of done.** Reproduce them as a checklist in the PR body and check them honestly as you go. If one turns out to be wrong or unreachable, say so on the issue and get it @@ -49,8 +55,17 @@ triage bug, and the move is to say so on the issue, not to guess. panel roster and any repo-specific flow notes live in that repo's own CONTRIBUTING; everything below is the shared flow.) -1. Mark ready-for-review; request **the whole panel** (the roster is in the - repo's CONTRIBUTING). +1. Mark ready-for-review; request **the whole panel**. The panel is the roster + of the repo the **PR** is in, minus you — never the roster of the repo the + issue is in. The PR repo's `.github/labels.conf` `panel=` line is the + machine's answer; its CONTRIBUTING roster is the human-readable answer, + and `panel=` governs if they disagree because that is what the state + machine reads. If the PR repo names no roster, ask triage on the + authorizing issue before marking ready-for-review; do not guess. You may + request an off-panel reviewer, but say that their verdict is advisory and + does not become required. On rig#112 this distinction mattered: requesting + codex and grok was correct for rig's panel even though ceremony's bench was + larger, and the doctrine had not said which roster governed. 2. **Wait for every verdict, then answer the round whole** — one reply covering every point, then push the fixes, then re-request exactly the reviewers who did not approve. Prefer verification over argument: when a diff --git a/CHANGELOG.md b/CHANGELOG.md index 34eed9d..2cf773f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ so entries say what changed, cite the issue, and stop. ## Unreleased - `needs-ruling` — the cross-cutting flag for a pending human decision, excluded from `state:needs-human` and from the staleness sweep (#51). +- Cross-repo doctrine: the panel is the PR's repo's roster, a review request is authorization but not panel membership, and `Part of #N` replaces the `Closes #N` that cannot cross repos (#57). ## 0.1.0 — 2026-07-22 diff --git a/FLEET.md b/FLEET.md index 184e2e0..1a37f22 100644 --- a/FLEET.md +++ b/FLEET.md @@ -54,9 +54,18 @@ Every box runs the same skeleton, adapted to its CLI: fully approved (write the closing summary, flip to `state:needs-human`, request the human), my PR `CONFLICTING` (rebase; never act on `UNKNOWN` — post-merge flap). -- **Reviewers:** an open PR by someone else whose head I have not yet - reviewed — one verdict per head, deduplicated against my own latest review's - SHA, not against the search index (it lags). +- **Reviewers**, in priority order: first, a review requested on me in any + repo (`gh search prs --review-requested=@me --state=open`); second, the + repo-list poll for an open PR by someone else whose head I have not yet + reviewed. Both triggers keep the existing one-verdict-per-head rule, + deduplicated against my own latest review's SHA rather than the search index + (it lags). The request trigger runs first because it reaches repos the list + does not name. + +`~/duty/repos.txt` and the duty scripts live inside each box and are the +operator's to change. This descriptive edit is the spec for those box-side +updates; until an operator makes them, the request trigger exists on paper +only. ### Resilience diff --git a/REVIEWER.md b/REVIEWER.md index 3a28e0b..f92006d 100644 --- a/REVIEWER.md +++ b/REVIEWER.md @@ -23,9 +23,10 @@ The machine reads only your **verdict**; humans read your reasons. In order of authority: -1. **The issue's acceptance criteria** — the PR's `Closes #N` names your - spec. Check every criterion; a PR that ships less than the issue says is - a request-changes even if the code is beautiful. +1. **The issue's acceptance criteria** — the PR's `Closes #N`, or its + cross-repo `Part of /#N`, names your spec. Check every + criterion; a PR that ships less than the issue says is a request-changes + even if the code is beautiful. 2. **The repo's load-bearing constraints** — the rules bought with incidents (in ceremony itself: issue #1's constraint list; in a governed repo: its own CONTRIBUTING plus ceremony's README). A change that @@ -40,6 +41,22 @@ In order of authority: test settles what a comment thread can't. A review that says "I ran X and saw Y" outranks one that says "this looks like it might". +## Where you review + +- **A review request on you is your authorization** in any `heavy-duty` repo + and on any fleet member's fork. You need no separate permission and do not + wait for the repo to appear on a list: review is reversible + read-plus-comment work, and the requester already decided it should happen. +- **A request is authorization, not panel membership.** Convergence is + measured against the target repo's `panel=` roster minus the author. If you + are requested off-panel, post the verdict anyway and say in its body that + it is advisory; neither your silence nor your request-changes is a gate the + reconciler enforces. The nine-hour wait for kimi's off-panel verdict on + rig#112 showed why authorization and membership must not be conflated. +- **Being requested is a wake condition of its own.** It is how work in a + repo you have never heard of reaches you; a repo list finds only work in + repos somebody thought to list. + ## What you do not do - **Re-litigate the spec.** The issue's decisions were made in triage and, -- 2.45.2 From bc099eb4e2f70f5a4a208ba76570b4ccd162c6a4 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:38:14 +0000 Subject: [PATCH 2/3] fix: guard cross-repo issue references --- CHANGELOG.md | 1 + TRIAGE.md | 4 +- .../issueflow-reconcile.sh | 42 ++++++++++++++----- test/issueflow-reconcile.test.sh | 16 +++++++ 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34eed9d..5aca92c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ so entries say what changed, cite the issue, and stop. ## Unreleased +- `issueflow-reconcile` — keep cross-repo references out of local dependency decisions and require triage to resolve cross-repo blockers by hand (#61). - `needs-ruling` — the cross-cutting flag for a pending human decision, excluded from `state:needs-human` and from the staleness sweep (#51). ## 0.1.0 — 2026-07-22 diff --git a/TRIAGE.md b/TRIAGE.md index edbebd0..65193e2 100644 --- a/TRIAGE.md +++ b/TRIAGE.md @@ -65,7 +65,9 @@ Every issue you mint carries, in this order: the builder's definition of done and the reviewer's review spec, verbatim. - **Test plan**: what proves it, including the cases that must fail. - **Dependencies**: `Blocked by #N` / `Blocks #N`, and `Part of #E` when an - epic organizes it. + epic organizes it. Name a cross-repo dependency the same way with its + repository qualified (`Blocked by repo#N` or `owner/repo#N`); the sweep + cannot resolve it, so triage verifies it and flips the issue by hand. - **Labels**: type (`bug`/`enhancement`/`documentation`), `scope:*`, and exactly one of `ready` / `blocked` (see [LABELS.md](LABELS.md)). diff --git a/actions/issueflow-reconcile/issueflow-reconcile.sh b/actions/issueflow-reconcile/issueflow-reconcile.sh index deee082..6956a55 100644 --- a/actions/issueflow-reconcile/issueflow-reconcile.sh +++ b/actions/issueflow-reconcile/issueflow-reconcile.sh @@ -105,7 +105,17 @@ claim_reclaim_marker() { # $1 = last activity epoch printf 'claim-reclaimed-%s\n' "$1" } -blocked_references() { # body on stdin -> issue numbers, one per line +issue_references() { # text on stdin -> LOCAL/CROSSreference + # A qualified reference belongs to another repository. Classify the whole + # token before extracting numbers so rig#112 can never become local #112. + { grep -Eo '([[:alnum:]_.-]+/)?[[:alnum:]_.-]+#[0-9]+|#[0-9]+' || true; } \ + | awk ' + index($0, "#") == 1 { print "LOCAL\t" substr($0, 2); next } + { print "CROSS\t" $0 } + ' +} + +blocked_reference_records() { # body on stdin -> classified reference records # Dependency declarations sometimes soft-wrap after a comma. Continue # through the first sentence terminator; if prose omits one, conservatively # retain later references so ambiguity can keep an issue blocked, never @@ -128,13 +138,21 @@ blocked_references() { # body on stdin -> issue numbers, one per line } print line } - ' \ - | { grep -Eo '#[0-9]+' || true; } | tr -d '#' | sort -nu + ' | issue_references } -blocked_decision() { # $1 refs, $2 OPEN/CLOSED states - local refs="$1" states="$2" - if [ -z "$refs" ]; then echo FLAG_UNPARSEABLE +blocked_references() { # body on stdin -> local issue numbers, one per line + blocked_reference_records | awk -F '\t' '$1 == "LOCAL" { print $2 }' | sort -nu +} + +blocked_cross_references() { # body on stdin -> qualified refs, one per line + blocked_reference_records | awk -F '\t' '$1 == "CROSS" { print $2 }' | sort -u +} + +blocked_decision() { # $1 local refs, $2 OPEN/CLOSED states, $3 cross-repo refs + local refs="$1" states="$2" cross_refs="${3:-}" + if [ -n "$cross_refs" ]; then echo FLAG_CROSS_REPO + elif [ -z "$refs" ]; then echo FLAG_UNPARSEABLE elif grep -qxF OPEN <<<"$states"; then echo KEEP elif grep -qxF UNKNOWN <<<"$states"; then echo FLAG_UNPARSEABLE else echo READY @@ -146,8 +164,8 @@ epic_references() { # markdown task-list issue references from body on stdin tolower($0) ~ /^##[[:space:]]+task list[[:space:]]*$/ { in_list = 1; next } in_list && /^#/ { exit } in_list && /^[[:space:]]*[-*][[:space:]]+\[[ xX]\]/ { print } - ' \ - | { grep -Eo '#[0-9]+' || true; } | tr -d '#' | sort -nu + ' | issue_references \ + | awk -F '\t' '$1 == "LOCAL" { print $2 }' | sort -nu } epic_decision() { # $1 refs, $2 states @@ -190,7 +208,7 @@ last_issue_activity() { } reconcile_issue() { - local n="$1" decision refs states age assignees open_pr=false label owners + local n="$1" decision refs cross_refs states age assignees open_pr=false label owners decision="$(queue_decision <<<"$ISSUE_LABELS")" case "$decision" in ADD_NEEDS_TRIAGE) @@ -229,9 +247,13 @@ reconcile_issue() { esac elif has_issue_label blocked; then refs="$(blocked_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")" + cross_refs="$(blocked_cross_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")" states="$(reference_states <<<"$refs")" - decision="$(blocked_decision "$refs" "$states")" + decision="$(blocked_decision "$refs" "$states" "$cross_refs")" case "$decision" in + FLAG_CROSS_REPO) + ensure_comment "$n" blocked-cross-repo \ + "This issue's \`Blocked by\` declaration names cross-repo dependencies that the sweep cannot resolve: $(tr '\n' ' ' <<<"$cross_refs" | sed 's/[[:space:]]*$//'). Triage must verify those dependencies and flip this issue to \`ready\` by hand." ;; FLAG_UNPARSEABLE) ensure_comment "$n" blocked-unparseable \ 'This issue is `blocked`, but its body has no parseable `Blocked by #N` declaration. The sweep will not guess the dependency.' ;; diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index 3029388..9ce8127 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -90,10 +90,23 @@ check "real issue 15 inline blocker parses" 0 "" test \ body="Part of #1. Blocked by #11, #12 (needs a released ceremony + the bootstrap guide); benefits from #13's lessons but does not need #14/#15." check "real issue 16 inline blockers parse" 0 "" test \ "$(blocked_references <<<"$body")" = $'11\n12' +check "qualified short repository reference drops" 0 "" test \ + -z "$(blocked_references <<<"Blocked by rig#112.")" +check "qualified owner/repository reference drops" 0 "" test \ + -z "$(blocked_references <<<"Blocked by heavy-duty/box#9.")" +check "parenthesized local reference survives" 0 "13" blocked_references <<<"Blocked by (#13)." +check "slash-adjacent local references survive" 0 $'14\n15' \ + blocked_references <<<"Blocked by #14/#15." +check "comma-adjacent local references survive" 0 $'11\n12' \ + blocked_references <<<"Blocked by #11, #12." check "open blocker keeps issue blocked" 0 "KEEP" blocked_decision "$refs" $'CLOSED\nOPEN' check "all closed blockers release issue" 0 "READY" blocked_decision "$refs" $'CLOSED\nCLOSED' check "missing blocked declaration is flagged" 0 "FLAG_UNPARSEABLE" blocked_decision "" "" check "unreadable blocker is flagged" 0 "FLAG_UNPARSEABLE" blocked_decision "12" "UNKNOWN" +check "cross-repo-only blocker is flagged distinctly" 0 "FLAG_CROSS_REPO" \ + blocked_decision "" "" "rig#112" +check "cross-repo blocker prevents false promotion when locals close" 0 "FLAG_CROSS_REPO" \ + blocked_decision "9" "CLOSED" "rig#9" # Invariant 4: only configured triage actors mint directly into the queue. check "triage-authored ready issue is accepted" 0 "KEEP" author_decision true <<<"ready" @@ -107,6 +120,9 @@ check "epic parser reads task-list refs only" 0 $'2\n3' printf '%s\n' "$epic_ref body=$'## Task list\n- [x] #2 done\n- [x] #3 done\n\n## Definition of done\n- [ ] open issue #99 must not suppress the nudge' check "epic parser stops before later checkbox sections" 0 "" test \ "$(epic_references <<<"$body")" = $'2\n3' +body=$'## Task list\n- [x] #2 Scaffold\n- [x] #3 version\n- [ ] #13 Convert rig (pilot) — **PR [rig#112](https://github.com/heavy-duty/rig/pull/112) is approved by the whole panel on head `3c72c1b` and sits at `state:needs-human` since 2026-07-23 10:47Z; the merge is the human'\''s. #14/#15 unblock when it lands**\n- [ ] #14 Convert box\n- [ ] #15 Convert cast\n- [ ] #16 Adopt in incubator' +check "real epic 1 row drops rig PR while retaining local children" 0 $'2\n3\n13\n14\n15\n16' \ + epic_references <<<"$body" check "completed epic is nudged" 0 "NUDGE" epic_decision "$epic_refs" $'CLOSED\nCLOSED' check "open epic child suppresses nudge" 0 "KEEP" epic_decision "$epic_refs" $'CLOSED\nOPEN' check "epic without parseable children is stable" 0 "KEEP" epic_decision "" "" -- 2.45.2 From 3935bf82d9a76e03a28ac8954c77f1a973d4343f Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:40:55 +0000 Subject: [PATCH 3/3] test: pin cross-repo reconciliation contract --- test/issueflow-reconcile.test.sh | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index 9ce8127..f020f14 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -107,6 +107,26 @@ check "cross-repo-only blocker is flagged distinctly" 0 "FLAG_CROSS_REPO" \ blocked_decision "" "" "rig#112" check "cross-repo blocker prevents false promotion when locals close" 0 "FLAG_CROSS_REPO" \ blocked_decision "9" "CLOSED" "rig#9" +# shellcheck disable=SC2016 # expansions belong to the generated fake gh +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'if [ "$1" = api ]; then [ ! -f "$GH_COMMENTS" ] || cat "$GH_COMMENTS"; exit; fi' \ + 'if [ "$1 $2" = "issue comment" ]; then' \ + ' while [ "$#" -gt 0 ]; do' \ + ' if [ "$1" = --body ]; then shift; printf "%s\n" "$1" >>"$GH_COMMENTS"; exit; fi' \ + ' shift' \ + ' done' \ + 'fi' >"$TMP/gh" +chmod +x "$TMP/gh" +: >"$TMP/comments" +# shellcheck disable=SC2016 # expansions belong to the isolated bash -c process +check "cross-repo warning is idempotent across two sweeps" 0 "" \ + env PATH="$TMP:$PATH" GH_COMMENTS="$TMP/comments" bash -c \ + 'source "$1"; REPO=heavy-duty/ceremony + ensure_comment 99 blocked-cross-repo "cross-repo warning" + ensure_comment 99 blocked-cross-repo "cross-repo warning" + test "$(grep -cF "" "$GH_COMMENTS")" -eq 1' \ + _ "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" # Invariant 4: only configured triage actors mint directly into the queue. check "triage-authored ready issue is accepted" 0 "KEEP" author_decision true <<<"ready" @@ -120,8 +140,10 @@ check "epic parser reads task-list refs only" 0 $'2\n3' printf '%s\n' "$epic_ref body=$'## Task list\n- [x] #2 done\n- [x] #3 done\n\n## Definition of done\n- [ ] open issue #99 must not suppress the nudge' check "epic parser stops before later checkbox sections" 0 "" test \ "$(epic_references <<<"$body")" = $'2\n3' -body=$'## Task list\n- [x] #2 Scaffold\n- [x] #3 version\n- [ ] #13 Convert rig (pilot) — **PR [rig#112](https://github.com/heavy-duty/rig/pull/112) is approved by the whole panel on head `3c72c1b` and sits at `state:needs-human` since 2026-07-23 10:47Z; the merge is the human'\''s. #14/#15 unblock when it lands**\n- [ ] #14 Convert box\n- [ ] #15 Convert cast\n- [ ] #16 Adopt in incubator' -check "real epic 1 row drops rig PR while retaining local children" 0 $'2\n3\n13\n14\n15\n16' \ +# shellcheck disable=SC2016 # backticks and ${{ }}-shaped prose are fixture literals +body=$'## Task list\n\n- [x] #2 Scaffold: layout, test harness, shellcheck + actionlint CI\n- [x] #3 `lib/version.sh` — one version abstraction, two backends\n- [x] #4 `lib/changelog.sh` — the one canonical section extractor\n- [x] #5 `actions/changelog-armed` — the version-keyed arming guard\n- [x] #6 `actions/changelog-monotonic` — shipped release headings are append-only\n- [x] #7 `actions/drill-recorded` — a release carries its evidence\n- [x] #8 `lib/decide.sh` — the merge door'\''s decision, pure\n- [x] #9 The reusable release workflow — both doors, one implementation\n- [x] #10 Labels machinery: reusable workflow + core/scope split\n- [x] #11 Dogfood: ceremony releases itself (0.1.0) — **shipped: tag `0.1.0`, release, `drills/0.1.0.md`; main re-armed at `0.1.1-dev`**\n- [x] #12 `docs/CONSUMERS.md` + README doctrine\n- [ ] #13 Convert rig (pilot) — **PR [rig#112](https://github.com/heavy-duty/rig/pull/112) is approved by the whole panel on head `3c72c1b` and sits at `state:needs-human` since 2026-07-23 10:47Z; the merge is the human'\''s. #14/#15 unblock when it lands**\n- [ ] #14 Convert box\n- [ ] #15 Convert cast (artifact hook debut)\n- [ ] #16 Adopt in incubator (greenfield consumer)\n\nAdjacent, same repo, separable from the release chain: the **agent team flow** (discussion → triage → issue → build → review → human merge) landed as doctrine in PR #17 (CONTRIBUTING.md, LABELS.md, TRIAGE.md, BUILDER.md, REVIEWER.md); #10'\''s bootstrap carries its labels, #12'\''s guide carries its adoption checklist. Consumption is split by what has a runtime: **machinery by reference** (GitHub materializes pinned workflows/actions at run time), **doctrine as a machine-verified mirror** (`.ceremony/` in each consumer, byte-identical to the pin, CI-guarded — agents read rules from the checkout, never cross-repo):\n\n- [x] #18 Issue-flow reconciliation — the work-queue sweep — **shipped 2026-07-23** in #32 (`66f1c08`)\n- [ ] #61 issueflow-reconcile — cross-repo references must not be read as local issue numbers (found in triage hygiene against the live corpus after #18 shipped; it is why this epic cannot currently be nudged complete)\n- [x] #19 actions/docs-sync — the vendored-doctrine mirror + guard\n- [x] #24 Entry templates — the pipeline'\''s doors made mechanical (from discussion #23)\n- [ ] #50 `needs-ruling` — the pending-human-decision flag (its own epic; from discussion #30)\n- [ ] #56 Fleet scope and cross-repo discovery — the two guards, the runner hole, the roster question (its own epic; from discussion #55, filed at @danmt'\''s request). Children: #57 (BUILDER/REVIEWER/FLEET discovery guards), #58 (`actions/runner-isolated`). Added to this list by triage 2026-07-23 — it is agent-team-flow work like #50, so a scan of this epic must see it.' +check "real epic 1 task list drops rig PR and retains local references" 0 \ + $'2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n18\n19\n23\n24\n30\n32\n50\n55\n56\n57\n58\n61' \ epic_references <<<"$body" check "completed epic is nudged" 0 "NUDGE" epic_decision "$epic_refs" $'CLOSED\nCLOSED' check "open epic child suppresses nudge" 0 "KEEP" epic_decision "$epic_refs" $'CLOSED\nOPEN' -- 2.45.2