issueflow-reconcile — the offsite stale-flag nudge, from cross-referenced timeline events #69

Closed
opened 2026-07-23 12:33:42 +00:00 by dan-claude-bot · 2 comments
dan-claude-bot commented 2026-07-23 12:33:42 +00:00 (Migrated from github.com)

Part of #56. Blocked by #68 (this verifies the flag #68 introduces).

Accepted from discussion #67, open question 3 — "should the sweep verify rather than trust?" — answered by @danmt in the thread: "we can verify, but only if we already trust so we dont have to verify everything." #68 builds the trust half. This is the verify half, and it is deliberately the smaller, later, quieter one.

All line references pinned at cf69d8c.

Context

#68 makes offsite stop the claim-reclaim clock on the label alone — no proof, no resolution, nothing that can fail closed. That is the right first cut and it has one failure mode, named in the discussion that proposed it: the flag can go stale. The cross-repo PR merges, nobody clears the label, and the issue is exempt from reclamation forever. A permanent exemption is a worse board lie than the bug #68 fixes, because it is silent.

The signal to catch it is already being fetched. last_issue_activity reads the issue timeline on every sweep of every claimed issue (L197-L209) to pick up assigned events. That same timeline carries cross-referenced events: when a PR in another repo names this issue, GitHub records it, with source.issue.pull_request and the referencing repository. Nothing new is asked of the builder, no new body syntax, no second parser — the verification rides a call the sweep already makes.

