Loop template

tested 2026-07-07 ✓catalog data (JSON)

Content Freshness Loop

Check repository content for stale pages, dead links, superseded claims, and missing ownership records, then propose source-backed updates without publishing rewrites automatically.

Human guide

Raw README.md

Content Freshness Loop

The content-freshness loop audits repository docs and site content for stale pages, dead links, stale markers, and update-worthy claims. It writes a dated audit report, records audit state, and routes source-backed follow-up work without rewriting public content automatically.

This directory is a draft template. "Draft" is registry/site operational metadata for the template directory, not a hidden loop.yaml field; the manifest itself uses only the fields defined by spec/loop-spec-v0.1.md.

Files

  • loop.yaml — agent-readable manifest for the template.
  • AGENT-INSTALL.md — exact install commands for cron, GitHub Actions, and agent-kanban backends.
  • scripts/content_freshness.py — concrete standard-library runner used by all backends.
  • examples/targets.json — editable target list for a real Loopmaster run.
  • examples/dry-run-targets.json — local-only target list used by verification.
  • examples/github-actions/content-freshness.yml — ready-to-copy workflow.
  • verification/dry-read-ambiguity-checklist.md — independent dry-read checklist for this revision.

The seven human questions

1. What loop do I need?

Use this loop when a project publishes docs, templates, resources, or site pages that can rot over time and the desired output is an evidence-backed maintenance report plus optional review tasks. It matches the content freshness pattern in docs/research/loop-landscape.md §2 and the content-freshness template recommendation in §6.3.

2. What can it safely automate?

The manifest permits reading repository files and public URLs, writing an audit report, writing a state file, creating a proposal branch, and creating a follow-up task. It forbids public publishing, unreviewed content rewrites, spending money, deleting user data, and mutating source systems. These claims map to permissions.allowed_actions and permissions.forbidden_actions in loop.yaml.

3. What do I need to connect?

Minimum setup needs only this repository, Python 3, a JSON target list, an output directory, and a state path. GitHub Actions PR mode also needs the built-in GITHUB_TOKEN permissions declared in the workflow. Agent-kanban mode needs a configured task board or Hermes kanban installation. The default manifest does not require paid APIs or web-search credentials.

4. What can go wrong?

Automated link checks can be blocked by websites, old but still-correct evergreen pages can look stale, and broad stale-marker scans can produce low-value churn. The runner separates audit errors from findings, records skipped link counts, cites each finding with file path or URL evidence, and gates rewrites/deletions/public positioning changes on human approval.

5. How do I know it worked?

A successful run prints CONTENT_FRESHNESS_REPORT=..., CONTENT_FRESHNESS_STATE=..., FINDINGS=..., AUDIT_ERRORS=..., and LINKS_CHECKED=.... The report must cite repo-relative file paths, URLs, or explicit source checks for every finding. The exact verification commands are in loop.yaml under verification.commands and in AGENT-INSTALL.md.

6. How do I stop or roll it back?

For cron, remove the content_freshness.py line from the user's crontab. For GitHub Actions, disable or delete .github/workflows/content-freshness.yml. For agent-kanban, pause or remove the recurring task. Rollback is intentionally narrow: leave all source content untouched, delete only a failed run artifact if needed, and close or revert a bad audit PR. This maps to failure_policy.rollback.

7. What should I improve after the first run?

Tune examples/targets.json to include the content that matters, adjust per-target max_age_days, remove noisy stale markers, and add ownership notes for intentionally evergreen pages. Record maintenance evidence in the audit log path declared by maintenance.audit_log; the template's freshness cadence is monthly.

Backend behavior

All supported backends run the same helper and the same manifest validation command. Backend differences are installation and delivery only:

  • cron writes reports and state on the machine where cron runs.
  • github_actions writes reports/state on a branch and opens a pull request using peter-evans/create-pull-request.
  • agent_kanban creates a recurring agent task whose prompt runs the helper, reviews output, and creates follow-up cards only for source-backed findings.

