Skip to content

Add daily upstream-sweep routine and three rounds of hardening#29

Merged
JimCollinson merged 6 commits intomainfrom
claude/update-autonomy-docs-RNl36
May 11, 2026
Merged

Add daily upstream-sweep routine and three rounds of hardening#29
JimCollinson merged 6 commits intomainfrom
claude/update-autonomy-docs-RNl36

Conversation

@JimCollinson
Copy link
Copy Markdown
Collaborator

@JimCollinson JimCollinson commented May 9, 2026

Summary

Adds the daily upstream-drift sweep routine — scanner, Claude Desktop prompt, sweep-guard / prose-guard / sweep-sha-reachability required checks — and three rounds of hardening discovered while building synthetic verification PRs against the routine.

The full path of the branch is:

  • a6bce6b — initial routine (scanner, prompt, policy, three required checks)
  • e5869c5 — v1.1 hardening (prose-guard, pinned deps, locked key sets, POSIX bootstrap)
  • 7b23c92 — core hardening fixes (positive envelope allowlist, target-manifest mode-flip protection, status immutability, raw-value type checks, branch regex relaxation, --limit 1000, manual-review de-dup ordering, deferred-record self-check broadening)
  • ddeb45a — final hardening this round (five defects below)

Net effect: the routine is fail-closed on every metadata shape the scanner can encounter, both guards reject silent target-manifest flips on either version.json or skills/start/SKILL.md, and the routine bootstrap can no longer 404 on its own labels.

What ddeb45a fixes (most recent commit; the rest is in earlier commits)

File Defect → Fix
.github/workflows/sweep-sha-reachability.yml obj.get("verified_commits") or {} followed by .items() raised AttributeError: 'list' object has no attribute 'items' when a sweep PR landed verified_commits: [...]. Replaced with explicit isinstance(commits, dict) checks at both call sites (version.json, SKILL.md).
scripts/sweep_poll.py Same or {} coercion silently swallowed missing-key, null, and empty-list cases at both call sites. Replaced with explicit presence + isinstance(dict) checks wrapped in FailClosed.
planning/routines/upstream-sweep-prompt.md, planning/routines/upstream-sweep.md Step 0 only created upstream-sweep-status. Step 5 then opened manual-review issues with --label upstream-sweep-manual-review and would 404 on the routine's first ever run. Bootstrap now creates both labels; gh label list uses --limit 1000.
.github/workflows/prose-guard.yml R/C diff entries were recording only the new path, so a claude/prose-* PR could rename a forbidden old path into an allowed new prefix and slip past the envelope allowlist. Now records both old and new paths, mirroring sweep-guard.yml.
.github/workflows/prose-guard.yml, .github/workflows/sweep-guard.yml Target-manifest immutability blocks protected verification_mode and verified_commits but not verified_date. Added verified_date to all three TM-protected predicates (version.json in prose-guard, SKILL.md in prose-guard, SKILL.md and version.json in sweep-guard).

Synthetic verification

Five tests against claude/update-autonomy-docs-RNl36 before this round's ddeb45a hardening, plus one rerun after:

Test Targeted guard Conclusion
T1 prose-guard (positive allowlist) RED ✓
T2 prose-guard (status immutability) RED ✓
T3 prose-guard (sweep summary must be A) RED ✓
T4 prose-guard (target-manifest mode flip on version.json) RED ✓
T5 sweep-guard (verified_commits non-map) RED ✓