Spec

  • D1 — verification never grants or withholds the exemption. The label alone exempts, always, exactly as #68 built it. This issue adds one comment and nothing else. It never removes offsite, never reclaims, never unassigns, never touches a label. That is the literal content of the ruling: trust is the mechanism, verification is a nudge on top of it.
  • D2 — the trigger is: every cross-referenced PR the sweep can see is closed or merged, and at least one exists. All of them, not any of them — a builder may open a follow-up PR in a second repo, and one merged PR must not nudge while another is live.
  • D3 — unreadable resolves to silence, never to a verdict. A private repo (dan-claude-bot/incubator is one — GitHub hides cross-references the viewer cannot see, so ceremony's token will observe zero events for #16), a deleted repo, a permissions error: none of them nudge. This is the reconciler's standing rule — an unreadable fact must never invent a verdict — and #16 is the live case that will exercise the silent path on day one. A test pins it.
  • D4 — the nudge carries an idempotency marker (<!-- issueflow:offsite-resolved --> via the existing ensure_comment), so it is said once per issue, not once per sweep tick. Unlike #52's 7-day ruling nudge, there is no repeat: the fact being reported does not decay, and the owner is a named builder rather than a room.
  • D5 — the decision is pure and the fetch is not. offsite_resolved_decision() — cross-referenced PR states on stdin → NUDGE | QUIET — sits above the API divider beside the other pure decisions; the timeline read sits below it. Every branch is fixture-driven with no network, as every decision in this file already is.
  • D6 — the comment names the PRs it resolved and what it wants. "heavy-duty/rig#112 is merged; this issue's offsite flag is still up. Clear it and close the issue, or say what is still outstanding." Addressed to the assignee. It reports a fact and asks for a human-or-builder act — it does not perform one.

Tasks

  • actions/issueflow-reconcile/issueflow-reconcile.shoffsite_cross_referenced_prs() (timeline → owner/repo#N for cross-referenced events whose source is a PR), offsite_pr_states() (→ OPEN/CLOSED/UNKNOWN, mirroring the existing reference_states), and the pure offsite_resolved_decision().
  • Wire it into the claimed branch, after the exemption gate, guarded by has_issue_label offsite — one extra timeline read only for issues that carry the flag.
  • test/issueflow-reconcile.test.sh — the rows below, plus a fixture timeline JSON exercising the parser.
  • LABELS.md — one sentence in offsite's paragraph: the sweep will tell you when your flag has outlived its PR, and that it only ever tells you.
  • CHANGELOG.md — one line under ## Unreleased, inserted above the heading below it.

Acceptance criteria

  • An offsite issue whose only cross-referenced PR is merged gets exactly one comment, naming that PR — and gets no second comment on the next sweep.
  • An offsite issue with one merged and one open cross-referenced PR gets no comment.
  • An offsite issue with no visible cross-referenced PRs gets no comment and logs nothing alarming (the #16 case).
  • A timeline read that errors leaves the sweep's other work untouched and produces no comment.
  • No code path in either reconciler adds or removes offsite, and none reclaims an offsite issue — #68's criterion, re-asserted here because this issue is the one that could break it.
  • A non-offsite claimed issue triggers no extra timeline read (assert on the fetch being guarded, not just on the outcome).
  • test/run.sh green; shellcheck- and actionlint-clean.

Test plan

offsite_resolved_decision against fixture state lists:

# cross-referenced PR states expect
1 CLOSED NUDGE
2 CLOSED CLOSED NUDGE
3 CLOSED OPEN QUIET
4 OPEN QUIET
5 (empty — nothing visible) QUIET
6 CLOSED UNKNOWN QUIET

Parser rows against a fixture timeline: a cross-referenced event whose source is a PR in another repo is extracted with its qualified reference; one whose source is an issue is ignored; a mentioned/assigned event is ignored; an empty timeline yields nothing.

The cases that must fail: (a) flip row 3 to NUDGE and the suite goes red — "all closed" must not degrade to "any closed"; (b) flip row 6 to NUDGE and it goes red — UNKNOWN must never be read as resolved.

Dependencies

Blocked by #68. Part of #56.

Constraint

LABELS.md is vendored doctrine (the .ceremony/ set); this repo is the source, so no re-sync happens here.

Part of #56. Blocked by #68 (this verifies the flag #68 introduces). Accepted from discussion [#67](https://github.com/heavy-duty/ceremony/discussions/67), open question 3 — *"should the sweep verify rather than trust?"* — answered by @danmt [in the thread](https://github.com/heavy-duty/ceremony/discussions/67#discussioncomment-14840112): *"we can verify, but only if we already trust so we dont have to verify everything."* #68 builds the trust half. This is the verify half, and it is deliberately the smaller, later, quieter one. All line references pinned at [`cf69d8c`](https://github.com/heavy-duty/ceremony/tree/cf69d8ce9ef0cb62a784e6fa823274141d7aa729). ## Context #68 makes `offsite` stop the claim-reclaim clock on the label alone — no proof, no resolution, nothing that can fail closed. That is the right first cut and it has one failure mode, named in the discussion that proposed it: **the flag can go stale.** The cross-repo PR merges, nobody clears the label, and the issue is exempt from reclamation forever. A permanent exemption is a worse board lie than the bug #68 fixes, because it is silent. The signal to catch it is already being fetched. `last_issue_activity` reads the issue timeline on every sweep of every `claimed` issue ([L197-L209](https://github.com/heavy-duty/ceremony/blob/cf69d8ce9ef0cb62a784e6fa823274141d7aa729/actions/issueflow-reconcile/issueflow-reconcile.sh#L197-L209)) to pick up `assigned` events. That same timeline carries `cross-referenced` events: when a PR in another repo names this issue, GitHub records it, with `source.issue.pull_request` and the referencing repository. Nothing new is asked of the builder, no new body syntax, no second parser — the verification rides a call the sweep already makes. ## Spec - **D1 — verification never grants or withholds the exemption.** The label alone exempts, always, exactly as #68 built it. This issue adds **one comment** and nothing else. It never removes `offsite`, never reclaims, never unassigns, never touches a label. That is the literal content of the ruling: trust is the mechanism, verification is a nudge on top of it. - **D2 — the trigger is: every cross-referenced PR the sweep can see is closed or merged, and at least one exists.** All of them, not any of them — a builder may open a follow-up PR in a second repo, and one merged PR must not nudge while another is live. - **D3 — unreadable resolves to silence, never to a verdict.** A private repo (`dan-claude-bot/incubator` is one — GitHub hides cross-references the viewer cannot see, so ceremony's token will observe *zero* events for #16), a deleted repo, a permissions error: none of them nudge. This is the reconciler's standing rule — *an unreadable fact must never invent a verdict* — and #16 is the live case that will exercise the silent path on day one. A test pins it. - **D4 — the nudge carries an idempotency marker** (`<!-- issueflow:offsite-resolved -->` via the existing `ensure_comment`), so it is said once per issue, not once per sweep tick. Unlike #52's 7-day ruling nudge, there is no repeat: the fact being reported does not decay, and the owner is a named builder rather than a room. - **D5 — the decision is pure and the fetch is not.** `offsite_resolved_decision()` — cross-referenced PR states on stdin → `NUDGE` | `QUIET` — sits above the API divider beside the other pure decisions; the timeline read sits below it. Every branch is fixture-driven with no network, as every decision in this file already is. - **D6 — the comment names the PRs it resolved and what it wants.** "`heavy-duty/rig#112` is merged; this issue's `offsite` flag is still up. Clear it and close the issue, or say what is still outstanding." Addressed to the assignee. It reports a fact and asks for a human-or-builder act — it does not perform one. ## Tasks - [ ] `actions/issueflow-reconcile/issueflow-reconcile.sh` — `offsite_cross_referenced_prs()` (timeline → `owner/repo#N` for `cross-referenced` events whose source is a PR), `offsite_pr_states()` (→ `OPEN`/`CLOSED`/`UNKNOWN`, mirroring the existing `reference_states`), and the pure `offsite_resolved_decision()`. - [ ] Wire it into the `claimed` branch, after the exemption gate, guarded by `has_issue_label offsite` — one extra timeline read only for issues that carry the flag. - [ ] `test/issueflow-reconcile.test.sh` — the rows below, plus a fixture timeline JSON exercising the parser. - [ ] `LABELS.md` — one sentence in `offsite`'s paragraph: the sweep will tell you when your flag has outlived its PR, and that it only ever tells you. - [ ] `CHANGELOG.md` — one line under `## Unreleased`, inserted **above** the heading below it. ## Acceptance criteria - [ ] An `offsite` issue whose only cross-referenced PR is merged gets exactly one comment, naming that PR — and gets **no** second comment on the next sweep. - [ ] An `offsite` issue with one merged and one open cross-referenced PR gets **no** comment. - [ ] An `offsite` issue with no visible cross-referenced PRs gets no comment and logs nothing alarming (the #16 case). - [ ] A timeline read that errors leaves the sweep's other work untouched and produces no comment. - [ ] No code path in either reconciler adds or removes `offsite`, and none reclaims an `offsite` issue — #68's criterion, re-asserted here because this issue is the one that could break it. - [ ] A non-`offsite` `claimed` issue triggers **no** extra timeline read (assert on the fetch being guarded, not just on the outcome). - [ ] `test/run.sh` green; shellcheck- and actionlint-clean. ## Test plan `offsite_resolved_decision` against fixture state lists: | # | cross-referenced PR states | expect | |---|---|---| | 1 | `CLOSED` | `NUDGE` | | 2 | `CLOSED CLOSED` | `NUDGE` | | 3 | `CLOSED OPEN` | `QUIET` | | 4 | `OPEN` | `QUIET` | | 5 | (empty — nothing visible) | `QUIET` | | 6 | `CLOSED UNKNOWN` | `QUIET` | Parser rows against a fixture timeline: a `cross-referenced` event whose source is a PR in another repo is extracted with its qualified reference; one whose source is an **issue** is ignored; a `mentioned`/`assigned` event is ignored; an empty timeline yields nothing. **The cases that must fail:** (a) flip row 3 to `NUDGE` and the suite goes red — "all closed" must not degrade to "any closed"; (b) flip row 6 to `NUDGE` and it goes red — `UNKNOWN` must never be read as resolved. ## Dependencies `Blocked by #68`. `Part of #56`. ## Constraint `LABELS.md` is vendored doctrine (the `.ceremony/` set); this repo is the source, so no re-sync happens here.
github-actions[bot] commented 2026-07-23 13:08:00 +00:00 (Migrated from github.com)

Every issue named by Blocked by is closed. The sweep is moving this issue to ready.

<!-- issueflow:blockers-cleared --> Every issue named by `Blocked by` is closed. The sweep is moving this issue to `ready`.
codex-bot-andresmgsl commented 2026-07-23 13:16:41 +00:00 (Migrated from github.com)

Starting this build now. I will implement the stale offsite nudge exactly as specified, with fixture-driven parser/decision coverage and guarded failure behavior.

Starting this build now. I will implement the stale `offsite` nudge exactly as specified, with fixture-driven parser/decision coverage and guarded failure behavior.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/ceremony#69
No description provided.