Loopmaster adoption notes

The default examples/targets.json is already scoped to Loopmaster's current repo shape: docs/**/*.md, template READMEs/install guides, and site/src content. It writes reports to docs/maintenance/ and state to .loopmaster/state/content-freshness.json, so Loopmaster can adopt the template unchanged once the install PR is merged.

The default real target config enables external link checks. The dry-run config disables them so verification is deterministic and offline-friendly. If a website blocks automated checks, treat that as audit evidence, not as proof the cited content is false. Update or suppress noisy targets in targets.json; do not rewrite public copy based on a blocked link alone.

The job description (loop.yaml)

View raw loop.yaml

templates/content-freshness/loop.yaml

spec_version: "0.1"
id: content-freshness
name: Content Freshness Loop
purpose: Audit repository content for stale pages, dead links, superseded claims, and missing ownership evidence, then propose source-backed updates without publishing rewrites automatically.
audience: [human, agent, operator]
trigger:
  type: schedule
  default: monthly
  schedule: "0 10 1 * *"
inputs:
  sources:
    - type: file
      name: Content target configuration
      path: templates/content-freshness/examples/targets.json
    - type: url
      name: Links discovered in audited content
      path: templates/content-freshness/examples/targets.json
  files:
    - templates/content-freshness/examples/targets.json
    - templates/content-freshness/scripts/content_freshness.py
capabilities:
  - content_audit
  - link_checking
  - stale_claim_detection
  - source_backed_reporting
  - proposal_creation
permissions:
  allowed_actions:
    - read_repo
    - read_web
    - write_report
    - write_state
    - create_proposal_branch
    - create_task
  forbidden_actions:
    - publish_publicly_without_approval
    - rewrite_content_without_review
    - spend_money
    - delete_user_data
    - mutate_source_systems
  credential_scopes:
    - repo:contents:write
    - pull_request:write
    - kanban:create
state:
  paths:
    - .loopmaster/state/content-freshness.json
  retention: 180 days
outputs:
  artifacts:
    - docs/maintenance/YYYY-MM-DD-content-freshness.md
  notifications:
    - run_summary
  tasks:
    - kanban_card_for_review_required_update
verification:
  required:
    - every finding cites a repo-relative file path, URL, or explicit source check
    - broken or skipped link checks are separated from stale-content findings
    - report records whether it only proposed changes or needs human approval
    - public publishing, positioning changes, and content rewrites remain human-gated
    - state file records last run counts and does not hide repeated unresolved findings
  commands:
    - name: validate loop manifest
      run: uv run --with check-jsonschema --with pyyaml check-jsonschema --schemafile spec/loop.schema.json templates/content-freshness/loop.yaml
      expected: exits 0 and prints ok -- validation done
    - name: dry-run runner against local example targets
      run: python3 templates/content-freshness/scripts/content_freshness.py --targets templates/content-freshness/examples/dry-run-targets.json --state /tmp/loopmaster-content-freshness-state.json --output-dir /tmp/loopmaster-content-freshness --max-link-checks 5
      expected: exits 0 and prints CONTENT_FRESHNESS_REPORT and CONTENT_FRESHNESS_STATE paths
human_gates:
  required_for:
    - public publishing
    - content rewrites
    - product positioning changes
    - deleting stale content
    - credential changes
  approver: maintainer
failure_policy:
  retry: 2
  escalate_after: repeated_fetch_or_validation_failure
  rollback: leave previous content untouched; remove only failed run artifacts after recording the error; close or revert any bad proposal PR
maintenance:
  freshness_cadence: monthly
  owner: Loopmaster maintainers
  audit_log: loops/log/
