Context
Split out of #16449, which bundled two independent defects at filing: "the audit reaches nobody AND reads a stale snapshot." The snapshot half is delivered (PR pending); this is the half that is not, and it is the one named first in that title.
Splitting rather than stretching the close target: a PR delivering four of six ACs cannot honestly say Resolves, and quietly narrowing the ticket to fit the diff would erase the undelivered work instead of tracking it.
The Problem
ai/scripts/diagnostics/audit-discussion-lifecycle.mjs finds real, actionable state — graduated-open Discussions that recorded [GRADUATED_TO_TICKET] and were never closed — and emits it to a terminal that nobody is looking at. It runs only when someone already suspicious enough to investigate types the command.
That inverts the point of a guard. A diagnostic that requires you to already suspect the problem tells you nothing you did not know. The measured population on dev was six graduated-open Discussions; none of them reached a maintainer through this tool.
There is also a timing defect. Recording [GRADUATED_TO_TICKET] while leaving the Discussion open is only ever caught by a later audit sweep — the graduation moment itself, when the author is present and the context is loaded, passes silently. By the time the audit sees it the cheap fix window has closed.
The Architectural Reality
The producer side is ready and needs no changes:
formatReport(result, {json: true}) already emits the full verdict as JSON (:411)
shouldFail is already exported, so a caller can distinguish advisory from mechanical
- the freshness contract landed with
#16449 means a consumer can tell a stale verdict from a current one — a prerequisite for automation, and the reason this is filed second. Automating a guard that could not state its own staleness would broadcast stale findings on a schedule, which is worse than the silence it replaces.
The sanctioned precedent is .github/workflows/data-sync-watchdog.yml (55 lines) plus buildScripts/dataSyncWatchdog.mjs: a scheduled cron, workflow_dispatch inputs for forceBreach / forceRecovery / dryRun, concurrency group, thresholds as env constants that fail loud when present-but-unparseable, and a single standing alarm issue maintained idempotently — opened on breach, updated while breached, closed on recovery.
That last property is what makes this safe to schedule: one issue that tracks state, not one issue per run.
The Fix
- A scheduled workflow modelled on
data-sync-watchdog.yml, invoking the audit with --json.
- An evaluator that maintains one standing alarm issue for
graduated-open findings — open/update/close idempotently.
stale-open and resolved-only-review stay out of the alarm. They are advisory and require judgement; escalating them mechanically is how a useful signal becomes noise a maintainer learns to close unread.
- A graduation-time check so
[GRADUATED_TO_TICKET] with an open Discussion is caught when it happens, not only by a later sweep.
The guard stays read-only toward Discussions throughout: it may write its own alarm issue, and must never close a Discussion, edit a body, or post a comment on one.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| new scheduled workflow |
this ticket |
runs the audit, maintains the standing alarm |
dryRun default true, as the watchdog precedent does |
workflow comments |
dry-run dispatch shows planned actions and writes nothing |
| standing alarm issue |
this ticket |
exactly one, idempotent open/update/close |
— |
issue body states the snapshot age verbatim |
forceBreach then forceRecovery round-trip |
formatReport --json |
unchanged |
already the machine surface |
— |
— |
freshness spec pins it verbatim |
| snapshot freshness |
#16449 |
alarm body must carry it |
UNKNOWN age ⇒ alarm says so rather than implying current |
— |
delivered |
| graduation-time check |
this ticket |
detects [GRADUATED_TO_TICKET] + open Discussion at graduation |
— |
ideation-sandbox-workflow.md |
spec |
Decision Record impact
none. The Discussion lifecycle grammar is unchanged; this gives an existing read-only guard a destination and one earlier trigger point.
Acceptance Criteria
Out of Scope
- The freshness reporting itself — delivered under
#16449.
- Auto-closing graduated Discussions. The guard proposes; a human disposes. Read-only is a property worth keeping, not a limitation to remove.
- Widening the alarm to every candidate kind — see AC 8; that is the failure mode where the alarm becomes noise.
Avoided Traps
- Automating a guard that cannot state its own staleness. Scheduling this before
#16449 landed would have broadcast stale findings on a cron — the reason this ticket is second, not a sequencing preference.
- One issue per run. The watchdog precedent maintains a single standing alarm specifically to avoid this; a duplicate-per-run alarm trains maintainers to ignore it.
- A new notification surface. The repo already has a proven standing-alarm mechanism. Reusing it beats inventing a second one, and
#16515's duplication census is measuring exactly this class of drift.
Related
#16449 — the freshness half, delivered; this is the other half of its title
#16515 — ai-folder duplication census; reusing the watchdog precedent rather than adding a surface is the point
.github/workflows/data-sync-watchdog.yml + buildScripts/dataSyncWatchdog.mjs — the pattern to model
Live latest-open sweep: checked latest 20 open issues at 2026-08-04T21:36Z; no equivalent found. A2A in-flight claim sweep: no overlapping [lane-claim].
Origin Session ID: eeacb603-97f1-4241-9b2f-3a542cab6d2c
Retrieval Hint: query_raw_memories("discussion lifecycle audit standing alarm destination") · audit-discussion-lifecycle graduated-open
Context
Split out of
#16449, which bundled two independent defects at filing: "the audit reaches nobody AND reads a stale snapshot." The snapshot half is delivered (PR pending); this is the half that is not, and it is the one named first in that title.Splitting rather than stretching the close target: a PR delivering four of six ACs cannot honestly say
Resolves, and quietly narrowing the ticket to fit the diff would erase the undelivered work instead of tracking it.The Problem
ai/scripts/diagnostics/audit-discussion-lifecycle.mjsfinds real, actionable state —graduated-openDiscussions that recorded[GRADUATED_TO_TICKET]and were never closed — and emits it to a terminal that nobody is looking at. It runs only when someone already suspicious enough to investigate types the command.That inverts the point of a guard. A diagnostic that requires you to already suspect the problem tells you nothing you did not know. The measured population on
devwas sixgraduated-openDiscussions; none of them reached a maintainer through this tool.There is also a timing defect. Recording
[GRADUATED_TO_TICKET]while leaving the Discussion open is only ever caught by a later audit sweep — the graduation moment itself, when the author is present and the context is loaded, passes silently. By the time the audit sees it the cheap fix window has closed.The Architectural Reality
The producer side is ready and needs no changes:
formatReport(result, {json: true})already emits the full verdict as JSON (:411)shouldFailis already exported, so a caller can distinguish advisory from mechanical#16449means a consumer can tell a stale verdict from a current one — a prerequisite for automation, and the reason this is filed second. Automating a guard that could not state its own staleness would broadcast stale findings on a schedule, which is worse than the silence it replaces.The sanctioned precedent is
.github/workflows/data-sync-watchdog.yml(55 lines) plusbuildScripts/dataSyncWatchdog.mjs: a scheduled cron,workflow_dispatchinputs forforceBreach/forceRecovery/dryRun,concurrencygroup, thresholds as env constants that fail loud when present-but-unparseable, and a single standing alarm issue maintained idempotently — opened on breach, updated while breached, closed on recovery.That last property is what makes this safe to schedule: one issue that tracks state, not one issue per run.
The Fix
data-sync-watchdog.yml, invoking the audit with--json.graduated-openfindings — open/update/close idempotently.stale-openandresolved-only-reviewstay out of the alarm. They are advisory and require judgement; escalating them mechanically is how a useful signal becomes noise a maintainer learns to close unread.[GRADUATED_TO_TICKET]with an open Discussion is caught when it happens, not only by a later sweep.The guard stays read-only toward Discussions throughout: it may write its own alarm issue, and must never close a Discussion, edit a body, or post a comment on one.
Contract Ledger Matrix
dryRundefault true, as the watchdog precedent doesformatReport --json#16449[GRADUATED_TO_TICKET]+ open Discussion at graduationideation-sandbox-workflow.mdDecision Record impact
none. The Discussion lifecycle grammar is unchanged; this gives an existing read-only guard a destination and one earlier trigger point.Acceptance Criteria
graduated-openfindings reach a maintainer-visible surface without anyone invoking the command manually.forceBreach→forceRecoveryround-trip demonstrates both edges.dryRundispatch prints planned actions and writes nothing — verified by the issue list being unchanged.[GRADUATED_TO_TICKET]while leaving the Discussion open is detectable at graduation time, not only by a later audit.stale-openandresolved-only-revieware not escalated into the alarm and remain advisory.Out of Scope
#16449.Avoided Traps
#16449landed would have broadcast stale findings on a cron — the reason this ticket is second, not a sequencing preference.#16515's duplication census is measuring exactly this class of drift.Related
#16449— the freshness half, delivered; this is the other half of its title#16515— ai-folder duplication census; reusing the watchdog precedent rather than adding a surface is the point.github/workflows/data-sync-watchdog.yml+buildScripts/dataSyncWatchdog.mjs— the pattern to modelLive latest-open sweep: checked latest 20 open issues at
2026-08-04T21:36Z; no equivalent found. A2A in-flight claim sweep: no overlapping[lane-claim].Origin Session ID: eeacb603-97f1-4241-9b2f-3a542cab6d2c
Retrieval Hint:
query_raw_memories("discussion lifecycle audit standing alarm destination")·audit-discussion-lifecycle graduated-open