rules
Create Rule
Create Rule › Request Body
titlebodyconfidencetagsstalenessCreate Rule › Responses
Successful Response
Get Rules Due For Revalidation
Approved rules this org's last nightly compute_rule_staleness run
flagged as past the staleness threshold — due for a human to confirm
still true, or open a deprecation/refresh PR. Backs gnt stale.
A stopgap, not the final home for this: the eventual plan is to surface this prompt in a weekly digest email, which doesn't exist yet — it's waiting on that digest itself and on the nightly contradiction-sweep worker (workers/tasks_contradictions.py), which also needs to land first. This list is the same underlying signal, surfaced as CLI visibility instead — wire it into the digest once that email exists rather than building a second parallel version of it.
Registered before /{rule_id} in this file, but doesn't actually need to be — "staleness"/"due" are two path segments, "{rule_id}" matches exactly one, so there's no route collision to order around.
query Parameters
limitGet Rules Due For Revalidation › Responses
Successful Response
Get Staleness Observability
Staleness-bot task 7's own observability surface: rules under TTL
watch (and how many have opted out via staleness: off), check-tier
pass-through (how many nightly checks stopped at NoChange/Medium/High,
over a trailing 30-day window), PRs opened, and Slack check-in
send/response counts — plus whether this org's bot-wide kill switch is
currently on. See gnt.staleness_observability for what composes what.
Registered before /{rule_id} for the same non-reason get_rules_due_for_revalidation's own comment already gives — no route collision to order around, "staleness" and "observability" are two path segments, "{rule_id}" only ever matches one.
Get Staleness Observability › Responses
Successful Response
Update Reviewable Rule
Edits the text a human is reviewing before it is proposed.
Approved and pending-merge rules remain immutable here: their review record lives in GitHub, so changing them requires the existing amendment or reject flow instead of silently rewriting what was reviewed.
path Parameters
rule_idUpdate Reviewable Rule › Responses
Successful Response
Propose Rule
Renders the rule to markdown and opens a PR against the org's
connected repo. This is approve's replacement — approval now means a
human merging that PR on GitHub, not an in-terminal keypress (see
docs/migration/RECONCILE_V2.md). The actual approved-status transition,
including the HMAC-signed write and the previous-version supersede
dance approve_rule used to do here, happens in the webhook handler
once a real merge is confirmed — this endpoint only ever writes
pending_merge, never approved, so none of that machinery belongs
in this function anymore.
path Parameters
rule_idPropose Rule › Responses
Successful Response
Batch Propose Rules
propose_rule's batched sibling — one branch, one
put_file per rule, ONE pull request for the whole batch, and every
rule in it moves to pending_merge together carrying the SAME
prNumber/prUrl. Built for gnt prebrain's draft-rule output (5-8
related rules reviewed as one PR), but not restricted to that caller.
Validation fails the WHOLE batch with a clear error naming the offending rule(s) rather than silently skipping or partially processing — a batch is one reviewable unit; a caller that asked for 5 rules and silently got 3 proposed has no way to know that happened short of diffing IDs itself. Nothing GitHub-side happens until every rule in the batch has already passed validation.
Batch Propose Rules › Request Body
rule_idsBatch Propose Rules › Responses
Successful Response
Edit Rule
Creates a new draft version of an approved rule. The old rule stays exactly as-is (still approved, still served) until the new version is itself approved — see approve_rule's supersede step.
path Parameters
rule_idEdit Rule › Request Body
titlebodyconfidencetagsstalenessEdit Rule › Responses
Successful Response
Create Rule From Document
The web-upload entry point of PDF ingestion (see docs/superpowers/specs/2026-08-04-pdf-ingestion-design.md) -- the only entry point available to a teammate with no CLI or GitHub access. Extracts text server-side (document_extract.extract_document_text), builds a draft rule directly from that text -- no LLM drafting step, matching routers/webhooks.py's ingest_webhook (the other ambient- third-party-content entry point, which also builds a CreateRuleRequest straight from already-final text rather than running an extraction model over it first) -- then immediately submits and proposes it, committing the original PDF into the same PR under sources/uploads/ so "every answer traces back to a merged PR" stays true for PDF-derived rules too.
enforce_document_upload_rate_limit, not require_entitled_admin directly: this still needs the same entitlement + admin-role gate propose_rule/batch_propose_rules apply (it opens a PR too), but also throttles per org -- see that dependency's own docstring in rate_limit.py for why a text-extraction-per-request endpoint needs a throttle propose_rule/batch_propose_rules don't.
Ordering, all deliberate: size/emptiness first (cheapest check), then the format-allowlist check (extension_from_filename against SUPPORTED_DOCUMENT_EXTENSIONS -- still just string comparisons, no I/O), then the GitHub-connection check (so a doc uploaded with no repo connected fails before any extraction/LLM-quota work happens at all, not after), then the quota/plan-cap gates (cheap DB reads -- an org that's already over its cap is told no before it burns up to _PDF_EXTRACTION_TIMEOUT_SECONDS of extraction time on a request that was always going to be rejected), then extraction, then the actual draft-rule + propose work.
extract_document_text is synchronous, in-process work (anydoc, a pure-Rust library -- no subprocess involved) that can still take real wall-clock time on a large PDF -- calling it directly inside this async def would block the whole event loop for every concurrent upload. Run via loop.run_in_executor against _PDF_EXTRACTION_EXECUTOR (a small, dedicated thread pool -- see its own comment above for why not asyncio.to_thread's shared default pool), wrapped in asyncio.wait_for so this request doesn't hang past _PDF_EXTRACTION_TIMEOUT_SECONDS. That wait_for bounds only the HTTP response -- it does NOT reclaim the worker thread a hung conversion is blocked on; see _PDF_EXTRACTION_TIMEOUT_SECONDS's own comment for what actually contains that.
Create Rule From Document › Responses
Successful Response