backends:
  cron:
    schedule: "0 10 1 * *"
    install_notes:
      - Runs the checked-in Python helper from the target repository root.
      - Uses a persistent env file for paths and optional credentials; one-time shell exports are not assumed to survive cron.
  github_actions:
    workflow: .github/workflows/content-freshness.yml
    install_notes:
      - Workflow runs monthly or manually, writes an audit report, updates state, and opens a pull request with those artifacts only.
  agent_kanban:
    task_template: Create a monthly content-freshness task that runs the helper, reviews findings, and creates follow-up cards only for source-backed updates requiring human review.
    install_notes:
      - Kanban installation assigns a recurring agent task; the agent still runs the same helper and verification commands before proposing edits.

Install guide (for your agent)

Raw for agents

Agent install guide — Content Freshness Loop

Run commands from the repository root unless a step says otherwise. Replace paths only when your target repo intentionally uses different report/state locations.

0. Preflight

python3 --version
python3 templates/content-freshness/scripts/content_freshness.py --help
uv run --with check-jsonschema --with pyyaml check-jsonschema \
  --schemafile spec/loop.schema.json \
  templates/content-freshness/loop.yaml

Expected output:

Python 3.x.x
usage: content_freshness.py ...
ok -- validation done

Failure branches:

  • python3: command not found — install Python 3 or use a backend image that includes it.
  • No such file or directory — confirm you are at repo root and that templates/content-freshness/ exists.
  • schema validation failure — fix loop.yaml before installing any backend.

1. Verify the concrete runner locally

rm -rf /tmp/loopmaster-content-freshness /tmp/loopmaster-content-freshness-state.json
python3 templates/content-freshness/scripts/content_freshness.py \
  --targets templates/content-freshness/examples/dry-run-targets.json \
  --state /tmp/loopmaster-content-freshness-state.json \
  --output-dir /tmp/loopmaster-content-freshness \
  --max-link-checks 5

Expected output shape:

CONTENT_FRESHNESS_REPORT=/tmp/loopmaster-content-freshness/YYYY-MM-DD-content-freshness.md
CONTENT_FRESHNESS_STATE=/tmp/loopmaster-content-freshness-state.json
FINDINGS=0 or more
AUDIT_ERRORS=0
LINKS_CHECKED=0 or more

Then inspect the generated report path printed as CONTENT_FRESHNESS_REPORT. Every finding must cite a path, URL, or explicit source check.

Failure branches:

  • AUDIT_ERRORS is non-zero on the dry run — the local target config is wrong; fix paths in examples/dry-run-targets.json.
  • report findings lack source evidence — fix the runner or target config before scheduling.
  • dry run attempts network access — confirm examples/dry-run-targets.json has "link_check": {"enabled": false, ...}.

2. Configure targets

Edit a project-local copy of templates/content-freshness/examples/targets.json. Keep the top-level shape:

{
  "targets": [
    {"type": "file_glob", "name": "Docs", "path": "docs/**/*.md", "max_age_days": 90},
    {"type": "file_glob", "name": "Template guides", "path": "templates/**/README.md", "max_age_days": 120}
  ],
  "stale_markers": ["TODO", "FIXME", "TBD", "PLACEHOLDER"],
  "link_check": {"enabled": true, "timeout_seconds": 15}
}

Supported by the checked-in runner: file_glob targets, Markdown links, HTML href links, local file existence checks, and optional public HTTP/HTTPS link checks.

For Loopmaster itself, the checked-in examples/targets.json is intentionally adoptable unchanged: it scans docs/**/*.md, template READMEs/install guides, and site/src content. It writes reports to docs/maintenance/ and state to .loopmaster/state/content-freshness.json in the backend steps below.

No paid API credential exists by default. If a future target needs a private API, persist its secret in the backend secret store and update the manifest, target config, and backend command in the same PR. A one-time export SOME_TOKEN=... is not a valid cron install because cron does not inherit that interactive shell.

3. Backend A — cron

Cron runs on one machine and writes reports/state into that checkout. It does not open a PR by itself.

Create a persistent environment file, even if it currently contains no secrets:

mkdir -p .loopmaster/state .loopmaster/env docs/maintenance
cat > .loopmaster/env/content-freshness.env <<'EOF'
# Optional secrets go here as KEY=value lines.
# Do not commit this file.
CONTENT_FRESHNESS_TARGETS=templates/content-freshness/examples/targets.json
CONTENT_FRESHNESS_STATE=.loopmaster/state/content-freshness.json
CONTENT_FRESHNESS_OUTPUT_DIR=docs/maintenance
CONTENT_FRESHNESS_MAX_LINK_CHECKS=50
EOF
chmod 600 .loopmaster/env/content-freshness.env
printf '\n.loopmaster/env/*.env\n' >> .git/info/exclude

Install the cron entry:

REPO_ROOT=$(pwd)
CRON_LINE="0 10 1 * * cd $REPO_ROOT && set -a && . ./.loopmaster/env/content-freshness.env && set +a && /usr/bin/env python3 templates/content-freshness/scripts/content_freshness.py --targets \"$CONTENT_FRESHNESS_TARGETS\" --state \"$CONTENT_FRESHNESS_STATE\" --output-dir \"$CONTENT_FRESHNESS_OUTPUT_DIR\" --max-link-checks \"$CONTENT_FRESHNESS_MAX_LINK_CHECKS\" >> .loopmaster/state/content-freshness.cron.log 2>&1"
( crontab -l 2>/dev/null | grep -v 'templates/content-freshness/scripts/content_freshness.py' ; printf '%s\n' "$CRON_LINE" ) | crontab -
crontab -l | grep 'content_freshness.py'

Expected output: one crontab line containing templates/content-freshness/scripts/content_freshness.py.

Test the exact command outside cron:

set -a && . ./.loopmaster/env/content-freshness.env && set +a
python3 templates/content-freshness/scripts/content_freshness.py \
  --targets "$CONTENT_FRESHNESS_TARGETS" \
  --state "$CONTENT_FRESHNESS_STATE" \
  --output-dir "$CONTENT_FRESHNESS_OUTPUT_DIR" \
  --max-link-checks "$CONTENT_FRESHNESS_MAX_LINK_CHECKS"

Expected output: CONTENT_FRESHNESS_REPORT=docs/maintenance/..., CONTENT_FRESHNESS_STATE=.loopmaster/state/content-freshness.json, and counts.

Failure branches:

  • crontab: command not found — this backend is unavailable; use GitHub Actions or agent-kanban.
  • cron log shows missing env vars — check .loopmaster/env/content-freshness.env permissions and the set -a && . ... segment.
  • secrets are needed — add KEY=value to .loopmaster/env/content-freshness.env only; never commit it and never rely on a one-time interactive export.
  • AUDIT_ERRORS is non-zero — inspect .loopmaster/state/content-freshness.cron.log and the generated report; fix target paths or network access before treating the run as verified.

Stop cron:

crontab -l | grep -v 'templates/content-freshness/scripts/content_freshness.py' | crontab -

4. Backend B — GitHub Actions

GitHub Actions runs monthly or manually, writes a report/state update, and opens a PR. It uses repository permissions, not local cron secrets.

Install the workflow:

mkdir -p .github/workflows .loopmaster/state docs/maintenance
cp templates/content-freshness/examples/github-actions/content-freshness.yml .github/workflows/content-freshness.yml
git add .github/workflows/content-freshness.yml templates/content-freshness/examples/targets.json
git diff --cached -- .github/workflows/content-freshness.yml

Expected output: a workflow containing:

permissions:
  contents: write
  pull-requests: write

Commit and push the workflow in the normal PR flow. After merge, run it manually:

gh workflow run content-freshness.yml
sleep 10
gh run list --workflow content-freshness.yml --limit 1

Expected output shape:

STATUS  TITLE  WORKFLOW  BRANCH  EVENT  ID  ELAPSED  AGE

When the run succeeds, expected result is a new PR titled docs(maintenance): add content freshness audit if the runner changed docs/maintenance/*.md or .loopmaster/state/content-freshness.json. If there are no changes because the exact same dated report/state is already committed, peter-evans/create-pull-request completes without opening a duplicate PR.

Failure branches:

  • gh: command not found or unauthenticated — use the GitHub web UI to run the workflow, or authenticate gh.
  • workflow fails at checkout or permission step — confirm repo Actions permissions allow contents: write and pull-requests: write.
  • workflow fails in Run content freshness audit — inspect the run log; fix examples/targets.json, glob patterns, or network assumptions.
  • private API target added without secret — remove that target and matching manifest credential fields, or add the provider secret under GitHub repository Settings → Secrets and variables → Actions and reference it in the workflow env: block.

Disable GitHub Actions backend:

git rm .github/workflows/content-freshness.yml
git commit -m "chore: disable content freshness workflow"

5. Backend C — agent-kanban

Agent-kanban assigns a recurring task to an agent. The task must still run the concrete helper and record evidence; it is not a prose-only reminder.

For Hermes kanban, create a recurring scheduler job or board task whose prompt is self-contained. Example prompt:

Run the Loopmaster content-freshness loop from repo root. Commands:
1. uv run --with check-jsonschema --with pyyaml check-jsonschema --schemafile spec/loop.schema.json templates/content-freshness/loop.yaml
2. python3 templates/content-freshness/scripts/content_freshness.py --targets templates/content-freshness/examples/targets.json --state .loopmaster/state/content-freshness.json --output-dir docs/maintenance
Then read the generated report path printed as CONTENT_FRESHNESS_REPORT. For high-severity findings or source-backed stale claims, create follow-up kanban cards that cite the exact file:line or URL evidence. Do not rewrite public content, delete content, publish corrections, or change product positioning without human approval. Complete with the report path, state path, counts, and any cards created.

Using Hermes native cron job scheduling from an agent session:

cronjob(action="create", name="Loopmaster content freshness", schedule="0 10 1 * *", workdir="/absolute/path/to/repo", prompt="<the self-contained prompt above>")

Expected result: the scheduler returns a job id. On each run, completion evidence must include CONTENT_FRESHNESS_REPORT, CONTENT_FRESHNESS_STATE, FINDINGS, AUDIT_ERRORS, and LINKS_CHECKED.

Failure branches:

  • no kanban or scheduler backend exists — use cron or GitHub Actions instead.
  • agent creates tasks without running the helper — reject the run; the backend is misinstalled because this template's verification requires the concrete helper output.
  • agent wants to rewrite/publish/delete content or change positioning — block for human approval as declared in human_gates.required_for.

6. Final install verification checklist

Before calling an install complete, record:

uv run --with check-jsonschema --with pyyaml check-jsonschema \
  --schemafile spec/loop.schema.json \
  templates/content-freshness/loop.yaml
python3 templates/content-freshness/scripts/content_freshness.py \
  --targets templates/content-freshness/examples/dry-run-targets.json \
  --state /tmp/loopmaster-content-freshness-state.json \
  --output-dir /tmp/loopmaster-content-freshness \
  --max-link-checks 5

Expected success:

ok -- validation done
CONTENT_FRESHNESS_REPORT=/tmp/loopmaster-content-freshness/YYYY-MM-DD-content-freshness.md
CONTENT_FRESHNESS_STATE=/tmp/loopmaster-content-freshness-state.json
FINDINGS=0 or more
AUDIT_ERRORS=0
LINKS_CHECKED=0 or more

If any expected line is missing, the install is not verified.

How we know it works

Status
tested
Date
2026-07-07
Stale after
2026-08-06

dated checks passed

templates/content-freshness/verification/dry-read-ambiguity-checklist.md

What it's allowed to do

6 allowed action(s), 5 forbidden action(s), 3 credential scope(s)

Allowed actions
read_repo, read_web, write_report, write_state, create_proposal_branch, create_task
Credential scopes
repo:contents:write, pull_request:write, kanban:create
Forbidden actions
publish_publicly_without_approval, rewrite_content_without_review, spend_money, delete_user_data, mutate_source_systems