Skip to content

Extract /swarm:review --pr publish path into a deterministic script#30

Merged
gering merged 4 commits into
mainfrom
task/swarm-pr-publish-script
Jul 15, 2026
Merged

Extract /swarm:review --pr publish path into a deterministic script#30
gering merged 4 commits into
mainfrom
task/swarm-pr-publish-script

Conversation

@gering

@gering gering commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replaces the model-interpreted prose shell of /swarm:review --pr's step-5 publish path with a testable scripts/pr-post.py helper.
  • Three prose iterations each regressed the same way (a forgeable heredoc terminator, an advisory-echo "stale check" that posted anyway, $PR_NUM/$PR_HEAD_OID referenced across tool calls that don't share shell state, per-cell sanitize rules with no enforcer). Moving it to code makes correctness mechanical + unit-tested.
  • Accepted residual from the --loop review of PR Add /swarm:review --pr: review a PR diff and post the result #29.

Changes

  • New plugins/swarm/scripts/pr-post.py
    • build — renders the exact comment body from structured gated rows + balance + PR meta through a deterministic per-cell sanitizer (escape |/backtick/newline; @&#64; and :///www.→entities to kill mentions + bare-URL autolinks anywhere in a cell; <>→entities to strip raw HTML; [/] to break link syntax; ort as an inert code span). The PR title (untrusted contributor input) runs the same sanitizer.
    • post — a real stale-head gate (re-reads the live head; a mismatch returns SWARM_PR_STALE and does not post) then gh pr comment --body-file with a self-cleaning temp file, rebuilt from the same JSON so what the confirm gate showed is what is sent.
    • Pure functions (sanitize_prose/sanitize_code/stale_gate/render_body); two-tier exit convention like loop-closeout.py.
  • New plugins/swarm/scripts/test_pr_post.py — covers the sanitizer (mention/pipe/newline/link/URL/HTML/entity-injection/backtick), the code-span sanitizer, the stale-head gate (match/mismatch/empty-live/empty-reviewed), body assembly, and a build subprocess round-trip.
  • SKILL.md step 5 shrinks to: assemble JSON (cells raw — script sanitizes) → build → human injection-scan + confirm once → post, branching on one token. PR_NUM/PR_HEAD_OID passed as explicit args, never via cross-call shell state. Residual note removed (now implemented).
  • Knowledge base updated (swarm-review-pipeline.md + _index.md).
  • Bump swarm 0.4.0 → 0.4.1 (hardening, no new surface).

Readiness

  • ✅ Uncommitted: clean
  • ✅ README: current (no skill add/rename; --pr already documented; helper scripts not enumerated per convention)
  • ✅ Version: 0.4.0 → 0.4.1, plugin.json + marketplace.json in sync
  • ➖ Changelog: N/A (repo has none)
  • ✅ Knowledge: updated
  • ✅ Tests: check-structure.py 0 errors (plugin tests pr-post + loop-closeout green)
  • ➖ Linter / Build: N/A (build-less markdown/python plugin repo)

Test plan

  • python3 plugins/swarm/scripts/test_pr_post.py passes
  • python3 scripts/check-structure.py reports 0 errors
  • python3 plugins/swarm/scripts/pr-post.py build --input <json> renders a sanitized body (mentions/URLs/HTML/pipes neutralized)
  • A live /swarm:review --pr <n> posts a comment via gh under the user's identity and honors the stale-head gate

🤖 Generated with Claude Code

gering and others added 4 commits July 15, 2026 22:09
The step-5 publish path of `/swarm:review --pr` was model-interpreted
prose shell, and every prose fix spawned a new prose-shell bug (a
forgeable heredoc terminator, an advisory-echo "stale check" that posted
anyway, cross-tool-call shell state, per-cell sanitize rules with no
enforcer). Move it into a testable helper.

- Add plugins/swarm/scripts/pr-post.py:
  - build  — render the exact comment body from structured gated rows +
             balance + PR meta through a deterministic per-cell sanitizer
             (escape |/backtick/newline; @->&#64; and ://,www.->entities
             to kill mentions + bare-URL autolinks anywhere in a cell;
             <>->entities to strip raw HTML; []->escaped to break link
             syntax; ort as an inert code span). The PR title (untrusted
             contributor input) runs the same sanitizer.
  - post   — a real stale-head gate (re-read the live head; mismatch
             returns SWARM_PR_STALE and does NOT post) then
             gh pr comment --body-file with a self-cleaning temp file,
             rebuilt from the same JSON so what was shown is what is sent.
  Pure functions (sanitize_prose/sanitize_code/stale_gate/render_body)
  keep it unit-testable; two-tier exit convention like loop-closeout.py.
- Add plugins/swarm/scripts/test_pr_post.py covering the sanitizer
  (mention/pipe/newline/link/URL/HTML/entity-injection/backtick), the
  code-span sanitizer, the stale-head gate (match/mismatch/empty-live/
  empty-reviewed), body assembly, and a build subprocess round-trip.
- Shrink SKILL.md step 5 to: assemble JSON (cells raw, script sanitizes)
  -> build -> human injection-scan + confirm once -> post, branching on
  one token. PR_NUM/PR_HEAD_OID passed as explicit args, never via
  cross-call shell state. Remove the residual note (now implemented).
- Document the extracted publish path in the knowledge base.
- Bump swarm 0.4.0 -> 0.4.1 (hardening, no new surface).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HpkMdwQfGsYzk1Vz3Vub4
An "ohne opus" swarm review (codex + grok-build + composer) of the new
publish helper surfaced seven confirmed findings; address them:

- Sanitizer: entity-encode markdown-active chars instead of backslash-
  escaping. `\|` became `\\|` under escaping and freed a live table
  pipe; `\[..\](url)` re-opened a link. A numeric entity (`&#124;`, ...)
  carries no literal metacharacter, so the delimiter/link/mention can
  never re-form. Also encode `* _ ~` (emphasis/strike). sanitize_code
  now STRIPS backtick + pipe (entities don't decode in a code span and
  escaping is bypassable there too).
- render_body validates header pr_num (digits) and head_oid (hex-only),
  not just the gh-target seam — a JSON pr_num like "29\n\n**evil**"
  otherwise injects markdown past the cell sanitizer.
- cmd_post threads the resolved --pr/--head-oid overrides back into the
  body so the header labels the PR/revision that is actually gated+posted.
- Stale gate fails CLOSED: an unreadable live head now blocks the post
  (SWARM_PR_HEAD_UNVERIFIED) instead of posting a possibly-stale review;
  body is built last so the gate is the final step before the comment
  (minimal TOCTOU window; residual race documented).
- _load_input validates rows is a list of dicts -> ValueError -> exit 2,
  instead of an uncaught AttributeError/TypeError + traceback + exit 1.
- Fix the "6-col omits Source" test to locate the table header by content
  (the added title line shifted the fixed index onto the title).
- Quote --input "<json>" in the SKILL examples (defensive).
- test_pr_post.py: add backslash-bypass, emphasis, header-injection,
  hex-only short-oid, and non-dict-rows cases. Update knowledge entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HpkMdwQfGsYzk1Vz3Vub4
A follow-up external-only swarm review (codex; grok+composer errored this
round) confirmed two misuse-contract edge cases the previous hardening
left open:

- render_body crashed on a non-string cell value: has_quelle detection
  ran `.strip()` on a raw `{"quelle": 1}`, and _short ran re.sub on a
  non-string head_oid — both raised past the ValueError handler and
  exited 1 with a traceback instead of the documented clean exit 2 /
  clean render. Coerce to str at both spots.
- An OSError from tempfile creation/write in cmd_post (full/read-only
  temp dir), after the stale gate passed, escaped uncaught -> exit 1 +
  traceback with no SWARM_PR_POST_ERR token. Wrap it in the operational
  error path so it emits the token the skill branches on.

Add tests for both. The prior round's seven findings did not recur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HpkMdwQfGsYzk1Vz3Vub4
@gering gering force-pushed the task/swarm-pr-publish-script branch from 0f3786c to 674eb0a Compare July 15, 2026 20:22
@gering gering merged commit d00cce4 into main Jul 15, 2026
1 check passed
@gering gering deleted the task/swarm-pr-publish-script branch July 15, 2026 20:23
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.

1 participant