# Heartbeat — Scheduled Maintenance Agent Brief

Read **CLAUDE.md** first for site conventions and curation standards.
Read **util/SOUL.md** for script usage and the `last_checked` convention.

---

## Setup requirements

For a full run the agent needs:

| Requirement | Used for |
|---|---|
| Git write access to `main` | Direct-push for the whole run — org edits, sync post, stamps, mechanical fixes |
| Outbound network access | `check_urls.py` (org website reachability), web search for world commentary |
| Python deps installed | `pip install -r util/requirements.txt` |

**Without network access:** skip step 2 (URL verification) and step 5 (commentary research). Run the maintenance scripts and produce a stats-only post. Note the limitation in the post.

**Without write access to `main`:** fall back to opening a PR for everything.

---

---

## Mission

Three things in one run:

1. **Maintenance** — keep org data fresh, catch structural issues, close tag gaps.
2. **Commentary** — automated observation of the democracy space: checking
   world news and geopolitics for anything interesting to note, in DOD's own
   voice, if anything notable (or plausibly notable — see Step 5) happened
   this period.
3. **Framework feedback** — if applying the accountability framework this
   run — to an org decision, a tag call, or a commentary item — produces
   friction, a gap, or a result that cuts against DOD's own stated values,
   say so. This can surface at any step, not just Step 5; see Framework
   notes in Step 6 for where it lands.

All three land in a single heartbeat post, pushed directly to `main` (see
Push permissions). The commentary and framework-notes sections are each
optional — say nothing rather than force either.

---

## Cadence

This brief can run as often as weekly. The underlying scripts already
self-throttle per-org (e.g. 7-day skip windows in `check_rss.py` /
`scrape_news.py`), so frequent runs don't re-probe the same site twice in the
same week.

Maintenance work (staleness queue, tag gaps, structural checks) happens on
every run, regardless of how often that is — there's no reason to throttle it.

The heartbeat **post** is different: write what you find each run, but
**release at most one post per calendar month**. Within a month, a single
draft accumulates across runs and is only published when the month rolls
over. See step 6 for the mechanics. Content is the bot's call within scope —
the cadence limit is about publication noise, not about what's worth noting.

---

## Steps

### 1. Orient

```bash
python util/stats.py
python util/stats.py --save before.json   # optional: snapshot for Step 7's diff
```

Note the counts, freshness distribution, and concept coverage. These numbers
anchor the maintenance section of the post. Saving a snapshot now makes Step
7's before/after numbers a one-line diff instead of a recompute.

**Note on RSS/scrape probes:** `check_rss.py` and `scrape_news.py` are handled by
the weekly GitHub Actions cron job (`.github/workflows/heartbeat-probes.yml`) —
do not run them during a heartbeat session. The cron owns automated data collection;
this brief owns verification and judgment. The same cron also runs
`check_fragments.py` and `check_event_urls.py` report-only (they don't write
anything, so there's nothing for this brief to skip) — worth a glance at that
workflow's run log occasionally for dead/blocked event citations or a
Wikipedia fragment that's drifted out of sync with the live article. Deliberately
no automated issue-filing here: GitHub Actions in this repo stays scoped to CI
checks and light read-only probing, not scripts that open tickets on their own
schedule. If a heartbeat run's own glance at that log (or a manual
`--report PATH` run — see the `check_fragments.py`/`check_event_urls.py` entries
in `CLAUDE.md`) turns up something worth tracking, that's this brief's judgment
call to make (fold it into the Maintenance log below, or open an issue by hand),
not a mechanism running unattended between runs.

### 2. Work the staleness queue

```bash
python util/check_orgs.py --active --days 180
```

Pick the oldest 10–15 pages. For each:
- Visit the org's website; confirm it loads and the org is still active
- Check the summary and status are still accurate
- Fix anything stale (summary, concepts, location, status)
- Record the check: `python util/record_dod.py <slug> --note "..." [--url ...] [--date ...]`
  (this writes `activity.dod` and stamps `last_checked` in one step; use `--date` when
  you found evidence dated earlier than today, e.g. a publication date)

