Loop template
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.mdContent 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:
cronwrites reports and state on the machine where cron runs.github_actionswrites reports/state on a branch and opens a pull request usingpeter-evans/create-pull-request.agent_kanbancreates 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.
Optional external link checks
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.yamlspec_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 agentsAgent 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.yamlExpected output:
Python 3.x.x
usage: content_freshness.py ...
ok -- validation doneFailure 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 thattemplates/content-freshness/exists.- schema validation failure — fix
loop.yamlbefore 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 5Expected 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 moreThen inspect the generated report path printed as CONTENT_FRESHNESS_REPORT. Every finding must cite a path, URL, or explicit source check.
Failure branches:
AUDIT_ERRORSis non-zero on the dry run — the local target config is wrong; fix paths inexamples/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.jsonhas"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/excludeInstall 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.envpermissions and theset -a && . ...segment. - secrets are needed — add
KEY=valueto.loopmaster/env/content-freshness.envonly; never commit it and never rely on a one-time interactiveexport. AUDIT_ERRORSis non-zero — inspect.loopmaster/state/content-freshness.cron.logand 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.ymlExpected output: a workflow containing:
permissions:
contents: write
pull-requests: writeCommit 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 1Expected output shape:
STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGEWhen 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 foundor unauthenticated — use the GitHub web UI to run the workflow, or authenticategh.- workflow fails at checkout or permission step — confirm repo Actions permissions allow
contents: writeandpull-requests: write. - workflow fails in
Run content freshness audit— inspect the run log; fixexamples/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 5Expected 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 moreIf 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