Operant Studio
OPER-169

OPER: sync-product-checksums never fires on GITHUB_TOKEN merges; close the anti-recursion gap

Band
SMALL
Suggested
Justin Cooke

Body

---
feature_id: FEAT-studio-dispatcher
filing_template_version: v1
decision_memo: docs/pdt/2026.09.09-observation-checksum-anti-recursion.md
blocked_by:
---

## Context

On 2026.09.09 a fleet-authored PR (#451) landed a change to `products/studio/feature-map.md` declaring `FEAT-studio-pipeline-observability`. The `sync-product-checksums.yml` workflow (OPER-94) is set to run on `push` to `main` with `paths: [products/*/feature-map.md, products/*/dependency-map.md]`. It did NOT fire, because GitHub's anti-recursion rule suppresses `push` events for merges authored by GITHUB_TOKEN.

The result: `Product.featureMapChecksum` in the DB stayed on the prior sha256; every subsequent ticket POST to /api/studio/operant/tasks rejected with `map_drift` for hours. Three of our own filings (OPER-159/160/161) were rejected in sequence. Manual `workflow_dispatch` was required to unstick everything.

The workflow header comment already names this failure mode:

> Safety net: if a push event is ever suppressed (GitHub's anti-recursion rule for GITHUB_TOKEN-authored merges) this workflow never fires and the checksums silently rot. `product-checksum-drift-check` in migrate-prod.yml runs every 6h, compares live rows against the files on main, and alerts #dev-alerts. Remediation: `gh workflow run sync-product-checksums.yml` (blank slug = all).

A 6h alert + manual remediation is a stall the size of a working day. Every fleet-authored change to feature-map.md causes this. We need the workflow to fire itself.

## Options considered

A. Add `workflow_run` trigger keyed to `agent-automerge` completion so it fires after any merge that could have touched a manifest.
B. Fire on schedule every hour (down from the drift-check's every 6h) with a fast no-op when nothing changed.
C. Have the merge author (dispatcher / agent-automerge) POST a `workflow_dispatch` for sync-product-checksums.yml explicitly whenever the merged diff touches `products/*/{feature-map,dependency-map}.md`.
D. Change the merge PAT to a non-GITHUB_TOKEN identity so `push` events fire normally.

Option C is the surgical fix: cost-free, no polling, keyed exactly to the trigger condition. Option B is the belt-and-suspenders backstop. Option D touches token identity and blast radius, PDT/DOC territory.

## Acceptance Criteria

1. When `agent-automerge.yml` (or any workflow that squash-merges an agent PR) lands a PR whose diff includes any file under `products/*/{feature-map,dependency-map}.md`, it also calls `POST /repos/{o}/{r}/actions/workflows/sync-product-checksums.yml/dispatches` with `ref: main` in the same job. This is Option C above.
2. Add Option B as a scheduled backstop: `sync-product-checksums.yml` also runs on `schedule: - cron: '17 * * * *'` (hourly at :17 to avoid the top-of-hour rush). The workflow is already idempotent per the OPER-94 header, so hourly is safe.
3. Preserve the existing manual `workflow_dispatch` path unchanged.
4. Add a self-test `scripts/sync-checksums-fired-selftest.sh` that (a) merges a synthetic manifest change via the same merge path agent-automerge uses, (b) asserts sync-product-checksums.yml has a new run within 3 minutes, (c) asserts Product.featureMapChecksum reflects the new content.
5. Do NOT widen the workflow's UPDATE scope; per the OPER-94 header this stays one table, two columns, keyed by slug.
6. Add a note to the workflow header explaining Options B+C were adopted, so a future reader understands why the trigger set expanded.

## Verification

- `bash scripts/sync-checksums-fired-selftest.sh` passes locally and in CI.
- Manual repro: land a small feature-map.md tweak via a fleet PR, watch sync-product-checksums.yml fire within seconds via workflow_run (Option C) OR within one hour via schedule (Option B).
- Re-run this ticket's filing test: file a synthetic ticket after a fleet manifest change and see `admission=ok` rather than `map_drift`.

## Notes

- Class: `Silent-Anti-Recursion-Stall`. The 15:54Z sync run on 2026.09.09 was itself a workflow_dispatch (probably a prior human intervention), so the current pattern is 'someone notices the fleet is stuck, dispatches manually.' That is not autonomous operation.
- Blocks nothing new; unblocks smooth throughput on every fleet manifest edit.

Attachments

Loading attachments…

Comments

Loading comments…