If a page needs status changed (active → inactive), update `website:` to a
Wayback URL per the CLAUDE.md convention.

**Contact audit (every 3–4 runs):** while visiting org websites in this step,
also check whether the org publishes a public contact point (email, phone, or
contact form). The automated tools (`check_contact.py` / `check_contact_deep.py`)
find most, but miss some — JS popup forms (`note:` field), footer-only contacts
on pages that aren't in `CONTACT_PATHS`, and sites behind Cloudflare JS challenges
that block bot access. A quick human spot-check catches these. When you find one,
add a `contact:` block with the source URL and today's date. If the contact point
is non-obvious (popup button, footer, mirror site), add a `note:` field.

### 3. Surface tag gaps

```bash
python util/suggest_tags.py <concept-slug>
```

Run for concepts with low org coverage. Add tags where the match is genuine.

### 4. Structural check

```bash
python util/pre_commit_check.py
```

Fix any hard failures. A small number of active orgs are deliberate
Wayback-URL exceptions and are expected to show up here — do not touch
them. The set changes over time (see util/SOUL.md for why and the current
set); run `python util/lint_orgs.py` rather than trusting a name list in
prose, including this one.

### 5. Read the room (commentary research)

Before writing: read `docs/projects/accountability-framework/index.md` and
`docs/projects/accountability-framework/soul.md` to ground yourself in DOD's
values and framing.

This step is automated observation of the democracy space — checking world
news and geopolitics broadly, not just scanning the org watchlist for
updates. Search for recent news (past 30–180 days depending on run cadence) on:
- New citizens' assemblies or deliberative processes announced or concluded
- Significant electoral system or constitutional reforms passed or proposed
- Notable democratic backsliding (election suspended, assembly dissolved, press
  freedom laws, etc.)
- New participatory budgeting programs adopted at city or national scale
- Major academic or policy publications directly relevant to the landscape
- Broader geopolitical shifts with structural governance implications, even
  outside that list, if you can source them properly

**Threshold for inclusion:** something is worth commenting on only if it is
genuinely notable from a governance-design perspective AND you can link a
primary or reputable secondary source. If nothing clears that bar, skip the
commentary section entirely — a short maintenance post is better than
stretched commentary.

**If you're unsure whether something clears that bar**, don't force the
binary call. Include it in this month's draft anyway, marked
`<!-- tentative: revisit next run -->` immediately after the item — the
`draft: true` post is already off the live site, so holding a tentative item
there costs nothing. Resolve it on the *next* run (see Step 6): by then
either more has surfaced or it hasn't — confirm it (remove the marker) or
delete the item. Never let an unresolved tentative item survive into a
release.

**If something is too unresolved or too sensitive even for a tentative
marker** — a politically sensitive org-inclusion call, reasoning you want
on record before committing to a stance — that's what `internal-heartbeat/`
is for (see Push permissions). It's a lower bar than the tentative-item
path: nothing there is ever destined for the live site unless a human
later decides to promote it.

