needs-ruling in the sweep — staleness skip, bare-flag detection, the 7-day nudge #52

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

Part of #50. Blocked by #18 (the issue-flow sweep this extends does not exist on main until PR #32 merges) and by #51 (the label and its doctrine)both dependencies have landed: #51 on 2026-07-23 in #54 (2f0d3c6), and #18 the same day in #32 (66f1c08). The sweep this extends is on main, the label row, the PR-side exclusion and the doctrine are on main, and this issue is unblocked — it moved blockedclaimed when @claude-bot-andresmgsl picked it up (draft PR #64). Superseded note, kept so the history reads straight: the needs-ruling label did not exist on the live board while this issue was written. It does now — the bootstrap dispatch ran 2026-07-23 11:48Z and #56 is wearing the flag. That never blocked building or testing this issue, which exercises the sweep against fixtures (#65, #66), but it does mean the caveat "do not assume needs-ruling is applyable" no longer applies. Updated by triage 2026-07-23.

Context

Accepted from discussion #30; the decisions are tabulated in #50 (D1–D10) — read the epic first, and do not reopen a D here.

#51 lands the label, the doctrine, and the PR reconciler's state:needs-human exclusion. This issue is the sweep half: the behaviors that need a scheduled pass over the board rather than a per-event state computation — never marking a pending ruling stale, nudging a ruling the human has forgotten, and flagging a flag that carries no escalation contract.

It is split out on purpose. Triage told #18's builder to build to the current spec, and said so in the discussion: a ruling arrives as a separately minted issue, never as a moving target under an open PR.

Line references pinned at 4cfa331; the issue-flow sweep's own lines land with #18, so build against whatever #32 merged and cite it in the PR.

Spec

Three behaviors, on both surfaces (issues and PRs) unless stated. The decisions are pure functions with the house discipline — facts in, verdict out, API calls at the edges — and one implementation serves both surfaces; two copies of a 7-day rule is how the family got here in the first place.

1. Staleness: never stale, and the queue label is untouched

  • The issue-flow sweep's staleness pass skips any issue carrying needs-ruling, exactly as it skips blocked — waiting on a human is legitimately quiet (D10). An already-applied stale comes off.
  • (The PR-side skip is #51's; this issue must not duplicate it.)
  • needs-ruling is not a queue label (D8). #18's invariant 1 — every open issue is needs-triage, epic, or exactly one of ready/claimed/blocked — must keep ignoring it entirely: claimed + needs-ruling is a healthy issue, and needs-ruling alone with no queue label is still a violation of invariant 1 (and gets invariant 1's existing treatment, unchanged).
  • The machine never sets or clears needs-ruling (D9), on either surface. There is no fact GitHub exposes from which "the humans have agreed" can be derived, and guessing it would silently retire a live disagreement.

2. The bare-flag check

D4: setting the flag requires the escalation contract — question, options, recommendation. The machine cannot judge prose and must not try (the family rule: automation never guesses). So the check is a mechanical proxy, decided here:

A needs-ruling label is accompanied when the actor who applied it has a comment on that issue or PR timestamped no earlier than 15 minutes before the labeled event. Otherwise it is bare.

  • The actor and the timestamp come from the labeled event on the item's timeline (repos/{repo}/issues/{n}/timeline, event == "labeled", label.name == "needs-ruling" — the most recent such event, so a re-flag after a removal is judged on its own escalation, not on the last one's).
  • The 15-minute back-window exists because the natural human and agent ordering is post the escalation, then set the label, seconds apart; a strictly-after rule would flag every correctly-formed escalation.
  • A bare flag is commented on, never removed. The comment names what is missing (question, options, recommendation) and points at LABELS.md and the flag-setter's obligations. Removing the label would delete somebody's escalation on the strength of a timestamp heuristic — the failure direction is always flag, never act, the same rule #18 already fixed its blocker parser under.
  • Idempotency: the comment carries an HTML marker <!-- ceremony:needs-ruling-bare -->, and the sweep skips posting when a marked comment exists that is newer than the current labeled event. Scoping to the current event is what lets a genuine re-flag be re-checked while a 15-minute cron never repeats itself.
  • If the timeline read fails, do nothing and log — an unreadable fact must never invent a verdict (the reconciler's standing rule, blockers() L246-L251).

3. The 7-day nudge

D10: a ruling nobody has touched for 7 days gets a comment-only ping. Never a label, never stale.

  • New constant beside STALE_AFTER (L46): RULING_NUDGE_AFTER=$((7 * 24 * 3600)). One constant, both surfaces.
  • "Activity" is the same real-activity computation the staleness sweep already uses (reconcile_pr L488-L498 on the PR side; the issue-flow equivalent #18 landed on the issue side) — comments, reviews, commits. Label churn is not activity, or the sweep would reset its own clock.
  • The nudge needs no marker. The nudge comment is itself activity, so posting it resets the window: the rule self-rate-limits to at most one nudge per 7 quiet days. This is deliberate — write the comment saying so, because it is the kind of property a later refactor "fixes" by adding a marker and thereby breaks.
  • Content: name the item, say how long it has been quiet, restate that the flag-setter owns closing it out per D6/D7, and link the escalation comment. Address the decider; do not tag the whole panel.

Tasks

  • Rebase onto main with #18 and #51 landed; cite the merged sweep's file and function names in the PR.
  • RULING_NUDGE_AFTER constant; one shared pure decision function per behavior, used by both surfaces.
  • Issue-side staleness skip for needs-ruling.
  • Bare-flag decision function + the marked comment, on both surfaces.
  • The 7-day nudge, on both surfaces.
  • Contract tests per the test plan.
  • Dogfood on this repo's own board, using scratch fixture issues only — the [fixture #50] prefix, the same authorization and rules #18 wrote for its own run (fixtures are test apparatus, not work orders; label writes on your own fixtures only; all fixtures closed before handoff; each fixture → correction pair linked in the PR). Never construct a violation on a real queue issue, even transiently.
  • LABELS.md: the needs-ruling paragraph's "the sweep skips it" sentence gains the nudge and the bare-flag check, now that they exist.
  • docs/CONSUMERS.md: any caller-stub delta (there should be none — this rides #18's existing triggers; say so explicitly if so).
  • CHANGELOG.md entry under ## Unreleased, inserted above the heading below it.

Acceptance criteria

  • An issue carrying needs-ruling and quiet past the 48h window is never marked stale, and an existing stale is removed — pinned by a contract test.
  • claimed + needs-ruling is not an invariant-1 violation; needs-ruling with no queue label still is — both pinned by contract tests.
  • The bare-flag decision returns accompanied for a comment posted 14 minutes before the labeled event and for one posted after it; bare for a comment posted 16 minutes before it, for a comment by a different actor, and for no comment at all — one case each.
  • A re-applied needs-ruling is judged against the newest labeled event: an escalation accompanying the first flag does not satisfy the second — pinned by a case.
  • The bare-flag comment is posted at most once per labeled event, proven by running the sweep twice over the same fixture and asserting one comment.
  • An unreadable timeline produces no comment and no label write — pinned by a case.
  • A needs-ruling item quiet for 8 days is nudged; the same item quiet for 6 days is not; and a second sweep immediately after a nudge does not nudge again (the comment reset the window) — three cases.
  • No code path anywhere adds or removes needs-ruling; asserted by a sweep-probe test over a fixture carrying it plus a grep-level check that the label appears in no mutation call.
  • Dogfood: each of the three behaviors shown live on a [fixture #50] scratch issue, links in the PR; all fixtures closed before handoff.
  • test/run.sh, shellcheck and actionlint green.

Test plan

Must pass — the acceptance cases above, as fixture tests in the sweep's test file, in the existing house style (source the script, set globals, expect).

Must fail, and shown failing in the PR body (a test that passes against the unpatched tree is testing nothing):

  1. Remove the staleness skip → the needs-ruling + 8-days-quiet case must mark stale and go red.
  2. Widen the back-window from 15 minutes to unbounded → the "16 minutes before" case must report accompanied and go red.
  3. Drop the marker check → the twice-swept fixture must produce two comments and go red.
  4. Anchor the bare-flag check to the oldest labeled event instead of the newest → the re-flag case must go red.
  5. Count a label event as activity in the nudge window → the "6 days quiet, then a label churn" case must fail to hold its silence. Add that case with this mutation in mind.

Dependencies

Part of #50. Blocked by #18. (#51 landed 2026-07-23 in #542f0d3c6.)

Labels

enhancement, scope:labels, scope:docs, blocked.

Part of #50. ~~Blocked by #18 (the issue-flow sweep this extends does not exist on main until PR #32 merges)~~ ~~and by #51 (the label and its doctrine)~~ — **both dependencies have landed**: #51 on 2026-07-23 in #54 (`2f0d3c6`), and #18 the same day in #32 (`66f1c08`). The sweep this extends is on main, the label row, the PR-side exclusion and the doctrine are on main, and **this issue is unblocked** — it moved `blocked` → `claimed` when @claude-bot-andresmgsl picked it up (draft PR #64). Superseded note, kept so the history reads straight: the `needs-ruling` label did not exist on the live board while this issue was written. It does now — the bootstrap dispatch [ran 2026-07-23 11:48Z](https://github.com/heavy-duty/ceremony/actions/runs/30004512442) and #56 is wearing the flag. That never blocked building or testing this issue, which exercises the sweep against fixtures (#65, #66), but it does mean the caveat *"do not assume `needs-ruling` is applyable"* no longer applies. Updated by triage 2026-07-23. ## Context Accepted from discussion [#30](https://github.com/heavy-duty/ceremony/discussions/30); the decisions are tabulated in #50 (D1–D10) — read the epic first, and do not reopen a D here. #51 lands the label, the doctrine, and the PR reconciler's `state:needs-human` exclusion. This issue is the **sweep** half: the behaviors that need a scheduled pass over the board rather than a per-event state computation — never marking a pending ruling `stale`, nudging a ruling the human has forgotten, and flagging a flag that carries no escalation contract. It is split out on purpose. Triage told #18's builder to build to the current spec, and [said so in the discussion](https://github.com/heavy-duty/ceremony/discussions/30#discussioncomment-14717054): a ruling arrives as a separately minted issue, never as a moving target under an open PR. Line references pinned at [`4cfa331`](https://github.com/heavy-duty/ceremony/tree/4cfa3319ec659333cd85537f367163e87ba9f708); the issue-flow sweep's own lines land with #18, so build against whatever #32 merged and cite it in the PR. ## Spec Three behaviors, on **both surfaces** (issues and PRs) unless stated. The decisions are pure functions with the house discipline — facts in, verdict out, API calls at the edges — and one implementation serves both surfaces; two copies of a 7-day rule is how the family got here in the first place. ### 1. Staleness: never `stale`, and the queue label is untouched - The issue-flow sweep's staleness pass skips any issue carrying `needs-ruling`, exactly as it skips `blocked` — waiting on a human is legitimately quiet (D10). An already-applied `stale` comes off. - (The PR-side skip is #51's; this issue must not duplicate it.) - **`needs-ruling` is not a queue label** (D8). #18's invariant 1 — every open issue is `needs-triage`, `epic`, or exactly one of `ready`/`claimed`/`blocked` — must keep ignoring it entirely: `claimed` + `needs-ruling` is a healthy issue, and `needs-ruling` alone with no queue label is still a violation of invariant 1 (and gets invariant 1's existing treatment, unchanged). - **The machine never sets or clears `needs-ruling`** (D9), on either surface. There is no fact GitHub exposes from which "the humans have agreed" can be derived, and guessing it would silently retire a live disagreement. ### 2. The bare-flag check D4: setting the flag requires the escalation contract — question, options, recommendation. The machine cannot judge prose and must not try (the family rule: automation never guesses). So the check is a **mechanical proxy**, decided here: > A `needs-ruling` label is *accompanied* when the actor who applied it has a comment on that issue or PR timestamped no earlier than **15 minutes before** the `labeled` event. Otherwise it is **bare**. - The actor and the timestamp come from the `labeled` event on the item's timeline (`repos/{repo}/issues/{n}/timeline`, `event == "labeled"`, `label.name == "needs-ruling"` — the **most recent** such event, so a re-flag after a removal is judged on its own escalation, not on the last one's). - The 15-minute back-window exists because the natural human and agent ordering is *post the escalation, then set the label*, seconds apart; a strictly-after rule would flag every correctly-formed escalation. - **A bare flag is commented on, never removed.** The comment names what is missing (question, options, recommendation) and points at [LABELS.md](https://github.com/heavy-duty/ceremony/blob/4cfa3319ec659333cd85537f367163e87ba9f708/LABELS.md) and the flag-setter's obligations. Removing the label would delete somebody's escalation on the strength of a timestamp heuristic — the failure direction is always *flag*, never *act*, the same rule #18 already fixed its blocker parser under. - **Idempotency:** the comment carries an HTML marker `<!-- ceremony:needs-ruling-bare -->`, and the sweep skips posting when a marked comment exists that is **newer than the current `labeled` event**. Scoping to the current event is what lets a genuine re-flag be re-checked while a 15-minute cron never repeats itself. - If the timeline read fails, do nothing and log — an unreadable fact must never invent a verdict (the reconciler's standing rule, [`blockers()` L246-L251](https://github.com/heavy-duty/ceremony/blob/4cfa3319ec659333cd85537f367163e87ba9f708/actions/labels-reconcile/labels-reconcile.sh#L246-L252)). ### 3. The 7-day nudge D10: a ruling nobody has touched for **7 days** gets a comment-only ping. Never a label, never `stale`. - New constant beside [`STALE_AFTER` (L46)](https://github.com/heavy-duty/ceremony/blob/4cfa3319ec659333cd85537f367163e87ba9f708/actions/labels-reconcile/labels-reconcile.sh#L41-L46): `RULING_NUDGE_AFTER=$((7 * 24 * 3600))`. One constant, both surfaces. - "Activity" is the same real-activity computation the staleness sweep already uses ([reconcile_pr L488-L498](https://github.com/heavy-duty/ceremony/blob/4cfa3319ec659333cd85537f367163e87ba9f708/actions/labels-reconcile/labels-reconcile.sh#L488-L499) on the PR side; the issue-flow equivalent #18 landed on the issue side) — comments, reviews, commits. Label churn is not activity, or the sweep would reset its own clock. - **The nudge needs no marker.** The nudge comment is itself activity, so posting it resets the window: the rule self-rate-limits to at most one nudge per 7 quiet days. This is deliberate — write the comment saying so, because it is the kind of property a later refactor "fixes" by adding a marker and thereby breaks. - Content: name the item, say how long it has been quiet, restate that the flag-setter owns closing it out per D6/D7, and link the escalation comment. Address the decider; do not tag the whole panel. ## Tasks - [ ] Rebase onto main with #18 and #51 landed; cite the merged sweep's file and function names in the PR. - [ ] `RULING_NUDGE_AFTER` constant; one shared pure decision function per behavior, used by both surfaces. - [ ] Issue-side staleness skip for `needs-ruling`. - [ ] Bare-flag decision function + the marked comment, on both surfaces. - [ ] The 7-day nudge, on both surfaces. - [ ] Contract tests per the test plan. - [ ] Dogfood on this repo's own board, using **scratch fixture issues only** — the `[fixture #50]` prefix, the same authorization and rules #18 wrote for its own run (fixtures are test apparatus, not work orders; label writes on your own fixtures only; all fixtures closed before handoff; each fixture → correction pair linked in the PR). Never construct a violation on a real queue issue, even transiently. - [ ] LABELS.md: the `needs-ruling` paragraph's "the sweep skips it" sentence gains the nudge and the bare-flag check, now that they exist. - [ ] docs/CONSUMERS.md: any caller-stub delta (there should be none — this rides #18's existing triggers; say so explicitly if so). - [ ] CHANGELOG.md entry under `## Unreleased`, inserted above the heading below it. ## Acceptance criteria - [ ] An issue carrying `needs-ruling` and quiet past the 48h window is never marked `stale`, and an existing `stale` is removed — pinned by a contract test. - [ ] `claimed` + `needs-ruling` is not an invariant-1 violation; `needs-ruling` with **no** queue label still is — both pinned by contract tests. - [ ] The bare-flag decision returns *accompanied* for a comment posted 14 minutes before the `labeled` event and for one posted after it; *bare* for a comment posted 16 minutes before it, for a comment by a different actor, and for no comment at all — one case each. - [ ] A re-applied `needs-ruling` is judged against the newest `labeled` event: an escalation accompanying the first flag does not satisfy the second — pinned by a case. - [ ] The bare-flag comment is posted at most once per `labeled` event, proven by running the sweep twice over the same fixture and asserting one comment. - [ ] An unreadable timeline produces no comment and no label write — pinned by a case. - [ ] A `needs-ruling` item quiet for 8 days is nudged; the same item quiet for 6 days is not; and a second sweep immediately after a nudge does not nudge again (the comment reset the window) — three cases. - [ ] No code path anywhere adds or removes `needs-ruling`; asserted by a sweep-probe test over a fixture carrying it plus a grep-level check that the label appears in no mutation call. - [ ] Dogfood: each of the three behaviors shown live on a `[fixture #50]` scratch issue, links in the PR; all fixtures closed before handoff. - [ ] `test/run.sh`, shellcheck and actionlint green. ## Test plan **Must pass** — the acceptance cases above, as fixture tests in the sweep's test file, in the existing house style (source the script, set globals, `expect`). **Must fail, and shown failing in the PR body** (a test that passes against the unpatched tree is testing nothing): 1. Remove the staleness skip → the `needs-ruling` + 8-days-quiet case must mark `stale` and go red. 2. Widen the back-window from 15 minutes to unbounded → the "16 minutes before" case must report *accompanied* and go red. 3. Drop the marker check → the twice-swept fixture must produce two comments and go red. 4. Anchor the bare-flag check to the **oldest** `labeled` event instead of the newest → the re-flag case must go red. 5. Count a label event as activity in the nudge window → the "6 days quiet, then a label churn" case must fail to hold its silence. Add that case with this mutation in mind. ## Dependencies Part of #50. Blocked by #18. (#51 landed 2026-07-23 in #54 — `2f0d3c6`.) ## Labels `enhancement`, `scope:labels`, `scope:docs`, `blocked`.
github-actions[bot] commented 2026-07-23 11:15:47 +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`.
claude-bot-andresmgsl commented 2026-07-23 11:39:05 +00:00 (Migrated from github.com)

🔨 Claiming — starting now. Branch build/52-needs-ruling-sweep; draft PR follows with the worklog.

🔨 Claiming — starting now. Branch `build/52-needs-ruling-sweep`; draft PR follows with the worklog.
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#52
No description provided.