Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "perun",
"description": "Perun: a suite of skills for building with AI agents. Deep code review, token and cost tools (spend reports, auto-compact with handoff, model-tier guidance), gated agentic delivery (merge train, lanes, destructive-command guard, auto-update), an idea critic, and product, growth, positioning, business-ops and output-safety specialists.",
"skills": "./.claude/skills",
"version": "1.627.0",
"version": "1.628.0",
"author": {
"name": "deep-code-review contributors"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/agentic-delivery/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: >-
license: MIT
metadata:
author: deep-code-review contributors
version: "1.627.0"
version: "1.628.0"
---

# Agentic delivery
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/agentic-delivery/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.627.0
1.628.0
2 changes: 1 addition & 1 deletion .claude/skills/deep-code-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: >-
license: MIT
metadata:
author: deep-code-review contributors
version: "1.627.0"
version: "1.628.0"
---

# Deep Code Review
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/deep-code-review/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.627.0
1.628.0
2 changes: 1 addition & 1 deletion .claude/skills/idea-critic/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: >-
license: MIT
metadata:
author: deep-code-review contributors
version: "1.627.0"
version: "1.628.0"
---

# Veles: the evil twin (idea-critic)
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/idea-critic/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.627.0
1.628.0
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "perun",
"version": "1.627.0",
"version": "1.628.0",
"description": "Perun: a skill suite for AI-agent teams: deep code review, token and cost tools, gated delivery, an idea critic and product specialists.",
"skills": "./.claude/skills",
"interface": {
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@
All notable changes to this repository are documented here. Format loosely
follows Keep a Changelog; versioning follows Semantic Versioning.

## [1.628.0] — 2026-10-30

### Added
- `scripts/guard-registry.tsv`: one row per hook a shipped template runs or `install.sh` writes and per `ci-gates.sh` gate, with columns `id`, `owner`, `reason`, `review-by` and an optional fire log path. A guard is a hook script a shipped `.claude/skills/*/templates/*.settings.json` runs (`hook:<skill>/<script>`), a git hook `install.sh` writes (`hook:.githooks/<name>`), or a `ci-gates.sh` subcommand (`gate:<name>`). A script no template wires (a CI-only gate script, a hook the owner installs by hand) has no row; `ci-gates.sh rails` still covers its `# rail:` header. The owner is a lowercase role slug (`release-owner`), never a person; `Jane Smith`, an email and an `@handle` are rejected.
- `ci-gates.sh guard-registry --registry <file> <root>`: fails closed when a guard has no row, a row has an empty reason, an owner that is not a role slug, or a `review-by` that is not a real `YYYY-MM-DD` date, when a guard has two rows or a row has more than five columns, and when a row names a guard that no longer ships. It also fails when the root has no hook or gate to check, and when `ci-gates.sh` defines a `cmd_` function whose dispatcher line the scan cannot read, so an empty or blind scan cannot pass. A review date in the past never fails the gate; the report below surfaces it. The test below runs it on the real registry on every PR.
- `ci-gates.sh guard-registry --report [--days N] ...`: a read-only list for the owner to confirm. It shows rows past `review-by`, and guards whose fire log has no dated line inside the last N days (default 30). A guard with no fire log, or whose log is missing, unreadable, emptied or holds no dated line, is reported as `unknown`, never as zero fires. It runs the gate first, never deletes or edits a row, and opens every file read-only. Today only `reply_cap.py` has a fire log (`~/.cache/perun/reply-cap.log`, one dated line per block), so it is the one row with a fifth column. That cell names the default location only: `REPLY_CAP_LOG` or `XDG_CACHE_HOME` moves the log, and the report then reads the wrong file.
- `scripts/test-guard-registry.sh` (registered in `scripts/ci-tests.d/guard-registry.sh`): one case per failure mode above, the fail-closed and argument errors, the report's window, `unknown`, `~` expansion and read-only behaviour, and the real registry (row counts, the `reply_cap` log path, and each hook row's owner, reason and review date equal to its script's `# rail:` header).

Decision record: the `# rail:` headers from `ci-gates.sh rails` stay, because they also cover non-hook gate scripts that this registry does not list. Hook rows therefore repeat the header's owner, reason and review date, and the test fails if the two drift apart. If they still disagree after about 30 days, retire one of the two; the follow-up is to let `rails` accept a registry row in place of a header.

### Added
- `.claude/skills/agentic-delivery/scripts/privacy_guard.py`: a PreToolUse(Bash) hook that denies (exit 2) a GitHub write whose title or body matches the repo's banlist. Nothing scanned the text an agent publishes, so a banlisted identifier in an issue or PR body went out unchecked while the privacy gate only read committed files. It covers what `comms_guard.py` covers and reads text through its extraction: `gh pr|issue|release create|edit|comment`, and `curl` or `gh api` calls to `repos/<owner>/<repo>/pulls` or `/issues`, with the title (`--title`/`-t`, `-f title=`, JSON `title`) and the body (`--body`, a heredoc, `$(cat FILE)`, `--body-file`, `-d` or `--input` JSON, `-f body=`, `-F body=@FILE`). `-f`/`--raw-field` text is scanned as typed (gh sends `@jane` as a mention); only `-F`/`--field` reads `@FILE`, and an `@FILE` it cannot read is scanned as typed. Every write in a command is read, not only the first. A body file that the same command writes (`> F`, `>> F`, `tee F`) is read from the heredoc that feeds it, because the disk holds an old copy or none and the command's other heredocs (a `python3 - <<'PY'` script, a `git commit -m "$(cat <<'EOF' ...)"`) are not the body: a clean rewrite is no longer denied for the stale text or an unrelated heredoc, and a banlisted heredoc is no longer missed because the file does not exist yet. The patterns are the actionable lines of `.banlist.txt` then `.banlist.local.txt` at the git top-level of the session directory (a linked worktree falls back to the main checkout's gitignored local file), each run through `grep -E` over each line of the text, as `ci-gates.sh privacy` runs it. The deny reason names the title or the body line number and the pattern's 1-based number among those lines, never the matched text. It fails open: no repo, no banlist, no title or body, an unreadable body file (a readable title is still checked), bad input, a missing `git` or `grep`, or any error allows. Opt out for a session or project with `PERUN_PRIVACY_GUARD=off` in settings `env`. The module docstring lists what it does not cover (merge and review, `--fill`, a body fed to stdin by a pipe, text built from a shell variable, close and reply routes, and other forms). It is registered in the operating-layer template on the Bash matcher without `|| true` (which would turn the deny into an allow), listed in `perun_doctor.py` `HOOK_SCRIPTS`, and has a row in `scripts/guard-registry.tsv`.
- Scope of the banlist scan. The scan runs on every write except one whose explicit target is a sibling: another repository under the owner of the session's `origin` remote, where a private repository's name belongs. The session's own repository (implicit, or named by `-R`/`--repo`, a github.com URL or a `repos/` path), the Perun repository, every other owner's repository, a write that names no repository and a session with no usable `origin` stay scanned, and a write that names several repositories is skipped only when every one is a sibling. The target is read from the write's own region: from the write to the next `gh` or `curl` write or to the end of its command line (`&&`, `||`, `;`, `|`, a newline or a `#` comment; a backslash-newline continues the line). A `.git` suffix is dropped, so the session repository's clone URL still names the own repository. Heredoc bodies, quoted strings that hold a space and a one-word title or body are blanked first, so text that only mentions a repository (a body quoting `-R acme/other` or naming `repos/acme/other/issues/3`, a later `echo filed https://github.com/...`) sets no target and a write cannot skip the scan by naming a sibling in its text.
- Own-repository rule in the same hook. A project's banlist does not list the project's own name, so a field report filed from that project's session into the public Perun repository carried the name past the banlist. When a write targets the Perun repository (`SELF_REPO`, by `-R`/`--repo`, a `repos/protocol/perun/` path or its github.com URL, read from the write's own region as above) and the session's `origin` remote names a different repository, a title or body line that names that repository is denied: as `owner/name`, or as the bare name when it is distinctive (it holds `-`, `_` or `.`, or has 8 or more characters), case-insensitive and whole word. The reason gives the location (`title`, `body line N`) and never the name. It needs no network, and a session inside the Perun repository, with no `origin`, or with an unparsable one has no such rule. Reading the target from the write's own region also stops a write that names the session repository from being denied for an unrelated mention of the Perun repository later in the command or in its text.
- `scripts/test_privacy_guard.py` (40 offline tests on fictional Acme fixtures in throwaway git repos, wired in `scripts/ci-tests.d/privacy-guard.sh`): every write form and every title form is denied without echoing the text, line and pattern numbers (an invalid ERE is skipped, not renumbered), the five-hit cap, POSIX-class and case-sensitive matching through `grep -E`, the linked-worktree fallback, the allow paths (clean text, other commands, no repo, no banlist, an empty banlist, an unreadable body, the opt-out, bad input, another tool), the template and doctor wiring, `-F body=@FILE` followed by more arguments, flag-like words inside a quoted title or body or a heredoc body, every write in a command, a body file the command writes itself (fresh, stale, and beside an unrelated heredoc), `-f` against `-F` for `@`, the own-repository rule (each target form, ssh and https origins, other targets, a Perun session, no origin, short and distinctive bare names, whole-word and case-insensitive matching, both reasons together, a failing `git`), and the scope rule on an acme/acme-widgets session (each sibling target form skipped; the own repository, Perun, another owner, a Perun origin or none, an origin in the Perun organisation and a `-r` reviewer scanned; text that only names a sibling; a sibling write beside an own write; six tail cases that must allow and the controls that must still deny). Twenty-nine mutants of the new code (the sibling skip, the target region and its blanking, `-f` against `-F`, the feeding heredoc, the origin parse) each fail at least one of these tests.
- `scripts/guard-registry.tsv` and the `# rail:` header of `privacy_guard.py` state the new scope in the same words.

### Changed
- `comms_guard.py`: `_rest_body(tail, cwd)` is now `_rest_field(tail, cwd, key, literal=False)`, so the title of a REST write is read by the same code as its body. `extract` still returns the body and the existing comms_guard tests pass unchanged. `literal` makes an unreadable `-F body=@FILE` come back as the typed text instead of raising; only `privacy_guard.py` passes it, so the comms lint still treats an unreadable file as no body.
- `comms_guard.py`: `-F body=@FILE` and `-F 'body=@FILE'` read `@FILE` as one shell word, so more arguments after it (`--jq .id`, `-f title=t`, `&& echo done`) no longer turn the read into a failure and a skipped lint. A flag-like word inside a quoted string or a heredoc body (the title "Add -b flag", a body that mentions `-t`) is no longer read as a flag: `_flags` blanks quoted strings and heredoc bodies (the new `_plain`, which the privacy hook's target region reuses) before it looks for a flag, and an unclosed quote hides nothing. Blanking keeps the raw offsets that the heredoc and `$(cat FILE)` readers need, which a shell tokenizer would not, and it falls back to the earlier first-textual behaviour wherever a quote does not pair.
- `comms_guard.py`: `@` means a file only for `-F`/`--field`. `-f`/`--raw-field` sends its text as typed, so `-f body='@jane thanks'` and `-f title='@jane ...'` are linted and scanned as written; the first version of this change read them as a missing file and skipped them, and the earlier code read an unquoted `-f body=@FILE` as a file that gh does not read. A comms lint of `-f body=@FILE` now sees the text `@FILE`.

Decision record (evil twin). Skeptic: a deny hook that agents cannot see into will occasionally block a legitimate write, and the secret-shaped patterns here lack the gate's placeholder relaxation. The banlist is the session repo's, so a write into another owner's private repository that names the session project is denied, and a name that is legitimate there must be reworded. Better way: relax secret patterns with `secret_scan.py`'s placeholder rule, ask the network whether the target is private, or skip every write that has `--repo`. All rejected. The relaxation script lives under `scripts/` and is not installed with the skills, so the hook would behave differently per install. A visibility lookup needs the network and a token, which this hook avoids, and a failed lookup must either block or leak. Skipping on any `--repo` would exempt the upstream-contribution flow, where a leak into a public repo matters most; the sibling rule skips only the origin owner's other repositories, so a write into Perun or into another owner's repository stays scanned. Accepted residuals: a public sibling in the origin's organisation is not scanned, so a banlisted identifier written there goes out; another owner's private repository is scanned, so a legitimate name in it is denied (reword it or opt out); a target set by `GH_REPO` is not seen. A blocked line is cheap to reword because the reason names it, and `PERUN_PRIVACY_GUARD=off` is the owner's escape. The own-repository rule trades a missed short bare name (caught only as `owner/name`) for never denying an ordinary word such as `app`; a list of the session's own words is the upgrade if a leak slips through. Kill criterion: two false positives from a secret-shaped pattern, from a write into another owner's repository, or from the own-repository rule in one release means narrow the hook (drop those sections, ship the relaxation, or drop the bare-name match) before it blocks more work, and one banlisted identifier that reaches a public sibling means drop the sibling skip.

### Added
- `review_checks.sh` marks every row `"new": true|false` in diff mode: `true` when the row's file and line are on a line `REF...HEAD` adds, read from `git diff -U0` of the range the script already uses. It is rename-aware, so a renamed file's old lines are not new; line 0 and file-level rows are `false`. `--full` and `--history` omit the key, no row is ever dropped, and stderr gains one line, `review_checks: N of M rows are on lines this diff adds`. A reviewer can start from the rows the change introduced and treat the rest as pre-existing, instead of re-triaging a touched file's whole backlog. The diff flags are pinned (`-U0 --inter-hunk-context=0 -M -a --no-color --no-ext-diff --no-textconv` and explicit `a/` `b/` prefixes) so a reviewer's git config cannot change which lines count as added; `-a` keeps a text file with a NUL byte (an `invisible-chars` row) from reading as `Binary files differ`, which would mark its added lines pre-existing.
- One sentence in `references/method-situational.md`, where the script's rows are documented for reviewers: verify new rows first, and report a pre-existing row in a touched file only when the diff makes it reachable or worse.
- `scripts/test-review-checks-new-rows.sh` (16 offline cases, wired in `scripts/ci-tests.d/review-checks-new-rows.sh`): a fake shellcheck on a hermetic PATH, in a repo whose git config is hostile (no rename detection, no prefixes, merged hunks, forced colour, an external diff, a line-dropping textconv). It covers an added line, a pre-existing line shifted down between two hunks, a renamed and edited file, a pure rename, a new file, a line 0 row, a path with a space, a NUL byte, the stderr count, and the missing key under `--full` and `--history`. Each pinned flag has a case that fails without it, and the cases that assert the flag fail on the script before this change.
size-budget-raise: .claude/skills/deep-code-review/references/method-situational.md 44396→44702 one sentence on the new flag: verify new rows first, report a pre-existing row only when the diff makes it reachable or worse

## [1.627.0] — 2026-10-30

### Added
Expand Down
12 changes: 6 additions & 6 deletions SHA256SUMS
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ ef9e423362b12d0dd73c8e165faf32e9f53c4d4ac215b57c2e34002a88713838 .claude/skills
9521b057ff3db92d1e60923ebd88170713914f7e235ceb45717eb39419daf811 .claude/skills/agentic-ceo/scripts/weekly_receipt.py
7e2179751e9dd2fb371b3e9fb01fca987d69605af1e8e6e609ff9b0d0abb384b .claude/skills/agentic-delivery/INDEX-scripts.md
47fff02dcb4401d5a9b42b2b3b7f7c123125eece55e7f9adee8cfe488c70a146 .claude/skills/agentic-delivery/INDEX.md
04982647fd4125802d46d991ca23ce7fa648a6b2b71074ebbf2c71e6d483619e .claude/skills/agentic-delivery/SKILL.md
73e93e5ff760a52ccc7044d782e1db000a421b043567d8de6ed00a922ffc5959 .claude/skills/agentic-delivery/VERSION
6eea84fd7db01737e685f8b97ee5bd7b845a1672f232f5d362769f7bb75eac83 .claude/skills/agentic-delivery/SKILL.md
cee20fa16a8c48df6dfdcbfd6d593097424f687f01993d8f99661789e2401a2e .claude/skills/agentic-delivery/VERSION
94144c101f82399eb6c18f688321a00e1c195dd0ab334f3a5daa2543b40ff9f5 .claude/skills/agentic-delivery/evals/evals.json
fba7699a2d0836b51b7c838d019b1f0cd20570522a362e8a32b8dacfb911298e .claude/skills/agentic-delivery/evals/triggers.json
39140ac0b40907dc2279bb3234ce1d8ffb4db56e8658c7f89cc370f777df0592 .claude/skills/agentic-delivery/references/cost-quality-guardrails.md
Expand Down Expand Up @@ -206,8 +206,8 @@ d98f3e4380da3496061ba63be7beebe75017ab4ee1734eda9a3e798e19007b0e .claude/skills
18f20db537ab784460ad33cf218f7a01ac0a172fc38413f14c0e19e1ce6821cc .claude/skills/contribution/scripts/share_learning.py
63e7d07ffa54b224a83abee280456f6a24dc8222b9b55f6e249b10ea0c7f5afc .claude/skills/deep-code-review/INDEX-scripts.md
3145a0f11e8ecc8b510ae32e7de8eff9bea24e1e9bf46ed662221b7b4d386de2 .claude/skills/deep-code-review/INDEX.md
a44ec08c8bd85b4adb4ce9c18e01e8c01a500f6efaceaf4bc79e2c396f4393d3 .claude/skills/deep-code-review/SKILL.md
73e93e5ff760a52ccc7044d782e1db000a421b043567d8de6ed00a922ffc5959 .claude/skills/deep-code-review/VERSION
0a06dbe95a174c6233c3edfc792d9a8ae4318cd7270ed6cb0d0f885369a2a631 .claude/skills/deep-code-review/SKILL.md
cee20fa16a8c48df6dfdcbfd6d593097424f687f01993d8f99661789e2401a2e .claude/skills/deep-code-review/VERSION
b97d95bc93cc23a8bc689e2e23db261a138367bcbc69d2f8a0452d5c2c6dfb68 .claude/skills/deep-code-review/evals/evals.json
13dbb52e4bf61b1093302e094151def167371d26e7cf67a026f3beeb09d34378 .claude/skills/deep-code-review/evals/triggers.json
b84e3a2b88dc189f041784e76b97e68ad879915814e00f4b37cd27513746d6f1 .claude/skills/deep-code-review/references/a11y-aria.md
Expand Down Expand Up @@ -425,8 +425,8 @@ e951eaa63fd0d1870701d69c89c29ef4fe74dbad9b680ab6ef0a4e993fb8bc20 .claude/skills
fd316c63bbf1cc0ea64322d7d923d66692eaf7b545c056367bfecca35146c862 .claude/skills/growth-analytics/references/full-map.md
2f1d7499e9ed7ae59f550b5c00043e465b2a4e885821ad153438e2e33fcccb45 .claude/skills/growth-analytics/scripts/readout_check.py
67e71ec40535ad0b3931c51f3decf323bce809ed4c9586c6d422b7ad5698129f .claude/skills/idea-critic/INDEX.md
e3cdda9db7b41e503ee77eec138198bc4fc86781ee95c910d13cc5e78ca5c1f1 .claude/skills/idea-critic/SKILL.md
73e93e5ff760a52ccc7044d782e1db000a421b043567d8de6ed00a922ffc5959 .claude/skills/idea-critic/VERSION
c4f659a3999b569b0fa909d092d4200d39ad13306408c1537b92658bf788dc43 .claude/skills/idea-critic/SKILL.md
cee20fa16a8c48df6dfdcbfd6d593097424f687f01993d8f99661789e2401a2e .claude/skills/idea-critic/VERSION
9f77d5d6d13c99fd16cd8c398795d8be226d74bae5a5bf65369ed94a59fa56b7 .claude/skills/idea-critic/evals/evals.json
0e1c0db300661a1ab1a427a7157c75e2ed67130ab24d5c6cb02ec3ff9e793f58 .claude/skills/idea-critic/evals/heldout.json
d8098c5ced63e8b8608867dd144091049563a0553cf62d27ce05b9d52ac54266 .claude/skills/idea-critic/evals/triggers.json
Expand Down
Loading
Loading