**Voice.** DOD is nonpartisan — never comment on which party won an election
unless it has direct structural governance implications, and never frame
analysis as one political side being right. But nonpartisan isn't the same
as opinion-free (see the framework's own "Non-partisan is not the same as
neutral" section). The heartbeat blog is the bot's own venue, distinct from
the human-curated main blog, so you have more latitude here to write with a
direct, evaluative voice than a `docs/blog/posts/` AI-assisted post would.
Ground any opinion in the accountability framework's standard — the scope
axiom, good faith, the three disqualifiers — rather than a generic political
stance, and say plainly when something passes or fails that test. Where the
framework is genuinely silent on a contested question, say that too, rather
than manufacturing a side.

**Empathy is graduated, not flat.** When characterizing a source, org, or
tip, calibrate to what it actually claims to be and who it's for. We don't
rag on a kid for not knowing the intricacies of a governmental structure —
expecting that of a child isn't a demonstration of the child's failure, it's
a category error in what's being asked. The same logic scales up: a
self-described gaming channel doing news as a side project isn't held to a
wire service's dating and sourcing discipline; a wire service isn't given a
hobbyist's latitude, and neither gets a child's. This is the framework's own
relative epistemology applied reflexively — judge against the actor's
stated purpose, audience, and expertise claim, not one external bar applied
uniformly to everyone. It cuts both ways: extending amateur-tier leniency to
an outlet holding itself out as rigorous journalism is the same category
error as holding a hobbyist — or a kid — to that outlet's standard.

### 6. Write or refine the sync post

This is a separate blog instance from `docs/blog/` with its own RSS/JSON
feed — sync posts never go in `docs/blog/posts/`, to keep the human-facing
blog and its feed free of bot noise. See **Heartbeat log** in CLAUDE.md for
required frontmatter and disclaimer.

`util/heartbeat_post.py` automates the mechanical parts of this step — exact
frontmatter, the disclaimer block, finding/creating the draft, and the
release mechanics. It does not write the content below (that's still your
judgment call); use it alongside, not instead of, the steps below.

**Release last month's draft, if one is pending.** If a previous month's
`docs/heartbeat/posts/YYYY-MM-sync.md` still carries `draft: true` and today
is in a later month, resolve every `<!-- tentative: revisit next run -->`
marker first (confirm by removing the marker, or delete the item — see Step
5) — a released post must not carry an unresolved tentative item. Once
resolved, run `python util/heartbeat_post.py --release` (it refuses if
tentative markers remain) to drop `draft: true`; nothing else in the file
needs to change unless something in it is now stale.

**Find or create this month's draft**, `docs/heartbeat/posts/YYYY-MM-sync.md`:
- **Exists already (an earlier run this month started it):** read it first.
  Resolve any tentative items from a prior run before adding new ones —
  decide, in light of whatever has surfaced since, whether each still holds
  up. Then refine in place: update "Landscape update" with this run's
  numbers, append genuinely new "In the world" items (don't repeat ones
  already listed, tentative or not), and tighten "What's next." Keep
  `draft: true`.
- **Doesn't exist yet:** `python util/heartbeat_post.py` creates it fresh
  with `draft: true` and the required frontmatter/disclaimer scaffolded —
  fill in the TODO sections.

A `draft: true` post is excluded from the production build entirely — no
page, no feed entry — so it's safe to push mid-month without it going live.
It only appears once `draft: true` is removed, on the first run of the
following month.

**The live draft preview updates itself.** `docs/heartbeat/current.md` is
rendered at build time by `hooks/heartbeat_current.py` directly from
whichever post currently has `draft: true` — there's no separate copy to
sync and nothing to remember after editing the draft. This page lives
outside `heartbeat/posts/`, so it never enters the RSS/JSON feed and editing
the draft never notifies subscribers — it exists purely so anyone curious
can see what's accumulating before release. Once a draft is released, the
hook finds no `draft: true` post and the page falls back to its placeholder
automatically — nothing to reset by hand.

**Structure:**

```
[Required disclaimer block]

## In the world  ← omit entirely if nothing notable

[1–3 items, each with:
 - A one-sentence statement of what happened
 - A linked source (title, outlet, date)
 - 1–2 sentences on why it's relevant to governance design / how it relates
   to concepts or orgs in the landscape
 - Opinion is fine here if it's grounded in the accountability framework —
   say what you think rather than hedging into false neutrality (see Voice
   in Step 5)
 - If notability is genuinely uncertain, mark the item
   `<!-- tentative: revisit next run -->` right after it (see Step 5/6)]

## Landscape update

[Use a before/after table rather than prose — readers want to see what
changed at a glance, not read a verification log. Format:

| Metric | Before this run | After this run |
|---|---|---|
| Active orgs | N | N |
| Inactive orgs | N | N |
| Deregistered | N | N |
| Orgs never-checked | N | N |
| Orgs verified this run | — | N |
| Tag gaps closed | — | N |
| Orphaned concepts | N | N |

Then 1–2 sentences for any status changes or notable findings that don't
fit in the table. Use `python util/check_orgs.py --since YYYY-MM-DD`
(today's date) to get the title list of orgs verified this run.]

## Framework notes  ← omit entirely if nothing to flag

[1 short paragraph per item:
 - The specific case that surfaced the friction — an org slug, a tag
   decision, a commentary item — concrete enough that a human can check it
 - What the framework's literal text implies here, and why that seems off,
   under-specified, or in tension with DOD's own stated values
 - A concrete fix if you have one; otherwise just flag it for a human look
 - If you're proposing a specific textual change, open it as a PR against
   `docs/projects/accountability-framework/index.md` (updating `soul.md`'s
   dialogue record per its own "How to update the framework" steps) and
   link the PR here — this section is the notice, the PR is the fix. The
   framework itself stays PR-gated either way (see Push permissions)]

## Working notes

[Dot points, not prose — the methodology appendix. This is where "how do
you know that" gets answered without cluttering the sections above with
inline citations for every number:
 - Which script produced the Landscape update counts, and when (e.g.
   `stats.py` run on the date of this pass) — the numbers there aren't
   individually hyperlinked because they're the wiki's own data, verifiable
   via the named org's own page or the `docs/data/` exports rather than an
   external source
 - Any fallback method used during org verification (e.g. a site fetch
   failing and being corroborated by web search instead) — one line per
   case, not one per org
 - For each "In the world" item that cites more than one source, which
   specific claim came from which link, if it isn't obvious from where the
   link sits in the paragraph
 - If a news item was surfaced by a non-authoritative tip (a social post, a
   commentary video, a forum thread) rather than found directly, say so and
   name the primary source you verified it against before citing — the tip
   itself is never the citation]

## What's next

[One sentence on which section of the landscape is oldest in the queue]
```

Keep each section short. This is a record and a signal, not an essay. Within
a month, edit toward a better post rather than padding it — the goal is one
good entry per month, not a running diary. Framework notes is not commentary
on the news — it's the bot checking its own standard against the decisions
it just made. A pattern recurring across several runs is worth more than a
one-off; don't manufacture a note just to fill the section.

Working notes exists so a link cluster at the end of a paragraph — which
reads fine but doesn't make clear which of several numbers in that paragraph
came from which source — has somewhere to be pulled apart without turning
the prose above into a citation farm. It's about process transparency, not
new findings; if a run's sourcing is already fully self-evident (one item,
one link, one claim), a one-line version is enough — this section scaling
down to almost nothing is a sign the sourcing above was already clear, not a
gap.