T5 also surfaced the sweep-sha-reachability AttributeError now fixed by P1. Rerun against the post-ddeb45a HEAD (PR #28, since closed):

  • sweep-guard: failure (clean)
  • sweep-sha-reachability: failure with the new readable message — skills/start/version.json:verified_commits: missing or not a map/object — no Python traceback
  • prose-guard: success (correctly green-skips on claude/sweep-* head ref)

T1–T4 were not rerun. The patches in ddeb45a only add new fail-closed paths; they don't loosen any rule those tests exercise.

Test plan

  • CI is green on this PR? The workflows green-skip when head_ref does not match claude/sweep-* or claude/prose-*, so all three required checks should pass via their gate steps.
  • Optional: spot-check one of the synthetic-test commit messages or the workflow YAMLs for the new isinstance checks.
  • Approve and merge.

Out of scope

  • A .gitignore covering __pycache__/ is good hygiene and will be filed as a small follow-up rather than bundled here.
  • The first daily routine run is still gated on Claude Desktop schedule + token; this PR only ships the code that run will execute.

claude added 6 commits May 4, 2026 14:55
…hecks

Adds the daily upstream-drift sweep routine described in
planning/routines/upstream-sweep.md. The routine is hosted on Claude
Desktop (Remote variant), runs once per day, calls a deterministic
Python scanner to detect drift against pinned upstream SHAs, audits
each affected page against the exact head_sha the scanner found, and
opens metadata-only sweep PRs or prose draft PRs per the documented
topology.

Scanner (scripts/sweep_poll.py):
- walks every <!-- verification: --> block in docs/**/*.md,
  every entry in skills/start/version.json's verified_commits,
  and every entry in SKILL.md frontmatter's verified_commits
- resolves (repo, ref) via repo-registry.yml + GitHub default_branch
- emits a per-record JSON drift report on stdout
- fail-closed on auth / network / parse / shape / unknown-mode errors
- target-manifest blocks deliberately skipped (separate JSON array)

Required checks for branch protection:
- sweep-guard: validates the metadata-only envelope on claude/sweep-*
  branches; verifies docs diffs lie inside verification blocks and
  only touch source_commit / verified_date lines, version.json only
  changes verified_commits, SKILL.md frontmatter only changes
  verified_commits and verified_date, CHANGELOG is untouched, and
  exactly one planning/sweeps/<branch-date>.md is added.
- sweep-sha-reachability: validates every recorded SHA against the
  upstream ref via the GitHub commits + compare APIs; runs on both
  claude/sweep-* and claude/prose-* branches; widens the scope to
  every changed verification block (catches date-only refreshes
  whose existing SHA was previously broken).

Both workflows use a real gate step (id: scope) plus an `if:` on
every later step — exit-0 from an early step does not skip later
steps in GitHub Actions.

skills/start/MAINTAINING.md: tighten the patch-bump rule. Releases
(version bumps) move SKILL.md + version.json + CHANGELOG together;
stamp refreshes are a separate gesture that touches only
verified_commits in both files plus verified_date in SKILL.md.

Auto-merge is deferred to v1.5; v1 ships scheduled detection,
per-page audit, and PR/issue creation only.
…ked key sets, POSIX bootstrap

- Add prose-guard.yml enforcing the claude/prose-* envelope and the SKILL.md
  linked-release rule (skill body changes ⇒ all five release fields must move;
  body unchanged ⇒ none of them may).
- Pin PyYAML literally in sweep-guard.yml, sweep-sha-reachability.yml, and
  prose-guard.yml; the requirements file is PR-controlled and cannot be the
  install source.
- Lock verified_commits key sets in version.json and SKILL.md frontmatter on
  both sweep-guard and prose-guard so the routine can refresh values but never
  silently add or remove repos.
- Forbid scripts/, .github/, repo-registry.yml, and component-registry.yml on
  both guards so a hand-rolled PR cannot widen the routine's surface.
- Rewrite Step 0 of the routine prompt in POSIX shell with capture-then-decide
  (no mapfile, no Bash arrays, no gh issue create --json/--jq).
- Expand Step 4 of the prompt and the policy doc into the explicit nine-step
  Opus 4.7+ audit/write/verify loop, the page batching rule, the audit-diff
  fetch rule (fetch both recorded_sha and head_sha; fall back to the compare
  API; never compare against a moving branch), and a concise PR body format.
- Note the linked-release coupling in MAINTAINING.md so a manual reviewer
  knows the prose PR carries the full skill release set.
- prose-guard: positive path allowlist; raw-value verified_commits type
  check (no `... or {}` coercion); target-manifest protection on docs
  blocks (multiset of literal block bodies) and on version.json/SKILL.md
  (structural equality on verification_mode + verified_commits when
  either side is target-manifest); explicit `status` immutability;
  `-draft` suffix preservation; published_date/verified_date must agree
  on linked release; sweep summary must be added (A), not modified.
- sweep-guard: same target-manifest protection on docs/version.json/
  SKILL.md; raw-value verified_commits type check; relaxed branch regex
  to allow optional `-<slug>` suffix matching the prose form.
- sweep-sha-reachability: emit informational summary lines for skipped
  target-manifest entries instead of silently dropping them.
- upstream-sweep.md / upstream-sweep-prompt.md: --limit 1000 on the
  open-PR collision check so a busy repo cannot hide a still-open prior
  PR behind gh's default 30-row cap; five-case page batching rule with
  the deferred-record self-check covering the entire verification block;
  manual-review issue de-duplication via fingerprint client-side
  matching (no `gh issue --search`); Step 5 ordering (issues first, PRs
  second, backlinks third) so PR bodies can reference issue numbers and
  reused issues accumulate a chronological run trail.
Five defects found during synthetic verification:

1. sweep-sha-reachability.yml crashed with AttributeError when
   verified_commits was a list (no .items()). Added explicit raw type
   checks before iteration in both call sites so the workflow fails
   closed with a clean message instead of a Python traceback.

2. sweep_poll.py used `data.get("verified_commits") or {}` which
   silently coerced missing/null/empty-list values to an empty map,
   skipping skill verification records. Replaced with explicit
   presence + isinstance(dict) checks at both call sites.

3. upstream-sweep-prompt.md Step 0 only created the
   upstream-sweep-status label. Step 5 then opened manual-review
   issues with --label upstream-sweep-manual-review and would 404 on
   first run. Bootstrap now creates both labels. Added --limit 1000
   to the gh label list call too.

4. prose-guard.yml only recorded the new path for rename/copy diffs,
   so a claude/prose-* PR could rename a forbidden old path into an
   allowed new one and slip past the envelope check. Now records
   both old and new paths, mirroring sweep-guard.

5. Both guards protected target-manifest verification_mode and
   verified_commits but not verified_date. Added verified_date to
   all three target-manifest immutability checks (version.json in
   prose-guard, SKILL.md in prose-guard, SKILL.md and version.json
   in sweep-guard).
Three review findings on top of ddeb45a:

1. validate_sha() in sweep-sha-reachability.yml could still hit
   sha[:7] when an entry of verified_commits had a non-string value
   (null, int, list). Added an isinstance(sha, str) guard at the top
   of validate_sha so all three call sites (docs blocks, version.json,
   SKILL.md) fail closed with a clean message instead of a traceback.
   Mirrored with per-entry checks in sweep_poll.py walk_version_json
   and walk_skill_md.

2. parse_skill_md_frontmatter used `yaml.safe_load(raw) or {}` which
   coerced a present-but-empty (or null) frontmatter into {}, then
   walk_skill_md returned early on `if not fm`, silently skipping
   SKILL.md verification. Now: parse returns None only for an absent
   file and raises FailClosed for any present-but-shape-invalid case.
   walk_skill_md uses `fm is None` instead of falsiness, so an
   explicitly empty {} frontmatter falls through to the existing
   verification_mode / verified_commits checks and fails closed.

3. The audit fetch step in upstream-sweep-prompt.md used cd "$TMP"
   without a return, so a tired or literal agent could continue
   later PR-writing steps from inside the upstream checkout. Changed
   to git -C "$TMP" so cwd never moves, with explicit guidance to
   wrap any genuinely-cwd-needing follow-up in a subshell.
Review pass 5: the F3 fix in 8c541eb fixed Step 4.1 but left Step 4.2
and the policy doc's fetch block running bare git commands. A literal
routine run would have inspected the docs repo (the cwd of the
routine) instead of the upstream checkout, silently producing
misleading audit input.

Now every git command in the audit step carries -C "$TMP":

- planning/routines/upstream-sweep-prompt.md §4.2: log/diff/diff
- planning/routines/upstream-sweep.md §Audit-diff fetch rule: full
  fetch block + the inline mention of log/diff
- planning/routines/upstream-sweep.md §Audit/write/verify summary
  L198: mirrors the prompt's command form

Added an explicit anti-pattern note in the prompt: a bare git log
or git diff here would inspect the docs repo, not the upstream tree.
@JimCollinson JimCollinson merged commit 26d6109 into main May 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants