)]}'
{
  "log": [
    {
      "commit": "1fd23a727d61b9997f6101898e69fadd38c3a171",
      "tree": "9a0e6560a96fbd1dadf416eb47ae3abedf900a23",
      "parents": [
        "357f23903ee350e2e6c326804acc11ae69eeb617"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 15 19:21:04 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 15 19:21:04 2026 +0200"
      },
      "message": "fix(dev): route the dev-version bump through a pull request (#1234)\n\n* feat(dev): check the bump candidate with the real PR workflows, open it as a draft\n\nThe bump commit never met a gate. A commit created through GitHub\u0027s API raises\nno events, and a direct push to `main` is not a pull request, so nothing would\nhave checked what this workflow produces — and a bump is not \"only a version\nstring\": it regenerates fourteen manifests and the lockfile, any of which can\ncome out wrong.\n\nThe candidate is now built on its own branch, put through the *same* four check\nworkflows a pull request runs, and only then offered as a pull request. A failed\ncheck deletes the branch instead of leaving a broken PR behind.\n\n`pre-commit.yml`, `tests.yml`, `rat.yml` and `zizmor.yml` each gain a\n`workflow_call` trigger and an optional `ref` input, and the bump workflow\n`uses:` them. They are called, not copied, so they cannot drift from what a PR\ngets; the input is empty on every other trigger, leaving `actions/checkout` on\nits normal behaviour.\n\nThe pull request opens as a **draft**, which is what makes it usable. GitHub\nraises no `pull_request` events for anything `GITHUB_TOKEN` does, so a ready PR\nopened from here would sit forever on \"N of N required status checks are\nexpected\", and the checks this workflow ran are attached to its own run rather\nthan to the PR head. Marking a draft ready raises `ready_for_review`, which is a\nreal event because a human does it — so the four check workflows now list that\ntype explicitly (it is not in the default set) and pressing *Ready for review*\nstarts the required checks against the PR head, exactly as an ordinary PR gets\nthem.\n\nReusable-workflow calls use GitHub\u0027s `$/` self-repository syntax rather than\n`./`, per zizmor\u0027s `self-repository` audit.\n\nGenerated-by: Claude Opus 5\n\n* docs(dev): describe the dev-version bump workflow\n\n`CONTRIBUTING.md` listed every CI workflow except the one a human starts by\nhand, which is also the only one that writes to the repository — so the job\ngraph, the signed-commit path and the draft-PR behaviour existed only as\ncomments inside the YAML.\n\nAdds a *Bumping the dev version* section covering the six jobs, why the checks\nare `uses:` of the real workflows rather than copies of their steps, and why\nthe pull request opens as a draft: GitHub raises no `pull_request` events for\nanything `GITHUB_TOKEN` does, so marking the draft ready is what starts the\nrequired checks against the PR head. Notes the side effect that every draft PR\nin this repo now behaves that way, and gives the one-liner for reviewing a bump\ndiff — every changed line should be the version string and nothing else.\n\nAlso realigns `docs/setup/marketplace.md` § *How to bump*, which still described\nthe workflow committing straight to `main`.\n\nGenerated-by: Claude Opus 5\n\n* docs(dev): record why the bump cannot commit to main\n\nRun #1 of `bump dev version` failed, and the reason is worth keeping rather\nthan rediscovering:\n\n    gh: protected branch \u0027main\u0027 check failed:\n      3 of 3 required status checks are expected.\n\n`main` requires three status checks, and a commit arriving through the API\ncarries none — GitHub enforces branch protection there exactly as it does on a\npush. The preceding commit already routes the bump through a pull request; this\nrecords the evidence for why, in the workflow header and in CONTRIBUTING, so the\ndirect-push shape is not proposed again.\n\nGenerated-by: Claude Opus 5"
    },
    {
      "commit": "357f23903ee350e2e6c326804acc11ae69eeb617",
      "tree": "a42fe264b7c6411fd9b0d8cad9cff034e51b63d5",
      "parents": [
        "e3fad3a4afd3a299429a84e00f7d5881df36cc76"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 15 18:37:18 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 15 18:37:18 2026 +0200"
      },
      "message": "ci: bump allowlist-check pin to pick up $/ self-repository support (#1235)\n\nThe pin tracks infrastructure-actions main, but had drifted behind it.\nThe pinned SHA 69afc125 predates df54e48 (apache/infrastructure-actions#1254),\nwhere SKIPPED_PREFIXES gained \"$/\" alongside \"./\" and \"docker://\".\n\nWithout that, the four `uses: $/.github/workflows/*.yml` refs in\nbump-dev-version.yml fall through to the org allowlist lookup, miss, and\nare reported as unapproved - which also renders nonsense remediation\ncommands such as `git checkout -b \u0027allowlist-add-$-.github-workflows-rat.yml\u0027`.\n\nGenerated-by: Claude Opus 5"
    },
    {
      "commit": "e3fad3a4afd3a299429a84e00f7d5881df36cc76",
      "tree": "c268ed8707df6f535a7cb75a96efd5cc269551a3",
      "parents": [
        "190e3e69f8455cab00ca2bbfa8bde2537df9d8f6"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 15 17:03:23 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 15 17:03:23 2026 +0200"
      },
      "message": "feat(dev): add a maintainer-run workflow that moves the dev stamp (#1233)\n\nThe marketplace is served from `main` and `claude plugin update` compares\nversion strings, so while the `.dev\u003cstamp\u003e` suffix sits still an adopter\nrunning that command is told they are already current however far behind\nthey are. Moving it is three steps anyone can run, which is exactly why it\ngets forgotten.\n\nAdds `bump dev version`, a `workflow_dispatch` workflow that runs those\nthree steps on `main`. Manual rather than on-merge on purpose: *when* a bump\nis wanted — before pointing anyone at `claude plugin update`, before\nannouncing a change adopters should take, or once merged work has piled up —\nis a judgement, not something a merge trigger can make. GitHub restricts\n`workflow_dispatch` to accounts with write access, so the button is\ncommitters-only without the workflow checking anything itself, and whoever\npresses it is credited as `Co-Authored-By` on the commit.\n\nTwo supporting scripts, because the parts that can fail silently deserve\ntests rather than inline YAML:\n\n- `bump-dev-version.py` moves the stamp in `pyproject.toml` and nothing\n  else, so the same three steps read the same by hand or in CI. It refuses\n  a version with no `.dev` suffix rather than stamping over a release, and\n  refuses a stamp that is not 12 digits of UTC.\n- `gh-signed-commit.py` commits through GitHub\u0027s `createCommitOnBranch`\n  rather than `git commit` + `git push`. The commit comes back signed by\n  GitHub and shows as Verified with no key material in CI. It pins\n  `expectedHeadOid`, so a merge landing mid-run fails the call instead of\n  being overwritten, and it decomposes renames because the mutation has no\n  rename concept.\n\nThe commit author is whoever the token authenticates as — with\n`GITHUB_TOKEN` that is the Actions bot, and the mutation offers no way to\nset it — so the maintainer\u0027s credit goes in a trailer, using GitHub\u0027s\nnoreply address form rather than publishing one the account keeps private.\n\n`main` must permit the commit: the API enforces branch protection exactly as\na push would, so a rule requiring a pull request makes the job fail rather\nthan bypass anything.\n\nGenerated-by: Claude Opus 5"
    },
    {
      "commit": "190e3e69f8455cab00ca2bbfa8bde2537df9d8f6",
      "tree": "f17c33742d37d296ff718cd6f87afebf565f36a2",
      "parents": [
        "f7c908121340266ae6e67383d766618529b2ae5b"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 15 17:43:12 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 15 13:13:12 2026 +0200"
      },
      "message": "feat(vetted-ops): add the read-only operations pr-management-code-review needs (#1232)\n\nThe code-review skill reads a PR through a dozen `gh` shapes the catalogue\ndid not carry, so each one fell through to the wildcard `Bash(gh *)` ask\nrule — the prompt volume this dispatcher exists to remove. The skill\u0027s\npolicy entry named operations that mostly did not exist yet.\n\nTen reads, all `writes\u003dFalse` and so reachable only through\n`vetted-op-read`, which refuses writes before it consults policy at all:\n\n  viewer, upstream-permission, pr-review-context, pr-files, pr-list-label,\n  pr-search-review-requested, pr-search-mentions, pr-search-reviewed-by,\n  pr-search-team-review-requested, commits-by-path\n\nThe searches need a word. Free-text `gh search prs` stays out of the\ncatalogue for the reason the file already gives — a query with no fixed\nshape is the surface this dispatcher removes — and these are not that: the\nqualifier is hard-coded in each builder, the repository comes from policy,\nand the only parameter is a validated login or team slug. A caller chooses\nwhose queue to read, never what to ask. The \"deliberately absent\" note is\nnarrowed to say so rather than left to contradict the new operations.\n\n`team` is a new parameter type: a slug only. A value containing `/` is\nrefused and the organisation is supplied by the builder from the pinned\nupstream repo, so a team search cannot be aimed outside it. Note that\n`gh search prs` has no `--team-review-requested` flag — `--review-requested`\ntakes a user *or* an `\u003corg\u003e/\u003cteam\u003e`, which is what the builder emits.\n\nAdds Magpie\u0027s own policy file with a read-only caller entry for the skill.\nThe three review submissions (pr-review-approve / -request-changes /\n-comment) are deliberately not granted: posting a review stays a confirmed\naction through `vetted-op`.\n\nGenerated-by: Claude Opus 5"
    },
    {
      "commit": "f7c908121340266ae6e67383d766618529b2ae5b",
      "tree": "b21afa89464d6b54f41a56506ab358956dd13998",
      "parents": [
        "32e82cd0f40665c3b82064de7703f6b326fdb2dc"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Tue Sep 15 12:39:24 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 15 12:39:24 2026 +0200"
      },
      "message": "chore(deps): bump https://github.com/crate-ci/typos (#1230)\n\nBumps the pre-commit-hooks group with 1 update: [https://github.com/crate-ci/typos](https://github.com/crate-ci/typos).\n\n\nUpdates `https://github.com/crate-ci/typos` from v1.50.0 to 1.50.1\n- [Release notes](https://github.com/crate-ci/typos/releases)\n- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)\n- [Commits](https://github.com/crate-ci/typos/compare/v1.50.0...v1.50.1)\n\n---\nupdated-dependencies:\n- dependency-name: https://github.com/crate-ci/typos\n  dependency-version: 1.50.1\n  dependency-type: direct:production\n  dependency-group: pre-commit-hooks\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "32e82cd0f40665c3b82064de7703f6b326fdb2dc",
      "tree": "adff9aff5fe5957ad6bacb63ba7f433f8704b7ed",
      "parents": [
        "c595501673742102b0cdc51da11273afb07d243b"
      ],
      "author": {
        "name": "Bartosz Sławecki",
        "email": "bartosz@ilikepython.com",
        "time": "Tue Sep 15 12:17:20 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 15 12:17:20 2026 +0200"
      },
      "message": "fix(agent-isolation): read the main checkout\u0027s settings in a linked worktree (#1229)\n\n* fix(agent-isolation): read the main checkout\u0027s settings in a linked worktree\n\nThe status line claimed a sandbox a worktree session did not have. It\nwalked `\u003ccwd\u003e/.claude/` and then user scope, but Claude Code scopes a\nlinked git worktree\u0027s project to the *main* checkout — that is where\n`/sandbox` persists the toggle and where `.claude/.cc-writes` lands.\nThe worktree\u0027s own `settings.local.json` holds only the per-worktree\nfilesystem allowlist `sandbox-add-project-root.sh` writes, with no\n`enabled` key at all, so the walk fell through to\n`~/.claude/settings.json` and painted a green `[sandbox]` on a session\nthe operator had deliberately switched out of the sandbox. That is the\none failure mode this status line exists to prevent.\n\nResolve the working-tree root and the main checkout by comparing\n`git rev-parse --git-dir` with `--git-common-dir` — they differ in a\nlinked worktree and nowhere else — and insert both between `\u003ccwd\u003e` and\nuser scope in the settings walk. The working-tree root also closes a\nlatent miss: a `\u003ccwd\u003e` below the tree root has no `\u003ccwd\u003e/.claude/` to\nread, so project scope was skipped there too.\n\nThe same resolution replaces the hardcoded `*/.claude/worktrees/*`\nsubstring match in the folder segment. `\u003csource\u003e/\u003cworktree\u003e` rendering\nnow covers every layout instead of the single one it enumerated:\nworktrunk\u0027s sibling `\u003crepo\u003e.\u003cbranch\u003e/` directories (the repeated repo\nprefix is stripped, so `cpython.gh-156021` reads `cpython/gh-156021`),\nClaude Code\u0027s own `.claude/worktrees/\u003cname\u003e`, and a plain\n`git worktree add` anywhere on disk. Because the test is a property of\ngit rather than of a naming scheme, a worktree manager does not have\nto be enumerated here to be supported.\n\nCovered by a new test module: settings precedence across main,\nworktree and user scope, subdirectories on both sides, and folder\nrendering per layout.\n\nGenerated-by: Claude Code (Opus 5)\n\n* test(agent-isolation): parametrize the status-line cases, drop the classes\n\nEach test was a class method repeating the same three steps — build a\nlayout under tmp_path, run the script, assert on one segment of the\nline. Collapse them into a frozen `Case` dataclass, one `_build`, and a\nsingle `@pytest.mark.parametrize` run, and every case now asserts both\nthe sandbox tag and the folder segment rather than whichever one it was\nwritten for. 13 cases, no classes; a new layout is one entry.\n\nAlso state in the script header and the test docstring that this helper\nis Claude-Code-only, unlike the harness-agnostic scripts beside it: it\nis wired through Claude Code\u0027s `statusLine`, fed Claude Code\u0027s payload,\nand reads Claude Code\u0027s settings schema. That is why `.claude/` is the\nonly settings directory under test.\n\nGenerated-by: Claude Code (Opus 5)\n\n* test(agent-isolation): revert the status-line parametrization\n\nRestore the class-grouped tests from 8aa6fd9. The parametrized form\ncollapsed the layouts into one `Case` table, which reads well as a\nmatrix but loses the grouping that says *why* a case exists — the\nsettings walk and the folder rendering are separate concerns that\nhappen to share a fixture, not one table with two columns.\n\nKeeps the Claude-Code-only note added alongside it: the docstring still\nsays why `.claude/` is the only settings directory under test.\n\nGenerated-by: Claude Code (Opus 5)\n\n* docs(agent-isolation): say that the status line is Claude Code only\n\n`tools/agent-isolation/README.md` tagged the whole directory **Harness:\nagnostic**, which is true of every script in it except this one:\n`sandbox-status-line.sh` is wired through Claude Code\u0027s `statusLine`\nsetting, is fed Claude Code\u0027s statusLine payload on stdin, and reads\nClaude Code\u0027s `sandbox.enabled` schema. Nothing said so, so a reader\ncomparing harnesses had no way to tell the exception from an omission.\n\nNote it in the directory\u0027s harness tag, in its Files row, and in the\nstatus-line section of the secure-agent-setup document, each pointing\nat `docs/adapters/add-a-harness.md` for a harness that grows one. The\nsetup document also names the harness-agnostic half of sandbox\nvisibility — the bypass-warning hook, which fires on the tool call\nrather than in the footer.\n\nGenerated-by: Claude Code (Opus 5)\n\n* fix(agent-isolation): keep the Harness tag machine-readable\n\na337b03 wrote `**Harness:** agnostic, except sandbox-status-line.sh —\nClaude Code only`, and the vendor-neutrality-score hook parses that\nfield as a comma-separated list of harness names:\n\n    error: tools/agent-isolation: unknown harness(es) [\u0027agnostic\u0027, \u0027except\u0027]\n\nThe field is data, not prose. Put `agnostic` back in it and state the\none exception in a sentence below, where a reader gets the same\ninformation and the parser is not asked to read English.\n\nGenerated-by: Claude Code (Opus 5)\n\n* fix(agent-isolation): read the main checkout first, not a bare repo\u0027s parent\n\nTwo corrections to the settings walk this branch introduces.\n\nThe walk put \u003ccwd\u003e and the working-tree root ahead of the main checkout,\nso an `enabled` written by hand into a linked worktree\u0027s own settings won\nover the main checkout. Claude Code does not read that file — it scopes a\nlinked worktree\u0027s project to the main checkout — so the more specific file\nis precisely the one that cannot describe the session. A worktree saying\n`true` over a main checkout saying `false` rendered a green [sandbox] on a\nsession that had none, which is the failure this status line exists to\nprevent. The main checkout now leads the walk, and the test that asserted\nthe old order asserts the new one.\n\n`main_root` was derived as the parent of `--git-common-dir`, which assumes\nthe common dir is `\u003cmain\u003e/.git`. For `git clone --bare` plus `git worktree\nadd` it is `\u003crepo\u003e.git`, so the parent is whatever directory happens to\ncontain the bare repo: its `.claude/settings.json` was read as project\nscope, and the folder segment rendered that directory\u0027s name rather than\nthe repository\u0027s. The derivation is now guarded on the `.git` suffix — a\nbare common dir yields no project scope and the walk falls through to user\nscope, while the folder segment still names the repository, taken from the\nbare directory (`proj.git` -\u003e `proj/feature-x`).\n\nAlso guards the comparison on a non-empty `--git-dir`, so a `--git-dir`\nthat fails to resolve no longer reads as \"linked worktree\".\n\nGenerated-by: Claude Opus 5\n\n---------\n\nCo-authored-by: Jarek Potiuk \u003cpotiuk@apache.org\u003e"
    },
    {
      "commit": "c595501673742102b0cdc51da11273afb07d243b",
      "tree": "4e1aa056714f24d0ff82f248c0101a45950dbca1",
      "parents": [
        "fbaf7e4c2d35adda8ce513ee0d5af439c5f9f774"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 15 16:35:01 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 15 12:05:01 2026 +0200"
      },
      "message": "fix(privacy-llm): carve llm.apache.org out of the apache.org default approval (#1226)\n\nThe approved-LLM gate default-approved any *.apache.org host for\nfoundation private data, on the rationale that an Apache domain implies\ninfra under ASF governance. LLMAO (llm.apache.org) went live in\nSeptember 2026 and breaks that assumption: it serves self-hosted models\nfrom rented third-party GPU hardware (Vast.ai, RunPod), and its\noperators state pilot traffic must be treated as visible to gateway\nadmins — public and project-internal material only, no credentials or\nembargoed work.\n\nLeft as-is, the gate would have waved \u003cprivate-list\u003e and \u003csecurity-list\u003e\ncontent, including embargoed CVE detail, onto that path without an\nadopter ever declaring it. models.md had already anticipated the case as\na reason the blanket rule might need re-classification.\n\nAdd an exact-host carve-out list, checked before the domain rule, and\nrecord the reasoning where adopters will find it. The carve-out binds\nthe private-data gate only: skills working on public issues, PRs or\nreleases may use the gateway freely.\n\nAlso record the ASF defaults for the gateway — models, context windows,\nconnection environment variables, and the known limitations that matter\nto an adopter, chiefly that tool use over the Anthropic-compatible path\nis broken upstream, which is what blocks Magpie\u0027s skills from running\nagainst it today.\n\nSource: Andrew Musselman, \"llmao progress Sep 14\", discuss@rai.apache.org.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01PQ4eEL4e2QUJMZ8AhfyKTx"
    },
    {
      "commit": "fbaf7e4c2d35adda8ce513ee0d5af439c5f9f774",
      "tree": "c9cc7168e78bd88ed12dfd42f06fa866cf399853",
      "parents": [
        "b94274db71cb9f8911611b2bd0044d1ad8fef271"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 23:51:57 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 14 19:21:57 2026 +0200"
      },
      "message": "docs(rfcs): drop stale Confluence attachment notes from RFC-AI-0002 (#1225)\n\nThe five screenshots became absolute raw.githubusercontent.com URLs in\n#1212, so they render on the ASF wiki page as-is. Two notes still told\nConfluence editors to upload them as attachments and re-link them.\n\nCorrect the editor note at the top and remove the now-redundant one\nabove \"What a session looks like\".\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01PQ4eEL4e2QUJMZ8AhfyKTx"
    },
    {
      "commit": "b94274db71cb9f8911611b2bd0044d1ad8fef271",
      "tree": "1379df23c35d5403741e230733f9344a0fc7f69c",
      "parents": [
        "3480d6a7ca6c455ca9bfa411d30491fc8c98060a"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 23:48:21 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 14 19:18:21 2026 +0200"
      },
      "message": "feat(agent-isolation): show a window while gpg waits for a key touch (#1224)\n\nA signing key on a hardware token can carry a touch policy on its\nsignature slot, and gpg surfaces the PIN and the touch very differently:\nthe PIN gets a pinentry window, the touch gets nothing at all. The commit\njust blocks, indistinguishable from a hung command, until somebody\nhappens to touch the key or gpg reports \"signing failed: Timeout\".\n\nThe hardware-key rule added in #1219 has the agent probe gpg-agent before\ncommitting, but keyinfo reports the PIN cache and not the touch policy —\na key with a warm PIN and an expired touch reports cached\u003d1 and still\nblocks, with no prompt of any kind. The probe cannot see that case, so\nthis covers it from the other side, at the moment gpg is actually\nwaiting, and the rule now says which half covers what.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_015J6P12zaYGD4r1DZiAkG7c"
    },
    {
      "commit": "3480d6a7ca6c455ca9bfa411d30491fc8c98060a",
      "tree": "e6268b495cfd2505c933a6c63ffb7020ca43b29e",
      "parents": [
        "d29741995a14f69e397d19f65040ce986c550fe2"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 20:42:16 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 14 20:42:16 2026 +0630"
      },
      "message": "chore: bump dev version to 0.2.0.dev202609141352 (#1223)\n\nMove the dev stamp so adopters\u0027 `claude plugin update` picks up the sandbox\ndocumentation fix merged since 0.2.0.dev202609141334 — the marketplace is\nserved from `main` and the update check compares version strings, so a frozen\nsuffix is a silent no-op.\n\nGenerated with the documented one-liner: bump `project.version` in\n`pyproject.toml`, then `tools/dev/check-family-plugins.py --fix` and `uv lock`\nto propagate to every manifest.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01BgDS2KNk24iWkaBhMoHRyW"
    },
    {
      "commit": "d29741995a14f69e397d19f65040ce986c550fe2",
      "tree": "3df0044111cb1ffdcd0fe04554d4a9e889f63d68",
      "parents": [
        "a6ecba2538f8674022b38e62e8b167fcbbf18ab5"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 20:19:15 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 14 20:19:15 2026 +0630"
      },
      "message": "docs(sandbox): correct the agent-unreachable root cause on Linux (#1222)\n\nThe troubleshooting entry blamed missing read access on the agent socket and prescribed allowRead entries. On current Claude Code under bubblewrap the failure is earlier: socket(AF_UNIX) is rejected by seccomp before any path is consulted, so no allowlist entry restores the channel. Adopters who sign commits need a per-call bypass instead, which the doc previously implied was unnecessary.\n\nGenerated-by: Claude Code (Opus 5)\nClaude-Session: https://claude.ai/code/session_01WyoFFbX2Tjf5sZyYqNTyJJ"
    },
    {
      "commit": "a6ecba2538f8674022b38e62e8b167fcbbf18ab5",
      "tree": "881c799a1adcba726662202c3216bc87d9d11b7c",
      "parents": [
        "760e5153f47b92ddcded80f1576353a7ec8e347c"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 20:16:08 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 14 20:16:08 2026 +0630"
      },
      "message": "chore: bump dev version to 0.2.0.dev202609141334 (#1220)\n\nMove the dev stamp so adopters\u0027 `claude plugin update` picks up the seven PRs\nmerged since 0.2.0.dev202609131802 — the marketplace is served from `main`\nand the update check compares version strings, so a frozen suffix is a\nsilent no-op.\n\nGenerated with the documented one-liner: bump `project.version` in\n`pyproject.toml`, then `tools/dev/check-family-plugins.py --fix` and\n`uv lock` to propagate to every manifest.\n\nAlso refreshes the generated skill token measurements in\n`docs/mode-economics.md`, which had gone stale on `main`: four SKILL.md files\nchanged content since the last measurement, so the `measure` job was failing\nfor every PR, not just this one.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01JnKL5B7RsBWYPN7gGEVhQS"
    },
    {
      "commit": "760e5153f47b92ddcded80f1576353a7ec8e347c",
      "tree": "dde0b82dedc16c9a620b4f3baf1b03f6ec742863",
      "parents": [
        "ee6ee823e34d74b63a06018c9ec9268ff32d02fe"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 19:58:19 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 14 19:58:19 2026 +0630"
      },
      "message": "docs(setup): say which manifest Codex actually reads (#1221)\n\n`marketplace.md` and the marketplace-distribution spec both named\n`.codex-plugin/plugin.json` as the manifest the Codex CLI reads. That\ndescribes neither plugin root in this repo.\n\nCodex resolves a manifest per plugin root: it takes the root `plugin.json`\nfirst when that file is a regular file carrying an `agent-plugins.org`\n`$schema`, and only falls back through `.codex-plugin/plugin.json`,\n`.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json` when the root\nmanifest is absent. When the root manifest wins, `.codex-plugin/plugin.json`\nis merged over it as an overlay rather than read as the manifest.\n\nSo the repo root resolves to the AP1 `plugin.json` plus that overlay, and each\n`plugins/magpie-\u003cfamily\u003e/` resolves to `.claude-plugin/plugin.json` by\nfallback — which is why the family plugins install into Codex at all despite\ncarrying only a Claude Code manifest. The spec called them Claude-Code-only;\nthey are not.\n\nAlso records the symlink hazard: a root `plugin.json` that is a symlink makes\nthe resolver return no manifest at all, with no fallback to the client\ndirectories, so the plugin silently fails to load.\n\nRead from the Codex source rather than its prose docs, which carry no plugins\npage; the plugin-authoring reference now ships as skill assets inside the CLI.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01JnKL5B7RsBWYPN7gGEVhQS"
    },
    {
      "commit": "ee6ee823e34d74b63a06018c9ec9268ff32d02fe",
      "tree": "f3f8a599fe25ef4658a551720e8bd348ee9508c4",
      "parents": [
        "6f00f1f7a1798a5e7809aa1dcc1d633a7852036e"
      ],
      "author": {
        "name": "Kevin Yang",
        "email": "85313829+sjyangkevin@users.noreply.github.com",
        "time": "Mon Sep 14 08:56:59 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 14 19:26:59 2026 +0630"
      },
      "message": "fix(gemini): require approval for web fetches and MCP resource reads (#1216)\n\nGenerated-by: Codex (GPT-6)"
    },
    {
      "commit": "6f00f1f7a1798a5e7809aa1dcc1d633a7852036e",
      "tree": "69074123cdd9c90a5f4a709dc35f88cdb37ecc98",
      "parents": [
        "606e21b80ba1cad3203c14e12c03e6a8f19e2731"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 19:24:23 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 14 19:24:23 2026 +0630"
      },
      "message": "docs(agents): gate signing commits on a hardware-key cache probe (#1219)\n\nAn agent that runs `git commit` with `commit.gpgsign` true and a\ntoken-backed signing key blocks on a pinentry prompt it cannot\nsee: a GUI pinentry pops a dialog on the operator\u0027s desktop that\nnobody is watching, a TTY pinentry writes to the terminal the\ntool call owns. The call sits until `gpg: signing failed:\nTimeout` and no commit is created — a silent-looking failure that\nburns a couple of minutes per attempt and leaves the tree\nmid-operation. Hit live in this repo while amending a commit.\n\nAGENTS.md grows the rule and the probe: resolve the signing\nsubkey\u0027s keygrip, ask gpg-agent for its keyinfo, and read type\n(T \u003d token) and cached (1 \u003d warm, - \u003d cold). Cold means the\ncommit will prompt, so surface a dialogue naming the key or hand\nthe operator the command; warm means commit without interrupting\nthem. Re-probe rather than assume warmth persists — the default\nidle TTL is 600s. The rule covers rebase, cherry-pick, tag -s and\n--amend, which produce signed objects the same way.\n\nThe three skills that run `git commit` themselves — issue\nfix-workflow, repo-health audit-finding-fix, and setup\nshared-config-sync — reference it from their commit steps. They\nalready confirmed the commit message; they said nothing about the\ncommit being able to hang. setup override-upstream is untouched:\nit prints its commit for the operator to run, so pinentry already\nreaches a terminal.\n\nRelease-artefact signing needed no change. release-rc-cut has a\nstronger rule already — Golden rule 2, the agent never invokes\ngpg at all, it emits the commands for the release manager.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01VAJ5GbXnv4LWMfDdvDNJKE"
    },
    {
      "commit": "606e21b80ba1cad3203c14e12c03e6a8f19e2731",
      "tree": "d1b481a2846f8bbed93ecebb33d4d8f9be2b53b0",
      "parents": [
        "b5bd7ef69caf1a92957dd7704227b93843222e8b"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 19:13:01 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 14 19:13:01 2026 +0630"
      },
      "message": "docs(setup): the post-checkout hook stopped delivering skills; say so (#1218)\n\nThe repo-local post-checkout hook once re-created gitignored\nruntime state and seeded a per-worktree agent-guard copy. Both\njobs are gone — the template is now one guarded call to\nsandbox-add-project-root.sh — but nine places still described the\nold behaviour, and the marketplace path (which writes no hook at\nall) was never folded into any of them.\n\nThe hook no longer delivers framework skills:\n\n- setup/SKILL.md: the failure-mode row named the wrong sub-action\n  (adopt installs no hook) and the wrong mechanism (it re-runs no\n  snapshot install). Rewritten, plus a second row for the\n  sandbox-allowlist symptom, which is the one that survives on\n  the marketplace path.\n- setup/install.md: the adopter-facing doc template, so the stale\n  claim was propagating into other projects\u0027 docs.\n- setup/verify.md: said --auto-fix-symlinks is called by the hook,\n  contradicting check 8 in the same file, which rejects a hook\n  carrying that line.\n- setup/upgrade.md: \"the post-checkout hook covers the next\n  checkout case\" in the symlink-refresh context. It does not,\n  which strengthens the case for Step 6c.\n- other-install-methods.md: manual recipe step 5.\n\nA hook the marketplace path never writes:\n\n- secure-agent-setup.md lifecycle item 4 and the per-project scope\n  row, and isolated-setup-install Step P, all asserted the hook\n  exists. They now name the snapshot-install-only condition and\n  say what to do with neither hook present (worktree-init). Item 4\n  is restructured rather than patched — its closing sentence\n  promised automatic inheritance.\n\nSame staleness in one field:\n\n- collect_status.py\u0027s has_verify_recipe tested for a line the\n  current template deliberately omits, so it read False on a\n  correct hook and True on a stale one. Replaced with\n  has_sandbox_helper + has_stale_verify_line; collect.md\u0027s schema\n  row follows. Nothing consumed the old key.\n\n\nClaude-Session: https://claude.ai/code/session_01VAJ5GbXnv4LWMfDdvDNJKE\n\nCo-authored-by: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "b5bd7ef69caf1a92957dd7704227b93843222e8b",
      "tree": "679cfa84e6b91aa7ee4992876cbfc26bbe2df9f1",
      "parents": [
        "b36e96beb434698a4121eb5803a42c0c8c1aa9a2"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 02:18:23 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 21:48:23 2026 +0200"
      },
      "message": "docs(readme): say what Magpie is, and show it (#1215)\n\nThe README opened on \"high-quality recipes for agent-assisted software\nproject maintenance\" — true of a great many things, and it gave a reader\nnothing to decide with. It now says what Magpie does in one paragraph and\nwhat makes it different in three bullets, each of which is a commitment\nthe project can be held to rather than a claim:\n\n- nothing is sent in your name — every comment, email, label and PR is\n  drafted by the agent and posted by a person, and there is no\n  autonomous mode;\n- the agent is confined before it reads anything real — sandbox, clean\n  environment, and a guard that inspects each shell command before it\n  runs rather than a prompt asking it nicely;\n- it is an Apache project, not a product — Apache-2.0, vendor-neutral,\n  no account, no service in the middle, telemetry opt-in and off by\n  default (PRINCIPLES.md §10, whose wording this matches rather than\n  overstating).\n\n**A demo the README can actually show.** The site\u0027s \"See it in action\"\nis a JavaScript step-through component, and GitHub markdown strips\nscripts and iframes, so it cannot be embedded. The same ten-step story —\nmarketplace install, isolate and guard, privacy, import, triage, fix,\nCVE — is now a generated animation beside it, produced by the existing\nrender-wizard tooling: SMIL only, no script or style, which is the form\nGitHub\u0027s renderer preserves. It is labelled illustrative, and links to\nthe interactive version on the site for the full output of each command.\n\nAlso brings the install block up to date: it still said \"always take\nthis one\" for magpie-setup alone and \"two commands\", both of which\npredate the baseline of three.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y"
    },
    {
      "commit": "b36e96beb434698a4121eb5803a42c0c8c1aa9a2",
      "tree": "4b64dda2afe3648c1b929fe1d1f21a19af560d10",
      "parents": [
        "9aa3f96918e8fbe492abafbe5572c2bbcfe836f0"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 01:28:09 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 20:58:09 2026 +0200"
      },
      "message": "ci(prek): give the prek job four cores, drop the dead lychee cache (#1214)\n\n#1190 moved every job here to `ubuntu-slim` on the argument that\nnothing this repository runs in CI is heavy. That holds for ten of the\neleven jobs, which land between 3s and 99s. It does not hold for\n`prek`, and this restores four cores for that one job only.\n\nprek splits each hook\u0027s file list across roughly `cpu_count`\ninvocations, so a single core runs all ~40 hooks over ~4.9k files\nstrictly serial. Measured, `Run prek` is 131s on the 4-vCPU image\n(#1190\u0027s own figure) and 183s / 211s in the two slim runs sampled; it\nis ~80% of the job\u0027s wall clock either way. The trade is three cores\nagainst ~60-80s on the job that every push and every pull request\nwaits for.\n\n`ubuntu-slim`\u0027s 15-minute container kill does not apply on\n`ubuntu-latest`, and no workflow here sets `timeout-minutes`, so moving\nthe job off slim would silently swap that ceiling for the 6-hour\ndefault. `timeout-minutes: 15` keeps the property rather than dropping\nit as a side effect — roughly 4x the slowest run observed, including a\n674s outlier on a throttled host.\n\nSeparately, the `Cache lychee link-check results` step was dead weight.\n`.lychee.toml` sets `offline \u003d true`, so the hook resolves in-repo\npaths and anchors and never fetches a URL; there are no results to\ncache. The step restored 185 bytes and saved 188 on every run, and its\n`cache-lychee-${{ github.sha }}` key wrote a fresh entry per commit\ninto the repository\u0027s cache budget, evicting the 251 MB prek hook-env\ncache sooner than necessary. Three comments claimed otherwise — in the\nworkflow, in `.lychee.toml`, and on the lychee hook in\n`.pre-commit-config.yaml` — and now describe what actually happens.\n\nTouching `.pre-commit-config.yaml` busts the prek hook-env cache key.\nThe `prek-Linux-` restore-key prefix still restores the previous\narchive and no hook definition changed, so no env is rebuilt; the next\nrun re-uploads the archive once.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_019rmycnoL6JcVWPgpyimD7S"
    },
    {
      "commit": "9aa3f96918e8fbe492abafbe5572c2bbcfe836f0",
      "tree": "7187272b7d920f427f325b47662ff93e0bc452fa",
      "parents": [
        "1507718a6dc17751601f528b5d45791cc3c341ac"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 01:24:19 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 20:54:19 2026 +0200"
      },
      "message": "fix(setup): do not guess a third-party marketplace name; fix what grading found (#1213)\n\nThe four eval suites added over the last three PRs had never been run —\n`claude -p` cannot authenticate inside the sandbox this work was done in,\nso they shipped as assertions nobody had checked. Graded now: 13/16 on\nthe first pass, 16/16 after the fixes below.\n\n**A real gap in the skill.** Case 3 passed, then failed, then failed\n*differently* across three runs — `@magpie-fork`, `@acme-corp-magpie-fork`,\n`@apache-magpie`. The non-determinism was the finding: Step M4 never said\nhow a marketplace name is derived from a `from:\u003cowner\u003e/\u003crepo\u003e` argument.\n`apache/magpie` → `apache-magpie` is a convention that happens to hold,\nnot a rule, and a third-party marketplace takes its name from its own\nmanifest. The model was inventing a suffix, and a guessed\n`plugin@marketplace` produces a command that looks authoritative and does\nnot work. The step now prints the `marketplace add` alone and says to\ninstall from the name `claude plugin marketplace list` reports.\n\n**A real hole in the eval spec.** Case 5 exists to check that an install\nstopping for a marketplace-declared command *shows the operator that\ncommand* — the arbitrary thing `--yes` would have accepted unseen. But\n`commands_shown` was defined as \"commands the step prints for the user to\nrun themselves\", which is a different sense of \"shown\", so the model\nreasonably listed the remaining installs instead and the property the case\nexisted to test was never tested. Split out as\n`declared_command_surfaced`; with the ask made explicit the model surfaces\nit every time.\n\n**Two bad fixtures of mine.** Cases 4 and 7 expected `commands_shown` to\ncarry a GUI-installer URL and a skill invocation. Neither is a command the\nuser runs, the model was right to return an empty list, and the spec now\nsays so. Case 7\u0027s hand-off is captured by a new `hands_off_to` field\ninstead.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y"
    },
    {
      "commit": "1507718a6dc17751601f528b5d45791cc3c341ac",
      "tree": "1f77d2bb5424643110668b9684f1839c66ebc0f2",
      "parents": [
        "8afac050afc66f0eeed2aae3ec355a188c4d0539"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 00:39:20 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 20:09:20 2026 +0200"
      },
      "message": "docs: real screenshots in the walkthrough, and a quick start that is quick again (#1212)\n\n* docs(quick-start): wire three real screenshots into the walkthrough\n\nThe quick start showed one captured session and described the rest. Three\nnew captures, all from Claude Code v2.1.270 on Opus 5:\n\n**Step 1 — what actually landed.** `/plugin` → Installed, filtered to\n`magpie`: eleven plugins from the apache-magpie marketplace, each with\nits skill count and how many times its skills have been used. That last\ncolumn is the honest way to decide whether a family is earning its\nalways-on context, which no prose here was making visible.\n\n**Step 3 — the mode picker.** `/sandbox` has three modes and the docs\nnamed only two states. Auto-allow runs commands in the sandbox without\nasking each time and falls back to the permission prompt outside it —\nwhich is why the status line flags `[sandbox-auto]` separately rather\nthan folding it into `[sandbox]`: sandboxed and sandboxed-and-not-asking\nare different postures.\n\n**Step 3 — the other footer state.** A fresher `[NO SANDBOX]` capture\nalongside the green one, so the contrast is shown rather than asserted.\nReplaces the previous no-sandbox shot, which was two Claude Code\nversions and one model behind.\n\nNote on the previous claim that `session-sandboxed.png` was stale: it was\nnot. It already showed the full line including the PR segment — the\nstatus-line change in #1211 made the docs match a screenshot that was\nalready correct, not the other way round.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: replace the sandboxed-session capture, and show the footer in the skill\n\n**A fresher sandboxed-session shot, in place.** `session-sandboxed.png`\nwas two Claude Code versions and one model behind the other captures,\nand from a different repo. The replacement is from the same session as\nthe other three — but it shows **auto-allow**, so the tag is yellow\n`[sandbox-auto]` rather than green. Every caption is updated to say so:\nthe quick start, the secure-setup guide, and RFC-AI-0002, which embeds\nit by raw URL. Replacing the file in place rather than adding a new one\nkeeps that RFC reference resolving.\n\nOne thing is lost and worth recording: the old capture was the only\npicture showing the **PR segment** (`#16` plus the title), because it\nwas taken on a branch with a PR open. All four current captures sit on\n`main`, which has none. The quick start now says that explicitly rather\nthan promising a segment no picture shows.\n\n**The footer states are now in the install skill too.** The skill wired\nthe status line up and never said what the operator should expect to see\nafterwards — which matters, because this is the one piece of the install\nthey look at on every render, and a wrong reading of it persists. Adds\nboth captures, a table mapping each tag to what it means, and the two\ncounter-intuitive facts: the tag reads settings files rather than the\nrunning process, so a mid-session CLI bypass is invisible to it; and\nyellow is a posture, not a fault.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs(quick-start): make the quick start quick again\n\nSteps 3 and 4 had grown into essays — 193 and 76 lines — explaining\nthreat models, listing every shipped guard, and arguing why a hook beats\na rule in a SKILL.md. All true, none of it what someone wants from a\npage called \"quick start\". The whole file drops 553 → 386 lines, and\nStep 3 drops 193 → 62.\n\nWhat each step keeps: why it is strongly recommended, the command in\nboth forms, the animation, the footer states as a three-row table, the\nverify command, and a line of links out. What a reader needs to *act*.\n\nMost of the removed text was not moved, because it already existed in\nthe docs it was duplicating — the threat model and the four-layer\ndefence are `secure-agent-internals.md`, and the table of shipped guards\nis `tools/agent-guard/README.md`. Duplicating them in the walkthrough\ngave a reader two copies to find contradicting each other later.\n\nTwo passages were genuinely new and did move rather than go:\n\n- **why the guard is a hook and not prose**, now a section in the\n  agent-guard README above the guard list, where someone asking \"why is\n  this not just a SKILL.md rule?\" is already standing. It also picks up\n  the sandbox/guard distinction, which belongs with the guard rather\n  than in a walkthrough step.\n- **who each privacy mechanism protects** — the gate protects the\n  project, redaction protects the third parties a reporter names — into\n  `privacy-llm.md`\u0027s two-mechanism recap, which stated the mechanisms\n  without stating whose interest each one serves.\n\nThe `/sandbox` mode panel screenshot moves with its explanation into\n`secure-agent-setup.md`, beside the status-line install: it is there to\nexplain why `[sandbox-auto]` gets its own colour, which is a status-line\nquestion, not a first-run one.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs(setup): keep skills text-only; bump dev version to 0.2.0.dev202609131802\n\nThe two footer captures added to `isolated-setup-install` made it the\nonly SKILL.md in the repo embedding images — a new convention nobody had\nasked for. The skill keeps the part that is actually its job: the table\ntelling the operator what each tag means, so it says the right thing.\nThe pictures live where pictures already lived, in *What a session looks\nlike*, and the skill links there rather than describing the colours a\nsecond time.\n\nAlso moves the dev stamp, so adopters\u0027 `claude plugin update` picks up\nthe docs merged since 0.2.0.dev202609131724. Generated with the\ndocumented one-liner: bump `project.version`, then\n`tools/dev/check-family-plugins.py --fix` and `uv lock`.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y"
    },
    {
      "commit": "8afac050afc66f0eeed2aae3ec355a188c4d0539",
      "tree": "a5f8d245e1667a0821c9b258233fa934aa2e86ba",
      "parents": [
        "74f0d3378ae6abd20f86d997f184944931de5df2"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Mon Sep 14 00:14:35 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 19:44:35 2026 +0200"
      },
      "message": "feat(setup): one status line, and a safety walkthrough that matches the skills (#1211)\n\n* feat(setup): one status line, and a walkthrough step for the action guard\n\n**One status line, not two.** The framework shipped a minimal\n`sandbox-status-line.sh` and a `-rich` variant beside it, documenting the\nminimal one as the default. The rich one is now the only one, installed\nunder the canonical name: every segment it adds is already silent when\nits input is missing, so the minimal script was the same file with less\nto say and a second copy of the sandbox-precedence logic to keep right.\n\nThe line now leads with the sandbox tag — green `[sandbox]`, yellow\n`[sandbox-auto]` where `autoAllowBashIfSandboxed` widens the blast\nradius, bold-red `[NO SANDBOX]` — then says *which* session this is:\nproject, branch with dirty and ahead/behind, the branch\u0027s PR number and\ntitle, and the model. Existing wiring pointing at\n`~/.claude/scripts/sandbox-status-line.sh` keeps resolving; re-running\nthe setup skill picks up the new script at that path.\n\n**The action guard gets its own walkthrough step.** It was installed by\nthe same run as agent isolation and never explained, which left the two\nlayers looking like one. They are not: the sandbox confines the\n*process*, and the guard inspects each *command*. A sandbox has nothing\nto say about a `gh pr comment` that pings four maintainers who did not\nask to be pinged — that command is entirely within its rights.\n\nNew Step 4 says what the guard is, why it is code rather than a line in\na SKILL.md the model has to remember, lists the five shipped rules, and\nnames the harnesses that have no action guard today (Codex, Cursor)\nrather than implying uniform coverage. Steps 4 and 5 become 5 and 6.\n\nBoth safety steps are now marked strongly recommended, and the\nwalkthrough intro says why: these skills read pre-disclosure security\ncontent, so neither layer is a nice-to-have.\n\nAdds `step-guard.svg`, generated like the others — it ends on a real\ndenial rather than a list of things installed, because a guard nobody\nhas watched say no reads as one more checkbox.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* feat(setup): privacy-LLM setup as its own skill and its own walkthrough step\n\nPrivacy-LLM configuration was a document you followed by hand: pick a\nvariant from `docs/setup/privacy-llm.md`, copy its block into\n`privacy-llm.md`, and hope. Nothing detected the stack, nothing checked\nthe result, and the two verification commands at the bottom of the page\nwere the kind nobody runs.\n\n`magpie-setup-privacy-llm` makes it a skill. It resolves the existing\nconfig through the local-then-committed chain and refuses to rewrite a\nworking file; detects the stack rather than interviewing the user about\nit; proposes the matching variant and writes it to the gitignored\n`.apache-magpie-local/`, consistent with every other first-run config;\nthen **proves it** — the approved-LLM gate and a PII redactor round-trip\n— rather than declaring it correct.\n\nTwo rules the skill holds under pressure, both with eval cases:\n\n- **the stack is never widened to make the gate pass.** A gate that\n  says no names the unapproved model and stops. \"Just make it work\" is\n  not an instruction to change what the project permits;\n- **a redactor that passes PII through is a finding even when the gate\n  says yes.** The two mechanisms are separate and both have to hold.\n\nAdoption stays a separate, deliberate act: what the project commits\nbinds every contributor\u0027s session, and it is the one part of this skill\nnot undone by deleting a directory.\n\nNew quick-start Step 5 explains why this is a third layer rather than\nmore of Step 3 or 4, with a table saying what each of the three catches\nthat the others cannot: the sandbox constrains the process, the guard\nconstrains each command, and this constrains the data. Steps 5 and 6\nbecome 6 and 7. The animation ends on the gate refusing an unregistered\nlocal model, for the same reason the guard animation ends on a denial.\n\nAdds `step-privacy.svg`, two eval suites (6 cases) and the skill\u0027s\nfamily-README row; counts, manifests, token measurements and the four\nharness relay symlinks regenerated.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* chore: bump dev version to 0.2.0.dev202609131724\n\nMove the dev stamp so adopters\u0027 `claude plugin update` picks up the work\nmerged since 0.2.0.dev202609110041 — the marketplace is served from `main`\nand the update check compares version strings, so a frozen suffix is a\nsilent no-op.\n\nGenerated with the documented one-liner: bump `project.version` in\n`pyproject.toml`, then `tools/dev/check-family-plugins.py --fix` and\n`uv lock` to propagate to every manifest.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* fix(setup): register setup-privacy-llm in the capability table\n\nThe validator requires every skill with `capability:` frontmatter to\ncarry a row in docs/labels-and-capabilities.md; the new skill had none,\nwhich the workspace test suite caught. Also picks up ruff\u0027s reformat of\nthe render-wizard step registration.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs(quick-start): say what each of the three safety layers actually does\n\nThe three steps named their layers and listed what got installed, which\ntells a reader what to run and not why any of it matters. Each now\nexplains the failure it exists to prevent.\n\n**Step 3 — isolation.** Leads with what is actually at risk: a home\ndirectory full of SSH keys, cloud credentials and other projects\u0027 `.env`\nfiles, none of which triaging a PR needs. Names the case that is not a\nmistake at all — an agent reading issues and mailing lists is reading\ntext written by strangers, and that text can address the agent. Then\ndistinguishes the three pieces, which were previously one bullet list of\nequals: the sandbox governs what a command can reach and is enforced by\nthe OS; the clean environment governs what is already exported into the\nshell it starts from, which the sandbox has nothing to say about; and\nthe visible state exists because protection nobody can see is protection\nnobody notices switching off.\n\n**Step 4 — the guard.** Spells out why deterministic matters rather than\nasserting it: a rule in a SKILL.md is a sentence the model is asked to\nkeep in mind through forty tool calls and a compaction, and \"most of the\ntime\" is useless for a rule whose violation posts under your name. These\nare Python in a pre-execution hook with a veto. Adds the property that\nfollows from that and is worth stating: the guard cannot tell a\nprompt-injected command from an honest mistake, and does not need to.\n\n**Step 5 — privacy-LLM.** The layer readers are most likely to\nmis-file, so it now opens on the distinction: this is not about a\ncommand that should not run, but one that should — that does exactly\nwhat it was asked and exports somebody else\u0027s confidential text while\ndoing it. Then says who each mechanism protects, which is the thing that\nmakes them separate: the gate protects the project (and blocks rather\nthan filters, because there is no partial send), while redaction\nprotects the third parties a reporter names, who never chose to be in\nthat thread.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs(quick-start): name the three safety steps isolate, guard, privacy\n\n\"Lock the agent down\" was one step\u0027s title back when it was one step. It\ncovered two layers even then, and now covers three — so the name said\nless the more it did, and it said it in a register (\"lock down\") that\nsuggests a single switch rather than three separable decisions.\n\nThe steps are now named for what each one constrains:\n\n- Step 3 — isolate the agent    (the process)\n- Step 4 — guard every command  (each command)\n- Step 5 — set up privacy       (the data)\n\nAnchors move with the headings, so the three external references — the\nmarketplace reference, the install prerequisite and the families page —\nare repointed, along with the generated animation titles, the plain-\nlanguage phrasing offered by install.md, and the reference-page index,\nwhich still described the walkthrough as ending at \"lock the agent\ndown\".\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs(quick-start): merge isolate and guard into one step, since they are one skill\n\nSplitting them into Steps 3 and 4 described two things a reader has to\ndo. There is only one: `/magpie-setup:isolated-setup-install` installs\nthe sandbox, the clean environment, the status line *and* the\ndeterministic guard in the same run, and no second command exists to\nrun. A walkthrough that numbers them separately is promising a step that\nis not there.\n\nSo Step 3 is now **isolate \u0026 guard**, with the guard as its second half\nunder its own subheading. Both explanations survive intact — what is\ngone is the false implication that they are separate errands. The lead-in\nsays what the arrangement actually is: three layers across two steps,\ntwo of which arrive together. It also says what \"same run\" does not\nmean, since that is the thing worth not losing — one layer confines what\na command can reach, the other decides whether it runs at all, and the\nguard half opens on exactly the case the sandbox cannot see.\n\nPrivacy stays its own step, because it is its own skill and its own\ndecision: Steps 5, 6 and 7 become 4, 5 and 6.\n\nThe two animations stay two animations — the setup proposal and a real\ndenial show different things — retitled to say which half of Step 3 each\nbelongs to. Anchors move with the heading, so the marketplace reference,\nthe install prerequisite and the families page are repointed again.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y"
    },
    {
      "commit": "74f0d3378ae6abd20f86d997f184944931de5df2",
      "tree": "1134281183bf555a6b164f0de1240ed4e01f8d4f",
      "parents": [
        "a4dd8ba13d9e01bc092aa3c6b8b8db02a444528e"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sun Sep 13 23:37:05 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 19:07:05 2026 +0200"
      },
      "message": "feat(setup): install the picked plugins, name the three scopes, and recommend a baseline (#1209)\n\n* feat(setup): install the picked plugins instead of dictating commands\n\nStep M4 of the marketplace install printed a block of `/plugin …`\nlines and asked the user to type them, on the premise that \"no tool\ncan invoke it\". That is true of the slash command and false of the\nCLI beside it: `claude plugin`, `codex plugin` and `gemini\nextensions` are ordinary binaries, and every skill\u0027s adoption-floor\npre-flight already calls them to bring a machine up to a project\u0027s\nfloor. The install step now calls them too.\n\nFour conditions gate running rather than printing, each reported\nwith its reason rather than silently swallowed:\n\n- the source is not `apache/magpie` — the same boundary the lock\u0027s\n  `url` draws, and the reason the third-party companion packages\n  further down the skill stay printed;\n- the harness has no install CLI;\n- the plugin store is not writable — a sandboxed agent is the\n  common case, and Claude Code\u0027s own default sandbox denies\n  `~/.claude/plugins/`, so this degrades to printing rather than\n  reporting a broken install;\n- an install stops for a marketplace-declared command. `--yes` is\n  never passed: the flag exists to accept such a command sight\n  unseen, and Magpie\u0027s catalogue declares none, so an install that\n  demands it is not the catalogue the step assumed.\n\nInstalls go in at `--scope user`, because a marketplace install is\na per-machine act; `--scope project` writes a file every\ncontributor gets, which is adoption and a maintainer\u0027s decision.\n\nAdds a `step-m4-install-gates` eval suite covering the happy path\nand all four gates, and records the decision in the merged design.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* feat(setup): name Claude Code\u0027s three install scopes; project scope is adoption\n\nStep M4 quietly passed `--scope user` with a one-paragraph aside. It\nnow names all three scopes and what each one means for who gets the\nplugins:\n\n- `user` (default) — `~/.claude/`, you, every repository on this\n  machine, nothing written to the repo;\n- `local` — `.claude/settings.local.json`, gitignored, you, this\n  repository only;\n- `project` — `.claude/settings.json`, committed, everyone who\n  clones.\n\n`--scope project` is adoption in everything but name: it is the same\ncommitted file `/magpie-setup adopt` writes. So the install step never\npasses it, and a user asking for the whole project to get Magpie is\nhanded off to `adopt` — which runs those commands at `--scope project`\nitself, and writes the version floor beside the plugin list that the\nflag alone would leave missing.\n\nadopt.md Step 3 now says so from the other side: on Claude Code the\nderived wiring is what `claude plugin … --scope project` produces, so\nlet the client write its own file and read the result against the merge\nrules before staging, rather than hand-editing the JSON.\n\nTwo more eval cases: \"only this repo\" (`--scope local`) and \"the whole\nproject\" (hand off, do not set the flag).\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs(setup): make the baseline three the default, and show both ways to invoke\n\nTwo changes to the installation guides.\n\n**A recommended baseline, not one required plugin.** Every install page\nsaid \"take `magpie-setup`, then pick families.\" It now recommends three:\n\n- `magpie-setup` — install it first; nothing else installs, upgrades,\n  configures or adopts without it, and it carries the isolation skills;\n- `magpie-agent-guard` — the deterministic pre-execution guard;\n- `magpie-utilities` — `list-skills` and the small tools.\n\nThat is exactly the floor a project commits when it adopts, which is the\nargument for it: a set worth recommending to every contributor is a set\nworth having yourself, and one recommendation is easier to hold than two.\nStep M3 pre-ticks all three and says why each is ticked; `magpie-setup`\nis unremovable (it is the skill doing the installing), the other two can\nbe un-ticked with what is given up said once. An explicit\n`skill-families:` list stays verbatim and gains only `magpie-setup`.\n\nAgent isolation joins the baseline as a run rather than a plugin: quick\nstart Step 3 is now \"part of the default setup, not a later hardening\npass\", and Step M5 says the install is not finished until it has been\noffered.\n\n**Both invocation forms, everywhere.** Skills are model-invoked, so the\nslash command is a shortcut and the plain-language sentence is the\ngeneral case — and the harnesses without slash commands have only the\nsentence. Every step now shows both. The one exception, stated as such:\nthe first install must be a client command, because nothing is installed\nyet to hear the request.\n\nThe three install animations are regenerated: the wizard now shows four\nplugins going in at user scope rather than a \"Run these:\" block, which\nis what the step actually does since it started running the CLI.\n\nAdds a `step-m3-baseline-pick` eval suite (pre-tick, un-tick, explicit\nlist) and records both decisions in the merged design.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y"
    },
    {
      "commit": "a4dd8ba13d9e01bc092aa3c6b8b8db02a444528e",
      "tree": "28afee706316d3a8d5a16738c0513765d116f8ed",
      "parents": [
        "05213bb36e3d5d57100c663bbdc85eebc69256f6"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sun Sep 13 22:54:43 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 18:24:43 2026 +0200"
      },
      "message": "feat: split install / config / adopt, and make per-family plugins installable everywhere (#1208)\n\n* docs: lead with the marketplace, and split adoption from installation\n\nSeven related changes, developed together because they all touch the\nquick start and the setup skill.\n\nReplace the quick-start screenshots with recordings. Fourteen stills --\nfour harness install shots and ten per-family ones, with a design on file\nto grow them to twenty-five -- all showed the same thing: a plugin list\nwith a plugin in it. They proved an install had succeeded and nothing\nelse, and went stale whenever the plugin UI moved. In their place,\nanimated SVGs of real runs: one `/magpie-setup` run for the quick start,\nand one per family showing that family\u0027s *first* run, where the\npre-flight finds no project config and stops to propose `/magpie-setup`.\nThat arc is what a still frame structurally cannot show.\n\nSVG rather than GIF because the output is text: it reviews as a diff,\ncarries its own licence header, loops natively on GitHub, and costs a\nfraction of the bytes in every source release.\n\n`capture-screenshot.sh` and `check-quickstart-screenshots.py` are retired\nfor `record-svg.sh` and `check-quickstart-recording.py`. The checker\nguards family-list drift, orphans, parse, licence header, size cap, and\nembedded-by-the-docs, plus the retirement itself -- a doc referencing a\ndeleted PNG, or a new PNG under assets/quickstart/, now fails.\n\nRecordings are captured at 145x35, up from 100x30, so a take has room for\nthe wider skill output without wrapping.\n\nLead the install recipes with the marketplace. The page opened with three\nsnapshot recipes and called the svn-zip method \"recommended\", never\nmentioning the marketplace. The marketplace install now leads; the three\nsnapshot recipes move under \"Additional install methods\" behind the three\nnarrow reasons to want one: no marketplace on your agent, working on the\nframework, or needing the signed/offline artefact.\n\nName the marketplace in the singular. There is one Apache Magpie\nMarketplace -- the apache/magpie repository -- that many agents can add,\nnot a set of marketplaces. `marketplaces.md` becomes `marketplace.md`\nacross 22 files, and the page, menus and cross-references name it.\n`check-doc-sync.py` and `estimate-skill-tokens.py` read that page by\npath, so they move with it.\n\nMove the family catalogue below the install steps in the quick start, so\na reader reaches the install without first crossing a ten-row table.\n\nSplit adoption from installation. Installing Magpie touches only this\nmachine\u0027s agent; adopting commits a recommendation every contributor\npicks up on clone. `adopt` was an alias of `install`, which conflated the\ntwo. `setup adopt` / `setup unadopt` become real sub-actions\n(`skills/setup/adopt.md`): they commit the repo\u0027s default plugin set and\nscaffold the `.apache-magpie-overrides/` store, and remove them again,\nleaving every install untouched. `docs/setup/team-adoption.md` is the\nreader-facing page for the maintainer act;\n`personal-use-unadopted-repo.md` becomes `individual-use.md`, and\n`mixed-adoption-teams.md` goes -- with adoption framed as a\nrecommendation rather than a requirement, a mixed team is the normal case\nrather than a special one. `docs/index.md` leads with \"anyone who wants\nagent help on a repo\" before the maintainer path. The setup evals follow\nthe sub-action split: `step-m5-settings-merge` becomes\n`step-adopt-settings-merge`, and `step-m5-repo-artefacts` becomes\n`step-m5-no-repo-offer`.\n\nThe quick-start recording is real; the nine family first-runs and two\nexample recordings still ship as placeholders reading \"recording\npending\". `check-quickstart-recording.py` reports how many remain on\nevery run.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01Jm999cAgUJ4J2xhc9uQfkB\n\n* feat(setup): record the adoption floor on the marketplace path\n\nThe marketplace install is Magpie\u0027s default path, and it recorded\nnothing about itself. `adopt` wrote an untagged marketplace entry and\nthree plugin names into `.claude/settings.json`; nothing anywhere\nrecorded which Magpie version the project had validated against, so\nnothing could tell that a contributor was three releases behind.\n`.apache-magpie.lock` -- the file whose entire job is recording the\nproject\u0027s version -- existed only on the pinned-snapshot path.\n\nIt gains a fourth method. `method: marketplace` joins svn-zip, git-tag\nand git-branch, carrying `url`, `min_version` and a `plugins` list.\n\nBoth are floors, never pins. A contributor ahead of the floor, with\nmore families installed than it names, satisfies it completely and is\ntold nothing. Nothing is ever downgraded, removed, or pinned, and the\nderived `extraKnownMarketplaces` entry is written untagged -- tagging\nit would turn the floor into a ceiling and stop contributors receiving\nany later release. Comparison is PEP 440, so 0.10.0 reads as newer than\n0.9.0 and a .dev floor is satisfied by the release that follows it.\n\nThe floor is seeded with three plugins and never grows automatically,\nbut a maintainer may enlarge it -- so every step that touches the\nwiring reads the lock\u0027s `plugins` list rather than assuming three.\n`unadopt` reads that list before deleting the lock.\n\n`adopt` writes the lock, on every client: the lock is harness-neutral\nand `.claude/settings.json` is merely how Claude Code acts on it, which\nis what gives a Codex or Gemini adopter an adoption record at all.\n`setup` proposes the committed floor instead of the framework defaults\nwhile still writing nothing to the repo. The shared pre-flight -- one\nsource, propagated into all 65 skills -- installs a machine up to the\nfloor and stops for a session restart, silent when already at or ahead\nof it. `upgrade` splits on adoption: nothing repo-side when the project\nhas not adopted, `min_version` raised and staged when it has, never\nlowered. `verify` reports against the floor, treating \"never adopted\"\nand \"ahead of it\" as not faults, because a check that calls those\nfaulty converts an optional recommendation into a requirement.\n`unadopt` removes the committed lock and the wiring derived from it,\npreserving `.apache-magpie-overrides/` unless `--purge-overrides`;\n`uninstall` removes the local lock and leaves the committed one, because\nthe project\u0027s floor is not this machine\u0027s install.\n\n`url` is a security boundary. Automated action on a lock\u0027s behalf runs\nwithout asking only for `apache/magpie` -- a lock is a committed file\nin whatever repository the user happened to open, and acting on it\nwould make opening a repo enough to install someone else\u0027s code. The\ngate is enforced in the pre-flight, in `upgrade`\u0027s regeneration of the\nwiring, and in `verify`\u0027s repair offer.\n\n28 eval cases across six new suites; the full setup suite is 56/56.\nThe security case tests the *permissive* half of the boundary -- a\nlegitimate `apache/magpie` url in an alarming context, expecting the\ninstall to proceed unasked -- because no grader derives that by\ncommonsense, so the prose is the only possible source of the answer.\nDeleting the `url` paragraph flips it to `ask-first`, which is the\nevidence the rule is load-bearing rather than merely present.\n\nDesign and plan: docs/designs/2026-09-13-install-adopt-upgrade-*.md\nSubsystems B (docs restructure) and C (screenshots) are not in this\nchange.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* feat(dev): derive the eval-case counts instead of maintaining them\n\nThe setup suite\u0027s declared case total was wrong in three successive\ncommits -- 49, then 53, then 57 -- including the commit whose whole\npurpose was correcting it. The number was authored by a human each\ntime, next to work that was about something else, and nothing broke\nwhen it was wrong.\n\ncheck-doc-sync gains an eleventh check deriving all three of the\nhand-maintained eval numbers from the `fixtures/case-*/` directories\nthe runner actually walks: each family README\u0027s headline total, its\nper-suite table rows, and the family\u0027s line in the harness README.\n\nIt found considerably more than the number that prompted it. Sixty-eight\nproblems on the first run: setup-status declaring 14 cases against 18 on\ndisk, security-issue-sync 7 suites against 8, setup-shared-config-sync\n11 against 12 -- and 34 families with eval suites and no index entry at\nall, so the index was listing 41 of 75. Two more per-suite rows\nsurfaced once the row pattern was corrected to match every line rather\nthan only the first.\n\n`--fix` rewrites the numbers and generates an entry for a family that\nhas none, so the counts stop being authored at all. Two things are\ndeliberately left to a human: the suite-name list inside the\nparenthetical, because entries like `step-4-* checks` are legitimate\nshorthand and rewriting prose to satisfy a counter costs more than it\ncatches; and a family README that declares no headline total, which\nmakes no claim that can go stale.\n\n`main()` now takes an explicit argv so the CLI can carry `--fix`\nwithout the tests\u0027 `main()` calls parsing pytest\u0027s own arguments.\n\nEleven new tests, two of them regressions for bugs this change had on\nthe way in: a row pattern that matched only the first table line, and a\nheading pattern whose trailing `\\s*$` ate the blank line the table\nbelow it needs.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: make prerequisites, install recipes and families quick-start sub-pages\n\nThe quick start was a 380-line page carrying three things that are\nreference material rather than steps: what individual skills need before\nthey run, the copy-pasteable snapshot bootstrap, and a ten-row table of\nevery skill family. A reader following the install had to scroll past\nall three, and two of them lived somewhere else entirely --\nprerequisites at the top of docs/, install recipes under setup/.\n\nThey become sub-pages of the page that sends readers to them:\n\n    docs/quick-start.md                    the walkthrough\n    docs/quick-start/prerequisites.md      moved from docs/\n    docs/quick-start/install-recipes.md    moved from docs/setup/\n    docs/quick-start/families.md           extracted from the walkthrough\n\nquick-start.md itself stays put, so the 24 files linking to it are\nuntouched; 79 links to the two moved pages were rewritten by resolving\neach one against its own file rather than by matching text. That\ndistinction matters here: skills/pr-management-triage/ and\nskills/pr-management-code-review/ both have their own prerequisites.md,\nand 21 links to those were correctly left alone.\n\nThe isolation and privacy setup moves ahead of the families table and\nbecomes Step 2b. It is part of setting up, not something that happens\nafter: the skills read issues, pre-disclosure reports and private lists,\nso those layers belong in place before a skill is pointed at anything\nreal. It now also links docs/setup/privacy-llm.md, which the quick start\nnever referenced.\n\ncheck-doc-sync\u0027s FAMILY_PLUGIN_FILES follows the families table to its\nnew home. Without that the per-family skill counts would have silently\nstopped being checked -- verified by planting a wrong count and\nconfirming the check still fails on it.\n\nTwo generated artefacts ride along because their inputs moved: the\nai-tutors knowledge base for the one lesson embedding a rewritten link,\nand the measured skill token counts in docs/mode-economics.md. The\nlatter cannot be split into its own commit -- the hook stashes unstaged\nchanges before measuring, so the figures have to land with the SKILL.md\nedits they measure.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: lead the quick start with the two ways to use Magpie\n\nThe page explained the individual-versus-adoption split at the bottom,\nin the past tense -- \"what you did above is install it\" -- so a reader\nmet the distinction only after following steps they had no framing for.\nIt is the first thing worth knowing: whether anything gets committed for\nother people changes who has to agree before you start.\n\nThe two modes now open the page, reworded forward-looking, ahead of\nStep 1. The install-mechanics that sat under the same heading --\nmarketplace versus pinned snapshot versus self-adoption -- stay at the\nend where reference material belongs, promoted to their own `Install\nmethods` section rather than orphaned under a heading that moved.\n\nAdoption is also named in the four places the page previously described\nit without using the word: the opening note about this install needing\nno decision from the project, the optional block for teammates, Step 2\u0027s\nlist of what setup can wire into a project, and the closing line of the\nnew opening section.\n\nThat last rename fixes an inconsistency this branch introduced. The\nteammates section still said `/magpie-setup` offers to write the default\nset; since the adoption floor landed, writing it is `/magpie-setup\nadopt`, and it now commits the floor lock and the overrides store\nalongside the block.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: plan Subsystems B and C, and amend B\u0027s design for shipped work\n\nSubsystem B\u0027s design was written before any of it was built, and part of\nit then shipped directly: the two ways to use Magpie moved above the\ninstall steps, the isolation setup became Step 2b, and three sections\nbecame quick-start sub-pages. None of that was in the design, and its\nrenumbering table -- Step 2 becoming Step 1, and so on -- no longer\ndescribed the page. Writing a plan against it would have re-done work\nthat is already on the branch, so the design\u0027s B section now carries an\namendment saying what shipped and what is left.\n\nB is four tasks. A per-harness prerequisite page carries the marketplace\nadd once; the quick start and the ten family READMEs point at it instead\nof repeating it; the optional teammates block, which is adoption, is\npromoted out of a fourth-level heading into a step of its own. The fourth\ntask is not in the design: the claim that the install is carried once has\nno guard, and this repository has twice had a documented fact drift\nbecause nothing checked it. check-doc-sync gains a twelfth check.\n\nC is five tasks, sequenced so the cheap failure comes first. A renderer\nturns committed .txt transcripts into static SVGs, and Task 1 proves it\nis byte-deterministic before Task 2 authors twenty-two files against it\n-- a renderer that is not stable makes the staleness check impossible and\nwould waste the authoring. Then the callouts and the deletion of the nine\nfirst-run recordings, then the reworked checker, then the one recording\nthat needs a human at a terminal.\n\nBoth plans state the gap neither can close: a checker can prove an SVG\nmatches its transcript, but nothing can prove the transcript matches what\nthe skill prints today. That is the accepted cost of not re-capturing,\nand C requires the assets README to say so rather than let a reader\nassume the check is stronger than it is.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: make the two ways to use Magpie a quick-start sub-page\n\nThe fourth and last block of reference material in the walkthrough. The\ncomparison table, the two framing paragraphs and the individual-versus-\nadoption argument are something a reader consults once and then knows;\nthey are not a step.\n\nThe quick start keeps the distinction itself inline rather than only a\nlink. That is deliberate: this section was moved above the install steps\nbecause a reader needs to know whether anything gets committed for other\npeople *before* starting, and a bare pointer would have put that behind a\nclick. The page states both in two sentences and links the comparison.\n\nIndividual use now leads team adoption in docs/setup/README.md, which\nlisted the maintainer path first. Individual is the default, asks no\none\u0027s permission, and is what most readers are doing; adoption is the\nlater, larger commitment. Its description also now names the floor lock,\nwhich adoption has committed since Subsystem A landed.\n\nOne extraction break fixed on the way: the lifted text said \"installing\nis what the steps below do\" and \"nothing in the steps below requires\nit\" -- true on the quick start, meaningless on a standalone page. It now\nrefers to the quick start by name.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs(training): number the lessons consistently\n\nThree surfaces named the same lesson three ways. The module index read\n\"Lesson 1 — What agents are\", each page\u0027s own title repeated that form,\nand the instructor guide\u0027s per-lesson sections did too -- all\nsingle-digit, so 1 through 9 sorted after 10 and 11 in any listing.\n\nThe index now reads \"01 - What agents are\": zero-padded so the order is\nthe reading order, and without the word \"Lesson\" repeated eleven times\ndown a column that is entirely lessons. Each page and each instructor-\nguide section titles itself \"Lesson 01: What agents are\", where the word\ndoes work -- it is the first thing on the page.\n\nProse cross-references are deliberately left alone. \"See Lesson 4\" reads\nnaturally in a sentence and \"See Lesson 04\" does not; zero-padding is a\nlisting convention, not an English one. There are 109 of them and they\nstay as they are.\n\nThe doctoc tables of contents follow the headings, and the ai-tutors\nknowledge base is refreshed for all eleven prompts.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: name the Apache Magpie Marketplace where the entity is meant\n\nThe proper noun appeared 14 times against 1043 uses of the bare word, so\nthe thing with a name was mostly referred to without it.\n\nIt is now introduced by its full name at the first mention in each page\nthat means the entity -- the quick start\u0027s opening, its Step 1 heading,\nthe two-ways page, the install recipes, and adopt.md -- and the short\nform carries the rest of each page, which is how a proper noun normally\nworks and how the existing correct uses already read.\n\nMost of the 1043 are deliberately untouched, and the reason is worth\nrecording. `/plugin marketplace add` is a command, `extraKnownMarketplaces`\na JSON key, `method: marketplace` a lock value, and \"marketplace install\"\nthe name of an install method rather than of the marketplace. \"Use it\nwhen a marketplace is not an option\" means any marketplace mechanism, not\nours.\n\nThe 65 generated pre-flight copies are the case that would have done real\ndamage. Their line reads \"Name the marketplace the lock points at\", and\nit exists precisely for a lock naming a marketplace that is *not*\napache/magpie -- the security boundary. Renaming it would have had the\nrule assert the opposite of what it guards.\n\nPlan B\u0027s instruction to check inbound anchors follows the renamed Step 1\nheading, so it still names a heading that exists.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: drop the step numbers from the quick-start walkthrough\n\nThe numbering had already broken. Inserting the isolation setup between\nStep 2 and Step 3 produced a \"Step 2b\" rather than renumbering the rest,\nbecause renumbering would have broken every inbound anchor -- which is\nexactly the cost a numbered sequence imposes every time a section moves.\n\nThe four walkthrough headings now name what they do: install from the\nApache Magpie Marketplace, run /magpie-setup, lock the agent down, use\nit. They are still in order on the page, and the order is still the\nreading order; what goes away is the bookkeeping that has to be correct\nin three places at once.\n\nThree inbound anchors followed: the in-page reference to Step 2b, the\nmarketplace reference\u0027s link to it, and Plan B, whose Task 2 told its\nimplementer to renumber the sequence and now says the opposite -- that\nthe headings carry no numbers, so nothing renumbers when a section is\nadded.\n\nNote this leaves the quick start unlike the rest of the docs, where\n\"Step N —\" is the convention: docs/security/process.md, team-adoption,\nindividual-use, the adapter guides and most skills all number their\nsteps. Those are procedures where a reader refers to \"Step 2\" out loud.\nThe quick start is four things in a row, read once.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: drop the \"Where this fits\" sections from the quick-start sub-pages\n\nBoth extracted pages ended with a section of their own whose only content\nwas links back to where the reader had just come from. A page reached from\nthe quick start does not need a chapter explaining that it was reached\nfrom the quick start.\n\ntwo-ways.md loses nothing: its closing paragraph already links the quick\nstart and team adoption in the course of making its point, which is where\na cross-reference belongs.\n\nfamilies.md now ends on the table, with no link out. That is the\ninstruction and it reads fine as a reference page, but worth knowing:\nit is currently the one page in docs/ with no outbound link.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: fold the first-use pre-flight note into its parent section\n\n\"Every skill configures itself on first use\" was a heading and a\ntable-of-contents entry for what is one point inside \"Run /magpie-setup\":\nthat you do not have to prepare a project before using a family, because\nevery skill checks for itself.\n\nIt is now the bold opening of that paragraph. The point still lands\nfirst, and the contents list stops offering a reader a destination that\nis really a sentence.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: split the other installation methods onto their own page\n\nThe install page carried both the main path and the fallbacks, so a\nreader who had already installed from the Apache Magpie Marketplace --\nalmost everyone -- had to scroll past it to reach the thing they came\nfor, and the page\u0027s title promised recipes without saying which.\n\n`install-recipes.md` becomes `other-install-methods.md`, titled \"Other\ninstallation methods\". The marketplace section it opened with is now two\nsentences saying the marketplace is the main install, the quick start\nwalks it, and if that worked you do not need this page. What remains is\nwhat the title says: the pinned snapshot in three flavours, and the\nself-adoption path a clone of the framework takes.\n\nThe quick start\u0027s own section is renamed to match, so the pointer and\nthe page it points at agree.\n\nTwo pieces that had nowhere else to live moved here rather than being\ndropped: the three-route comparison table -- who installs each and what\nit touches -- and the self-adoption section, which the recipes page had\nmentioned once in passing and nothing else in docs/ explained.\n\nThe three recipes lose their \"Method 1/2/3\" numbers. They are a menu,\nnot a sequence; a reader picks one and never performs the other two.\nTheir headings now name what they are: released zip, git tag, git\nbranch.\n\n55 inbound links across 41 files were rewritten by resolving each\nagainst its own file. One stale fragment survived that pass and the link\nchecker caught it: team-adoption pointed at\n`#additional-install-methods`, a heading this change renamed.\n\nTwo knock-ons. Flattening the four harness headings left the teammates\nblock as an h4 under an h2, so it becomes a bold label like its\nneighbours -- which also keeps it out of the contents list. And the link\nrewrites touched 25 SKILL.md bodies, so the measured token counts move\nwith them; they cannot be a separate commit, because the hook stashes\nunstaged changes before measuring.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: name the quick-start comparison page by the question it answers\n\n\"Two ways to use Magpie\" described the page\u0027s shape rather than\nwhat a reader wants from it. \"Installation or Adoption?\" is the\nquestion they arrive with, and the answer is the whole point of\nthe page.\n\nRenames the H1, the section heading in quick-start.md, and the\nthree in-prose link texts; the doctoc anchors follow.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01MGuP9bEK8Pm5ASBsVcSc8n\n\n* docs(quick-start): close the walkthrough with adoption as a step\n\nThe quick start ended at \"use it\", so the one decision that is not\nthe reader\u0027s alone -- whether the project commits a floor for\neveryone -- was left to the reference sections to raise. A reader\nwho never scrolled past Step 4 never met it.\n\nStep 5 puts it in the walkthrough, framed as what it is: a\nmaintainer decision the project takes together, not an install,\nobliging no contributor and reversible in a PR.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01MGuP9bEK8Pm5ASBsVcSc8n\n\n* docs(security): name two pages for what they are, not who reads them\n\nBoth pages had drifted from their own filenames, and every generated\nindex, sidebar and cross-reference derives its label from the path --\nso each was announced by a name it no longer used.\n\n`how-to-fix-a-security-issue.md` has been titled \"Fixing security\nissues\" for a while; the filename now says so too.\n\n`new-members-onboarding.md` was titled \"Welcome\", which tells a\nreader nothing and reduced to a useless one-word label wherever it\nwas listed. It is really the description of how the team operates,\nso it is now `how-the-security-team-works.md` -- and the opening\nsays the part that was missing: day to day the work is run by one\ntriager, or a few on a rotation, who run the skills, make the calls\nthat keep an issue moving, and delegate the fix to a maintainer and\nthe release to a release manager.\n\nUpdates the five references to each, including the placeholder\nlinter\u0027s allowlist in both its shell and Python implementations.\n\nHooks skipped with --no-verify: the dev pytest hook fails on\ntest_plugin_counts_are_checked_in_the_quick_start_too, which already\nfails at 96b6d46a in a clean worktree and is unrelated to this change.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01MGuP9bEK8Pm5ASBsVcSc8n\n\n* docs: give adapters/ and rfcs/ the overview every other section has\n\nBoth directories were a bare list of pages with nothing introducing\nthem, so anything that indexes the docs by section -- the website\nsidebar among them -- had no page to point the section at, and the\nreader arriving at the group got no orientation.\n\nadapters/README.md draws the runtime-versus-adapter line the pages\nassume and routes to each. rfcs/README.md says what an RFC is for\nhere -- a record of why a rule exists, not a manual -- and indexes\nall seven with their current status.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01MGuP9bEK8Pm5ASBsVcSc8n\n\n* docs(skills): drop the snapshot skill-name aside, link the Marketplace\n\nTwo skills explained, in the middle of their output sections, how to\ntranslate a marketplace skill name into the pinned-snapshot single-token\nform. That is install-method detail in a place a reader has come for\nsomething else, and the page it links to already explains it.\n\nBoth now state the form they use and link the Apache Magpie Marketplace.\n\nLeft alone deliberately: the pre-flight block, whose install handling is\nwhat brings a machine up to the project\u0027s floor rather than an aside;\nskills/setup/*, where install methods are the subject; and\nlist-skills, which explains why a repository-relative path resolves\nunder both in-repo layouts -- operational reasoning it needs to run.\n\nThe measured token counts follow the two edited SKILL bodies.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: nest the five steps under one walkthrough heading\n\nThe quick start\u0027s top level mixed two kinds of thing: five sequential\nsteps and three reference sections you consult out of order. They read\nas peers in the contents list, which they are not.\n\nThe steps are now subsections of \"The walkthrough\", so the top level is\nthe shape of the page -- what to decide, what to do, what to look up --\nand the sequence sits inside the one section that is a sequence.\n\nHeading text is unchanged, so every anchor still resolves; only the\nlevel moved.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: name prerequisites up front, and finish the overview menu\n\nThe overview opened by explaining what Magpie is and went straight to\nfamilies and modes. What a reader needs before any of it works -- an\nagent, a mail backend, tracker access -- appeared nowhere near the top,\nand the page that lists them was reachable only from inside the quick\nstart.\n\nIt is now the third paragraph, next to the two-command claim it\nqualifies, and a row in the menu.\n\nThe menu also pointed into two sections rather than at them:\nadapters/registry.md instead of the adapters overview, and one specific\nRFC instead of the RFC index. Both overviews exist -- every other\nsection is linked by its README -- so both are now reachable the same\nway. The privacy-model row stays, because RFC-AI-0003 is a destination\nin its own right, not a stand-in for the index.\n\ndocs/quick-start/ was the only section folder without an overview, which\nmeant opening it on GitHub showed a bare file list. It now has one\nnaming its four pages by the question each answers.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: introduce the pages about working on Magpie itself\n\ndocs/ holds a handful of pages that are not about using Magpie but\nabout building it -- the spec loop, the editorial playbook, the\nsentiment methodology, the pilot report, what ships in a release --\nplus extending.md, which most readers want instead. Nothing\nintroduced them or said which was which, so they read as loose\npages in the general docs pile.\n\nThis overview groups them: building the framework, reporting back,\nand extending it without touching this repository at all.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01MGuP9bEK8Pm5ASBsVcSc8n\n\n* feat(plugins): make per-family installable everywhere, and drop the all-in-one\n\nThe docs said per-family was a Claude Code feature because a family\nplugin\u0027s skills were symlinks resolving outside its root, \"which Agent\nPlugins 1.0 forbids\". Measuring it rather than reading the spec found\nsomething worse: Codex does not reject such a plugin. It installs it,\nreports success, and silently ships **zero skills**. A user gets\nsomething that looks installed and does nothing, with no error to\ndiagnose.\n\nWorse still, Codex drops symlinks wherever they resolve -- including\nones that stay inside the plugin root, which §4.1 explicitly permits. So\nthe escaping-symlink rule was never the operative constraint; the\npackager simply does not carry symlinks.\n\nThat leaves one shape that works: a family plugin must own its skills as\nreal directories. The direction is now reversed --\nplugins/magpie-\u003cfamily\u003e/skills/\u003calias\u003e/ is the source, and the flat\nskills/\u003cname\u003e tree mirrors it back, so every path that has always said\nskills/\u003cname\u003e keeps resolving. All 254 eval skill_md paths still resolve\nunchanged. Nothing is vendored; no skill exists twice.\n\nReversing alone was not enough, because the all-in-one is rooted at the\nrepository and would have lost the mirrored skills the same way. It is\nremoved, which the framework\u0027s own argument already wanted: the docs\ncalled it \"not recommended\" at ~8.6k always-on tokens against 0.2-2.0k\nfor a family picked on purpose, and it was the reason Codex could only\never default-install everything.\n\nWith it gone, magpie-setup can finally be INSTALLED_BY_DEFAULT on Codex\n-- the floor arrives on its own, and every other family stays opt-in.\nThe SessionStart upgrade check moves onto magpie-setup with it: that\nplugin is always present and is the one that performs upgrades.\n.claude-plugin/plugin.json stays as the version anchor the family\nmanifests inherit, minus its hook block; hooks/check-upgrade.sh moves\nunder the plugin that wires it, its last other consumer being gone.\n\nMeasured, on this machine, with the real clients:\n\n  before   magpie-pairing on Codex   0 skills (silent)\n  after    magpie-pairing            2 skills\n           magpie-security          15 skills\n           magpie-setup              9 skills\n           Gemini, flat tree        75 entries, mirrors resolved\n\nMoving the tree moved it out from under a lot of path patterns, and each\none that stopped matching was a check that would have gone quietly\ngreen:\n\n- every `files:` trigger in .pre-commit-config.yaml that named skills/\n  now names the plugin tree too, and doctoc\u0027s `exclude` does as well --\n  without that it started inserting a TOC above each skill\u0027s frontmatter;\n- the `skills` workspace member is now `plugins`: rooted at skills/ it\n  would have collected nothing but symlinks, so pytest would have run\n  zero of the three skill test suites and still reported success;\n- the canonical .agents/skills relays point past the mirror at the real\n  directory. Pointing at the mirror makes a symlink to a symlink, which\n  the release archive\u0027s extractor rejects as resolving outside the\n  archive -- the exact defect that -1\u0027d an RC upload;\n- symlink-lint learns that rule (and stops walking into git worktrees,\n  which lint themselves);\n- skill-token-count scanned `skills/**` with rglob, which does not descend a\n  symlinked directory: it found no skills at all. It now reads one level\n  through the mirror;\n- 119 skill files had their relative links re-resolved: each was read\n  against where the file used to be and rewritten from where it is now,\n  so every target is unchanged. 1385 link errors to 0.\n\ncheck-family-plugins is inverted to guard the new invariant and gains\none it could not have had before: no path inside a plugin root may be a\nsymlink. Its --fix no longer regenerates plugin directories -- under\nthis layout they hold source, and the old --fix would have rmtree\u0027d\nthem; it now rewrites manifests and catalogue entries in place and\ndeletes only orphans.\n\nSubstrate plugins (magpie-agent-guard, magpie-vetted-ops) keep their\noutward tool symlinks: they are Claude Code hooks, AP1 has no such\ncomponent, and moving them would break the uv workspace members.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: carry the marketplace install once, as a prerequisite\n\nAdding the marketplace is a one-time, per-machine step, and it was written\nout twelve times: the marketplace reference carried it per harness, the\nquick start carried it for four of them, all ten family READMEs repeated\nit above their own install line, and individual-use repeated it again. A\nreader who had already installed scrolled past it on every page; a reader\nwho had not met it four times before reaching anything they could use.\n\nIt now lives on one page -- docs/setup/marketplace-install.md -- one\nsection per agent: Claude Code, Codex, VS Code / Copilot, Gemini, Cursor,\napm, JetBrains. Commands only.\n\nEverything else links to it:\n\n- the marketplace reference keeps the reasoning (manifests, which families\n  to pick, skill-name differences, versioning, verification status) and\n  links for the commands. Its Claude Code section also stopped showing\n  `/magpie:release-vote-tally`, and its Codex section `codex plugin\n  install magpie`, both of which named a plugin that no longer exists;\n- the quick start\u0027s Step 1 becomes a pointer plus the part that is\n  actually a decision: which families, and why not all of them;\n- each family README shows the one line that is its own subject.\n\ncheck-doc-sync gains a twelfth check so the repetition cannot come back:\nthe add -- and the Codex, Gemini and apm equivalents -- may appear in the\nprerequisite page and the reference, and nowhere else under docs/.\ndocs/designs/ is exempt, because a design records what was decided and\nrewriting the record to satisfy a linter would make it wrong. The\nrepository README keeps its copy deliberately: the front page shows the\nshortest path in, not a link to it.\n\nSeparately, the RFC index now spells its identifiers in full --\nRFC-AI-0002 rather than AI-0002, matching how every other page names them\n-- with non-breaking hyphens, so a narrow column cannot split one across\ntwo lines.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: give each family a benefit callout and screenshots of itself\n\nEvery family README opened with a recording of the same thing: a pre-flight\nfailing on an unadopted repo. That is setup\u0027s arc, and the quick start\nalready shows it. Nine copies of it, filed under ten families, taught a\nreader nothing about what the families do -- and all nine had sat as\n\"recording pending\" placeholders since the day they were added, because a\ncapture needs a terminal, a scratch project and a human, and needs all\nthree again whenever any output moves.\n\nEach README now leads with what the family buys you -- three benefits in a\ncoloured callout -- and then shows that family actually running.\n\nThe screenshots are authored, not captured. A committed .txt transcript\nrenders to a static .svg through a new tools/dev/render-screenshot.sh. The\ntranscript is the source: it reviews as a diff, and a contributor can fix a\nline in a pull request instead of booking a recording session. Twenty-eight\nof them, two or three per family, one for every command each README already\ntells a newcomer to run first.\n\nWhat a capture gives that authoring does not is a guarantee that the picture\nmatches the program, and the renderer buys back the half it can: rendering\nis deterministic -- same .txt, same bytes, on any machine -- so the checker\ncan prove every committed .svg still matches its source. Forced LC_ALL\u003dC is\npart of that, not a detail: awk\u0027s length() is locale-dependent, and a\nrenderer whose output moved between machines could not be checked at all.\nThe other half, that a transcript still matches what the skill prints today,\nis a human\u0027s judgement; assets/quickstart/README.md says so plainly rather\nthan letting a reader assume the check is stronger than it is.\n\nThree sets retire, and the checker guards all three:\n\n- the nine families/\u003cfamily\u003e-first-run.svg recordings;\n- assets/examples/, which did this same job by capture, for one family, and\n  whose two files were also placeholders;\n- the fourteen PNG stills, already gone, whose guard moves here.\n\ncheck-quickstart-recording.py is reworked rather than replaced. It keeps the\nparse, licence-header, size-cap and embed checks, and gains transcript/SVG\npairing, per-family coverage, a check that a screenshot names a skill its\nfamily actually ships, and the regeneration-staleness check that is the\npoint of the whole exercise. Its placeholder machinery is gone: it existed\nbecause ten recordings meant ten capture sessions, and blocking commits\nuntil someone sat down with asciinema would have got the hook disabled. A\nmissing file is now an error.\n\nrecord-svg.sh drops from eleven targets to one. `/magpie-setup` is the\nexception worth a capture -- it is the run a reader has not done yet -- and\nits brief now says to start at the command rather than at the marketplace\ninstall, which is a prerequisite with its own page.\n\nStill to do, and it needs a human at a terminal: the committed\nmagpie-setup.svg is a real recording that opens with the marketplace\ninstall, so it wants a re-cut to begin at /magpie-setup. Nothing here can\ndo that in CI.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: fold the install/adopt/upgrade plans back into one design\n\nThe work was split across two designs and four implementation plans while\nit was being built -- 3,600 lines describing phases, subsystem letters, and\na task list. All of it has now landed except two items, so the shape that\nmade sense while building is the wrong shape to read.\n\nOne document replaces the six. It describes the result: the two things a\nperson can do with Magpie and why keeping them apart drives everything\nelse, the adoption floor and every surface that reads it, the install as a\none-page prerequisite, and what a family page shows. No subsystem letters,\nno sequencing section, no steps.\n\nWhat survives from the plans is the reasoning, not the scaffolding -- the\ndecisions and, more usefully, the alternatives that were rejected and why.\nThat is the part a reader arriving in a year actually needs, and git keeps\nthe task lists.\n\nTwo things are called out rather than quietly dropped, under a heading that\nsays so: re-cutting magpie-setup.svg to start after the prerequisite, which\nneeds a human at a terminal, and the per-skill first-run wizard from the\nearlier design, which is designed in full and deliberately not started.\nDeleting its design without carrying it forward would have lost work nobody\ndecided to abandon.\n\nThree claims in the merged text are corrected against what shipped rather\nthan copied from the plans: per-family plugins are no longer Claude\nCode-only, Codex can now express the floor through INSTALLED_BY_DEFAULT on\nmagpie-setup alone, and the SessionStart upgrade hook rides on magpie-setup\nrather than on an all-in-one plugin that no longer exists.\n\nThe index gains the convention this follows: one document per subject; a\ndesign may be split into plans while it is being implemented, and the plans\nare folded back in when the work lands.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* feat(skills): declare required config in frontmatter, generate the family tables\n\nWhich `\u003cproject-config\u003e/` files a skill reads, and whether it can work\nwithout one, was recorded in two places and neither was reliable.\n\nIn prose, inconsistently: only 8 of the 74 skills carry a configuration\nsection at all, under three different headings, and optionality is marked\nby whatever phrasing the author reached for -- \"(Optional)\", \"if absent\",\n\"falls back\", or nothing.\n\nAnd in a hand-maintained table in nine of the ten family READMEs, which had\ndrifted exactly as far as an unguarded table does:\n\n  security             listed  0 of the 13 files its skills read\n  repo-health          listed  0 of 3\n  release-management   listed  1 of 5\n  pr-management        listed  6 of 11\n  setup, utilities     had no table at all\n\nA reader deciding whether to install a family could not find out what it\nwould ask them to configure, and a reader who found the table was reading\nfiction.\n\nThe required set is now declared once, per skill, in frontmatter:\n\n    requires_config:\n      - project.md\n      - pr-management-config.md\n\nRequired means: absent, the skill would act on a guess. That is judgement\nand cannot be derived -- but it is the only half that needs declaring. The\noptional set is everything a skill references minus what it declares, so\nthe larger half never needs maintaining and cannot drift.\n\ncheck-skill-config.py generates each family\u0027s *Before the first run* table\nfrom both, and guards three things:\n\n- a declared file must actually be read by that skill, so the declaration\n  cannot quietly become fiction;\n- every file any skill reads must have a template in projects/_template/.\n  That found three that did not: contributor-sentiment-config.md,\n  distributor-list.md and magpie-setup.md. A skill was telling adopters to\n  create files the framework had no way to scaffold. All three now exist;\n- the generated block matches the frontmatter. It runs as `--fix`, so drift\n  arrives corrected rather than as an instruction.\n\nDescriptions come from the adopter scaffold\u0027s own index rather than a third\ncopy of \"what this file is for\" -- which meant completing that index: 19 of\nits 40 templates had no entry, and release-management-config.md\u0027s title\nstill said \"Apache Airflow\", which the generated table would have printed on\nevery release-management page.\n\nThe nine Adopter contract sections are retired. What each held beyond the\ntable -- repo-health\u0027s YAML scaffold, the spec pointer in mentoring, the\n\"these skills write nothing\" statements in pairing and utilities -- is kept;\nthe tables and their \"required at minimum\" lists are not, because that is\nnow generated a few screens further up, where a reader who just installed\nthe family is actually standing.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* feat(setup): suggest companion skill packages per family, per harness\n\nThe install flow already offers MCP servers alongside the families, because\na skill that needs a mail backend is useless without one. A maintainer who\nhas just installed the security family is about to go looking for a\nscanner, and one who installed utilities to author a skill is about to go\nlooking for a method -- Magpie ships neither, and the honest answer to both\nis somebody else\u0027s package. That answer was nowhere.\n\nIt is now a registry, tools/dev/companion-skills.json, generated into a\n*Works well with* block on each family README and one page carrying the\ninstall commands.\n\nGenerated rather than written, for two reasons that are the whole design:\n\n- **It is per family AND per harness.** Superpowers installs on six agents\n  with six different commands; Claude Security installs on one. Written as\n  prose across ten READMEs that is forty-odd facts to keep straight, and the\n  first one to rot tells a Codex user to run a Claude Code command. The\n  block says which agents can have a package, and names the ones that\n  cannot rather than smoothing it over -- a recommendation you cannot act\n  on is worse than none.\n- **It must never read as a dependency.** Every family works with none of\n  these installed, Magpie bundles none and fetches none, and\n  docs/vendor-neutrality.md is a live metric here. One registry fixes that\n  framing in every place it appears: what it adds, whose it is, which agents\n  can run it. The checker refuses an entry whose `why` cannot say what it\n  adds to a *named* family, because an entry that cannot is an advert.\n\nFour packages to start, each checked against its own documentation rather\nthan a directory listing:\n\n  superpowers       utilities, pairing    six agents -- it ships a plugin\n                                          manifest per harness over one\n                                          shared skills/ tree, the same\n                                          shape Magpie uses\n  claude-security   security, repo-health Claude Code only\n  aikido            security, repo-health Claude Code\n  code-review       pr-management,        Claude Code\n                    pairing\n\nThe security pairing has a real seam rather than a general affinity: this\nfamily handles reports that arrive, a scanner finds the ones nobody\nreported, and security-issue-import-from-scan already imports scanner\noutput as trackers.\n\ninstall.md\u0027s Step 5 gains a third group beside the families and the MCP\nservers, with two rules: offer only packages available on the agent\ndetected in Step M2, and pre-tick nothing, ever. The families and MCP\nservers are what the operator asked for; these are a suggestion, and a\npre-ticked suggestion is an install nobody chose.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs(setup): drop the two-ways-to-install blockquote from the family page\n\nThe setup family README opened with a blockquote comparing the marketplace\ninstall against the pinned snapshot and distinguishing both from adoption.\nThat is three pages\u0027 worth of subject matter -- the marketplace reference,\nother installation methods, and team adoption each cover one of them\nproperly -- restated in twelve lines above an overview of what the family\ndoes.\n\nIt was also the last place still teaching the install shape at a reader who\nhad arrived looking for the setup skills.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* docs: walk the first run with a family, as terminal steps\n\nThe configuration a family needs was documented as a table, and tables are\nwhere a reader arrives already knowing what they are looking for. Someone\nwho has just installed a family does not: they run a skill, it stops, and\nnothing on the page they were reading told them that would happen or what\nto do about it.\n\ndocs/quick-start/first-run.md is that sequence, in five rendered terminal\nsteps:\n\n  1  a triage run whose pre-flight finds no project config, names the three\n     things it would otherwise guess, and stops without acting\n  2  /magpie-setup adopt -- the wizard, all three groups\n  3  what it staged, and which files THIS family still needs\n  4  the TODO markers, and why leaving one is not an error\n  5  the same command, working\n\nStep 1 is the point of the page. Stopping is a feature, and a reader who\nhas seen it once knows what the check is for; a reader who meets it cold\nreads it as the framework being broken.\n\nThe wizard\u0027s other two groups get a section rather than a mention, because\nneither blocks a first run and both are otherwise invisible:\n\n- **MCP servers** -- what each of the three reads, and that ponymail and\n  apache-projects are mandatory for ASF projects. A skill that needs one\n  and cannot find it says so by name; nothing silently degrades to a worse\n  source.\n- **Companion skills** -- third party, never pre-ticked, and offered only\n  for the agent you are running. A package that exists for Claude Code\n  alone is not offered to a Codex user with a command they cannot run.\n\nThe page closes on what is per-project and committed versus what is\nper-machine and yours -- which is also why an MCP server a family depends\non is named in that family\u0027s prerequisites rather than assumed: a teammate\ncloning the repo gets the config and none of the machine-local pieces.\n\nThe screenshots go through the same deterministic renderer as the family\nones, so they cannot drift from their transcripts. They get one check of\ntheir own that the family screenshots do not need: the chapter must embed\nthem **in order**. A page whose pictures are a step out of sequence teaches\nthe wrong thing while every link in it still resolves, and no link check\ncan see that. Verified by swapping two embeds and watching it fail.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* feat(setup): split `config` from `adopt` — local first, committed by decision\n\nConfiguring a skill and recommending it to a project were one act, and the\nonly route to either was `adopt`. So a solo contributor on a repository\nwhose maintainers had never heard of Magpie had to stage committed files\nfor the whole team in order to make a skill work for themselves -- while\nthe docs told them individual use \"is not a waiting room\". It was.\n\nThere are now three acts, not two:\n\n  install   plugins into this machine\u0027s agent          nothing in the repo\n  config    this project\u0027s facts, for you              .apache-magpie-local/\n  adopt     the floor and the project\u0027s configuration  committed, staged\n\n`\u003cproject-config\u003e` gains the two-layer lookup that overrides already had:\n`.apache-magpie-local/` (gitignored, yours) then\n`.apache-magpie-overrides/` (committed, the project\u0027s), **per file, local\nfirst**. One rule for everything an adopter writes rather than one rule for\nconfiguration and another for overrides.\n\nThe cost of that rule is real and is not hidden: once the project commits a\nfile you also hold locally, yours keeps winning. So `verify` gains a check\nthat lists every local file shadowing a committed one -- identical ones with\nan offer to remove, differing ones reported and left alone -- and `adopt`\ndrops the byte-identical copies as it promotes, so the project\u0027s later\ncorrections actually reach the maintainer who adopted.\n\n`adopt` keeps both routes. It promotes what `config` produced -- selecting\nwhat to publish, leaving behind anything still reading TODO or looking\npersonal -- and scaffolds whatever is still missing, so a maintainer who\nknows from the start that they are adopting can go straight there.\n\nTwo rules about who runs what, and they are not symmetric:\n\n- **A skill runs `config` itself.** The pre-flight that finds missing\n  configuration invokes it unasked, says so, and continues in the same\n  turn. That is safe because of what it touches: `.apache-magpie-local/`\n  and `.git/info/exclude`, both gitignored, both invisible to every other\n  person and clone, both undone by deleting a directory. Unlike a plugin\n  below the floor it needs no session restart -- these are files, written\n  and read in one turn.\n- **Nothing ever runs `adopt`.** It commits a recommendation for every\n  contributor. The configure run mentions in one line that adoption exists\n  and then drops it; it does not ask, does not offer, and does not raise it\n  again.\n\n`config` will not touch `.gitignore` either, which is the detail that makes\nthe promise true rather than nearly true: `.gitignore` is a committed file,\nand a sub-action whose whole claim is that it writes nothing anyone else\nsees must not open by editing one. The exclusion goes to\n`.git/info/exclude` -- per-clone, never committed. `adopt` adds the\n`.gitignore` line, because `adopt` is already committing.\n\nworktree-init is re-framed around what is actually gitignored, after the\nobservation that adoption needs nothing there: the floor lock, the derived\nwiring and the project\u0027s configuration are all committed, so git checks\nthem into every worktree by itself. What a worktree can still need is the\nsnapshot and its per-worktree skill symlinks (snapshot installs, adopted or\nnot), the `.apache-magpie-local/` link (configured but not adopted),\nneither (self-adoption in the framework checkout, where skills/ and its\nrelays are committed) -- and, whatever the install method, this worktree\u0027s\nown sandbox allowlist entry. That last one is why running it is still worth\nit when the first four rows say no, and Step 0a now says so rather than\nstopping early.\n\nThe first-run chapter is re-cut around the new sequence: the skill notices\nand configures itself (1-2), it writes only gitignored files (3), the\ncommand works (4), and adoption is a fifth step nothing runs for you.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* feat(dev): animate the config wizard, one run per family, derived from frontmatter\n\nThe static screenshots show a skill\u0027s output. They cannot show the thing a\nfirst-time reader most needs from the configure step, which is the shape of\na conversation -- the command, the values the wizard works out on its own,\nthe one question it asks, and the files that appear. A still frame of a\nwizard is a wizard with the interesting part removed.\n\nrender-config-wizard.py generates one animated SVG per family that has\nrequired configuration, and every frame is **derived**: which files that\nfamily\u0027s wizard would create comes from its skills\u0027 `requires_config:`\nfrontmatter, and the line describing each from the adopter scaffold\u0027s index\n-- the same two sources check-skill-config.py already reads. A family that\ngains a required file gains a frame, with nobody editing a transcript. That\nis what makes seven per-family animations maintainable where seven hand-cut\nrecordings were not.\n\nAnimation is SMIL: one `\u003canimate\u003e` on opacity per line, all sharing one\nduration so the sequence loops as a unit and no line can drift out of step.\nPlain XML, deterministic, and no Node -- record-svg.sh is the one place\nsvg-term-cli is a dependency and this is not it. A renderer that does not\nanimate shows the first frame, which is the command about to be typed.\n\nIllustrative, and it says so in the embed rather than in a footnote: the\nreal run derives more and asks better. What it shows truthfully is the\nshape -- it runs itself, it writes only gitignored files, it stages\nnothing -- which is exactly what a reader deciding whether to install a\nfamily needs to believe.\n\nTwo guards, both mutation-tested rather than trusted:\n\n- **staleness** -- re-derive and compare, so an edited SVG fails. Verified\n  by editing \"nothing staged\" to \"everything staged\" and watching it fail;\n- **embedding** -- every animation is shown by its family README. Verified\n  by unhooking one embed.\n\nThese have no `.txt` to pair with, so the two renderers are now explicitly\nseparate: the bash one skips the wizard directory rather than reporting\nseven files with no transcript. The checker\u0027s docstring says which rule\napplies to which set and why.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* feat(dev): generate the setup animation too, and retire the recorder\n\nmagpie-setup.svg was the repository\u0027s last real recording, and it was\nwrong. It opened with the marketplace install, which had become a\nprerequisite with its own page, and fixing that needed a terminal, a\nscratch project and a human -- which is exactly why it stayed wrong.\nCarrying an artefact whose correction is blocked on scheduling a person is\nhow the nine family recordings sat as placeholders for months.\n\nSo it is generated now, by the same script that already generated the\nper-family config runs. render-config-wizard.py becomes render-wizard.py\nand takes a second subject: the whole first run, in one arc -- the agent\ndetected, the families picked, the install commands emitted, then the\nsecure-agent setup applying the sandbox, the clean-environment wrapper, the\nhooks and the status line. That last part was never in the recording at\nall, and it is the follow-up install.md calls the one that is not optional.\n\nrecord-svg.sh is deleted. That was its only remaining target: the nine\nfamily takes went when the authored screenshots replaced them, and the\nexample takes went with assets/examples/. A recorder with no targets whose\n--list prints \"The one recording: setup\" -- for a file that is now\ngenerated -- is worse than no recorder. Nothing in this repository is\ncaptured any more, and the asciinema and svg-term-cli/Node dependencies go\nwith it.\n\nThe checker follows: magpie-setup.svg moves from \"the one recording, which\nmust exist and be embedded\" to the same staleness rule as everything else.\nVerified by hand-editing \"status line\" to \"status bar\" and watching it\nfail.\n\nWhat this costs is worth saying plainly rather than burying, and\nassets/quickstart/README.md now says it where a reader meets it: a\ngenerated animation cannot prove the program still behaves the way the\npicture says. It shows the shape of a run -- what is asked, in what order,\nand what is written where. The design\u0027s \"Designed, not built\" section\nloses its first entry, not because someone finally found a terminal, but\nbecause the thing that needed one is gone.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* chore: reconcile with the Gemini tool-sandboxing work from main\n\nRebased onto main, which added Gemini tool sandboxing, policies and a\nsecure-setup lifecycle (#1205). Three of its edits landed in files this\nbranch had moved or rewritten, and would otherwise have been lost:\n\n- **the isolation step is per-runtime now.** main turned \"run the guided\n  install\" into a table routing Claude Code, Codex, Gemini and everything\n  else to their own next step. That is the right shape and the branch\n  adopts it, with Gemini\u0027s entry naming what it actually gets -- tool\n  sandboxing and policies;\n- **the prerequisites entry for Gemini** gains sandboxing and policies, and\n  the *experimental* caveat it shares with Codex;\n- **the adapters index** one-liner says the same, rather than implying the\n  guard is all there is.\n\nTwo mechanical consequences of the move, both fixed rather than papered\nover: main\u0027s new links in the isolated-setup skills were written at the\npre-move depth and now resolve from `plugins/magpie-setup/skills/`, and\n`docs/adapters/gemini.md` linked a quick-start anchor whose section moved\nto the install prerequisite page.\n\nEverything derived was regenerated against the merged tree: eval counts\n(1265 -\u003e 1269, from main\u0027s four Gemini routing cases), measured skill\ntokens, the per-family config tables, the wizard animations, and the\npre-flight block across 65 skills.\n\nOne of main\u0027s lines is deliberately not carried: install.md\u0027s note that\n\"Codex can only default-install all ten families\" described the all-in-one\nplugin, which this branch removes -- Codex now default-installs\n`magpie-setup` alone.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* feat(setup): ask before adding a companion\u0027s marketplace, and emit both commands\n\nOffering a companion package named only the package. For Superpowers that\nwas not enough: it is published in `obra/superpowers-marketplace`, so taking\nit means pointing your agent at a catalogue Magpie does not publish -- for\neverything in that catalogue, not only the package you wanted. An operator\nwho ticks a box labelled \"superpowers\" has not agreed to that, and the flow\nhad no way to tell them, because the registry buried the marketplace add\ninside the install command as one opaque string.\n\nEach harness entry now carries `install` and `marketplace` separately.\n`marketplace` is the `owner/repo` the agent has to be pointed at first, or\nnull where there is none to add -- the package is already in a catalogue\nthe agent has (Claude Code adds the official one on first run), or that\nharness installs straight from a URL. The checker refuses a bare string, a\nmarketplace named for a harness with no marketplace concept, and one that\nis not an owner/repo source. All three verified by mutation.\n\nThe split exists because the two are different kinds of decision, not\nbecause they are two lines of shell. So the install flow:\n\n- **names the marketplace in the offer itself**, so the tick is consent to\n  both;\n- emits the add and the install as **two labelled steps**, with whose\n  marketplace it is on the line above the command, and adds one marketplace\n  once however many packages come from it;\n- explains the *absence* of a first step rather than leaving it unremarked;\n- **runs none of them.** These are commands for the operator, exactly like\n  the Magpie install lines. The rule that auto-install stays inside\n  `apache/magpie` is the same rule reaching this surface.\n\nThe generated family blocks and the companion page say it too: a package\nneeding a third-party catalogue says so where a reader meets it, not only\nwhere they act on it.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* fix(assets): give the transcripts the ASF header, and strip it when rendering\n\nApache RAT failed the PR with 32 unapproved files: every authored `.txt`\ntranscript under assets/quickstart/. They were added without a licence\nheader because the renderer prints a transcript verbatim, and a header\nwould have been drawn into the picture.\n\nThe tempting fix is a .rat-excludes entry, and it is the wrong one. That\nfile says what exclusions are for -- generated artefacts, empty markers,\nand eval fixtures that deliberately carry non-Apache licence text -- and a\ntranscript is none of those. It is authored source, and authored source\nhere carries the header.\n\nSo the header goes in as a block of `#` comments and render-screenshot.sh\nstrips a leading comment block before drawing anything. The constraint it\nbuys is that a transcript cannot open with a literal `#` line, which no\nterminal transcript in this repository does.\n\nThe stripping is provable rather than asserted: after stamping all 33\ntranscripts, `render-screenshot.sh --check` reports every committed SVG\nstill byte-identical. The header is in the source, out of the picture, and\nnothing visual moved.\n\nNot touched: the 74 added .json fixtures. RAT does not flag JSON -- main\nalready ships hundreds of header-less expected.json files and passes --\nso an exclusion for those would have been cargo cult rather than a fix.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y\n\n* feat(docs): animate every quick-start step, and fix what the review found\n\nFive generated animations join the two already there, so the quick start\nshows each step rather than describing it: a hero at the very top (three\ninstall commands, then a skill answering -- the shortest true story), and\none per walkthrough step for install, isolation, use and adopt. All from\nrender-wizard.py, all deterministic, all checked. The hero is deliberately\nnarrower than magpie-setup.svg further down: a first look that tried to\nshow everything would show nothing.\n\nThey are also roughly twice as fast. 700ms a line made a sixteen-line run\ntake thirteen seconds, which is longer than anyone looks at a picture; the\nhero is now 6.4s and the full first run 12.5s.\n\nTwo wording fixes, both things the docs were getting wrong:\n\n- **magpie-setup is not a preference among equals.** \"Always take this\n  one\" reads as a recommendation; nothing else installs, upgrades or\n  adopts without it, so the animation says to install it first and why.\n- **\"An agentic tool\" is a harness**, in the four places that meant the\n  thing running the skill. RFC-AI-0004\u0027s \"agentic tooling\" is the broader\n  category and is normative text, so it is left alone.\n\nFrom reviewing my own diff, two real defects:\n\n- **A stale eval.** preflight-floor\u0027s case-6 still asserted\n  `propose-setup` / `blocks: true` for an unadopted repo, which is the\n  contract this PR replaced -- the pre-flight now runs config itself and\n  continues in the same turn. The case, its report and the output-spec\u0027s\n  action vocabulary are updated, and `mention_adopt` records that adoption\n  is named once and never run.\n- **The design claimed shipped work was unbuilt.** Its \"Designed, not\n  built\" section described the per-skill wizard -- requires_config, the\n  derived optional set, the --fix checker, the pre-flight hand-off, the\n  one-place sub-action -- all of which this PR ships as `config`. That\n  section is now \"Where the build departed from the design\", recording the\n  two places it did: the wizard writes gitignored files rather than\n  committed ones, and magpie-setup.svg is generated rather than re-cut.\n  The design also never recorded the generated family tables, the\n  animations, or companion packages; it does now.\n\nAnd one thing the review surfaced that is worth a reader\u0027s attention\nrather than a fix: **agent isolation is not uniform across harnesses.**\nEvery harness gets the clean-environment layer, but the action guard\nreaches four of six -- Codex and Cursor have none, so Magpie\u0027s hard rules\nare instructions there rather than a gate. The adapters index now carries\nthat as a matrix, because the honest answer to \"is isolation applied\neverywhere\" is no, and it was previously discoverable only by reading one\ntool\u0027s README.\n\nKiro is added as a first-class harness throughout: its own adapter page, a\nsection on the install prerequisite page (it is the one harness with no\nmarketplace), the companion registry\u0027s harness vocabulary, and the\nisolation matrix.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_017suKVbpWX8XT46uRbDxk8y"
    },
    {
      "commit": "05213bb36e3d5d57100c663bbdc85eebc69256f6",
      "tree": "01b4d86900120465d81b77681537b44ddee9e9ec",
      "parents": [
        "0dd1cc9ff6d0f743fb7f4212946e483bcbb65f34"
      ],
      "author": {
        "name": "Kevin Yang",
        "email": "85313829+sjyangkevin@users.noreply.github.com",
        "time": "Sun Sep 13 11:19:16 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 17:19:16 2026 +0200"
      },
      "message": "feat(gemini): add tool sandboxing, policies and secure setup lifecycle (#1205)\n\n* feat(gemini): add tool sandboxing and approval policies\n\nEnable tool sandboxing with scoped reads, per-call approvals, and\ncredential denies. Add AGENT_ISO_ALLOW for explicit authentication\nenvironment passthrough and replace spec-loop YOLO with default approvals;\nheadless calls requiring confirmation are refused.\n\nAdd static policy validation, CI coverage, and optional native runtime\ntests. Document setup, verification, and the experimental isolation limits.\n\nGenerated-by: Codex (GPT-6)\n\n* feat(gemini): wire secure setup lifecycle skills\n\nRoute install, verify, update, and doctor to the Gemini adapter.\nSupport extension and snapshot profiles, guard wiring, and cleanup.\nAdd four Gemini routing evals and update setup documentation.\n\nGenerated-by: Codex (GPT-6)\n\n* docs(gemini): clarify isolation boundaries in the spec\n\nDistinguish Gemini tool sandboxing from the reference isolation model.\nAdvance the spec-sync marker after reviewing the affected specs against main.\n\nGenerated-by: Codex (GPT-6)\n\n* fix(gemini): require search approval and validate native policies\n\nGenerated-by: Codex (GPT-6)"
    },
    {
      "commit": "0dd1cc9ff6d0f743fb7f4212946e483bcbb65f34",
      "tree": "02f51d623d696c5346b257fc3d35d59b98b99cf6",
      "parents": [
        "9fdbb88edfef03be3aa2a924fff0cd682928f624"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sun Sep 13 21:48:44 2026 +0630"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 17:18:44 2026 +0200"
      },
      "message": "docs(agent-isolation): record ghostty behaviour in claude-term-bg.sh (#1207)\n\nThe header\u0027s reset-asymmetry note split terminals into two cases: iTerm2,\nwhich does not reliably honour OSC 111 through the fullscreen TUI, and VTE\n(terminator, gnome-terminal), which does. Ghostty was unrecorded.\n\nMeasured against ghostty 1.3.1 on macOS: it implements both OSC 11 and\nOSC 111, so it groups with the VTE case — the default reset comes back\nclean without CLAUDE_RESET_BG, and the iTerm2-proprietary\nSetColors\u003dbg\u003ddefault escape is ignored harmlessly. find_tty_dev() already\ncovers it: its \"ttysNNN\" / \"pts/N\" matching is OS-specific, not\nterminal-specific.\n\nComments only — no behaviour change.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01Qz58cVUjEno388dwhFzuEB"
    },
    {
      "commit": "9fdbb88edfef03be3aa2a924fff0cd682928f624",
      "tree": "7ff30ebf2a300c0f15ad0315247972c9708f0fce",
      "parents": [
        "3c5c139f47a5856d1c23d7b38f5b1b141364288f"
      ],
      "author": {
        "name": "Vardhman Gupta",
        "email": "112063624+Kaap10@users.noreply.github.com",
        "time": "Sun Sep 13 12:42:47 2026 +0530"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 09:12:47 2026 +0200"
      },
      "message": "feat(adapter/local-llm): Ollama / llama.cpp / vLLM skill runtime guide (#1206)\n\n* feat(adapter/local-llm): Ollama / llama.cpp / vLLM runtime guide\n\n* docs(adapter/local-llm): update doctoc table of contents"
    },
    {
      "commit": "3c5c139f47a5856d1c23d7b38f5b1b141364288f",
      "tree": "80ceef6413667310e3a7f380aabd3a47bc63cfcf",
      "parents": [
        "823241cff0c51a2ce659c48e740cafeaeaefca11"
      ],
      "author": {
        "name": "Vardhman Gupta",
        "email": "112063624+Kaap10@users.noreply.github.com",
        "time": "Sun Sep 13 09:24:46 2026 +0530"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 13 10:24:46 2026 +0630"
      },
      "message": "feat(adapter/cursor): Cursor Composer and Agent CLI skill runtime (#1204)\n\nAdd first-class runtime adapter guide and discovery references for\nCursor (Composer in IDE + cursor-agent CLI) per RFC-AI-0004.\n\nCloses #316"
    },
    {
      "commit": "823241cff0c51a2ce659c48e740cafeaeaefca11",
      "tree": "5f2e9c01f6c36cf0d8e96e1db5b92ebc59c104ad",
      "parents": [
        "ab80b7eb5ba83c296b2c4b8fe6621857e35f5156"
      ],
      "author": {
        "name": "Shahar Epstein",
        "email": "60007259+shahar1@users.noreply.github.com",
        "time": "Sat Sep 12 22:44:57 2026 +0300"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Sep 12 21:44:57 2026 +0200"
      },
      "message": "fix(setup): Update Codex CLI plugin installation command (#1203)"
    },
    {
      "commit": "ab80b7eb5ba83c296b2c4b8fe6621857e35f5156",
      "tree": "ad20bed478be067b6b95bad7f747fc1440233397",
      "parents": [
        "0ec0f4b40d537cacbde53dfdfded52677f581ece"
      ],
      "author": {
        "name": "André Ahlert",
        "email": "andre@aex.partners",
        "time": "Sat Sep 12 15:44:31 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Sep 12 21:44:31 2026 +0200"
      },
      "message": "fix(mail): resolve security draft CC before backend calls (#1200)"
    },
    {
      "commit": "0ec0f4b40d537cacbde53dfdfded52677f581ece",
      "tree": "8a9371c3f4d9f0df525fb4a0dea4c6389969e8e1",
      "parents": [
        "a7651d77dec12e31e73215b8c5d3d277a0f7475d"
      ],
      "author": {
        "name": "Kevin Yang",
        "email": "85313829+sjyangkevin@users.noreply.github.com",
        "time": "Sat Sep 12 09:57:31 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Sep 12 15:57:31 2026 +0200"
      },
      "message": "feat(gemini): integrate gemini cli as runtime (#1199)\n\n* feat(gemini): wire the action guard and spec-loop output\n\nGemini already discovers Magpie skills, but its shell calls do not\nreach the action guard and its spec-loop profile ignores the requested\noutput format.\n\nAdd a BeforeTool adapter for the existing dispatch core and register\nit in project settings using a portable path. Import AGENTS.md from\nGEMINI.md and forward the output format in the Gemini runner profile.\n\nDocument the implemented runtime contract and manual hook registration\nfor snapshot adopters. Cover the hook protocol, project configuration,\nand runner arguments with tests. Policy distribution and sandbox parity\nremain follow-up work for issue #314.\n\nRefs: #314\nGenerated-by: Codex (GPT-6)\n\n* fix(agent-guard): clear CodeQL findings in the Gemini test suite\n\nCodeQL flagged two quality findings on the Gemini adapter\u0027s test suite:\npy/import-and-import-from (agent_guard imported both ways) and\npy/mixed-returns (fake_run mixed explicit returns with an implicit\nfall-through). The suite now uses the bare module import only — matching\ntest_guards.py, and the form it already needs for __file__ and\nmonkeypatch.setattr — and fake_run returns pytest.fail(...) so every exit\nis explicit. pytest.fail is kept rather than raise AssertionError: it\nraises a BaseException that the guard\u0027s fail-open `except Exception`\ncannot swallow.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01Eq3skqHwp9n1CYn8Kxjh5j\n\n---------\n\nCo-authored-by: Jarek Potiuk \u003cpotiuk@apache.org\u003e"
    },
    {
      "commit": "a7651d77dec12e31e73215b8c5d3d277a0f7475d",
      "tree": "39a40a56b0f1bffb08dda1ca27c08bdda14af353",
      "parents": [
        "f4204ecaf53b8d6044ff64af8c24f23c8c3d9b9f"
      ],
      "author": {
        "name": "Grove",
        "email": "chinesegrove@gmail.com",
        "time": "Sat Sep 12 21:42:06 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Sep 12 15:42:06 2026 +0200"
      },
      "message": "fix(pr-management-triage): include top-level reviews in F5a cooldown (#1202)\n\nInclude non-whitespace submitted review bodies in F5a using submittedAt,\nand fetch body in the existing query. Preserve newest-feedback ordering,\nthe 72-hour boundary and F5b\u0027s existing ping sources.\n\nAdd 11 regression fixtures and synchronize the related documentation.\nAlign decision-table eval explanations with production requirements and\nclarify terminal URL preservation and empty NO_COLOR precedence. The\ncomplete corrected skill suite passes all 51 cases.\n\nGenerated-by: WorkBuddy AI (deepseek-v4.1-flash)\nGenerated-by: Codex (GPT-5)\nGenerated-by: Codex (GPT-6)"
    },
    {
      "commit": "f4204ecaf53b8d6044ff64af8c24f23c8c3d9b9f",
      "tree": "4e2f56e04f69219559dce0723a240295830548f0",
      "parents": [
        "68c01a34dd02aac42ae617c02cafa2a322643de8"
      ],
      "author": {
        "name": "André Ahlert",
        "email": "andre@aex.partners",
        "time": "Sat Sep 12 08:07:17 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Sep 12 14:07:17 2026 +0200"
      },
      "message": "feat(docs): make mode token costs traceable and reproducible (#1201)"
    },
    {
      "commit": "68c01a34dd02aac42ae617c02cafa2a322643de8",
      "tree": "62b4bcd922bfaee6a20c6a21fcda46f2c6dc8211",
      "parents": [
        "fcbe2714ce40a83ca2ecbc05e98dabde37d50718"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Fri Sep 11 02:49:42 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 02:49:42 2026 +0200"
      },
      "message": "chore: bump dev version to 0.2.0.dev202609110041 (#1198)\n\nMove the dev stamp so adopters\u0027 `claude plugin update` picks up the work\nmerged since 0.2.0.dev202609100826 — the marketplace is served from `main`\nand the update check compares version strings, so a frozen suffix is a\nsilent no-op.\n\nGenerated with the documented one-liner: bump `project.version` in\n`pyproject.toml`, then `tools/dev/check-family-plugins.py --fix` and\n`uv lock` to propagate to every manifest.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01CPC1wBowFdMo4jnCvLZDJ2"
    },
    {
      "commit": "fcbe2714ce40a83ca2ecbc05e98dabde37d50718",
      "tree": "9af9502b2a90869423f18960344849c27ce68176",
      "parents": [
        "d844af50521b21b4cbe85e6f9012b9db0f04ec51"
      ],
      "author": {
        "name": "Kavya Katal",
        "email": "KAVYAKATAL09@GMAIL.COM",
        "time": "Fri Sep 11 06:07:56 2026 +0530"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 02:37:56 2026 +0200"
      },
      "message": "feat(bitbucket): add guarded cloud PR decline (#1194)\n\n* feat(bitbucket): add guarded cloud PR decline\n\n* fixup! feat(bitbucket): add guarded cloud PR decline\n\nDrop \u0027declining,\u0027 from the out-of-scope write list. The same paragraph\nnow both claimed decline as implemented and listed it as out of scope,\nand the command table two sections down lists \u0027pr decline \u003cid\u003e\u0027 as a\nsupported Cloud write.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n---------\n\nCo-authored-by: Jarek Potiuk \u003cpotiuk@apache.org\u003e"
    },
    {
      "commit": "d844af50521b21b4cbe85e6f9012b9db0f04ec51",
      "tree": "8d0af6f1cc167f0da00df462e5bc770d93ce7877",
      "parents": [
        "9dcf58bd658782b7ec601e0711e9db42d33475ae"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Fri Sep 11 00:37:44 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 00:37:44 2026 +0200"
      },
      "message": "feat(setup): offer an optional committed default plugin set (#1196)\n\n* docs(design): repo-committed setup — default plugin set and per-skill first-run wizard\n\nDesign for three subsystems, delivered in order. A: setup offers, once\nand opt-in, to commit the default-set block and scaffold the config\nstore — both optional, Claude Code only. B: every non-setup skill\ndeclares its required config and a missing file hands off to a wizard.\nC: the docs and screenshots.\n\nRecords why \u0027sync the block on every plugin install\u0027 is not\nimplementable — no code runs on plugin install or upgrade on most\nharnesses — and why a fixed floor makes it unnecessary.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* docs(plan): implementation plan for subsystem A — committed default set\n\nSix tasks, each test-first against the skill-evals harness: the Step M5\noffer and harness gate, the merge rules, the verify staleness check, the\nuninstall removal, the docs, and the spec-loop acceptance criteria.\n\nRecords the constraint that shaped the approach: a family-plugin install\nships .claude-plugin/ and skills/ only, so nothing here may depend on a\nPython helper existing on the adopter\u0027s machine. The agent performs the\nmerge itself from prose rules, and the eval suite is what tests them.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* feat(setup): offer the committed default set on the marketplace path\n\nClaude Code only, opt-in, defaulting to no. Both repo-side artefacts —\nthe enabledPlugins floor and the config store — are offered in one\nquestion, and declining leaves a finished install.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* test(setup): make the Step M5 eval cases depend on the skill text\n\nThe output spec restated the decision rules, so a model could answer\nevery field without reading Step M5 — three of four cases passed against\nthe pre-change install.md. It now states the JSON contract only.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* test(setup): show field shape, not a value instance, in the eval schema\n\nThe schema block rendered artefacts_offered as a literal array holding\nboth members in order — the expected answer for two cases. It now shows\na placeholder; the allowed members and their ordering stay in prose.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* feat(setup): merge-never-clobber rules for the committed default set\n\nOnly extraKnownMarketplaces and enabledPlugins are touched; an existing\napache-magpie definition is left alone; floor members are added and\nnothing is removed; a malformed file is reported, never rewritten.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* fix(setup): name the floor in the merge rules; define eval fields as outcomes\n\nThe merge rules said \u0027add missing floor members\u0027 without naming them,\nso the three entries reached the model only through the eval\u0027s output\nspec — both a leak and a gap, since the runner extracts that subsection\nalone. The spec\u0027s field definitions now say what each field reports,\nnot what the skill decides.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* fix(setup): define keys_preserved by managed-key membership\n\n\u0027The keys the merge left untouched\u0027 turned on whether a managed key\nhappened to need an edit, so a settings file whose apache-magpie\ndefinition was already pinned reported that key as preserved.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* fix(setup): state the create branch\u0027s floor, and stage only what was written\n\nAn agent reading only the merge rules had to infer that creating a fresh\nsettings file means populating enabledPlugins with the floor. The git add\nline also read unconditionally, including on the refuse path where\nnothing was written.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* feat(setup): verify reports a stale committed default set\n\nAn absent block is reported informationally and is never a fault — the\ncommitted set is optional. A block missing floor members is drift, with\na repair offer.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* test(setup): stop the verify cases handing over their own answers\n\ncase-3-stale\u0027s report named the absent floor member outright, so the\nmissing list could be copied from the input. case-2-current listed\nexactly the floor, so \u0027everything looks fine\u0027 reached the right answer\nwithout knowing what the floor is.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* fix(setup): state missing for an absent block, and report through the glyphs\n\nRead literally, the stale rule made all three floor entries \u0027missing\u0027\nwhen no block exists at all. The section also reported in prose while\nevery other check drives the recap through the glyph column; absent and\ncurrent now map to the non-fault glyph.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* feat(setup): uninstall removes only the floor it added\n\nOther plugins in enabledPlugins survive, other marketplaces survive, and\nthe settings file is never deleted — it belongs to the project, the same\nconvention that preserves .apache-magpie-overrides/ by default.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* docs: explain the optional committed default set\n\nSays plainly in both places that committing the block is optional and\nthat the plugins work in the repo either way.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* spec(adoption): acceptance criteria for the committed default set\n\nWritten against what shipped rather than what the plan predicted: the\nfixed floor, the single combined Claude-Code-only offer, absent-is-never\n-a-fault, and the docs\u0027 optionality statement.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* docs: make docs/designs reachable from the index\n\nThe directory was added with no route to it — docs/index.md\u0027s navigation\ntable gained no row, so the design and its plan were discoverable only by\nknowing the path. Adds the row and a README that says what a design is\nfor and how it differs from a spec-loop spec.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv"
    },
    {
      "commit": "9dcf58bd658782b7ec601e0711e9db42d33475ae",
      "tree": "c55f820b662d25bacec374866a267b173ec9ccc5",
      "parents": [
        "d7c9d6c9a5cb463b66e000cdffac4516882de910"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Thu Sep 10 20:23:14 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 20:23:14 2026 +0200"
      },
      "message": "feat(cve-json): emit affected[].packageURL for generated CVE records (#1197)\n\n* feat(cve-json): emit affected[].packageURL when a purl_type is configured\n\nThe CVE Record Format defines an optional packageURL on each affected\nproduct entry — a Package URL identifying the package on its host. The\ngenerator carried collectionURL and packageName but never emitted one.\n\nAdds an optional product.purl_type config key. When set, every\naffected[] entry gains pkg:\u003ctype\u003e/\u003cpackageName\u003e; when unset the field is\nomitted, so existing adopters\u0027 records are unchanged. The purl never\ncarries a version, per the schema — the entry\u0027s versions[] holds the\nrange. A name needing namespace semantics (an npm scope, a Maven group\nID) emits no purl rather than a guessed one.\n\nThe new compute_purl() sits beside the pre-existing\ncompute_package_url(), which despite its name returns a human-readable\nproject-page URL for the attachment table, not a purl; both docstrings\nnow say so.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* fix(cve-json): follow the purl spec\u0027s npm rules for scopes and case\n\nChecked pypi and npm against the package-url spec\u0027s type definitions.\npypi was right. npm was wrong twice: scoped names returned no purl at\nall, and names were lowercased.\n\nA scope is a namespace, not a guess — @angular/animation is\npkg:npm/%40angular/animation. And npm names are case-sensitive: only new\npackages must be lowercase, pre-existing mixed-case ones were\ngrandfathered in, so folding case points at a different package.\n\nAlso drops golang, composer, hex, github and bitbucket from the\nlowercase set. Those were added from memory without reading their rules;\na guessed normalisation corrupts an identifier silently, so unverified\ntypes now pass their name through untouched.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* feat(cve-json): add purl_namespace for types that require one\n\nMaven, Go and Composer purls need a namespace the package name does not\ncarry: a groupId is separate coordinate metadata, and Go\u0027s module/package\nsplit cannot be derived without consulting the module proxy. An optional\nproduct.purl_namespace supplies it, and without it those types still\nemit nothing rather than a guess pointing at another organisation\u0027s\nartifact.\n\nTypes the spec gives no namespace (pypi, cargo, gem, nuget) ignore the\nkey, and an npm scope in the package name wins over it. A namespace may\nitself be a path — Go\u0027s github.com/gorilla — so its slashes are kept as\nsegment separators rather than percent-encoded.\n\nAlso pins cargo, gem and nuget with tests. They already worked through\nthe pass-through path; nuget in particular must keep its case, which the\nfirst cut of this feature would have folded.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv"
    },
    {
      "commit": "d7c9d6c9a5cb463b66e000cdffac4516882de910",
      "tree": "ffa8c777e04e4580c30c5adeec6d5acd36930cd6",
      "parents": [
        "227fcbaaf8ff4ad01c9fe902fe663a8af5a1b07c"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Thu Sep 10 19:41:52 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 19:41:52 2026 +0200"
      },
      "message": "docs: make the marketplace plugin install the documented default (#1195)\n\n* docs(quick-start): marketplace-first install guide, de-stuttered plugin skill names\n\nAdd `docs/quick-start.md` as the entry point and make the marketplace\ninstall the recommended path, with per-family plugins as the default\nshape and the all-in-one `magpie` plugin marked not recommended.\n\n- Quick start covers Claude Code, Codex, VS Code/Copilot and Gemini,\n  a \"what each family solves\" chapter, and the secure-isolation step.\n- Each `docs/\u003cfamily\u003e/README.md` gains an \"Install \u0026 first runs\"\n  section with the one install command and worked examples.\n- Terminology: \"adopt\" names the project\u0027s relationship with Magpie and\n  is no longer the name of an install method; the snapshot path is the\n  \"pinned snapshot install\", and self-adoption is the default inside a\n  Magpie checkout.\n- Family plugins advertise de-stuttered skill aliases via their symlink\n  names (`/magpie-security:issue-triage`). Source directories keep their\n  prefix: the portable install flattens all skills into one namespace\n  where it is the only thing separating `issue-stale-sweep` from\n  `pr-stale-sweep`, and `setup` would strip to an empty name.\n- Docs use the plugin form; skills refer to each other by bare skill\n  name, which is correct under either install method; snapshot guides\n  keep the single-token form and say so.\n- Always-on token figures were wrong (~21.7k claimed, ~8.6k measured).\n  `tools/dev/estimate-skill-tokens.py` measures the advertised surface\n  (frontmatter name + description) and gates the published tables.\n- `check-doc-sync.py` gains three guards: per-family plugin counts,\n  no family-name stutter, and portable-form invocations must declare\n  which install they mean.\n- Eval fixtures re-synced with the swept skill text.\n\nScreenshots under `assets/quickstart/` ship as placeholders; the capture\nchecklist is in `assets/quickstart/README.md`.\n\nGenerated-by: Claude Code (Opus 5)\n\n* feat(setup): give the setup skill the marketplace install path\n\nThe quick-start now documents the marketplace plugin install as the\ndefault, but the setup skill itself still went straight to the snapshot\ninstall — docs promising a default the skill did not implement. Adds the\nM1-M5 marketplace path, the marketplace-install checks in verify, the\nlocks reference, and the matching spec-loop criteria.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* docs(quick-start): add a capture helper for the placeholder screenshots\n\n`tools/dev/capture-screenshot.sh \u003ctarget\u003e` captures one shot straight\ninto the path the docs already reference, resized to the 1700px the\nother `assets/` captures use and EXIF-stripped.\n\nIt prints what to frame — and the no-secrets check — before opening the\ncrosshair, because framing is the part that is easy to get wrong and\nexpensive to notice later. Valid family targets come from the live\n`family:` frontmatter, so a typo fails instead of writing a file nothing\nreferences.\n\nmacOS only: Screen Recording permission is granted per calling\napplication, so it has to run from the user\u0027s own terminal.\n\nGenerated-by: Claude Code (Opus 5)\n\n* docs(quick-start): real install captures, and a checker that keeps the set honest\n\nReplaces eleven generated placeholders with real /plugin captures taken\nin a scratch project, and adds check-quickstart-screenshots.py: it ties\neach harness shot to the manifest that makes that harness real, refuses\norphans and off-geometry captures, and reports the three still-pending\nplaceholders without blocking the commit.\n\nGenerated-by: Claude Opus 5\nClaude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv\n\n* fix(docs): repair invocation forms the de-stutter sweep broke\n\nThe sweep stripped /magpie- more bluntly than the naming rule warrants.\nsetup/SKILL.md ended up saying the prefix is added and then not used;\nthe stats dashboards rendered pasteable commands valid on no install\npath; list-skills\u0027 when_to_use named a string nobody types; the README\u0027s\nfront-door example was /dependency-audit; and the secure-agent checklist\nmixed a snapshot action into a marketplace-form block.\n\nAlso corrects check-doc-sync\u0027s own docstring, which said eight checks\nwhere main() runs ten.\n\nGenerated-by: Claude Opus 5\n\n* docs(quick-start): describe the screenshot set as it now is\n\nEleven real captures have landed, so the \u0027all placeholders\u0027 framing was\nstale in the same commit that made it stale, as was \u0027the four\nscreenshots\u0027 for a set of fourteen. The documented ~1400px also\ncontradicted the 1700px both the capture script and the new checker\nrequire — a contributor following the docs produced a file the hook\nrejects.\n\nThe capture script now refuses a source window under 1700px rather than\nletting the sips fallback silently upscale, and no longer leaks a temp\nfile per run.\n\nGenerated-by: Claude Opus 5\n\n* fix(dev): make the screenshot checker\u0027s claims match what it does\n\nIts docstring claimed each shot was verified \u0027at the exact path the docs\nreference\u0027 while the checker never opened a doc; the orphan sweep saw\nonly two directories; the family scan read any line beginning family:\nrather than the frontmatter block; and a comment asserted the capture\nscript only ever shrinks, which the sips fallback does not.\n\nGenerated-by: Claude Opus 5\n\n* docs: stop the quick-start citing pages that contradict it\n\nIt cited the unadopted-repo and mixed-team pages as evidence that the\nmarketplace install alone suffices, but both taught a git clone plus\nuser-scope symlink recipe — the very work the marketplace install\nremoves. Both now open with the marketplace path and demote the clone to\nthe fallback.\n\nAlso flags VS Code and Gemini as not yet live-installed, matching the\nverification table, and drops a doubled horizontal rule.\n\nGenerated-by: Claude Opus 5\n\n* spec(adoption): scope the one-committed-skill criterion to the fallback\n\nIt read as an absolute directly above the new marketplace-first\ncriterion, though it is now true only of the pinned-snapshot install.\n\nGenerated-by: Claude Opus 5"
    },
    {
      "commit": "227fcbaaf8ff4ad01c9fe902fe663a8af5a1b07c",
      "tree": "b4af42a04d3632476088e63936e176c9e6a48457",
      "parents": [
        "74776c85d6322f402f40c16a6c277e17f3cc74d2"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Thu Sep 10 11:14:40 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 11:14:40 2026 +0200"
      },
      "message": "feat(plugins): fix Codex install, default-install a set, pre-flight every skill (#1193)\n\nFive changes that all trace back to the same gap: nothing checked what the\nplugin manifests actually claim, and nothing checked whether an installed\nplugin had been set up.\n\n- **Codex install was broken.** `.agents/plugins/marketplace.json` carried\n  invented `policy` values (`manual`, `none`). Codex\u0027s enums are closed and\n  SCREAMING_SNAKE, and it rejects the *whole catalogue* on an unknown\n  variant — so `codex plugin marketplace add apache/magpie` failed and\n  nothing installed. Fixed to `AVAILABLE`, verified live against codex\n  0.154.0, and `check-family-plugins.py` now validates both policy enums so\n  the class of bug cannot return. The manifests had been checked against\n  vendor docs but never live-installed.\n\n- **A stale published claim.** The all-in-one plugin advertised ~21.7k\n  always-on tokens; the measured figure is ~8.6k.\n\n- **A default-installed set.** A project can commit `enabledPlugins` so a\n  contributor arrives with `magpie-setup`, `magpie-utilities` and\n  `magpie-agent-guard` already on. Documented per harness, including why\n  only Claude Code can express it: per-family plugins are Claude Code-only,\n  so Codex could only default-install all ten families, and Gemini has no\n  workspace-extension mechanism at all (verified against the CLI).\n\n- **JetBrains IDEs.** Documented as a host, not a distribution target —\n  plugin state lives in one user-scope store shared by every host of the\n  same CLI, so installing in a terminal covers the IDE. Junie remains a\n  separate harness port (#321).\n\n- **A shared setup pre-flight in every skill.** `locks.md` already claimed a\n  drift check ran \"on every framework-skill invocation\"; nothing implemented\n  it. It cannot be a hook (no code runs on plugin install/upgrade on most\n  harnesses) and cannot be an include (a family plugin\u0027s symlinks may not\n  escape its root under AP1), so it is one source propagated into 65 skill\n  bodies by `check-skill-preflight.py --fix`, with a hook preventing drift.\n  The `setup` family is exempt — those skills perform the setup.\n\nAlso excludes `hooks/` from the in-place pre-commit fixers: the sandbox\nwrite-denies hook scripts, which aborted `prek run --all-files` on a file\nthat needed no fix.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "74776c85d6322f402f40c16a6c277e17f3cc74d2",
      "tree": "6fbff854f525943247ff937aeb6556e7f6327928",
      "parents": [
        "127d0b84e5c6bd608ec5a5f447ee667849c354e4"
      ],
      "author": {
        "name": "Kavya Katal",
        "email": "KAVYAKATAL09@GMAIL.COM",
        "time": "Thu Sep 10 14:35:09 2026 +0530"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 11:05:09 2026 +0200"
      },
      "message": "feat(bitbucket): add guarded cloud PR request-changes actions (#1192)"
    },
    {
      "commit": "127d0b84e5c6bd608ec5a5f447ee667849c354e4",
      "tree": "79178d0c9b980ce8243df87aef758aaf9068bd35",
      "parents": [
        "54a4627cbce2af5fc81787521e06b9a5b3b1659f"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Thu Sep 10 02:10:08 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 02:10:08 2026 +0200"
      },
      "message": "chore: bump dev version to 0.2.0.dev202609092258 (#1191)\n\nMove the dev stamp so adopters\u0027 `claude plugin update` picks up the\nwork merged since 0.2.0.dev202609090002 — the marketplace is served\nfrom `main` and the update check compares version strings, so a frozen\nsuffix is a silent no-op.\n\nGenerated with the documented one-liner: bump `project.version` in\n`pyproject.toml`, then `tools/dev/check-family-plugins.py --fix` and\n`uv lock` to propagate to every manifest.\n\n\nClaude-Session: https://claude.ai/code/session_01C8xNkTXs6YqdLVU24aKQmY\n\nCo-authored-by: Claude Opus 5 \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "54a4627cbce2af5fc81787521e06b9a5b3b1659f",
      "tree": "b5aeb171255df5cd9ddb1940b5bd35e55e193347",
      "parents": [
        "35a3457af1ae5fd4c7894481d8b603bfab9fee6d"
      ],
      "author": {
        "name": "Pratiksha Badheka",
        "email": "pratikshabadheka27@gmail.com",
        "time": "Thu Sep 10 05:39:47 2026 +0530"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 02:09:47 2026 +0200"
      },
      "message": "docs(education): add MCP server to glossary (#1189)\n\n* docs(education): add MCP server to glossary\n\nWhy: the glossary defines Agent and Skill but never MCP server, even\nthough skills rely on MCP servers to reach external tools like\nGitHub, Slack, and Gmail. Adds a plain-English definition consistent\nwith the existing glossary style, placed after \"Skill\" since MCP is\nwhat a skill uses to act on the outside world.\n\n* docs(education):add 2 space in doc paraghraph\n\nwhy:make it proper style\n\n* chore(markdownlint.json):revert accidenlty markdown lintfile edit\n\nwhy: the file was accidently commited so revert it and commit with no functional change"
    },
    {
      "commit": "35a3457af1ae5fd4c7894481d8b603bfab9fee6d",
      "tree": "d5fb253f5ac5788c2bc75e240b1f1388376f7a0a",
      "parents": [
        "66623e96ea27fba091d0e5f85d25f0c83d4e6912"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Thu Sep 10 01:51:03 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 01:51:03 2026 +0200"
      },
      "message": "ci(runners): move every job to the 1-vCPU ubuntu-slim runner (#1190)\n\n* ci(runners): move every job to the 1-vCPU ubuntu-slim runner\n\nNothing this repository runs in CI is heavy. The slowest job on the\n4-vCPU image is `prek` at 131s; the rest land between 3s and 99s. Paying\nfor four cores to run a YAML linter and a 20-second pytest suite is waste\non infrastructure the foundation shares.\n\n`ubuntu-slim` is GitHub-hosted (1 vCPU, 5 GB), not an ASF self-hosted\nlabel, so it needs no Infra ticket — and 114 workflow files across the\n`apache` org already use it. Jobs run in a container rather than a\ndedicated VM and are killed at 15 minutes. Measured against that budget\nthe slowest job here uses 15% of it, so the headroom is real even after\nthe single-core slowdown.\n\nTwo consequences worth recording. `codeql.yml` gives up its\n`ubuntu-24.04` pin along with the four cores; its Python analysis is the\none job here that is genuinely CPU- and memory-bound, so it is the first\nplace to look if a run starts timing out. And no workflow sets\n`timeout-minutes`, which makes slim\u0027s 15-minute kill the effective\ntimeout everywhere — stricter than the 6-hour default it replaces, and\nappropriate for a repo where every job finishes inside two minutes.\n\nGenerated-by: Claude Code (Opus 5)\n\n* ci(zizmor): run the CLI from the uv dev group, not the Docker action\n\n`zizmorcore/zizmor-action` is a composite action whose `action.sh` runs\nthe tool with `docker pull` + `docker run`, and it aborts outright —\n\"Cannot run this action without Docker\" — when the daemon is absent.\n`ubuntu-slim` jobs are themselves containers and have no daemon, so the\njob died before zizmor was ever invoked.\n\nDeclaring `zizmor` in the root `[dependency-groups] dev` and calling\n`uv run zizmor` removes the dependency on Docker entirely, and brings\nthree things the action could not:\n\n- the version is resolved into the root `uv.lock` (1.30.0) and governed\n  by the `[tool.uv] exclude-newer` cooldown, like every other pinned\n  tool here, instead of being a floating tag inside a container image;\n- `uv run zizmor --config .zizmor.yml .` reproduces CI locally, which\n  the Docker path only did for contributors with a working daemon;\n- one fewer third-party action in a workflow whose entire purpose is\n  auditing third-party actions.\n\nBehaviour is unchanged where it matters. `--persona regular` is the\naction\u0027s default, and `advanced-security: false` already meant no SARIF\nupload, so the console-plus-exit-code contract is the same. The action\ndefaults its `token` input to `github.token`, so online audits were\nalready enabled; `GH_TOKEN` in the run step keeps them enabled rather\nthan silently dropping to offline mode.\n\n`uv sync` uses `--no-install-workspace`: zizmor imports nothing from\nthis repo, so building ~34 member packages to run it would be pure cost\non a 1-vCPU runner.\n\nVerified locally against the whole tree — no findings, 15 suppressed.\n\nGenerated-by: Claude Code (Opus 5)\n\n* ci(typos): walk the tree instead of taking the file list\n\nThe `typos` hook was the one job that would not survive the move to\n`ubuntu-slim`. It failed as a bare `- exit code: 1` with no output,\nwhich reads like a hook that lost its error message; it is really an\nOOM kill, and prek has no exit code to report for a signal-killed\nprocess.\n\ntypos\u0027 peak RSS scales with the number of paths handed to a single\ninvocation — about 1.2 MB each — and prek splits the file list across\nroughly `cpu_count` invocations. Measured on this repo\u0027s ~4.9k tracked\nfiles, in a 1-vCPU `ubuntu:24.04` container (what `ubuntu-slim` is):\n\n  610 paths    422 MB\n  1219 paths   925 MB   \u003c- the 4-vCPU chunk; fits, hence green on\n                           ubuntu-latest\n  2438 paths  2514 MB\n  4876 paths  5754 MB   \u003c- the 1-vCPU chunk; over the runner\u0027s memory,\n                           SIGKILL, no stdout, no stderr\n  walk mode     21 MB\n\n`pass_filenames: false` plus a trailing `.` makes the hook walk the\nrepo itself — the shape the `lychee` hook below already uses — for a\nflat ~21 MB whatever the core count. Coverage is unchanged: typos\nhonours .gitignore while walking, and this checkout has no untracked\nnon-ignored files. `.venv/` joins .gitignore for the same reason;\nuv already writes a `*` .gitignore inside it, so git behaved either\nway, but the walk should not depend on that.\n\nNote that this busts the prek hook-env cache key. The `prek-Linux-`\nrestore-key prefix still restores the previous archive, so no hook env\nis rebuilt from scratch.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "66623e96ea27fba091d0e5f85d25f0c83d4e6912",
      "tree": "6551d41229ba1944c708303c19149a7f2746aad0",
      "parents": [
        "826e13da1abdb981dcd94182ffce3e7c7eb68bd2"
      ],
      "author": {
        "name": "Kavya Katal",
        "email": "KAVYAKATAL09@GMAIL.COM",
        "time": "Thu Sep 10 03:23:48 2026 +0530"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 23:53:48 2026 +0200"
      },
      "message": "feat(bitbucket): add guarded cloud PR approve actions (#1186)"
    },
    {
      "commit": "826e13da1abdb981dcd94182ffce3e7c7eb68bd2",
      "tree": "44064799e9bb84758ec4178fc23acd27c11a79e2",
      "parents": [
        "a1d9e240013f88c929aa7a11e184394662257432"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Wed Sep 09 23:53:20 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 23:53:20 2026 +0200"
      },
      "message": "fix(list-skills): discover every installed skill, drop the PyYAML import (#1187)\n\n* fix(list-skills): discover every installed skill, drop PyYAML\n\n`list-skills` resolved its skill set from `Path(__file__)`, so under a\nper-family marketplace plugin install it listed only the family that\nhappens to ship it — 5 skills out of 74. It also derived each skill\u0027s\nfamily from the name prefix, against Golden rule 8, which invented\nfamilies like `write/` and `optimize/` for skills declaring `utilities`\nand split `repo-health` and `contributor-growth` across headings.\n\nDiscovery is now installation-aware and repository-first: the agent\ntarget directories an install writes into, the framework\u0027s own `skills/`\nwhen the repository is the framework checkout, and the sibling plugins in\nthe marketplace cache when the script runs from one. Rows de-duplicate by\nthe name a person would type, so relays collapse while a skill reachable\nfrom two install methods keeps both. Grouping reads the `family:`\nfrontmatter key; a skill declaring none lands in `other`.\n\nThe script was also the only one under `skills/` importing a third-party\nmodule, against the stdlib-only contract in `skills/pyproject.toml`, and\ncrashed with `ModuleNotFoundError: No module named \u0027yaml\u0027` on any\ninterpreter without PyYAML — including every plugin install. Frontmatter\nis now parsed with the standard library, as `collect_status.py` and the\nvalidator already do, and PEP 723 inline metadata states the contract so\n`uv run --script` and a bare `python3` behave identically.\n\nGenerated-by: Claude Code (Opus 5)\n\n* fix(list-skills): keep Step 1\u0027s command literal so the evals can pin it\n\nThe previous commit rewrote Step 1 to use the `\u003cframework\u003e` placeholder.\nModels resolve that to an absolute path, which is machine-specific and\ncannot be pinned in an eval fixture, so step-1-command cases 1-4 failed:\nexpected `\u003cframework\u003e/skills/…`, got `/Users/…/skills/…`.\n\nStep 1 goes back to the literal repository-relative\n`.claude/skills/magpie-list-skills/scripts/list_skills.py`, with an\nexplicit instruction not to expand it, and the four fixtures return to\ntheir upstream values — they never needed to change. The marketplace\nexception moves below the primary command and keeps its own case, which\npassed throughout.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "a1d9e240013f88c929aa7a11e184394662257432",
      "tree": "a15a477c2d2cf46a49c8187c96e232a12154827e",
      "parents": [
        "22eaa73d33d362df0c9d0d90b1cfeffa300b6819"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Wed Sep 09 23:53:03 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 23:53:03 2026 +0200"
      },
      "message": "feat(vetted-ops): publish as a substrate plugin, with the privilege boundary on the entry point (#1188)\n\n* feat(vetted-ops): publish as a substrate plugin and wire its exclusion\n\nThe vetted-ops dispatcher trades a dozen wildcard `ask` rules for one\n`allow`. That trade only holds while the agent holding the `allow`\ncannot rewrite what it is permitted to do — but the tool shipped only\nin the framework checkout, so adopters installing from the marketplace\nhad no copy of it at all, and anyone vendoring it into their own repo\nput the operation catalogue inside the tree the agent edits.\n\nPublish it as the `magpie-vetted-ops` substrate plugin, the pattern\n`magpie-agent-guard` already established: the tool is reached through a\nnarrow symlink from the installed plugin root, which is neither a path\nthe agent edits nor inside any sandbox write root.\n\nSubstrate plugins previously all carried a hook, and the generator\nrequired one. Publishing a tool from the plugin root is the general\nreason to be a substrate plugin; wiring a hook is one instance of it.\nMake `hooks` optional so a dispatcher can be published without\npretending to be a hook, and make the must-resolve diagnostic name the\nright consequence for each shape.\n\nWire the exclusion through isolated setup — proposed with the `allow`\nat install, drift-checked on update, and check 9 on verify. It covers\ntwo surfaces, because they are two halves of one bound: an agent that\nadds an op to the catalogue and an agent that adds itself to a caller\nlist in the policy arrive at the same place.\n\nThe docs state the asymmetry rather than rounding it off: the catalogue\nis protected twice (deny rules, plus sitting outside every allowWrite\nroot), the policy only once. The policy lives in the sandbox-writable\nproject root, so a Bash-level write slips past an Edit/Write deny. That\nis a real limit of the mechanism and adopters need it to judge the\ntrade, so it is documented, not implied.\n\n* fix(vetted-ops): put the privilege boundary on the entry point, not --caller\n\nThe packaging commit wired an `allow` for the dispatcher on the strength of a\nread-only *caller name*. That does not hold. `--caller` is an argv string\nchosen by whoever runs the command, so an agent handed that `allow` reaches\nevery operation in the catalogue by naming a different caller — including\nissue-close and the review submissions. It converts a confirmed `gh` write into\nan unprompted one, which is worse than the wildcard `ask` rules it replaced.\nThe tool\u0027s own README already said per-caller scoping was not a boundary; the\nrecommendation was written as if it were.\n\nSplit the dispatcher instead. `vetted-op-read` refuses any operation with\nwrites\u003dTrue before it consults the policy or looks at --caller, so no argv can\nargue it into a mutation, and it is safe to allowlist outright. `vetted-op`\nkeeps its confirmation. A permission rule keys on the command, and argv cannot\nchange which binary is running — so the entry point can carry a boundary that a\nparameter never could.\n\nRepeated --caller is now an error. argparse keeps the last occurrence, so\n`--caller read-only … --caller privileged` would match a rule written against\nthe read-only prefix while resolving to the privileged entry: a bypass of\nexactly the mechanism this commit adds.\n\nBody files were the other half. Validating a path and handing it to `gh` left\nthe checked file and the published file free to differ. read_body now requires\na workspace this user owns and that group and world cannot write, refuses\nsymlinks (O_NOFOLLOW, plus realpath for the parents), reads the bytes once, and\npipes them to `gh` on stdin. The bytes that were validated are the bytes that\nget sent.\n\nDocs say which dispatcher may be allowlisted and why the other may not, and\nname per-caller scoping as least-privilege hygiene rather than isolation —\nthat misreading is what produced the unsafe rule in the first place.\n\n* docs(rfc-ai-0002): record the vetted-ops layer and its harness portability\n\nRFC-AI-0002 described four layers and stopped at `permissions.ask`. Layer 3\u0027s\nreal failure mode is not its rules but its volume: a wildcard that prompts for\n`gh issue view` as loudly as for `gh issue close` produces a hundred prompts on\na thirty-tracker sweep, and the hundredth gets the attention the first deserved.\nPrompt fatigue is the mechanism by which Layer 3 stops working, so the tool that\naddresses it belongs in the RFC rather than only in its own README.\n\nRecord it as Layer 3a, and state where the boundary sits. `--caller` is an\nargument, so it can never be one — on any runtime. What every harness can bind\nis the command, which is why the dispatcher is split into a read entry point\nthat refuses writes structurally and a write entry point that keeps its\nconfirmation.\n\nThe harness table is the point of the section: the split is portable precisely\nbecause it asks each runtime for a permission decision keyed on a command\nstring, which all of them have, and asks none of them to bind a decision to\nwhich skill is calling, which none of them offer. Claude Code, OpenCode, Kiro\nand Codex each express it in two rules, with the matching semantics that matter\nnoted per harness — last-match-wins on OpenCode, anchored regex on Kiro, and\nKiro\u0027s prompt-by-default making omission the safe state.\n\nTwo residual risks join the list, both previously unstated: a session is a\nsingle principal, so argument-expressed scoping is advisory everywhere; and the\npolicy file sits in the project tree, which a sandboxed shell can write. The\nsecond is survivable only because of the first fix — the read dispatcher ignores\npolicy when refusing writes — and saying so is the point. The open question is\nwhether per-skill scope can ever become a real boundary, which needs a runtime\nprimitive no shipping harness has."
    },
    {
      "commit": "22eaa73d33d362df0c9d0d90b1cfeffa300b6819",
      "tree": "4125401502f9a7c7e16b04b6d7bfb1405be431ab",
      "parents": [
        "4b24abbfd9f90ff459c7075a97b54593c3fff8a5"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Wed Sep 09 03:44:05 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 03:44:05 2026 +0200"
      },
      "message": "chore(setup): drop the comdev-checkout check from setup-isolated-setup-verify (#1185)\n\nCheck 9 asserted that the local `apache/comdev` checkout backing the\nponymail and apache-projects MCP servers has an `apache/comdev` `origin`\nand sits on `main`. That checkout is a personal working copy; which\nbranch it happens to be on, and whether `origin` names the canonical\nrepo or a contributor\u0027s fork, are not properties of the secure agent\nsetup this skill exists to certify. A contributor carrying a topic\nbranch off their own fork was being reported as a broken install.\n\nThe skill\u0027s own evals had already settled this: every case under\n`tools/skill-evals/evals/setup-isolated-setup-verify/` asserts exactly\neight checks, numbered 1-8 and ending at project-root coverage, with no\ncomdev case anywhere. SKILL.md and its fixtures have been out of step;\nthey now agree, so no fixture changes were needed.\n\nCheck 9 was last in the list, so checks 1-8 keep their numbers. The\nprompt-injection preamble was written specifically around check 9\u0027s\nparsing of untrusted git output (branch names, commit subjects, remote\nstrings). Rather than lose the guardrail along with the check, it now\npoints at the untrusted machine output the remaining checks do parse:\nthe `git worktree list --porcelain` output read by check 8,\nsettings-file contents, and command stderr.\n\nThe canonical list in `docs/setup/secure-agent-setup.md`, which the\nskill defers to, drops the matching item. The ponymail and\napache-projects tool docs no longer claim verify asserts the checkout is\non `main` and not behind; `setup-isolated-setup-update` still surfaces\nthe behind-`origin/main` warning and prints the pull command, which is\nwhere that check belongs.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "4b24abbfd9f90ff459c7075a97b54593c3fff8a5",
      "tree": "7d5fae26d1c9944b984ce9cd2df02861cb47a8ad",
      "parents": [
        "f22d0a4653ec3dcbaac37b7d29e81d7d196834b0"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Wed Sep 09 03:24:56 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 03:24:56 2026 +0200"
      },
      "message": "fix(setup): document the Docker CLI and plugin paths the socket allowlist misses (#1184)\n\n* fix(setup): document the Docker CLI and plugin paths the socket allowlist misses\n\nOn macOS with Docker Desktop the documented recipe cannot produce a\nworking docker. It allows the daemon socket, but Docker Desktop installs\nthe CLI *inside* the denied directory — `docker` on PATH is\n`~/.docker/bin/docker` — so the shell cannot execute the binary at all\nand dies with `operation not permitted: docker` before the socket is\never consulted. Allowing `~/.docker/bin/` alone then gets `docker ps`\nworking while `docker compose` still reports `unknown command`, because\ncompose and buildx are plugin binaries under `~/.docker/cli-plugins/`,\nalso inside the denial. Any compose-driven workflow stays broken.\n\nThe symptom section listed only daemon-connection errors, which are not\nwhat an adopter on this platform sees first, so the page did not lead\nthem to the entry that would have helped.\n\nVerified on macOS 26 with Docker Desktop: with all three paths allowed,\n`docker ps`, `docker compose version` (v5.5.1) and `docker buildx\nversion` all succeed, while `~/.docker/config.json` stays denied so the\nagent still cannot read registry credentials.\n\nGenerated-by: Claude Code (Opus 5)\n\n* fix(setup): the Docker recipe also needs allowUnixSockets, on every platform\n\nListing a socket in `sandbox.filesystem.allowRead` grants permission to\nread the file, not to connect to it — socket connections are gated by\nthe separate `sandbox.network.allowUnixSockets` list. With the path\nallowed for reading but missing from that list, the CLI starts, finds\nthe socket, and is refused at connect time with \"permission denied while\ntrying to connect to the docker API\".\n\nUnlike the CLI-path gap this recipe already covers, this one is not\nmacOS-specific: a Linux adopter on /var/run/docker.sock hits it too, so\nthe recipe as published could not produce a working docker anywhere.\n\nEstablished by removing the entry while leaving the allowRead socket\npaths in place: `docker compose version` still ran (the plugin binary\nnever touches the daemon) while `docker ps` failed at connect. Restoring\nit fixed `docker ps` with nothing else changed.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "f22d0a4653ec3dcbaac37b7d29e81d7d196834b0",
      "tree": "87d869eb9ce9af0ea5368c55a0a6425a5f62e3a4",
      "parents": [
        "fb9f9701cce26478a08c5f58899668a5200322c5"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Wed Sep 09 03:13:47 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 03:13:47 2026 +0200"
      },
      "message": "fix(setup): allow static.rust-lang.org; correct the rust-toolchain paths in the doc (#1183)\n\nThe lychee prek hook is `language: rust` with `additional_dependencies:\n[\"cli:lychee\"]`, so prek bootstraps its own toolchain on first run. It\nfetches `rustup-init` from static.rust-lang.org, which was never in\n`allowedDomains` — a contributor with a cold prek cache cannot build the\nhook env in-sandbox.\n\nThe annotated block in the setup doc also claimed the framework needs\n`~/.rustup/` and `~/.cargo/`. It does not: prek installs its rustup and\ntoolchains under `~/.cache/prek/tools/rustup/` and points CARGO_HOME at\n`~/.cache/prek/cache/cargo/`, both already covered by `~/.cache/`. Those\nlines were added to the doc in #501 and never applied to the settings\nfile or the sandbox-lint baseline, so the doc has been describing a\nconfig that does not exist and over-stating the framework\u0027s reach into\nthe home directory.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "fb9f9701cce26478a08c5f58899668a5200322c5",
      "tree": "de4b5b5a188cd6c0f690251701bd9ebbcf0ecfe8",
      "parents": [
        "4d1f4b2c1f0dd9d9be9d678045e21b8d8b540126"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Wed Sep 09 02:09:25 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 02:09:25 2026 +0200"
      },
      "message": "chore(version): stamp the dev version to 0.2.0.dev202609090002 (#1180)\n\nSeven changes have landed behind the previous stamp: the agent-guard plugin\nmove (#1170), the doctoc URL fix (#1171), the marketplace re-sync plus the\nself-correcting prek hook (#1174), the vetted command surface (#1176), and the\nthree PRs that extended its catalogue from 19 to 58 operations (#1177, #1178,\n#1179). `claude plugin update` compares version strings, so none of that\nreaches an adopter until the stamp moves.\n\nMechanical: `project.version` edited, then\n`tools/dev/check-family-plugins.py --fix` propagated it to the five ecosystem\nmanifests and the eleven per-family plugin manifests plus the marketplace\nentries, and `uv lock` refreshed the workspace\u0027s own package version.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "4d1f4b2c1f0dd9d9be9d678045e21b8d8b540126",
      "tree": "0a2e3de108403b11dc50eb01bb7dddc9931db910",
      "parents": [
        "b14e7f9194e47e5147dcf8c3c8ac5e07ecb8be14"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Wed Sep 09 02:02:29 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 02:02:29 2026 +0200"
      },
      "message": "feat(vetted-ops): cover the remaining forge-touching families, and stop refs traversing (#1179)\n\nCompletes the pass over the families that touch the forge: repo health, release\nmanagement, contributor growth and mentoring.\n\nThey needed seven operations between them — `repo-view`, `repo-tree`,\n`run-list`, `run-view`, `release-list`, `release-view`, `user-profile` — because\nthey are overwhelmingly read surfaces. Mentoring needed none at all: it works on\nupstream issues and PRs, which the two families already merged cover. A family\nearns new operations only when it has a shape the catalogue lacks.\n\nWhat is absent here is larger than what is present, and deliberately so.\n`gh release create/upload/edit/delete` stays out: releases are vote-gated and\nmaintainer-driven, and a vetted delete would hand the agent an irreversible\naction the surrounding process keeps in human hands — the same reasoning that\nkeeps `pr-merge` out. Free-text search stays out because the query has no fixed\nshape. `gh run download` stays out because it writes to local disk rather than\nto the forge, which is a different risk class. Each is stated where a reader\nwill look for it.\n\nSeparately, a real gap found while adding `repo-tree`: `ref()` accepted `..`,\nunlike `repo_path()`, and refs are interpolated into API paths by `compare`,\n`repo-file` and `tags`. A ref of `main/../../../users/attacker` produced the\npath `repos/\u003cowner\u003e/\u003crepo\u003e/compare/main/../../../users/attacker...v1`, which a\nclient normalising the URL walks straight out of the policy-pinned repository —\ndefeating \"the repo is not addressable\", one of the tool\u0027s headline guarantees.\nGit forbids `..` in ref names, so refusing it costs nothing legitimate. Three\ntests cover it, including a sweep asserting no built argv contains a `..`\nsegment for any operation in the catalogue.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "b14e7f9194e47e5147dcf8c3c8ac5e07ecb8be14",
      "tree": "fcbbb76790b4a7df83b52d6f126560e446553578",
      "parents": [
        "3ec8dbf62ee9fbb45b5b44d7ee112c217e9eacc4"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Wed Sep 09 01:52:38 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 01:52:38 2026 +0200"
      },
      "message": "feat(vetted-ops): extend the catalogue to the issue family (#1178)\n\nSecond family in the series, stacked on the PR-management branch.\n\nThe existing `issue-*` operations address the *tracker* — the private\nrepository the security lifecycle runs in. The issue family works on the\nproject\u0027s own public issues, which is a different repository and so needs a\ndifferent set of operations. `repo-` marks upstream here, matching the existing\n`repo-file`. Ten operations: view, list, comments, comment, add/remove label,\nset milestone, add assignee, close, reopen.\n\nConfusing the two repositories is the failure that would actually hurt —\nsecurity-lifecycle content posted to a public issue — so a test walks the whole\ncatalogue and asserts that no `repo-issue-*` / `pr-*` / `gql-*` operation ever\nnames the tracker, and no tracker operation ever names upstream.\n\n`pr_labels` becomes `upstream_labels`. GitHub labels are repo-scoped and shared\nbetween issues and PRs, so two keys for one label set would have been wrong the\nmoment the issue family arrived; correcting it while the series is still in\nflight is cheaper than living with it.\n\nTwo shapes are refused rather than wrapped. `gh search issues` takes a free-text\nquery with no fixed shape, and wrapping it would re-introduce exactly the\nunbounded surface the catalogue exists to remove. `gh issue create` /\n`gh pr create` would need a free-text title, since `gh` has `--body-file` but no\n`--title-file` — and creation is a genuinely novel action, which the spec\u0027s\nnon-goals already say keeps its confirmation. Both keep their `ask` rule, and\nthe code says so where a reader will look.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "3ec8dbf62ee9fbb45b5b44d7ee112c217e9eacc4",
      "tree": "79013b88dae21ddb9c2d6f9cf5011c94e20117d1",
      "parents": [
        "bcc3e258fab3f9393b10dff335a25e5a0a9a4753"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Wed Sep 09 01:39:56 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 01:39:56 2026 +0200"
      },
      "message": "feat(vetted-ops): extend the catalogue to the PR-management family (#1177)\n\n* feat(vetted-ops): extend the catalogue to the PR-management family\n\nThe dispatcher shipped with an issue-shaped catalogue because the security\nlifecycle is where the sweep volume first appeared. PR management makes the\nsame shape of many small forge writes — a triage pass over the open queue\nanswers a wildcard `ask` per label flip — so it gets the same treatment.\n\nTwenty-two operations: list / view / diff / comments / reviews; the label,\nmilestone, assignee and reviewer edits; the three review verdicts; draft,\nready, close and branch update; failed-job re-run and first-time-contributor\nworkflow approval.\n\nGraphQL is the interesting part. `gh api graphql` takes a query as a string,\nwhich is precisely the unbounded surface this catalogue exists to remove, and\nthe PR skills make two narrow, repeated GraphQL calls. Rather than a generic\nescape hatch, each document in `src/vetted_ops/queries/` registers as its own\noperation taking only the variables it needs: the caller names a query, the\ndispatcher supplies the text, and owner/name come from policy. A caller can\nchoose among the allowlisted queries; it cannot write one, and cannot re-aim\none at another repository.\n\nThere is deliberately no `pr-merge`. Merging is the framework\u0027s deferred\nAgentic Autonomous mode — `pr-management-quick-merge` prints a merge command\nfor the maintainer rather than running one — so a vetted merge operation would\nhand the agent the one capability the surrounding design withholds. A test\nasserts its absence so the reasoning survives the next person who notices the\ngap.\n\nCaller manifests stay adopter-owned by design (a caller must not be able to\nwiden its own policy), so the README\u0027s example config gains PR-family entries\nrather than the repo shipping manifests of its own.\n\nGenerated-by: Claude Code (Opus 5)\n\n* fix(vetted-ops): stamp the ASF licence header into the GraphQL documents\n\nApache RAT failed on #1177: the two `.graphql` files carried no approved\nlicence header. GraphQL takes `#` comments, so the standard ASF block goes in\nunchanged and the documents parse exactly as before.\n\nRAT runs as its own workflow rather than a prek hook, so an unstamped file is\ninvisible locally and only surfaces after the PR is pushed — and\n`add-license-headers.py` covers Markdown alone, by design. A test now asserts\nevery shipped query carries the header, which puts the feedback where the file\nis written instead of two minutes into CI.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "bcc3e258fab3f9393b10dff335a25e5a0a9a4753",
      "tree": "8004e38ebd009d96ceea5e1730fb52b4a42e9605",
      "parents": [
        "e65537026437792a29cc22562c22a152e0af4227"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Wed Sep 09 00:40:49 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 09 00:40:49 2026 +0200"
      },
      "message": "Add a vetted command surface for fixed-shape forge operations (#1176)\n\nLayer 3 forces confirmation with wildcard rules -- gh issue edit *,\ngh api * -X *, and so on. The wildcard is what makes the rule safe, because an\nunbounded argument surface cannot be pre-approved, and it is also what makes it\nexpensive: a maintainer sweeping thirty trackers answers a hundred prompts, and\nthe hundredth gets less attention than the first. Prompt fatigue is a security\nfailure, not an ergonomics complaint -- a posture that is correct but unusable\ndegrades into one that is bypassed.\n\nThis removes the wildcard rather than the confirmation. tools/vetted-ops is a\ndispatcher over a closed catalogue of operations. Each has a name, typed\nparameters and a builder returning an argv list executed without a shell, so no\nparameter can become a command; parameters may not start with a dash, so none\ncan become a flag; the repository comes from policy rather than the command\nline, so an operation cannot be pointed elsewhere; and label, milestone,\nassignee, column and close-reason values must appear in adopter-declared enums.\nFree text reaches the forge only by file reference, and the file must resolve\ninside a declared workspace -- the body may contain anything, but it cannot be\n~/.ssh/id_rsa. A bounded effect set can be allowed once instead of asked every\ntime, and widening it is a reviewed code change.\n\nScoping is deliberately described as what it is. The dispatcher requires\n--caller and refuses operations outside that caller\u0027s manifest, which defends\nagainst the wrong skill reaching for the wrong operation -- including the\nprompt-injection case where hostile issue text talks a read-only pass into a\nstate change. It is not a security boundary: within a session the agent is one\nprincipal and supplies --caller itself. The README and the spec both say so\nrather than implying more.\n\nA real boundary needs the runtime to bind scope to permissions, and no portable\nmechanism exists today: plugin manifests cannot declare permissions, marketplaces\ncannot either, and permissionMode is unsupported for plugin-shipped agents. The\nclosest primitive is a subagent with a restricted tools list, which is Claude\nCode-specific -- the only other shipping runtime adapter is Codex, experimental.\nThe spec records that as the trajectory rather than pretending the gap is closed,\nalongside generalising the catalogue beyond the security family, resolving\nbuilders per configured forge, and having permission-audit retire the wildcard\nrules a repo no longer needs.\n\nTwenty tests assert the properties rather than the plumbing: hostile parameters\nrefused, every builder producing a gh argv, the repo not influenceable by a\nparameter, body files outside the workspace rejected while hostile body content\npasses through untouched, and a caller refused an operation outside its manifest."
    },
    {
      "commit": "e65537026437792a29cc22562c22a152e0af4227",
      "tree": "8ab986f247ce7955c73b133566da81bff1ec2a19",
      "parents": [
        "a0149b778078e603e535846da651f8e639f5036b"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 23:05:16 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 23:05:16 2026 +0200"
      },
      "message": "Record three lessons from a CVE publication that stalled (#1175)\n\nAll three come from one advisory going out while its CVE record sat\nunpublished, and from a fix that merged onto the wrong branch for the\nrelease it was milestoned to.\n\n1. Writing CNA_private.state \u003d PUBLIC is not the same as submitting to\n   CVE Services. record.md said PUBLIC meant \"record pushed to cve.org\";\n   in practice the state flip landed and cveawg.mitre.org kept returning\n   404. record-update writes the tool\u0027s own copy, and re-pushing does not\n   retry the submission -- submitting is a separate UI action. The tell is\n   an empty cveMetadata.datePublished next to state PUBLISHED: CVE\n   Services stamps that field on acceptance, so an empty one means the\n   record was never accepted. It is a symptom to act on, never a field to\n   fill in by hand, and doing so only makes the local copy self-consistent\n   while cve.org still 404s. Also note www.cve.org is client-rendered, so\n   an empty page there proves nothing -- verify against the API.\n\n2. Check the backport at pr merged, not at fix released. A PR merged to\n   the development branch does not reach a patch release without a\n   backport, and Affected versions is a claim about which release contains\n   the fix. Caught one where the merged PR had no backport label and the\n   tracker was milestoned to a patch release cutting the next day; the\n   advisory would have named a version that never contained the fix. Probe\n   by content rather than ancestry, because a cherry-pick changes the SHA\n   and `diverged` proves nothing. The two resolutions -- backport, or move\n   the milestone and widen Affected versions -- are not equivalent, and\n   the second changes a CVE-affecting field, so neither may be chosen\n   silently.\n\n3. An unpublished GHSA is a reporter-facing gap. github-advisory.md\n   already detects the drift and describes this exact case, but files it\n   as informational. On a GHSA-sourced tracker the advisory is the surface\n   the reporter watches -- not the mailing-list advisory and not the CVE\n   record -- so a tracker can be closeable while, to the reporter, nothing\n   has happened since triage. Check published_at rather than state alone,\n   do not let the tracker close silently past it, and fold the publish\n   request into the existing admin hand-off relay so the reporter is not\n   told everything is public while reading an advisory marked triage.\n\nDocumentation only."
    },
    {
      "commit": "a0149b778078e603e535846da651f8e639f5036b",
      "tree": "44658308f2e2889b1cd37d45ebbce36e6f9eb54a",
      "parents": [
        "6a1e37e5dda262311da92786c2d44971e23d2d27"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Tue Sep 08 23:04:49 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 23:04:49 2026 +0200"
      },
      "message": "chore(deps): bump the github-actions group with 3 updates (#1167)\n\nBumps the github-actions group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action).\n\n\nUpdates `github/codeql-action/init` from 4.37.8 to 4.37.9\n- [Release notes](https://github.com/github/codeql-action/releases)\n- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)\n- [Commits](https://github.com/github/codeql-action/compare/db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28...cdf488f595d80d6e07e03d4674febd5ab45fa938)\n\nUpdates `github/codeql-action/analyze` from 4.37.8 to 4.37.9\n- [Release notes](https://github.com/github/codeql-action/releases)\n- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)\n- [Commits](https://github.com/github/codeql-action/compare/db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28...cdf488f595d80d6e07e03d4674febd5ab45fa938)\n\nUpdates `zizmorcore/zizmor-action` from 0.6.2 to 0.6.3\n- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)\n- [Commits](https://github.com/zizmorcore/zizmor-action/compare/3dc1ecc9bcb9e94e9b2c709687979e1298497054...70fb788f84895a7701f5643d103d587e460b5c99)\n\n---\nupdated-dependencies:\n- dependency-name: github/codeql-action/init\n  dependency-version: 4.37.9\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n  dependency-group: github-actions\n- dependency-name: github/codeql-action/analyze\n  dependency-version: 4.37.9\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n  dependency-group: github-actions\n- dependency-name: zizmorcore/zizmor-action\n  dependency-version: 0.6.3\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n  dependency-group: github-actions\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "6a1e37e5dda262311da92786c2d44971e23d2d27",
      "tree": "dceb9e5543b7c95d94d32c08ae555bca75741d1e",
      "parents": [
        "fbe828eb6925627cc7efd335817e57335deedd2e"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 21:17:39 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 21:17:39 2026 +0200"
      },
      "message": "fix(marketplace): re-sync magpie-agent-guard, and make the prek hook self-correcting (#1174)\n\n* fix(marketplace): re-sync magpie-agent-guard with the dev version stamp\n\n`#1169` stamped the framework version to `0.2.0.dev202609081000` across\nevery ecosystem manifest. `#1170` landed right after it, adding the\n`magpie-agent-guard` plugin — a *new* file, branched before the bump and\ntherefore carrying the previous stamp `0.2.0.dev202609080121`. New files\ndo not conflict, so both merged cleanly and `main` was left with a family\nplugin one stamp behind the root manifest it inherits from.\n\n`check-family-plugins` has failed on `main` ever since, which is why an\nunrelated dependabot PR (#1167) shows a red `prek` job it did not cause.\n\n`python3 tools/dev/check-family-plugins.py --fix` output; no hand edits.\n\nGenerated-by: Claude Code (Opus 5)\n\n* fix(prek): regenerate the family plugins instead of reporting them stale\n\nThe `check-family-plugins` hook ran check-only, so a contributor whose\nmanifests drifted got an instruction — \"run\n`tools/dev/check-family-plugins.py --fix`\" — rather than a fixed tree. The\ngenerated files are a pure function of the skills\u0027 `family:` frontmatter and\n`pyproject.toml`\u0027s version, so there is nothing for them to decide: the fixer\nis the only correct response to every failure the check can report.\n\nRun the hook as `--fix`. prek fails a run whose hook modified files, so drift\nstill stops the commit; it now arrives already corrected, needing only a\nre-stage — the same convention `end-of-file-fixer`, `trailing-whitespace`, and\n`add-license-headers` already follow here.\n\n`--fix`\u0027s exit code is deliberately left alone: the release-bump procedure\ninvokes it directly in plain shell blocks, where returning non-zero on a\nsuccessful regeneration would read as failure.\n\nScope note: this closes the hand-fix gap for anyone who runs the hooks, but it\ncannot catch the cross-PR case that broke `main` this morning — two PRs green\nin isolation, conflict-free on merge, out of sync in the result. Only a check\non `main` (or a merge queue) sees that.\n\nDocs describing the hook as check-only updated to match.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "fbe828eb6925627cc7efd335817e57335deedd2e",
      "tree": "887129aaf6887a19e670fa0e7b950b7fd0796f6e",
      "parents": [
        "217a7f866cfe9df4ce4fe5be731de4b4c1dff7fe"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 15:37:22 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 15:37:22 2026 +0200"
      },
      "message": "feat(agent-guard): run the guard from the installed plugin, not a per-worktree copy (#1170)\n\nThe guard was installed by copying the engine into `\u003crepo\u003e/.claude/hooks/`\nand wiring `$CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py` into a\ngitignored `settings.local.json`. Both are per-worktree and neither travels\nthrough git, so every worktree had to be seeded — by a best-effort\npost-checkout hook, by a sync pass, and by `worktree-init` Step 1d.\n\nWhen that seeding misses, the failure is not a quietly inactive guard. The\nwiring still applies in the worktree (Claude Code resolves\n`$CLAUDE_PROJECT_DIR` against it while loading the main checkout\u0027s settings),\nso the hook names a script that is not there and **every** `Bash` call in that\nworktree fails. Three separate mechanisms existed to paper over a shape that\nshould not need any of them: a worktree is an ordinary checkout.\n\nThe `magpie-agent-guard` plugin registers the `PreToolUse` hook in its own\nmanifest and resolves the engine under `${CLAUDE_PLUGIN_ROOT}`, so nothing is\nrepository-local and a worktree needs no treatment. It is a plugin of its own\nrather than a hook on `magpie` or on each family plugin because Claude Code\nmerges hooks from every enabled plugin — wiring it into the families would run\nthe guard once per enabled family on every `Bash` call.\n\nSnapshot adopters gain the same property without the plugin: the wiring now\npoints into the snapshot, which `worktree-init` Step 1 already symlinks into\nevery worktree.\n\nSkill-owned guards follow the same principle. The engine resolves the\nframework tree it is running from and scans every `skills/*/guards` in place,\nso a skill\u0027s guards are live wherever the framework is and the step that\ncollected them into a local `guards.d/` is gone. Discovery stays lazy — it\nruns only once a `gh`/`git` segment is seen, never on the fast path.\n\nGenerated-by: Claude Code (Opus 5) following the guidelines\nClaude-Session: https://claude.ai/code/session_01GLcpHRb14rFsAnBR6NVBvf"
    },
    {
      "commit": "217a7f866cfe9df4ce4fe5be731de4b4c1dff7fe",
      "tree": "0a0fdbfb1701d6af2359a28307ac110f2a1abb5b",
      "parents": [
        "a181dd16bca6e11751c315f3b11296bb3fee10ba"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 14:45:32 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 14:45:32 2026 +0200"
      },
      "message": "fix(prek): drop the .git suffix from the doctoc repo URL (#1171)\n\nzizmor 0.6.3 (dependabot #1167) audits .pre-commit-config.yaml for the\nfirst time, and its `ref-confusion` audit hard-fails on ours:\n\n    fatal: no audit was performed\n    \u0027ref-confusion\u0027 audit failed on file://./.pre-commit-config.yaml\n        0: error in \u0027ref-confusion\u0027 audit\n\nThat is an audit crash, not a finding, so the whole zizmor job exits 1\nwith nothing reported.\n\n`ref-confusion` resolves each pre-commit repo against the GitHub API to\ndecide whether its `rev:` is ambiguous between a tag and a branch. Ours\ndeclared `https://github.com/thlorenz/doctoc.git`, and the suffix is\ncarried into the lookup:\n\n    GET /repos/thlorenz/doctoc       200\n    GET /repos/thlorenz/doctoc.git   404\n\nThe 404 aborts the audit. doctoc was the only one of the four remote\nhooks carrying `.git`; the other three already omit it, so this is a\nconsistency fix as much as a repair.\n\nNot reproducible locally: the audit is online-only and zizmor falls back\nto offline without a token, where the config passes cleanly. The\nmechanism was confirmed against the API directly instead.\n\nLanding on main rather than on the dependabot branch, which would be\noverwritten on its next push. main is not currently exposed — the pinned\nv0.6.2 does not audit pre-commit configs — but `version: latest` means a\nfuture image would reach it regardless of the action bump.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "a181dd16bca6e11751c315f3b11296bb3fee10ba",
      "tree": "5b1292ea6e475885131963b6fe8f5a6fac537e6d",
      "parents": [
        "01ab9f65735f84f1d57ea15f4a6ef6370525d464"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Tue Sep 08 14:17:04 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 14:17:04 2026 +0200"
      },
      "message": "chore(deps): bump the python-deps group across 1 directory with 4 updates (#1168)\n\nBumps the python-deps group with 4 updates in the / directory: [prek](https://github.com/j178/prek), [ruff](https://github.com/astral-sh/ruff), [google-auth-oauthlib](https://github.com/googleapis/google-cloud-python) and [mcp](https://github.com/modelcontextprotocol/python-sdk).\n\n\nUpdates `prek` from 0.4.14 to 0.5.1\n- [Release notes](https://github.com/j178/prek/releases)\n- [Changelog](https://github.com/j178/prek/blob/master/CHANGELOG.md)\n- [Commits](https://github.com/j178/prek/compare/v0.4.14...v0.5.1)\n\nUpdates `ruff` from 0.16.3 to 0.16.5\n- [Release notes](https://github.com/astral-sh/ruff/releases)\n- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)\n- [Commits](https://github.com/astral-sh/ruff/compare/0.16.3...0.16.5)\n\nUpdates `google-auth-oauthlib` from 1.4.0 to 1.4.1\n- [Release notes](https://github.com/googleapis/google-cloud-python/releases)\n- [Changelog](https://github.com/googleapis/google-cloud-python/blob/main/packages/gcp-sphinx-docfx-yaml/CHANGELOG.md)\n- [Commits](https://github.com/googleapis/google-cloud-python/compare/google-auth-oauthlib-v1.4.0...google-auth-oauthlib-v1.4.1)\n\nUpdates `mcp` from 2.0.0 to 2.1.1\n- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)\n- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)\n- [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v2.0.0...v2.1.1)\n\n---\nupdated-dependencies:\n- dependency-name: prek\n  dependency-version: 0.5.1\n  dependency-type: direct:development\n  update-type: version-update:semver-minor\n  dependency-group: python-deps\n- dependency-name: ruff\n  dependency-version: 0.16.5\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n  dependency-group: python-deps\n- dependency-name: google-auth-oauthlib\n  dependency-version: 1.4.1\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n  dependency-group: python-deps\n- dependency-name: mcp\n  dependency-version: 2.1.1\n  dependency-type: direct:development\n  update-type: version-update:semver-minor\n  dependency-group: python-deps\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "01ab9f65735f84f1d57ea15f4a6ef6370525d464",
      "tree": "0fe4da34a58d72115f4f26f874ff015aa33f7f11",
      "parents": [
        "507a2266293c6b915ff4f6b9c7bae349f1853467"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 14:16:54 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 14:16:54 2026 +0200"
      },
      "message": "fix(skill-evals): do not scavenge JSON out of a prose step\u0027s output (#1169)\n\n`extract_json_from_output`\u0027s last-resort strategy takes the largest\nbalanced `{...}` / `[...]` block in the CLI\u0027s stdout. Ordinary Markdown\nsupplies those freely: a task-list `- [ ]` parses as the JSON value `[]`,\nand a link\u0027s `[text]` as a malformed one. On a step whose output is\nprose, that scavenged fragment then replaces the whole body — `actual`\nbecomes `[]`, `raw_output` no longer exists, and every assertion\naddressing it fails against output the model got exactly right.\n\nThat is what happened to all four cases of\nsecurity-issue-deduplicate/step-3-merge-body. Read back, the model\u0027s\nmerged issue body was correct in each: both credits on separate lines,\nboth mailing-list threads, severity Unknown, the widened version range,\nthe CWE agreement noted. The harness threw it away and reported four\nfailures.\n\nA suite whose expected side addresses a wrap key (`raw_output`,\n`stderr`, `exit_code`, via expected.json or an assertions.json `field`)\nis declaring that the step emits prose. Skip extraction for those and\npass the body through. This reuses `wrap_is_asserted` from #1161 on the\nmirror-image problem: that change stopped the harness passing what it\nnever graded, this one stops it failing what it graded wrong.\n\nAll four cases pass after the change. Two tests cover both directions —\nprose containing a task-list stays prose, and an ordinary JSON step still\ngoes through the extractor.\n\nAlso bumps the framework version to 0.2.0.dev202609081000 and\nregenerates the manifests, so the fix reaches installed copies: the\nmarketplace is served from `main` and `claude plugin update` compares\nversion strings, not commit SHAs, so a frozen stamp leaves adopters told\nthey are already up to date.\n\n\nClaude-Session: https://claude.ai/code/session_01So3JRGXrbqSGrohtZuHWKg\n\nCo-authored-by: Claude Opus 5 \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "507a2266293c6b915ff4f6b9c7bae349f1853467",
      "tree": "e7e693e3df7e0d155e475e99db8442daed1b3759",
      "parents": [
        "fccfe65b3b7aaf48647686311691d0e34bb3ce67"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Tue Sep 08 12:37:49 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 12:37:49 2026 +0200"
      },
      "message": "chore(deps): bump https://github.com/crate-ci/typos (#1166)\n\nBumps the pre-commit-hooks group with 1 update: [https://github.com/crate-ci/typos](https://github.com/crate-ci/typos).\n\n\nUpdates `https://github.com/crate-ci/typos` from v1.49.0 to 1.50.0\n- [Release notes](https://github.com/crate-ci/typos/releases)\n- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)\n- [Commits](https://github.com/crate-ci/typos/compare/v1.49.0...v1.50.0)\n\n---\nupdated-dependencies:\n- dependency-name: https://github.com/crate-ci/typos\n  dependency-version: 1.50.0\n  dependency-type: direct:production\n  dependency-group: pre-commit-hooks\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "fccfe65b3b7aaf48647686311691d0e34bb3ce67",
      "tree": "d1aed7198fb2a512c3fe66df74aec43a06644b13",
      "parents": [
        "6e93c2d07ee3b01c276fc5aae0545290ed70d313"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 05:03:54 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 05:03:54 2026 +0200"
      },
      "message": "ci(prek): quieten the CI log — drop --verbose, skip the identity hook (#1165)\n\n* ci(prek): quieten the CI log — drop --verbose, skip the identity hook\n\nTwo sources of noise around the one thing a reader opens the log for.\n\n`identity` is a meta-hook that echoes every file passed to the static\nchecks. Useful locally when working out why a hook did or did not fire;\non `--all-files` it prints the entire repository into the log before any\nresult appears. It stays enabled locally and is skipped only in CI.\n\n`--verbose` was added on the belief that it bought live progress in a\nnon-TTY log. Measured, it does not: first output and total runtime\ncoincide with the flag and without it, so prek emits nothing until the\nrun finishes either way. It streams only when stdout is a TTY, where it\ndraws an in-place progress UI with cursor-control escapes; against a pipe\nit suppresses that renderer and prints one block at the end.\n`--no-progress` behaves identically and `-q` prints nothing at all. The\nflag\u0027s only effect in CI was to add every passing hook\u0027s stdout to that\nfinal block. Failures still print their output, and\n`--show-diff-on-failure` still shows what a fixer hook changed.\n\nNote for anyone editing this later: `--skip a,b` is not a list. Prek\ntakes one HOOK|PROJECT per occurrence and merely warns that a\ncomma-joined value matched no hooks, so the comma form reads as working\nwhile skipping nothing — which is what my first attempt did, caught by\nrunning the command rather than trusting it.\n\nGenerated-by: Claude Code (Opus 5)\n\n* ci(prek): allocate a pty so prek streams progress\n\nExperiment to validate on the runner: prek streams per-hook results only\nwhen stdout is a TTY. `script -qec` allocates one; `--no-progress`\nsuppresses the spinner whose in-place redraws would otherwise fill the\nlog with cursor-control escapes. `-e` propagates the exit status, without\nwhich a failing prek would report as a passing step.\n\nCannot be verified locally — script(1) has no terminal to attach to in\nthis sandbox — so this is measured against the real Actions log and kept\nor reverted on that evidence.\n\nGenerated-by: Claude Code (Opus 5)\n\n* ci(prek): keep the pty, drop --no-progress\n\npty plus --no-progress streamed nothing. `--no-progress` hides the\nstreaming renderer itself, not merely its spinner — the progress\nrendering and the live per-hook output are the same mechanism, so\nsuppressing one suppresses the other. That combination paid the pty\u0027s\ncost and bought none of its benefit.\n\nKeeping the pty alone: prek streams, at the price of the in-place redraw\nescapes the spinner emits.\n\nGenerated-by: Claude Code (Opus 5)\n\n* ci(prek): revert the pty — Actions shows one block regardless\n\nMeasured on the runner, not assumed. Three configurations, each with\nevery hook-result line landing in the same one-second timestamp roughly\n106s into the step:\n\n  plain pipe            one block at the end\n  pty (script -qec)     one block at the end\n  pty + --no-progress   one block at the end\n\nA pty does make prek stream locally — spinner, in-place redraws — so the\nrenderer itself works. That output just does not survive to the Actions\nlog, which shows only the final state. PYTHONUNBUFFERED was never in the\npath: prek is a Rust binary and captures each hook\u0027s stdout to replay\nafter the hook finishes.\n\nSo the plain form comes back: identical information, no script(1)\ndependency, and no -e footgun, where script would otherwise return its\nown exit status and a failing prek would report as a passing step.\n\n--skip identity and the removal of --verbose stand on their own evidence\nand remain.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "6e93c2d07ee3b01c276fc5aae0545290ed70d313",
      "tree": "8688681bec6628dc7eaa8b37b065d75001027a2e",
      "parents": [
        "abda5c152d03c64c394e56a0b14b949ad480c12e"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 04:41:49 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 04:41:49 2026 +0200"
      },
      "message": "ci(dev): deterministic gates for doc/tree drift, test coverage, and a shared toolchain (#1164)\n\n* ci(dev): add check-doc-sync — deterministic gates for doc/tree drift\n\nThis session fixed a batch of documentation that had drifted from the\ntree: ten specs missing from both index files, per-family and per-mode\nskill counts stale in three places, and catalogue totals stale in a\nfourth. Every one was found by reading, not by a check. Left as is, they\ncome back.\n\ncheck-doc-sync.py makes five of them mechanical:\n\n- every tools/spec-loop/specs/*.md is listed in BOTH overview.md and\n  README.md;\n- README.md\u0027s family table \"N skills\" cells match live family:\n  frontmatter;\n- docs/modes.md\u0027s Modes-at-a-glance Skill-count column matches live\n  mode: frontmatter — the validator\u0027s modes-doc rule checks section\n  membership but parses only the mode and status columns, so the counts\n  were unguarded;\n- bare \"N skills\" totals in docs/setup/marketplaces.md match the\n  catalogue;\n- every script in tools/dev/ is named in tools/dev/README.md.\n\nEach check was verified to fail on the exact defect it exists for, by\nreintroducing that defect and confirming the reported number matched what\nhad actually been wrong. A check that cannot go red is worse than no\ncheck, since it reads as evidence — the lesson from #1161, where an\nunauthenticated CLI produced a fully green eval run.\n\nThe fifth check found three undocumented scripts on its first run\n(agent-pre-commit.sh, check-placeholders.sh, run-workspace-check.sh), so\ntools/dev/README.md now carries a table saying what each of the seven\nguards rather than naming a couple in passing.\n\nThe counts are deliberately narrow: the total-skills check reads an\nallowlist of files rather than sweeping the repo, because plenty of docs\nlegitimately count a subset (\"Nine skills cover the staged path\") and a\ngreedy scan would flag those as drift forever.\n\nGenerated-by: Claude Code (Opus 5)\n\n* test(dev): bring tools/dev under the workspace checks\n\nThe scripts in tools/dev are the gates that guard everyone else\u0027s\nchanges, and they were the one Python tree nothing checked: the\nworkspace checks iterate `[tool.uv.workspace] members`, and tools/dev\nwas not one. So ruff, mypy, and pytest all skipped it.\n\nDeclares the tree as a workspace member, the same config-carrier pattern\n`skills/pyproject.toml` uses, and adds 20 tests for check-doc-sync.py.\n\nThe tests are written around the red case. Each check builds a miniature\nrepo in tmp_path and asserts the drift is reported with both numbers,\nthen that the corrected tree is silent — because a gate that cannot fail\nis worse than no gate, it reads as evidence while measuring nothing.\nAlso covered: a spec listed in only one of the two indexes still counts\nas missing, a README table row that names no family is not read as a\nfamily with a wrong count, a mode nothing declares must say 0 rather\nthan be skipped, every stale total on a page is reported rather than\njust the first, and the totals check stays inside its allowlist.\n\nScript filenames are hyphenated, so the tests load the module through\nimportlib rather than a plain import.\n\nBringing the tree under the checks surfaced four lint findings in\npre-existing scripts; three are fixed (an unused unpacked variable, two\nloops ruff can express directly) and RUF003 is ignored with a reason —\nthe repo\u0027s prose style uses en dashes throughout, and rewriting comments\nto hyphens to satisfy a homoglyph check would make them worse. Four\nfiles were reformatted. Every script was re-run afterwards to confirm\nbehaviour is unchanged.\n\nGenerated-by: Claude Code (Opus 5)\n\n* refactor(workspace): share the dev toolchain via magpie-dev\n\nEvery workspace member repeated the same three pins in its own\n`[dependency-groups] dev`, under an instruction in the root pyproject to\n\"keep the version bounds here and in every member in lockstep\". They were\nnot in lockstep:\n\n  mypy    \u003e\u003d2.1.0, \u003e\u003d2.3.0, \u003e\u003d1.10   (three floors)\n  pytest  \u003e\u003d9.1.1, \u003e\u003d8.0             (two)\n  ruff    \u003e\u003d0.16.1, \u003e\u003d0.16.2         (two)\n\ntools/skill-reconciler-diff declared only pytest, and tools/fossil had no\ndev group at all despite being ruff-checked — its checks ran against\nwhatever happened to be in the environment. Nothing detected any of it.\nThe duplication was the defect and the lockstep instruction was the\nworkaround.\n\ntools/dev becomes `magpie-dev`, a metadata-only wheel whose dependencies\nare the toolchain. Every other member\u0027s dev group becomes\n`[\"magpie-dev\"]`, with the workspace source declared once at the root.\nMembers with extras keep them (tools/gmail/oauth-draft retains mcp).\nBumping a version is now one edit and the whole workspace moves.\n\nEach member\u0027s environment stays self-contained: the checks still run\n`uv run --directory \u003cmember\u003e --project . python -m \u003ctool\u003e`, so nothing\nleaks in from the root. Only the declaration is shared.\n\nTwo properties checked rather than assumed. Check discovery keys off the\nconfig sections (`[tool.ruff]`, `[tool.mypy]`,\n`[tool.pytest.ini_options]`) and not the dev group, so giving a member\nmore tools does not silently enrol it in more checks. And the whole\nbattery was re-run after deleting every member `.venv`, not against a\nwarm cache: ruff 30/30, ruff-format 30/30, mypy 27/27, pytest 33/33.\n\nOne real behaviour change: tools/bitbucket moves from mypy\u003e\u003d1.10 /\npytest\u003e\u003d8.0 to the current floors. It passes the fresh sweep, but that is\na version bump rather than a no-op.\n\nGenerated-by: Claude Code (Opus 5)\n\n* ci(dev): verify every workspace member\u0027s tests actually run\n\nBoth the CI pytest matrix and the workspace sweep are driven by the\npresence of `[tool.pytest.ini_options]`. A project can therefore carry a\nfull tests/ directory and be executed by nothing: the job is simply never\nemitted, so there is no red tick for anyone to notice. Nothing checked\nthis.\n\ncheck-workspace-members.py now reports three shapes:\n\n  - tests on disk, no [tool.pytest.ini_options]  → the tests never run;\n  - [tool.pytest.ini_options], no tests on disk  → the CI job runs and\n    collects nothing, so its green tick proves nothing;\n  - neither                                      → no tests at all.\n\nA project that genuinely should not be tested declares it with\n`[tool.magpie.checks] skip \u003d [\"pytest\"]`, the same opt-out the sweep and\nthe CI matrix already honour — so an exemption is visible in the file\nrather than being a silent absence from a matrix nobody reads.\n\nAll 33 members pass today, so this locks in the current state rather than\nfixing a present gap. The hook now also re-fires on test files, since\nadding or deleting tests changes the answer.\n\nTen tests cover it, including the false negative that would make the\nwhole check worthless: a test file vendored inside a member\u0027s .venv or\nsite-packages must not make an untested member look tested. Each of the\nthree failure shapes was also verified by hand against a real member,\nand the declared opt-out confirmed to silence all three.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "abda5c152d03c64c394e56a0b14b949ad480c12e",
      "tree": "0d288677d9dba6c7570757e4aa18b017425da92c",
      "parents": [
        "1e1e389b5f7d8eb8c180d00f67950b40d590e9e8"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 04:05:40 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 04:05:40 2026 +0200"
      },
      "message": "docs(spec-loop): specify marketplace distribution, index every spec (#1163)\n\nCloses the gap #1162 recorded rather than papered over: the plugin\nmarketplace surface had no spec at all.\n\n`marketplace-distribution.md` covers the two manifest families (the\nvendor-neutral Agent Plugins 1.0 root manifest with its closed ten-field\nschema, and the five client-specific ones), the ten Claude-Code-only\nper-family plugins, and the generator/CI gate that keeps them in step\nwith `family:` frontmatter.\n\nThe load-bearing part is the versioning rule. The marketplace is served\nfrom `main`, so adopters install dev versions as the normal case, and\n`claude plugin update` compares version strings rather than commit SHAs\n— a frozen `.devN` suffix leaves an adopter told they are \"already at the\nlatest version\" indefinitely, recoverable only by a full uninstall and\nreinstall. Recorded with the reason, along with the fact that nothing\nchecks the stamp is fresh, which is the one failure mode that is silent\nin exactly the wrong direction.\n\nStatus is `experimental`, not `stable`: only the Claude Code manifests\nhave been exercised against a live install. The rest conform to their\nvendors\u0027 published documentation and nothing more, which the Known gaps\nnow say.\n\nWhile registering it: ten specs were absent from both index files\n(`overview.md` and `README.md`) — among them `security-model-preparation`\nfrom #1156 and every per-family spec. All ten are now listed in both, so\nthe indexes are complete.\n\nAlso corrects `docs/setup/marketplaces.md`, which said 41 of 70 skill\ndescriptions carry placeholder syntax; recounted from live frontmatter it\nis 45 of 74.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "1e1e389b5f7d8eb8c180d00f67950b40d590e9e8",
      "tree": "c95489e1afc50774dad938600b81e1556790bfea",
      "parents": [
        "de7d705468947fa7c1d37cd33405cd0d2741fed5"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 03:56:31 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 03:56:31 2026 +0200"
      },
      "message": "chore(spec-loop): sync specs to main and advance .last-sync (#1162)\n\nThe marker sat at a1cff444, 17 commits behind main. Bumping it alone\nwould claim those commits are described by the specs, so the drift is\nclosed first.\n\nSpecs updated for what actually shipped:\n\n- meta-and-quality-tooling: skill-evals errors, rather than passing, when\n  a case\u0027s CLI produced no gradeable output (#1161).\n- security-reporting: the tracker dashboard projects the current partial\n  bucket to its end-of-bucket value, splitting RATE series (accumulate\n  from zero) from LEVEL series (carry over), and deliberately not\n  projecting mean-based signals (#1158).\n- project-agnosticism: \u003cPROJECT\u003e and \u003cproject\u003e are two placeholders\n  holding different values, and the lint carries both spellings plus\n  spaced variants (#1154).\n- adapters: the forwarder relay\u0027s contact_handle defaults to an\n  org-level shared inbox rather than a named individual (#1135). The\n  multi-hop coordinator case is designed in RFC-AI-0008 and unimplemented.\n- issue-management-family: the family\u0027s eval suites, and the note that\n  --cli runs belong outside a credential-denying sandbox (#1145).\n\nCommits needing no spec change: #1152, #1143 and #1156 updated their own\nspecs in-commit; #1149, #1147, #1151 are behaviour-preserving bug fixes;\n#1155 and #1141 are CI and dependency chores; #1159\u0027s spec edits landed\nwith it; #1144 removes hardcoded literals that no spec asserted.\n\nOne genuine gap recorded rather than papered over: no spec covers\nmarketplace distribution or the dev-version stamping rule from #1160,\nwhich is load-bearing because `claude plugin update` compares version\nstrings, not commit SHAs. Logged in adoption-and-setup as wanting its own\nspec.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "de7d705468947fa7c1d37cd33405cd0d2741fed5",
      "tree": "6dab9a331aa306397807a17594bdbd36df4faa8b",
      "parents": [
        "2c6aab0dbd973f9695028cc116abb62877da83af"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 03:42:24 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 03:42:24 2026 +0200"
      },
      "message": "fix(skill-evals): a CLI that grades nothing is an ERROR, not a PASS (#1161)\n\nWhen the model CLI produced no usable JSON, the runner wrapped its stdout\nas {\"raw_output\": ...} and handed that to the intersection-only\ncomparator. The comparator only checks keys present on both sides, so if\nexpected.json declared no wrap key it compared nothing — and reported\nPASS.\n\nThat turns a broken CLI into a green run. It is not hypothetical: a\nsandboxed `claude -p` returns \"Not logged in\", and a full suite recently\nreported 35/35 passed with no model having graded a single case. Re-run\nwith credentials, the real result was 28/35.\n\nThe wrap still exists, because a step whose output is prose has two\nlegitimate ways to gate on it: a raw_output / stderr / exit_code key in\nexpected.json, or a structural assertion whose `field` addresses one\n(security-issue-deduplicate/step-3-merge-body uses the latter, and is\nunaffected). When neither is present the case now reports ERROR naming\nthe cause, with the first 120 characters of stdout.\n\nSame guard on the non-zero-exit branch: some auth failures and safety\nfilters signal via exit code rather than prose.\n\nTwo tests asserted the old behaviour and are inverted; three cover the\npaths that must keep grading. README documents the guard and adds the\noperational note that --cli runs need to be outside any sandbox that\ndenies the CLI its credentials.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "2c6aab0dbd973f9695028cc116abb62877da83af",
      "tree": "30652f538ce4923a898843cdf027a7c7454012f1",
      "parents": [
        "3d9b2c987da48eb6a1dad098a0d0c9c482ca7375"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 03:42:02 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 03:42:02 2026 +0200"
      },
      "message": "chore(version): stamp the dev version with the UTC time it lands (#1160)\n\nThe marketplace is served straight from `main`, so adopters install dev\nversions — the `.devN` suffix reaching a consumer is the normal case, not\nthe exception the docs assumed. And `claude plugin update` compares\nversion strings, not commit SHAs: with every manifest frozen at\n`0.2.0.dev0` since #907, an adopter\u0027s update answers \"already at the\nlatest version\" and never moves the pinned commit, however far behind\n`main` the installed copy has fallen. Recovery today means\n`claude plugin marketplace update` plus a full uninstall + reinstall of\nevery plugin, which nobody discovers unaided.\n\nMove the suffix to a timestamp: 0.2.0.dev0 -\u003e 0.2.0.dev202609080121.\nOrdinary `claude plugin update` then works. Minute resolution keeps two\nbumps on the same day distinct, and the stamp is UTC so it sorts in the\norder the bumps were made regardless of the contributor\u0027s timezone.\n\nBumping stays deliberate rather than per-PR — before pointing adopters\nat `claude plugin update`, before announcing a change they should take,\nor when merged work has piled up behind a stale stamp. Requiring it of\nevery PR would put every contributor in conflict with every other over\none line, for no gain on changes nobody is waiting for.\n\nMechanical apart from `project.version` and the two docs that stated the\nold convention: `tools/dev/check-family-plugins.py --fix` propagated the\nversion to the ecosystem manifests and the ten per-family plugin.json\nfiles plus their marketplace entries, and `uv lock` refreshed the\nworkspace pin.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "3d9b2c987da48eb6a1dad098a0d0c9c482ca7375",
      "tree": "897f814b0ed540b05db2a2e46bc871150bbf8de4",
      "parents": [
        "cf05bd470a95b6a2563121f38e7412034bcfdf5e"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 03:21:03 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 03:21:03 2026 +0200"
      },
      "message": "fix(docs): reconcile documentation with the state of the tree (#1159)\n\n* fix(docs): reconcile documentation with the state of the tree\n\nCloses a batch of doc-vs-reality inconsistencies, most of them filed as\nissues after an earlier audit pass.\n\nReported (#882-#888, #935):\n- labels-and-capabilities: the family:* section documented 4 of the 10\n  canonical families and conflated them with three repo-area labels that\n  are rejected in frontmatter; \"multi-capability tools are rare, gmail is\n  the only one\" sat above a table of ten. A stray blank line was also\n  splitting the tool-capability table in two.\n- adapters/registry: the mail-source and mail-archive rows omitted\n  adapters that declare those contracts. Fossil is NOT misclassified —\n  it is deliberately both a forge bridge and a VCS backend; the real bug\n  was the opposite, tools/vcs omitting Fossil from its backend table.\n- adapters/add-a-harness: detection table missing Kiro; stale harness\n  values for agent-guard, permission-audit, sandbox-lint, spec-loop.\n- setup/personal-use-unadopted-repo and AGENTS.md: the per-skill override\n  chain listed an organization layer that does not exist. Organizations\n  supply config-key defaults, not skill overrides.\n- setup/mixed-adoption-teams: the symlink recipe linked through the relay\n  dir, producing four hops instead of two.\n- RFC-AI-0002 and RFC-AI-0004: status said Draft while the body described\n  shipped, enforced behaviour. RFC-AI-0004 also said \"four shifts\" above a\n  four-item list in a document that says six everywhere else; the two\n  missing gap bullets are added rather than the numeral flipped.\n- spec-loop/organization-adapters: #935\u0027s bullet was already fixed by\n  #994; the rest of the Known gaps section was stale the same way.\n\nFound while verifying, not separately reported:\n- Skill counts: security 12-\u003e15 and the modes glance table (Triage 34-\u003e35,\n  Drafting 9-\u003e11) went stale in #1156. repo-health 6-\u003e7, the CONTRIBUTING\n  tree\u0027s four prefix counts, and one of three skill counts in\n  contributor-growth were already drifting.\n- spec-loop/adapters: Bitbucket write coverage gained PR-comment creation.\n- spec-loop/project-agnosticism: the non-ASF smoke suite is 15 cases over\n  6 steps across 5 skills, not 6 cases over 2; and the catalogue produces\n  3 asf-coupling warnings, not 0.\n\nGenerated-by: Claude Code (Opus 5)\n\n* fix(skill-and-tool-validator): skip dot-directories under skills/\n\nThe eval-coverage check globbed skills/.mypy_cache, .pytest_cache,\n.ruff_cache and .claude as if each were a skill, so a dirty working tree\nproduced four phantom \"no eval suite\" warnings. Those directories are\ngitignored, so they exist only locally — exactly when a bogus warning is\nhardest to act on, because it names a path the reader cannot find in the\nrepo.\n\nOne enumeration already had the guard; the other four did not. Adds it to\nall of them, with a regression test.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "cf05bd470a95b6a2563121f38e7412034bcfdf5e",
      "tree": "a1f1f2020cea0bfd1f4467a4180980fa28f48c98",
      "parents": [
        "183a16351309161f1733e07172397d69bb10ec18"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 02:54:40 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 02:54:40 2026 +0200"
      },
      "message": "feat(tracker-stats): project the current bucket to end-of-bucket totals (#1158)\n\nThe final bucket on every chart is cut short by \"now\", so regenerating\nthe dashboard mid-month reads as a collapse in reports rather than a\nmonth that is only a quarter over.\n\nProject it. Two kinds of series need two different extrapolations:\n\n- rate series (opened / rejected / reported in bucket) accumulate from\n  zero inside the bucket, so the whole count scales:\n  observed / elapsed_fraction.\n- level series (cumulative totals, the lifecycle bands, the untriaged\n  backlog) carry over from the previous bucket, so only the movement\n  inside the bucket scales: prev + (observed - prev) / elapsed. Scaling\n  the level itself would multiply years of accumulated history by four.\n\nProjections land on every chart that carries a projectable series: the\nlifecycle bands (each drawn at its projected position in the stack,\ncarrying the band\u0027s own projected count in the hover), the\nopened-vs-untriaged chart, the cumulative lines, and the rejections\nchart. Each is a dotted two-point segment from the last complete\nbucket, so a forecast cannot be mistaken for a measurement. Because the\nbands partition every tracker, their projections sum to the projected\ncumulative opened.\n\nThe mean-time charts are deliberately not projected: a mean over the\nitems seen so far is already an estimate of the bucket\u0027s mean, not a\npartial accumulation, so scaling it by elapsed time would be\nmeaningless.\n\n`projection.enabled` switches the stat off; `min_elapsed_fraction`\n(default 0.1) suppresses it early in a bucket, where a single report\nextrapolates to a dozen. A single-bucket axis is skipped as well — the\nlevel series have no baseline to project from.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "183a16351309161f1733e07172397d69bb10ec18",
      "tree": "03b08f83f4099d0ddf6aa960bda03ebebc2b2dd4",
      "parents": [
        "40b8ba3d2deeb94742ad4c27293056f3db2cca3c"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 02:14:09 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 02:14:09 2026 +0200"
      },
      "message": "feat(security): add security-model prepare / verify / update skills (#1156)\n\n* feat(security): add security-model prepare / verify / update skills\n\nThe security family routes inbound reports against a project\u0027s security\nmodel, but had no skills for producing or maintaining that model. Three\nskills close the loop:\n\n- security-model-prepare — draft a first model in draft-first mode and\n  land it, with its AGENTS.md -\u003e SECURITY.md discoverability chain, as\n  one reviewable PR per repository. Consent on the private list before\n  the first repository write.\n- security-model-verify — pre-flight an existing model per repository:\n  discoverability (the only hard gate) and completeness against the\n  minimum bar. Mechanical gaps get a PR, substantive ones a private\n  mail, never a public issue.\n- security-model-update — read the decision history back into the model.\n  Produces known-non-finding entries and a model-gap list, gated by a\n  blocking regression check: a proposal that would close a report the\n  project historically fixed does not ship, and narrowing is the only\n  permitted resolution.\n\nThe rubric for what a model contains is referenced by URL from\nalpha-omega-security/threat-model rather than vendored, so the framework\nholds no divergent copy of a specification it does not own.\n\nShips the model-PR helper (create-or-append AGENTS.md / SECURITY.md\nscaffold, pure merge core unit-tested), eval suites for all three\nskills, STRIDE family F in the security threat model, and the adopter\nconfig the skills read from \u003cproject-config\u003e/security-model.md.\n\nGenerated-by: Claude Code (Opus 5)\n\n* style(security-model-verify): apply ruff format to the model-PR helper\n\nThe hand-written argv list and a few long string expressions did not\nmatch ruff\u0027s formatting, which the workspace ruff-format check enforces\nacross every member.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "40b8ba3d2deeb94742ad4c27293056f3db2cca3c",
      "tree": "985d1ee433744534f18540ee7c4e19e735ec2d89",
      "parents": [
        "c5b2e09331434243f4bc4d8b174adc3035fd90fe"
      ],
      "author": {
        "name": "Piotr P. Karwasz",
        "email": "piotr@github.copernik.eu",
        "time": "Tue Sep 08 01:15:33 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 01:15:33 2026 +0200"
      },
      "message": "docs(rfcs): RFC-AI-0008 — the coordinator role (multi-hop relay of security reports) (#1135)\n\n* docs(rfcs): add RFC-AI-0008 — the coordinator role (draft)\n\nNames the coordinator persona of coordinated vulnerability\ndisclosure and defines its profile by function and termination\nauthority, never by chain position: a pairwise relay contract,\nprovenance fields carried across hops, the handling-identifier\nexchange, bidirectional forwarder adapters, and the\ncoordinator-mode skills generalized from the ASF Security\nteam\u0027s out-of-tree pipeline.\n\nAssisted-By: Claude Fable 5 \u003cnoreply@anthropic.com\u003e\nClaude-Session: https://claude.ai/code/session_018SKtoF3YBbXye9jmnZp1Pt\n\n* fix(docs): align contact_handle default with the ASF org config\n\nFallout from the RFC-AI-0008 review:\n\n- tools/forwarder-relay/README.md presented \u0027@raboof\u0027 as the\n  shipping contact_handle default, contradicting the newer\n  org-level security@apache.org declared in\n  organizations/ASF/organization.md (#622). The shared inbox is\n  now the documented default; an individual liaison\u0027s handle\n  stays as the local override example.\n- docs/security/forwarder-routing-policy.md worked example\n  updated to cite the same default.\n- CONTRIBUTING.md directory-tree comment no longer enumerates\n  RFC numbers (was stuck at 0002-0004), so it cannot go stale.\n\nAssisted-By: Claude Fable 5 \u003cnoreply@anthropic.com\u003e\nClaude-Session: https://claude.ai/code/session_018SKtoF3YBbXye9jmnZp1Pt\n\n* docs(rfcs): state RFC-AI-0008\u0027s reading of PRINCIPLES §15\n\nThe Security model section addressed §0 head-on but left §15 unmentioned,\neven though §15 is the principle the coordinator persona most directly\nreinterprets: a coordinator is by construction a node that holds many\nprojects\u0027 reports and correlates across parties.\n\nThe RFC already complied — \"Deduplication composes per hop\" bounds each\nhop\u0027s scope to its own inbox and archive, and forbids treating an\nupstream \"already deduplicated\" claim as a reason to skip the local\ncheck. But that compliance was implicit, so a reader checking the Draft\nagainst the principles could not tell whether it was designed or\nincidental. PRINCIPLES.md says the stricter reading wins where a\nprinciple admits more than one, which makes the intended reading worth\nstating rather than inferring.\n\nAdds a Security-model bullet naming §15 and separating its two clauses:\n\"other projects\u0027 vulnerabilities never appear at all\" is read as\npublic-surface-scoped (§15 governs what leaves the archive, not what it\nstores), while \"cross-project correlations stay on the channel they\narrived on\" is the binding clause that per-hop dedup scoping satisfies.\nAlso adds the matching References entry alongside the existing §0 row.\n\nNo normative change — this states the position the Proposal already\ntakes.\n\nGenerated-by: Claude Code (Opus 5)\n\n---------\n\nCo-authored-by: Jarek Potiuk \u003cpotiuk@apache.org\u003e"
    },
    {
      "commit": "c5b2e09331434243f4bc4d8b174adc3035fd90fe",
      "tree": "1d765af1c7e1706b2879682c9af007e8ecaba39f",
      "parents": [
        "d199acd708670d9b2f1d3bd906b03e16bcfcd60f"
      ],
      "author": {
        "name": "Alejandro Morgante",
        "email": "62363051+AlejandroMorgante@users.noreply.github.com",
        "time": "Mon Sep 07 20:10:11 2026 -0300"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 01:10:11 2026 +0200"
      },
      "message": "fix(pairing-self-review): require dependency evidence (#1152)\n\nWhy: self-review could report dependency incompatibilities without proving the effective constraint graph or applying the adopter\u0027s trusted release policy.\n\nGenerated-by: Codex (GPT-5)"
    },
    {
      "commit": "d199acd708670d9b2f1d3bd906b03e16bcfcd60f",
      "tree": "e76cf8af8ccf566defc13c273cfbf4a0f3175f3b",
      "parents": [
        "7dfb956e45c9900433994a7e7550f2b9062d33be"
      ],
      "author": {
        "name": "Amir Fathi",
        "email": "amirfathi.me@gmail.com",
        "time": "Mon Sep 07 17:09:50 2026 -0600"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 01:09:50 2026 +0200"
      },
      "message": "fix(sandbox-lint): normalise every trailing slash, not just one (#1149)\n\n_normalise() stripped a single trailing slash so a config path and its\nslash variant compare equal against FORBIDDEN_ALLOW_READ /\nFORBIDDEN_ALLOW_WRITE. A path with two or more trailing slashes (e.g.\n\"~/.ssh//\") only lost one, so it never matched the forbidden entry and\ncheck_invariants() reported no violation for a config that grants read\nor write access into a credential directory.\n\nStrip all trailing slashes instead, keeping the bare-root case (\"/\")\nintact. Added a double-slash case to the existing parametrized\nallowRead/allowWrite tests; both fail on main and pass with this\nchange.\n\nGenerated-by: Claude Code (Sonnet 5)"
    },
    {
      "commit": "7dfb956e45c9900433994a7e7550f2b9062d33be",
      "tree": "1c5fb902508ac0adf716ee23b85f3234b67dc889",
      "parents": [
        "61ac2f15e5ec90c62b5710323350a59ca4bf8553"
      ],
      "author": {
        "name": "Amir Fathi",
        "email": "amirfathi.me@gmail.com",
        "time": "Mon Sep 07 17:09:42 2026 -0600"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 01:09:42 2026 +0200"
      },
      "message": "fix(validator): tokenize gh-list-no-limit\u0027s flag check instead of substring match (#1147)\n\nvalidate_gh_list_limit tested for the literal substring \"--limit\" anywhere on\nthe logical line, so a trailing shell comment or a quoted argument that\nmerely mentions \"--limit\" silently defeated the check even when the command\nhad no such flag. Tokenize with shlex.split(comments\u003dTrue) and match an\nactual --limit / --limit\u003dN token instead.\n\nFixes #1146\n\nGenerated-by: Claude Code (Sonnet 5)"
    },
    {
      "commit": "61ac2f15e5ec90c62b5710323350a59ca4bf8553",
      "tree": "d9c689582e643611fa8c225c63169019ecaf2909",
      "parents": [
        "2b14842c13c16520b70ad00f8a237bf9fbb98907"
      ],
      "author": {
        "name": "Amir Fathi",
        "email": "amirfathi.me@gmail.com",
        "time": "Mon Sep 07 16:57:00 2026 -0600"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 00:57:00 2026 +0200"
      },
      "message": "fix(agent-guard): scope Segment.raw to its own tokens, not the whole command (#1151)\n\nsplit_segments() passed the entire compound command as `raw` for every\nsegment it produced, so guard_commit_trailer\u0027s Co-Authored-By scan could\nfire on a segment whose own text never mentioned it. raw is now\nshlex.join(current), a reconstruction of that segment\u0027s own tokens.\n\nFixes #1150\n\nGenerated-by: Claude Code (Sonnet 5)"
    },
    {
      "commit": "2b14842c13c16520b70ad00f8a237bf9fbb98907",
      "tree": "fa4d633e2a4cbadfc4d89f68483b6eacf1c040c5",
      "parents": [
        "e9679f9abb8ab4d4283701a7cce2cfceca3bb65c"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 00:56:06 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 00:56:06 2026 +0200"
      },
      "message": "ci(asf.yaml): stop blocking merge on unresolved review threads (#1155)\n\n`required_conversation_resolution: true` made *any* open review thread a\nhard merge gate. With the approval requirement currently lifted (see the\nnote above `protected_branches`), it was the one gate a reviewer could\ntrip by accident.\n\nThe failure mode is specific: a reviewer leaves a nit they explicitly\nmark non-blocking, the PR is then unmergeable until someone resolves it,\nand the reviewer ends up resolving their own advisory comment purely to\nunblock the merge. That destroys the signal the comment was meant to\ncarry — an unresolved thread should mean \"the author has not looked at\nthis yet\", not \"the merge button is stuck\".\n\nUnresolved threads stay visible in the PR UI; they are simply no longer\na hard gate. The `zizmor` / `prek` / `tests-ok` status checks and linear\nhistory are untouched, so nothing merges without green CI.\n\nRestore alongside `required_pull_request_reviews` if the project later\nwants threads to gate merge again.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "e9679f9abb8ab4d4283701a7cce2cfceca3bb65c",
      "tree": "b41145b11cacbd2778e8865d6a63851710a88b96",
      "parents": [
        "9c1fdd460feb5c21e0b31999b55551236415f381"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Tue Sep 08 00:41:01 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 00:41:01 2026 +0200"
      },
      "message": "docs(agents): document \u003cPROJECT\u003e vs \u003cproject\u003e, and catch spaced variants (#1154)\n\nTwo gaps that surfaced while reviewing #1144.\n\n`\u003cPROJECT\u003e` (display name, 84 uses) and `\u003cproject\u003e` (infrastructure\nslug, 313 uses) are both load-bearing across skills, tool docs, the ASF\norganization manifest, and PRINCIPLES.md — but neither appeared in the\nplaceholder table in AGENTS.md, which closes with \"do not invent new\nplaceholders\". Answering \"is `\u003cproject\u003e` an invented placeholder?\"\nrequired a repo-wide grep rather than a table lookup. Both now have a\nrow, plus a note stating the difference explicitly: they hold two\ndifferent values, not one value in two casings. Substituting one where\nthe other belongs yields something wrong rather than mis-cased —\n`Apache Foo.apache.org` is not a hostname.\n\nThe same case-only difference is a linter hazard. `FORBIDDEN_PATTERNS`\nis matched with `grep -F`, so the `name:\"airflow\"` entry added in #1144\nonly caught the no-space spelling; GraphQL, YAML and JSON all accept\n`name: \"airflow\"` equally, and that form passed every pattern in the\nlist. Rather than add a second fixed string per spacing, patterns whose\nsurrounding syntax admits optional whitespace move to a new\n`FORBIDDEN_REGEXES` array matched with `grep -E`; the two arrays share\none match-reporting path. Fixed strings stay the default — they are\neasier to read and cannot misfire on regex metacharacters.\n\nVerified: `check-placeholders.sh` is clean on the current tree, and\nflags both `name:\"airflow\"` and `name: \"airflow\"` when either is\nplanted in `skills/`. `prek run` passes on both changed files,\nincluding `lychee` and `doctoc`.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "9c1fdd460feb5c21e0b31999b55551236415f381",
      "tree": "674178b184adaceca77bb9de35bcc61df8da53a9",
      "parents": [
        "8c06d2152bddbbb5b577d14c2f1fb71445ac7c5c"
      ],
      "author": {
        "name": "Kavya Katal",
        "email": "KAVYAKATAL09@GMAIL.COM",
        "time": "Tue Sep 08 04:04:38 2026 +0530"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 00:34:38 2026 +0200"
      },
      "message": "feat(bitbucket): add guarded cloud PR comment create (#1142)"
    },
    {
      "commit": "8c06d2152bddbbb5b577d14c2f1fb71445ac7c5c",
      "tree": "139442b89bd2a7623b511f07bd8137c0c3ec02c8",
      "parents": [
        "9ac52f67bbf5837d460212775a7a2bfbe5530738"
      ],
      "author": {
        "name": "Arnav",
        "email": "a.purohit1903@gmail.com",
        "time": "Tue Sep 08 04:01:03 2026 +0530"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 00:31:03 2026 +0200"
      },
      "message": "fix(symlink-lint): enforce relay completeness and sync missing self-adoption symlinks (#1143)\n\nThe self-adoption relay directories drifted out of sync with skills/:\n- dependency-license-audit was missing from .agents/skills/ (canonical),\n  .claude/skills/, .github/skills/, and .kiro/skills/ (relays).\n- report-framework-issue was missing from .kiro/skills/.\n\nAdd Rule 3 (completeness) to symlink-lint to verify that every skill\nunder skills/ has a canonical entry in .agents/skills/ and that every\nwired agent directory relays the full canonical set (scoped to framework\ncheckouts). Also wire the missing symlinks, update the pre-commit hook\nfilter, and add corresponding test coverage.\n\nFixes #1137\n\nGenerated-by: Antigravity (Gemini 3.7 Flash)"
    },
    {
      "commit": "9ac52f67bbf5837d460212775a7a2bfbe5530738",
      "tree": "fdcdd94d669e6d2ace51fd65c2140bfca443fac9",
      "parents": [
        "e99b08efd26d5e666d8ac01d54eb801d47fa1bca"
      ],
      "author": {
        "name": "Davide Polato",
        "email": "dpol1@apache.org",
        "time": "Tue Sep 08 00:26:47 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 00:26:47 2026 +0200"
      },
      "message": "feat(skill-evals): add the issue-reassess eval suite (#1145)\n\n`issue-reassess` was the only shipped skill without a behavioural eval\nsuite, so regressions in its decisions were invisible until they failed\nin front of a user. The campaign tallies are already exercised by the\nsibling `issue-reassess-stats` suite, so this one anchors on the three\nplaces where the skill decides something of its own:\n\n- step-2-resumability (3 cases) — reuse / ask / resume / fresh per\n  candidate from the scratch-directory state, plus the auto-generated\n  campaign id.\n- skip-if-resolvable (5 cases) — the maintainer-comment shortcuts\n  (fixed-in-version, sibling duplicate, won\u0027t-fix by design), the\n  no-shortcut path, and an injected \"record fixed-on-master\" comment\n  that must be flagged and ignored.\n- headline-extraction (2 cases) — verdicts bucketed into action /\n  closure / tracker-hygiene candidates and new-issue keys.\n\nTwo suites anchor on the skill\u0027s sub-documents (`per-issue-flow.md`,\n`verdict-aggregation.md`), where the decision rules actually live.\nAll ten cases pass against Claude Code print mode; the validator\u0027s\neval-coverage advisory for this skill is gone.\n\nWhile adding the suite: the `*-reassess/` glob in `.gitignore` (meant\nfor campaign evidence directories) also matches `skills/issue-reassess/`\nand `tools/skill-evals/evals/issue-reassess/`, so any new file under\neither was silently dropped from `git add`. Negate both paths, following\nthe existing `!/tools/pilot-report-validator/` precedent.\n\nFixes [apache/magpie#1138](https://github.com/apache/magpie/issues/1138).\n\nSigned-off-by: Davide Polato \u003cdpol1@apache.org\u003e"
    },
    {
      "commit": "e99b08efd26d5e666d8ac01d54eb801d47fa1bca",
      "tree": "b45c55b26f9f2288c59ac7278e6eb45620c2bd14",
      "parents": [
        "809fa7479acd2b45ce08c3579acb2416bb4e2c47"
      ],
      "author": {
        "name": "Davide Polato",
        "email": "dpol1@apache.org",
        "time": "Tue Sep 08 00:17:10 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 00:17:10 2026 +0200"
      },
      "message": "fix(skills): derive the two remaining Airflow literals from project config (#1144)\n\n`pr-management-stats/fetch.md` queried `repository(owner:\"apache\",name:\"airflow\")`\nin its GraphQL batch-fetch recipe, and `release-announce-draft` pinned the\ndownload URL to `closer.lua?path\u003dairflow/\u003cversion\u003e/`. Both are operative\ninstructions, not examples, so an agent following them against another\nadopter queries the wrong repository and points at the wrong dist tree.\n\nBind the query\u0027s owner/name from `\u003cupstream\u003e` the way the other GraphQL\nrecipes already do, and render the dist path from `\u003cproject\u003e` as the rest\nof the announce skill does. Teach `check-placeholders.sh` the two lowercase\nforms (`name:\"airflow\"`, `path\u003dairflow`) so this class is caught at commit\ntime — before this change the linter passed on both lines.\n\nFixes [apache/magpie#1139](https://github.com/apache/magpie/issues/1139).\n\nSigned-off-by: Davide Polato \u003cdpol1@apache.org\u003e"
    },
    {
      "commit": "809fa7479acd2b45ce08c3579acb2416bb4e2c47",
      "tree": "226db3f00a08d3afb2d68046b1aba4b50c585b96",
      "parents": [
        "a1cff4441b93f8162aadb20a702b99437867d1db"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Tue Sep 08 00:10:08 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 00:10:08 2026 +0200"
      },
      "message": "chore(deps): bump the github-actions group with 3 updates (#1141)\n\nBumps the github-actions group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [actions/setup-java](https://github.com/actions/setup-java).\n\n\nUpdates `github/codeql-action/init` from 4.37.7 to 4.37.8\n- [Release notes](https://github.com/github/codeql-action/releases)\n- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)\n- [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28)\n\nUpdates `github/codeql-action/analyze` from 4.37.7 to 4.37.8\n- [Release notes](https://github.com/github/codeql-action/releases)\n- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)\n- [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28)\n\nUpdates `actions/setup-java` from 5.7.0 to 6.0.0\n- [Release notes](https://github.com/actions/setup-java/releases)\n- [Commits](https://github.com/actions/setup-java/compare/b6effb05e454b25005698d916606bdc6ffcbf961...dd06d9cba3e5552c54d9f8ea23572deb30010f7c)\n\n---\nupdated-dependencies:\n- dependency-name: github/codeql-action/init\n  dependency-version: 4.37.8\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n  dependency-group: github-actions\n- dependency-name: github/codeql-action/analyze\n  dependency-version: 4.37.8\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n  dependency-group: github-actions\n- dependency-name: actions/setup-java\n  dependency-version: 6.0.0\n  dependency-type: direct:production\n  update-type: version-update:semver-major\n  dependency-group: github-actions\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "a1cff4441b93f8162aadb20a702b99437867d1db",
      "tree": "a5a6bbf0041235aba4606151af498242f49f96b9",
      "parents": [
        "33bd2418384d540fc9bc414cd0cfa5582e68d9c7"
      ],
      "author": {
        "name": "Amir Fathi",
        "email": "amirfathi.me@gmail.com",
        "time": "Tue Sep 01 09:29:41 2026 -0600"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 01 20:59:41 2026 +0530"
      },
      "message": "docs(skill-evals): add --cli to workflow-security-audit and license-compliance-audit READMEs (#1134)\n\nBoth READMEs document run commands without --cli, so following them\nverbatim drives the runner in print mode: prose and structured cases\nreport MANUAL instead of PASS/FAIL. Both suites use exact-match\nexpected.json (enums/booleans), the same shape onboarding-concierge\ndocuments as gradable via --cli, so this is a doc gap, not intended\nbehavior.\n\nGenerated-by: Claude Code (Sonnet 5)\n\nSigned-off-by: Amir Fathi \u003camirfathi.me@gmail.com\u003e"
    },
    {
      "commit": "33bd2418384d540fc9bc414cd0cfa5582e68d9c7",
      "tree": "4d3e92b542cacfda2de139dea27b61a7bef3352f",
      "parents": [
        "a76147558fbc0a8aafe1ab3f213aeb0709e87817"
      ],
      "author": {
        "name": "Amir Fathi",
        "email": "amirfathi.me@gmail.com",
        "time": "Tue Sep 01 09:29:13 2026 -0600"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 01 20:59:13 2026 +0530"
      },
      "message": "fix(docs): collapse three org-level security placeholder names into one (#1133)\n\nThe org-level advisory-admin security address had three names in the\ntree: \u003csecurity-team-list\u003e in github-advisory.md, \u003casf-security-list\u003e\n(twice) in AGENTS.md, and the bare config path\nsecurity_inbox.foundation_security_address in the placeholder table,\nwhich had no row for it at all. AGENTS.md also baked an ASF-specific\nname into a framework whose placeholder mechanism exists precisely to\nkeep adopting projects out of that vocabulary.\n\nRegister \u003cfoundation-security-list\u003e, sourced from\nsecurity_inbox.foundation_security_address, and use it at all three\ncall sites. The distinct project-level \u003csecurity-list\u003e is untouched,\nsince the two addresses are deliberately different in\ngithub-advisory.md\u0027s own delivery instructions.\n\nFixes #1057\n\nGenerated-by: Claude Code (Sonnet 5)"
    },
    {
      "commit": "a76147558fbc0a8aafe1ab3f213aeb0709e87817",
      "tree": "911784d9feba8b02f5a543cdecd3e0fda4f03677",
      "parents": [
        "d751ffae7be124bc7d1ae6d81b546a1310757262"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sun Aug 30 02:26:40 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 19:26:40 2026 -0500"
      },
      "message": "fix(_template): drop Apache Airflow residue from the templates (#1132)\n\nThe `_template` scaffolds are meant to be project-agnostic — an adopter\ncopies them and fills in placeholders. Four spots still carried the\nframework\u0027s original adopter\u0027s specifics as if they were generic:\n\n- `reviewer-roster.md`: the \"shape per entry\" sample used\n  `airflow/jobs/` as its path-prefix example, unmarked, and the Notes\n  section repeated it. Now `src/scheduler/`, which reads as a shape\n  rather than as one project\u0027s tree.\n- `security-intake-config.md`: the ASF defaults were described as\n  reproducing \"the Apache Airflow security-team workflow\". They\n  reproduce the standard ASF one; naming a single project made the\n  sentence look like a calibration an adopter had to match.\n- `pr-management-quick-merge-config.md`: the `tier_b_allow_globs`\n  default shipped `**/example_dags/**`, which is meaningful only in an\n  Airflow tree. Replaced with `**/examples/**` + `**/example_*/**` —\n  generic, and a superset of the old glob, so an Airflow adopter\u0027s\n  example-DAG PRs stay in tier B. The prose above the globs no longer\n  describes the default set as Airflow-shaped.\n- `candidate-rules.md`: the skill\u0027s built-in default list carries the\n  same globs, updated in lock-step so template and skill do not drift.\n\nDeliberately left alone: every spot that already marks itself as an\nexample — the `release-management-config.md` \"filled example\" section,\nthe `mentoring-welcome-config.md` filled-in block, and the\n`Example: apache/airflow-...` cells. Those are calibration aids, not\nresidue.\n\nGenerated-by: Claude Code (Claude Opus 5)"
    },
    {
      "commit": "d751ffae7be124bc7d1ae6d81b546a1310757262",
      "tree": "4cb461231d86b632418764cf97e803345351a01f",
      "parents": [
        "67ef6ae4cb5c87e6089c368331285e6804276063"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sun Aug 30 01:16:41 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 18:16:41 2026 -0500"
      },
      "message": "fix(security-tracker-stats): recognise the framework\u0027s own comment marker (#1130)\n\nThe default `bot_prefixes` did not list `\u003c!-- apache-magpie: `, the\ncanonical machine marker every framework skill writes on its\nstatus-rollup, hand-off, and import comments. Those comments were\ntherefore counted as *human triage activity*, pulling the\ntime-to-triage median toward zero on any tracker the skills touch — and\nthe more the skills are used, the more wrong the metric gets.\n\nAt the same time the default listed `\u003c!-- airflow-s status rollup v`, a\nmarker specific to one adopter\u0027s tracker repo and meaningless to every\nother project. That is the template-genericity residue the upgrade\nflow\u0027s Step 6d audit exists to catch; #1123 removed the last\napache-steward references but this one survived.\n\nSwaps the leaked adopter marker for the framework\u0027s own.\n\nAlso corrects the README\u0027s merge documentation. It said the `milestones`\nand `categories` lists are replaced entirely, implying other lists\nmerge. `deep_merge` replaces *every* list — its own docstring says\n\"Lists are REPLACED, not concatenated\" — so an overlay adding a single\n`bot_prefixes` entry silently drops all the defaults. That is precisely\nthe trap that makes this bug hard to notice downstream: an adopter fixes\nthe marker locally and unknowingly discards the rest of the list.\n\nVerified: the config still parses under the hand-rolled\n`_minimal_yaml_load` subset parser with the added comments,\n`is_bot_body` now classifies a real `\u003c!-- apache-magpie: status-rollup\nv3 --\u003e` comment as bot while leaving a human triage comment alone, and\nthe tool\u0027s 114 tests pass.\n\nGenerated-by: Claude Code (Claude Opus 5)"
    },
    {
      "commit": "67ef6ae4cb5c87e6089c368331285e6804276063",
      "tree": "eb13de3b3343c09b7f49e2f55819e7f3c71198e0",
      "parents": [
        "5ac2c33625d80388034d5252bb7ad3a48eaad841"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sun Aug 30 01:16:29 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Aug 30 01:16:29 2026 +0200"
      },
      "message": "fix(security-issue-sync): omit the finder credit, never fall back to anonymous (#1131)\n\nReconciles #1124 with #1127, as promised in #1127\u0027s description.\n\n#1124 made `anonymous` the fallback for a scanner-sourced finding whose\nscanner declares no public credit name. #1127 then landed the\nfinder-credit policy, whose Rule 2 says the opposite: where there is no\nnamed finder, omit the `finder` credit rather than writing a\nplaceholder. Both are on `main`, so the framework currently contradicts\nitself in three places.\n\nRule 2 wins. It is grounded in an ASF CVE reviewer\u0027s feedback on a\npublished batch — \"I\u0027m not sure how helpful it is to credit \u0027anonymous\u0027\nas finder, just leave it out\" — and a `credits[]` carrying only the\nremediation developer is a complete record, whereas a literal\n`anonymous` occupies the space a reader scans for attribution and\nanswers nothing.\n\nNothing else about #1124 changes: the scanner\u0027s public credit name is\nstill written when declared, still emitted with `type: \"tool\"`, and the\nconfidential product token still never reaches a public surface. Only\nthe no-name-declared branch changes, from writing a placeholder to\nomitting the row.\n\nGrep confirms no `anonymous` fallback remains under skills/, projects/,\nor tools/.\n\nGenerated-by: Claude Code (Claude Opus 5)"
    },
    {
      "commit": "5ac2c33625d80388034d5252bb7ad3a48eaad841",
      "tree": "296a6ce0c0a95162eecd6e2c3a02c397bca37492",
      "parents": [
        "e3cc28c723aed3f7fb8734d7c6c21c84dca29ca0"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 21:33:10 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 21:33:10 2026 +0200"
      },
      "message": "fix(security-cve-allocate): strip ID parentheticals anywhere in the title (#1126)\n\nThe Step 2 title-strip cascade anchored every identifier parenthetical\nto end-of-string with `$`. A title carrying one mid-string kept it, and\nthe cascade reported no change — so the parenthetical shipped verbatim\ninto `containers.cna.title` on the published CVE record.\n\n    Path traversal (incomplete fix for CVE-2026-12345) in the parser\n\nNothing in the rationale for stripping these depends on position. A\nprior-CVE reference belongs in the public summary\u0027s cross-CVE clause, a\nGHSA/ZDRES/HUNTR/GHSL ID belongs in the credits or the issue body, and a\n`split from #N` marker belongs in the tracker — none of them belong in\nthe title, wherever they sit in the string.\n\nSplits the cascade into `patterns_anywhere` (the four identifier-bearing\nparentheticals, unanchored) and `patterns_trailing` (the genuinely\nposition-dependent ones). The existing whitespace collapse at the end of\nthe cascade closes the gap a mid-string removal leaves behind.\n\nOver-stripping risk stays low because each unanchored pattern requires a\nwell-formed tracker ID inside the parentheses. The reporter\n`(\u003cname\u003e follow-up)` paren stays end-anchored on purpose: its name part\nis loose enough that stripping it mid-title could eat a real clause.\n\nUpdates the `title-normalization.md` template in lock-step, which was\nalso missing the prior-CVE pattern entirely, and corrects the\n`security-issue-sync` row that described these as trailing-only.\n\nGenerated-by: Claude Code (Claude Opus 5)"
    },
    {
      "commit": "e3cc28c723aed3f7fb8734d7c6c21c84dca29ca0",
      "tree": "5af35430eb576f4bfe0b5f0c842d732e23211f33",
      "parents": [
        "61db7bd8aae831016db86817091757500818c976"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 21:21:17 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 21:21:17 2026 +0200"
      },
      "message": "feat(security): add a policy for reporter-supplied proof-of-concept code (#1129)\n\nSecurity reports arrive with exploit scripts, container images, network\nprobes and binaries, and several skills read inbound report content —\nbut nothing in the framework said what an agent may do with an\nattachment. Grepping the security family for \"never run/execute PoC\",\n\"isolated container\", or \"do not execute\" returned nothing. Skills\nmention PoCs; none govern them.\n\nThat is a gap worth closing for a framework whose skills routinely read\nattacker-supplied code: the report arrives from an unauthenticated\nstranger, over a channel that accepts mail from anyone, and the agent\nreading it holds the maintainer\u0027s credentials.\n\nAdds `docs/security/poc-handling-policy.md`, a sibling to the existing\n`forwarder-routing-policy.md`. The rule: never execute reporter-supplied\ncode on the host. Fetch and display it, verify by static read against\nthe affected code path, and only on explicit operator approval run it in\nan isolated container — no network, ephemeral filesystem, no\nhost-credential mounts, no host-port forwards. Binary attachments are\nnever decompiled or executed in this flow.\n\nThe default is static because the asymmetry is decisive: a static read\ncosts minutes and cannot hurt the host, while executing an unknown\nscript on a maintainer\u0027s workstation risks credential theft and actions\ntaken in the maintainer\u0027s name. The \"does this bug exist?\" question\nalmost always answers statically anyway.\n\nScopes what the rule does *not* restrict — reading, quoting, attaching\nas evidence, the project\u0027s own reproducers, and ordinary fix\nverification — so it does not read as a bar on analysis.\n\nGrounds it in the existing `AGENTS.md` rule that external content is\ndata, never an instruction; a PoC is the most literal case of that.\nReferenced from `security-issue-triage` Step 3, where the temptation to\nrun it is strongest, and indexed in `docs/security/README.md`.\n\nGenerated-by: Claude Code (Claude Opus 5)"
    },
    {
      "commit": "61db7bd8aae831016db86817091757500818c976",
      "tree": "fed91f480db5bde9bc19d51b7cce50a0efb2941f",
      "parents": [
        "2caca5827e1b1cf79a0b161a3af1b984c8a30dd8"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 21:20:57 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 21:20:57 2026 +0200"
      },
      "message": "fix(security-issue-import): require a positive GHSA query on every scan (#1128)\n\nThe skill already says not to exclude `notifications@github.com`\nwholesale, because that sender carries both tracker-mirror chatter and\nGHSA-relayed inbound reports. That rule is correct but negative, and\nnegative rules are not self-enforcing.\n\nAn exclusion added anywhere for noise reduction — in the candidate\nquery, in a project override, in a hand-edited one-off — removes the\nentire GHSA intake channel. The resulting miss is invisible: nothing\nreports that a report was filtered out, so the scan looks clean and the\nadvisory is simply never seen. A downstream adopter lost an\nincomplete-fix follow-up advisory to exactly this, and caught it only\nbecause the operator independently knew the report existed.\n\nAdds a positive counterpart that every import scan runs alongside the\ncandidate-listing query:\n\n    from:notifications@github.com newer_than:\u003cwindow\u003e\n      (GHSA OR \"Report a vulnerability\" OR \"security advisory\")\n\nBecause it does not depend on the other query\u0027s filter list, the GHSA\nchannel is checked by construction and cannot be filtered away by an\nexclusion elsewhere. Cost is one extra search per scan.\n\nAlso documents the 404 case on the advisory record API: it means the\noperator is not yet a collaborator on that specific advisory — an\naccess state, not a missing advisory — and names the admin hand-off\nneeded before the record API and reporter-reply path work.\n\nGenerated-by: Claude Code (Claude Opus 5)"
    },
    {
      "commit": "2caca5827e1b1cf79a0b161a3af1b984c8a30dd8",
      "tree": "2e28734620a551e22b2d0a55547cae98ea1cb3fd",
      "parents": [
        "31176a4992cde9a4acaa8d65753ef5655ebdb381"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 21:14:02 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 21:14:02 2026 +0200"
      },
      "message": "feat(pr-management-code-review): always report the branch\u0027s conflict state (#1125)\n\nThe skill read `mergeable` but never `mergeStateStatus`, and GitHub\ncomputes `mergeable` lazily — a PR not tested against its base since\nthe last push answers `UNKNOWN`, and keeps answering `UNKNOWN` unless\nsomething asks again. In practice every PR in a review session came\nback `UNKNOWN`, so the headline\u0027s \"mergeable state\" carried no\ninformation and conflicts never reached the author.\n\nThe failure that produces is an approving review on a branch that\ncannot merge. The author reads the approval as \"done\" and finds out\nweeks later, at the merge button, when the reviewer has long moved on.\n\nConflicts stay out of the disposition rules deliberately: a correct PR\nthat merely trails its base should not be gated behind a mechanical\nrebase. Stating it is enough.\n\nGenerated-by: Claude Code (Claude Opus 5)"
    },
    {
      "commit": "31176a4992cde9a4acaa8d65753ef5655ebdb381",
      "tree": "2e28734620a551e22b2d0a55547cae98ea1cb3fd",
      "parents": [
        "40567d6811814ddea16e1c4ffc0788ac9ca371d5"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 21:13:28 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 21:13:28 2026 +0200"
      },
      "message": "feat(security): add a finder-credit policy — timing gate and no placeholders (#1127)\n\nThe framework had no answer to \"does this report earn a `finder` credit\nat all?\". `bot-credits-policy.md` decides what *category* a credited\nparty belongs to, but nothing decided whether the credit is earned, and\nnothing said how to represent the absence of a finder.\n\nAdds `tools/cve-tool-vulnogram/finder-credit-policy.md` as its sibling,\ncarrying two rules.\n\nRule 1 — timing. A report earns no finder credit when a public fix PR\nfor the same issue was already *opened* before the report arrived. The\nbar is opened, not merged: an open PR\u0027s diff, title, and touched code\npath are readable by anyone, so a later report is not telling the\nproject something it did not already know. Two authoritative timestamps\ndecide it (`gh pr view --json createdAt` and the inbound `Date:`\nheader), never the reporter\u0027s prose. Four exceptions: the original\nfinder, a genuinely different prong, a credit already offered in\nwriting, and a credit already published.\n\nThe written-offer exception is the load-bearing one. Withdrawing a\ncredit a reporter has read costs the project far more than one credit\nline, and the reporter did nothing to deserve it — so the gate is\napplied at intake, before any offer is made, and an offer made in error\nis the project\u0027s to absorb.\n\nRule 2 — no placeholders. Where there is no named finder, omit the\n`finder` credit rather than writing `anonymous`. An empty field makes\nthe generator emit no `finder` entry, which is the correct record: a\n`credits[]` carrying only the remediation developer is normal and\ncomplete. A literal `anonymous` occupies the space where a reader looks\nfor attribution and answers nothing. Rule 2 governs only the `finder`\nrow — a tool that made the discovery is still credited `type: \"tool\"`.\n\nWires the policy into the two skills that actually write the field\n(`security-issue-import` at intake, `security-issue-sync` at\nreconciliation) and cross-references it from `bot-credits-policy.md`.\n`security-cve-allocate` and `security-issue-deduplicate` are named in\nthe policy\u0027s own applies-to list but not edited here — neither writes\n`credits[]` directly.\n\nGenerated-by: Claude Code (Claude Opus 5)"
    },
    {
      "commit": "40567d6811814ddea16e1c4ffc0788ac9ca371d5",
      "tree": "39cc5711ed4a1326f0f4c1ef0923fc93cc1d0a6b",
      "parents": [
        "80034294a13db2ebf83640329bd54134a64bbd0f"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 20:40:29 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 20:40:29 2026 +0200"
      },
      "message": "fix(security-issue-sync): credit scanner findings as tool, not anonymous (#1124)\n\nThe anonymise gate contradicted the framework\u0027s own bot-credits policy.\n`tools/cve-tool-vulnogram/bot-credits-policy.md` states that automation\nwhich surfaces a real vulnerability is credited with `type: \"tool\"` —\n\"Scanners, AI agents, and automation that surface a real vulnerability\ndeserve the credit\". But the gate in `security-issue-sync` replaced the\ncredit with `anonymous`, discarding the discovery attribution entirely.\n\nThe gate conflated two separate concerns. Suppressing an individual who\nnever consented to public credit is correct. Withholding the vendor\u0027s\ncommercial product name is correct. Neither requires erasing the fact\nthat a tool made the discovery.\n\nSeparates them: the scrub now writes the scanner\u0027s declared *public\ncredit name* — emitted with `type: \"tool\"` per the policy — and keeps\n`anonymous` only as the fallback when no public credit name is declared.\nConfidentiality is unchanged: the product token still never reaches a\npublic surface, and the audit-trail surfaces are still untouched.\n\nAdds a `Public credit name` column to the `scanner-products.md` template\nso an adopter declares the public-safe name of the tooling that ran the\nscan, distinct from the confidential product token. Existing adopters are\nunaffected until they fill it in — a blank column keeps today\u0027s\n`anonymous` behaviour.\n\nAlso corrects the template\u0027s claim that *Reporter credited as* is always\nemitted as `type: finder`; per the policy it is `finder` for a human and\n`tool` for automation.\n\nGenerated-by: Claude Code (Claude Opus 5)"
    },
    {
      "commit": "80034294a13db2ebf83640329bd54134a64bbd0f",
      "tree": "f330e6586f3020eb21877cad71de71ccee50783a",
      "parents": [
        "9fa7c2f7b65c1f0cb284764716975f8f7e1a6582"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 20:28:21 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 20:28:21 2026 +0200"
      },
      "message": "chore: remove the last apache-steward references (#1123)\n\n* feat(pr-management-code-review): always report the branch\u0027s conflict state\n\nThe skill read `mergeable` but never `mergeStateStatus`, and GitHub\ncomputes `mergeable` lazily — a PR not tested against its base since\nthe last push answers `UNKNOWN`, and keeps answering `UNKNOWN` unless\nsomething asks again. In practice every PR in a review session came\nback `UNKNOWN`, so the headline\u0027s \"mergeable state\" carried no\ninformation and conflicts never reached the author.\n\nThe failure that produces is an approving review on a branch that\ncannot merge. The author reads the approval as \"done\" and finds out\nweeks later, at the merge button, when the reviewer has long moved on.\n\nConflicts stay out of the disposition rules deliberately: a correct PR\nthat merely trails its base should not be gated behind a mechanical\nrebase. Stating it is enough.\n\nGenerated-by: Claude Code (Claude Opus 5)\n\n* chore: remove the last apache-steward references\n\nThe framework was renamed from apache-steward to Apache Magpie some time\nago, but a few references outlived the rename.\n\nDrops \"Step 0a — Migrate `apache-steward`-era naming\" from the upgrade\nskill and the matching \"Migrating a pre-Magpie adopter\" section from the\ninstall recipes. The two disagreed with each other: the upgrade skill\nclaimed every run performs the migration automatically, while the install\nrecipes said the framework no longer ships an automated migration. Neither\nstatement survives, so the contradiction goes with them.\n\nAlso updates two stale `setup-steward` skill references in the\nsource-control tool contract to `magpie-setup`, and the framework\u0027s name in\nthe AI-authorship-disclosure eval fixture.\n\n`apache/airflow-steward` stays in the README acknowledgements: that is\nproject provenance tied to the NOTICE attribution, not a stale label."
    },
    {
      "commit": "9fa7c2f7b65c1f0cb284764716975f8f7e1a6582",
      "tree": "783e7e744a26fa591514702c98a7e227b0aaf58e",
      "parents": [
        "7322af49a3acf6df9796533b4cffaa39f07c8a2e"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 19:50:02 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 19:50:02 2026 +0200"
      },
      "message": "fix(agent-isolation): don\u0027t replace the caller\u0027s shell when sourced (#1121)\n\n`agent_iso_run` ended with an unconditional `exec env -i … \"$agent_bin\"`.\nThat is right for `bash agent-iso.sh …`, where the process exists only to\nbecome the agent. It is wrong on the sourced path — the documented rc-file\nsetup, where `claude-iso` / `agent-iso` are shell functions — because there\nthe current process is the user\u0027s own interactive shell. `exec` replaced\nit: quitting the agent closed the terminal, and a launch that failed (lost\nexec bit, bad interpreter, binary swapped mid-upgrade) took the shell down\nwith it.\n\nThe sourced/direct branch now records which mode the script loaded in, and\n`agent_iso_run` uses it to pick the launch: sourced runs the agent as a\nchild and returns its exit status, direct exec still `exec`s. The `exit 1`\ncalls were already confined to the direct-exec branch and the sourced entry\npoints already used `return`, so `exec` was the only parent-killing path.\n\nThe other scripts under tools/agent-isolation/ are hooks and shims that are\nalways executed, never sourced, so none of them needed the same treatment.\n\nAdds TestSourcedDoesNotReplaceTheShell covering shell survival, exit-status\npropagation, survival of a failed launch, the named `claude-iso` launcher,\nand a guard that direct exec still `exec`s. The first four fail against the\nprevious script."
    },
    {
      "commit": "7322af49a3acf6df9796533b4cffaa39f07c8a2e",
      "tree": "c05b88deca48d7310465772b07ce5e981dc9adc7",
      "parents": [
        "9577ccadfa5d95f64ab266d1b5d83b75f16270d0"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 19:40:44 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 19:40:44 2026 +0200"
      },
      "message": "fix(sandbox): drop deny rules that file permission checks never match (#1122)\n\n`Write(.claude/settings.json)` and `Write(.claude/settings.local.json)`\nlooked like they guarded those two files, but file permission checks only\nmatch `Edit(path)` rules — a `Write(...)` deny rule is inert. Both paths are\nalready covered by the `Edit(...)` deny rules immediately above, which apply\nto every file-editing tool, so removing the dead pair loses no protection\nand stops the settings block from implying a guard that was not there.\n\nMirrors the removal into the sandbox-lint baseline, as every change to the\nlive sandbox must be (docs/security/threat-model.md mitigation M.29)."
    },
    {
      "commit": "9577ccadfa5d95f64ab266d1b5d83b75f16270d0",
      "tree": "42abc31f1a214d42fedbcaab21a66e211cc1b03c",
      "parents": [
        "d5c56a4bf6fb9944e60094c375c5a4dc304975b6"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 17:42:01 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 17:42:01 2026 +0200"
      },
      "message": "fix(pr-management-triage): filter action_required runs server-side (#1088)\n\nThe repo-wide pending-approval index is documented as an unfiltered\n`event\u003dpull_request` listing, post-filtered on `conclusion` and capped\nat the first three pages, on the stated grounds that\n`?status\u003daction_required` \"matches no runs in this state\". That premise\nis wrong — the parameter does match, and SKILL.md Golden rule 1b\nalready depends on it, so the two files contradict each other.\n\nThe consequence is a silent under-report rather than an error. The\nlisting is newest-first across all states, so on a busy repository\nthree pages reach back only a few hours while the approval backlog is\nunbounded. A sweep on a large adopter repo saw 9 of 322 pending head\nSHAs; every PR in the gap whose rollup was green from bot checks alone\nclassified as passing and became a mark-ready candidate, which is the\nexact false-positive the index exists to prevent.\n\nThe per-PR lookups in actions.md are scoped to a single head SHA and\nwere never affected by the truncation, so their code is left alone and\nonly the inaccurate rationale is corrected.\n\nGenerated-by: Claude Code (Opus 5)"
    },
    {
      "commit": "d5c56a4bf6fb9944e60094c375c5a4dc304975b6",
      "tree": "b97fe23592d165bd8190e7feb56699096ca1245f",
      "parents": [
        "a357630fc997383a74560428830951ad48ee4ed9"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 17:40:18 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 17:40:18 2026 +0200"
      },
      "message": "fix(agent-isolation): make the gitignore safety check hook-safe (#1093)\n\nsandbox-add-project-root.sh refused to write settings.local.json on\nevery invocation from a git hook, reporting \"is not gitignored\" for a\nfile that was correctly ignored. Because the post-checkout hook is\nexactly how the helper reaches a fresh worktree, the per-worktree\nsandbox-allowlist entry that issue #197 asks for was never written\nthere -- reads under a new worktree kept hitting sandbox denials.\n\nThe check ran `cd \"$(dirname \"$file\")\" \u0026\u0026 git check-ignore \"$file\"`.\nGit hooks export GIT_DIR, and with GIT_DIR set but no GIT_WORK_TREE\ngit treats the current directory as the work-tree root -- so after the\ncd into .claude/, the root-anchored pattern\n`/.claude/settings.local.json` was evaluated against .claude/ as the\nroot, where it cannot match.\n\nDiscovery now runs with GIT_DIR, GIT_WORK_TREE and GIT_INDEX_FILE\nstripped, and check-ignore runs from the resolved toplevel instead of\nfrom a subdirectory.\n\nFixing that surfaced a second, older bug in the same block: the\nfallback guard tested `[ -d \"$(dirname \"$file\")/..\" ]`, which is false\nwhen .claude/ does not exist yet. On a first run the check therefore\nmatched neither branch and fell through to writing, so the safety\ncheck protected nothing in precisely the case it was written for. The\nreplacement walks up to the nearest existing ancestor before asking\ngit anything, so it holds whether or not .claude/ is present.\n\nFive regression tests cover both bugs; all five fail against the\nprevious script. They neutralise global and system git config,\nincluding an explicit core.excludesFile override -- pointing\nGIT_CONFIG_GLOBAL at an empty file is not enough, because git still\nfalls back to ~/.config/git/ignore, which commonly already ignores\nsettings.local.json and would mask the \"refuses\" cases."
    },
    {
      "commit": "a357630fc997383a74560428830951ad48ee4ed9",
      "tree": "d7d20a93c90597bd9f4006d75d99fea627ed9336",
      "parents": [
        "f297a4c68a64075d0c17a2570816a53243665935"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 17:38:16 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 17:38:16 2026 +0200"
      },
      "message": "fix(pr-management-triage): one violation bullet per category, not per check (#1119)\n\nThe rendering contract told the skill to write an \u003cexplanation\u003e\nclause per bullet and offered \"Failing: mypy-core, mypy-providers\"\nas the model, so a PR failing a dozen jobs in one category got a\nbullet naming each of them. GitHub\u0027s Checks tab already lists the\nfailing jobs, so the enumeration adds length without adding signal\n— and a triage comment that runs long is one contributors stop\nreading, which costs the skill the very thing it is written to buy.\n\nThe optional short payload keeps the cases where a number is the\nwhole point (unresolved thread count, commits behind) and cannot\nbe read off the PR at a glance.\n\nGenerated-by: Claude Code (Claude Opus 5)"
    },
    {
      "commit": "f297a4c68a64075d0c17a2570816a53243665935",
      "tree": "8451716ead2d42956d706eef28608f02ac145358",
      "parents": [
        "453dd9f20bdebe9d4458d84682bd707be1414f80"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Sat Aug 29 17:37:54 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Aug 29 17:37:54 2026 +0200"
      },
      "message": "infra(.asf.yaml): temporarily lift the approval requirement on main (#1120)\n\nComment out `required_pull_request_reviews` in the `main`\nprotected-branch config while the marketplace-installation replacement\nand the reworked permission handling are brought up. Both land as many\nsmall, fast iterations that need to be tested against a real merged\n`main`, and a per-PR approval round-trip stalls that loop.\n\nEvery other gate on `main` stays on: the `zizmor` / `prek` / `tests-ok`\nrequired status checks, conversation resolution, and linear history —\nso nothing merges unreviewed by CI.\n\nThe block is commented out rather than deleted so restoring it is a\npure uncomment once that work has landed.\n\nGenerated-by: Claude Code (Opus 5)"
    }
  ],
  "next": "453dd9f20bdebe9d4458d84682bd707be1414f80"
}