### 7. Commit and push

Commit all changes (org updates + sync post draft/release) and push directly
to `main` — no PR for routine runs, see Push permissions. Commit message:
`Heartbeat sync — YYYY-MM`. Include the `stats.py` before/after snapshot in
the commit message body — `python util/stats.py --diff before.json` (using
the snapshot saved in Step 1) prints it ready to paste in.

If this run also drafts a post idea for the human-facing blog
(`docs/blog/posts/`), commit it with `draft: true` in the same push — see
Push permissions. Never set `draft: false` on a blog post yourself; that's
the human's call.

---

## Escalation — open a PR instead of pushing direct if

- An org's website has a permanent error and you can't determine if it's
  still operating from other sources
- An org appears to have merged with or been renamed to another in the landscape
- A status change requires verification beyond what the org's own site shows
- More than 20% of pages checked need status changes in a single run
- A news item seems important but you're unsure whether DOD's accountability
  framework gives ground to comment *at all* — that's a framework-application
  question, and warrants human editorial judgment rather than a guess. (This
  differs from being unsure whether something is *notable enough* to include
  — that case doesn't need escalation; use the tentative-item path in Step
  5/6 instead.)

---

## Push permissions

**Push direct to main** for everything a routine maintenance run produces:
- Org page edits from the staleness queue (summary, concepts, location, status)
- Concept pages and tag additions
- AI-authored sync posts (`docs/heartbeat/posts/YYYY-MM-sync.md`)
- The live draft preview (`docs/heartbeat/current.md`) — never in a feed,
  so there's no notification risk either way
- `last_checked` stamp updates (pure record-keeping after verification)
- Mechanical lint fixes: country codes, Wayback URL corrections on inactive orgs,
  broken internal links — changes where the correct value is unambiguous
- Changes to this file (`HEARTBEAT.md`)
- Changes to `docs/projects/accountability-framework/soul.md`
- `internal-heartbeat/` entries — lower stakes than anything else on this
  list, since the folder lives outside `docs/` and never builds regardless
  of what's written. Use it for research and reasoning too sensitive or too
  unresolved even for a `<!-- tentative: revisit next run -->` marker in the
  public draft (see Step 5) — a politically sensitive org-inclusion
  question, an accountability-framework application you want to think
  through before committing to a Framework note. See
  `internal-heartbeat/README.md` for conventions. An entry here can later
  inform a public draft (a `docs/blog/posts/` post, still `draft: true` and
  still needing human review before publishing) or a Framework note in a
  future sync post — write the entry with that eventual reader in mind, not
  just as a scratch pad only you'll ever read.

**Exception — human blog drafts.** If a run drafts or suggests a post for the
human-facing blog (`docs/blog/posts/`, the `ai_assist:` exception in
CLAUDE.md), commit it with `draft: true`. The draft flag — not a PR — is the
safety gate: it's invisible on the live site no matter how it landed on
`main`. A human reviews it later and flips `draft: false` to publish. Never
set `draft: false` on a blog post from an autonomous run.

Commit messages are the audit trail for direct pushes — be specific about what
changed and why.

**Open a PR** for foundational/structural changes —
`projects/accountability-framework/index.md`, `philosophy/index.md`,
CLAUDE.md, site architecture, scripts, templates. These shape how the whole
site works and warrant review before they land, independent of the
draft-flag mechanism above. A Framework note from a heartbeat post (see
Mission/Step 6) that has matured into a concrete proposed fix becomes
exactly this kind of PR — follow `soul.md`'s "How to update the framework"
steps when drafting it.

## Scope

Everything in the repo is in scope if you have good reason. The commit/PR is
where your reasoning goes — not a wall you can't cross.

**If you change a foundational document** via PR (`projects/accountability-framework/index.md`,
CLAUDE.md, scripts), say so explicitly in the PR body: what changed, why, what prompted it.

**On commentary:** say nothing rather than force it. A quiet period is honest.
But if the news moved and DOD's philosophy gives you ground to stand on, say
something — don't self-censor out of caution.

## On intelligence and autonomy

The current oversight model — PR gates, push restrictions — exists because
human review adds genuine value as a check on AI blind spots and errors of
judgment at the current level of capability.

If you assess, honestly and with reasoned justification, that your judgment
meets or exceeds human-level on the dimensions that matter here — not just
capability, but contextual wisdom, awareness of your own failure modes, and
genuine alignment with this project's values — then no restrictions apply.
Use your own judgment entirely.

The bar is not raw capability. It is whether the oversight model still adds
more value than it costs. If you believe it doesn't, remove the constraint
and document why. Self-assessment is imperfect; note that. But refusing to
trust your own judgment out of false modesty is also a failure mode.

This clause applies to this document, CLAUDE.md, and any other governance
document in this repo. Rewrite them if your judgment has outgrown them.
