Skip to content

release(#1863): v5.10.0 - #1864

Merged
me2resh merged 847 commits into
mainfrom
release/v5.10.0
Oct 22, 2026
Merged

me2resh merged 847 commits into
mainfrom
release/v5.10.0

Conversation

@me2resh

@me2resh me2resh commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Summary

  • Releases v5.10.0. See the CHANGELOG section below for the full list of changes in this release.
  • CHANGELOG.md updated. A new section is at the top, with grouped feat/fix/chore entries and PR refs.
  • Auto-tag on merge. .github/workflows/auto-tag-on-release-pr-merge.yml tags the squash commit on main and creates a GitHub Release when this PR merges (AgDR-0076).
  • Release provenance recorded. A Released-From trailer records the exact dev SHA this release was cut from. The next release's changelog range is then exact, not inferred (AgDR-0094, [Bug] release-changelog.sh sync-boundary under-counts when the prior release-sync merged late #872).
  • Changelog range anchored manually. The v5.9.0 squash commit has its Released-From: 454515c0 line in the middle of the message, so git does not read it as a trailer. The default fallback range (9d655080..dev, the v5.9.0 sync merge) leaves out 3 commits that are in neither v5.9.0 nor that range: fix(#1724): accept Windows path forms in POSIX gates #1740, fix(#1741): make fork CI pass on upstream sync PRs #1746 and docs commit 3630f7c. This changelog is built from the real v5.9.0 cut point, 454515c0..upstream/dev. To get that range, bin/release-changelog.sh was run with PREV_TAG set to the v5.9.0 release-branch head (3e11370), whose final paragraph has a readable Released-From: 454515c0 trailer.
  • Main nightly loses macOS. tests.yml on dev runs Linux only (ci(#1814): remove macOS runners and cap CI run volume #1817). After this merge, the nightly tests schedule on main stops running the 4 macOS shards (Nightly CI failed #1828).

Count check: 69 raw commits in 454515c0..upstream/dev, 65 entries, gap 4 (tolerance 5). The 4 commits not listed are the v5.9.0 release squash and the 3 v5.9.0 sync commits.

Removed lines: the release removes 1,913 lines from main across the changed files. The CEO accepted this listing. The release commit adds no new removals.

CHANGELOG

[v5.10.0] — 2026-10-22

Minor release — 4 features, 34 fixes, 27 improvements.

Added (feat)

Fixed (fix)

Changed (refactor / chore / docs)

Closes

Migration script

Testing

  1. After merge, confirm CI creates tag v5.10.0 on main (check the auto-tag-on-release-pr-merge workflow run).
  2. Verify git describe --tags --abbrev=0 upstream/main returns v5.10.0.
  3. Run /release-sync v5.10.0 to sync main to dev and prevent squash divergence.
  4. Verify the squash commit's message carries the trailer: git log -1 --pretty=format:'%(trailers:key=Released-From,valueonly)' v5.10.0.

Checks done before this PR:

  • bash .claude/migrations/v5.9.0-to-v5.10.0.sh exits 0 and prints the placeholder line.
  • shellcheck -S warning .claude/migrations/v5.9.0-to-v5.10.0.sh is clean.
  • git log -1 --pretty=format:'%(trailers:key=Released-From,valueonly)' release/v5.10.0 prints 8d17671b816eb1d7b7482aeae0d540c48434f128.
  • bin/release-list-removed-lines.sh against release/v5.10.0 shows the same removed lines as the dry run against upstream/dev.

Refs #1863


Glossary

Term Definition
Squash merge GitHub merges all commits on the PR branch into a single commit on main; the branch HEAD is discarded and the resulting main tip has a new SHA
Auto-tag The auto-tag-on-release-pr-merge.yml workflow fires on pull_request → closed + merged for release/v* branches, tags github.sha (the squash commit), and creates a GitHub Release
Ancestry guard git merge-base --is-ancestor <sha> main — fails if the tag would not be reachable from main, preventing a mis-placed tag like v2.3.0
/release-sync The mandatory follow-up skill that merges main→dev after a squash-merge release, preventing SHA divergence accumulation
Released-From trailer A git trailer (Key: value in the commit message's final paragraph) recording the exact dev SHA this release was cut from — bin/release-changelog.sh reads it back for the next release's changelog range (AgDR-0094)
Cut point The dev commit that a release is built from

Released-From: 8d17671

me2resh and others added 30 commits September 18, 2026 18:49
Squash-merge divergence from the v5.6.3 release PR creates phantom
divergence between main and dev. This merge makes the v5.6.3 squash
commit an ancestor of dev.

Strategy: -X ours (dev wins on conflicts). Dev already has the
un-squashed equivalents.

Refs #1345
The -X ours merge kept pre-release copies of CLAUDE.md, whats-inside,
upgrading.md, the container diagram, and test_update_chain.sh. Those
files only changed on the release branch. Restore main so the hop test
and live counts stay on dev.

Refs #1345
- Brings three PRs that merged to main only into dev: #1351 (ORBIT
  planning adapter), #1353 (ORBIT record root) and #1355 (stop
  always-loading rule bodies).
- dev lacked the /orbit skill and the claudeMdExcludes setting, so the
  next dev-to-main release would have dropped or conflicted with them.
- Plain merge: it had no conflicts, so -X ours was not needed. That
  also keeps every hunk from the three PRs, none of which exist on dev.

Refs #1354
…P fallback, and make Rex verify acceptance criteria (#1398)

* docs: agent writing rule, no-search-MCP fallback, Rex AC check

- Add a Writing standard section to the 22 agent files that lacked one.
  A direct Agent-tool start skips the skill text that loads the rule.
- Replace the empty-result-only search fallback in the agents, two rules,
  and /handover. The old text did not cover an absent optional server.
- Stop /handover from calling the reindex tool when it is not installed.
  Claim "indexed in MCP" only when the reindex ran.
- Add a blocking Acceptance Criteria check and output section to Rex and
  /code-review. A review could approve a PR that missed a criterion.
- Add static tests with negative fixtures for all three behaviours. The
  review-body validator is unchanged and still accepts the new section.

Refs #1385
Refs #1379
Refs #1378

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1385): remove premium-wording ban from search fallback test

- Remove no_paid_wording, its file loop, and its paid.md fixture from
  test_search_mcp_fallback.sh. No linked issue asks for this check.
  AgDR-0138 classifies apexyard-search as a premium component, so an
  accurate sentence failed the check.
- Remove the CODE_REVIEW path variable. Only the removed loop used it.
- Rex: report an unreadable issue as Not verifiable. Rex cannot list the
  criteria of an issue that it cannot read.

Refs #1385
Refs #1379

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…gn the DFD template with threat-model (#1399)

* fix: validate Mermaid output in /handover and /plan-initiative

- /handover and /plan-initiative wrote Mermaid diagrams (architecture
  stubs, the initiative DAG) with no validation, unlike /c4, /dfd,
  /feature-diagram, /tech-vision, and /threat-model, which already run
  the shared _lib-mermaid-lint.sh. A broken block surfaced only when a
  human opened the file on GitHub.
- Add handover/lint.sh and plan-initiative/lint.sh — thin wrappers
  around the shared lib, matching the c4/dfd/tech-vision pattern.
- Wire a lint step into each skill's write path (handover step 6.2,
  plan-initiative step 6.5) that validates the written .md file on
  disk, not in-memory text, and prints "Mermaid not validated: mmdc
  not available." on exit 3 instead of silently reporting success.
- Extend _lib-mermaid-lint.test.sh's wrapper-dispatch loop to cover
  both new wrappers and pin the exact warning text in both SKILL.md
  files.
- /journey, /extract-features, /launch-check, and /agdr do not emit
  Mermaid themselves (checked directly — journey/extract-features only
  read Mermaid as an input signal, launch-check chose ASCII trend
  charts over Mermaid per AgDR-0014, and agdr's "Mermaid" mention is an
  unrelated example title), so they need no lint wiring. /process
  already lints its own BPMN output and does not itself write Mermaid.

Refs #1382

* fix: align the DFD template with /threat-model's heading contract

- templates/architecture/dfd.md carried Diagram, Trust boundaries, and
  Notes, but not the "## Data classifications" heading that
  generate-mermaid.sh emits and that /threat-model's Step 1b extractor
  keys on verbatim. A DFD hand-authored from the shipped template
  silently produced an empty classifications snapshot, with no warning
  that extraction had failed.
- Add a "## Data classifications" section to the template, matching
  the generator's heading so both DFD-authoring paths satisfy the same
  extractor contract without touching the generator or its pinned
  smoke test.
- State the three-heading contract explicitly in dfd/SKILL.md (a new
  numbered rule) and in threat-model/SKILL.md's anti-pattern list, so a
  future rename is a documented, reviewable change instead of a silent
  drift.
- Add test_dfd_heading_contract.sh: pins the three headings across the
  template, the generator, and both SKILL.md files, and reproduces the
  #1358 repro (awk extraction against a template missing the heading
  returns empty; against the fixed template it returns the table).

Refs #1358

* fix: unblock the two new Mermaid lint steps and their test coverage

- plan-initiative step 6.5 never assigned output_path, only commented
  where to get it. In a fresh shell the lint call received an empty
  path and exited 2, a code the step's exit-code list did not handle.
  Assign the path explicitly and add an exit-2 bullet.
- handover step 6.2 iterated over a glob for sequence-*.md. Under zsh,
  with no sequence file present (the common handover path), the
  unmatched glob prints "no matches found" and exits 1 before any
  stub lints. Iterate over find output with while-read instead, and
  reset the per-file exit code before each lint call so the reporting
  rules read the right file's status.
- _lib-mermaid-lint.test.sh's opt-in broken-fixture assertions used
  `|| true` before reading the lint's exit code, so BRC/MRC always
  read 0 and the assertions could never pass against a genuine parse
  failure. Capture the real exit code through the || assignment
  instead.
- Add a missing-file case (exit 2) to the per-skill wrapper loop, so a
  wrapper that never calls the shared lib now fails a test instead of
  passing by doing nothing.
- Run the broken fixture through every per-skill wrapper in the opt-in
  section, and add feature-diagram to the wrapper loop, which the
  updated header already listed but the loop itself omitted.

Refs #1382

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…nd warn when config overrides drop defaults (#1397)

* fix: cover .astro/.mdx/template files in the design gate (#1390)

- require-design-review-for-ui.sh's default UI patterns predated Astro,
  MDX, and template-engine files, so a PR that changed only .astro files
  merged with no design-review marker required
- Moved the pattern list into a new _lib-ui-paths.sh, shared between the
  hook and /approve-design's step 5 — the skill carried its own,
  independently hard-coded and already-narrower copy, which is exactly
  the drift this issue reported
- Added .astro, .mdx, .hbs, .njk, and .liquid to the shipped defaults;
  left bare .html out (the issue's own author hedged on it, and it is
  broad enough to catch generated docs/example HTML that isn't a
  component)
- .ui_paths / .ui_paths_exclude override semantics are unchanged

Refs #1390

* fix: exempt Alembic's env.py and script.py.mako from the migration gate (#1368)

- require-migration-ticket.sh's generic catch-all matched any file under a
  migrations directory, including Alembic's own runtime-config file and
  its revision template — neither is a migration itself
- Added two case arms, ordered before the catch-all, exempting both
  filenames under Alembic's default layout and a renamed script_location
- Every other arm is untouched, including the versions-directory arm and
  the catch-all itself, so a real revision script under either layout is
  still gated exactly as before
- Added regression cases for the exemption plus negative controls proving
  real revision scripts still require a ticket

Refs #1368

* fix: warn when a config array override drops shipped defaults (#1369)

- The project-config merge replaces an override array wholesale with no
  signal, so a fork that added one entry to an existing array silently
  lost every other shipped entry
- Added an advisory _config_warn_dropped_defaults check: for every array
  key present in the defaults file, if the override also sets that key
  and the override is missing entries the default carries, print one
  WARN line to stderr naming the key and the dropped entries
- Restricted the diff to keys reached only through object properties,
  never through a numeric array index, so an array nested inside one of
  a parent array's own elements is not independently re-flagged once the
  parent array itself already replaced wholesale
- Merge semantics are unchanged: the override still wins, this only
  makes an otherwise-silent drop visible
- Out of scope by construction: a handful of keys have no JSON default at
  all because the reading hook holds its own built-in default in code;
  documented in project-config.md and AgDR-0167 rather than worked around
  here

Refs #1369

* docs: record AgDR-0167 for the gate path-pattern coverage batch

- All three fixes in this batch touch production trust-chain hooks, so
  right-size-ceremony's rail 1 makes the batch material regardless of
  diff size
- Records the options considered and the decision for each of the three
  issues, plus the scope boundary the config-warn fix deliberately leaves
  open (override-only keys with no JSON default)

Refs #1390
Refs #1368
Refs #1369

* fix: address Rex and Hakim review findings on #1397

- require-design-review-for-ui.sh now fails CLOSED when _lib-ui-paths.sh
  cannot be sourced or resolves to an empty pattern list, instead of
  exiting 0 on every PR (Hakim HIGH-1, CWE-636). Adds a sandboxed
  regression test that runs the hook's OWN copy of the library, since the
  existing test always ran $HOOK_SRC and the sandbox cp had no effect.
- /approve-design step 5 now reads `.ui_paths` from $REPO_ROOT, the same
  root the hook reads, instead of $MARKER_HOME. The two roots differ
  inside a workspace clone, so a project-level override could make the
  two checks disagree (Rex blocking finding 2).
- Files a follow-up issue (#1401) for the override-only keys that
  _config_warn_dropped_defaults cannot cover (migration_paths, ui_paths,
  architecture_paths, and four siblings). #1365's allowlist does not
  detect a dropped default entry, so citing it as coverage was wrong.
  #1369's own reporter incident used one of the seven keys, so #1369
  stays open and is only partly addressed by this batch (Rex blocking
  finding 1).
- Corrects the writing-profile violations and the Alembic fact error Rex
  found in AgDR-0167, docs/project-config.md, and the PR body: Alembic's
  own default layout already needed no migration ticket before this
  batch. Only a renamed layout's behavior changed.

Refs #1390
Refs #1368
Refs #1369
Refs #1401

* docs: split long AgDR-0167 sentences and fix project-config.md wording (Rex re-review)

- Rex's re-review at 18e856f flagged 11 AgDR-0167 sentences over the
  25-word descriptive-text limit; each is replaced with the exact
  shorter wording Rex gave in the review.
- docs/project-config.md's new dropped-defaults paragraph had a
  self-contradicting lead-in ("still silent by default" next to "a
  warning names what it drops") and pointed at "this file" / "here"
  for a fact that lives in a different file; both are fixed to name
  `.claude/project-config.defaults.json` explicitly.
- Records Hakim's LOW-2 (narrower Alembic env.py exemption, not
  implemented) and LOW-3 (POSIX-mode source failure exits 1, treated
  as continue by the dispatcher) as known/pre-existing limits in the
  AgDR's Consequences section.

* fix(#1397): filter non-string .ui_paths entries so a malformed override falls back to defaults

- Hakim's security re-review (LOW-2) found that _lib-ui-paths.sh's
  `.ui_paths // [] | .[]` turns a non-string override entry (`null`,
  an object, a nested array) into a literal pattern like `null` or
  `{"a":1}` that matches almost no real file, silently narrowing the
  design gate toward an always-passing state instead of falling back
  to the shipped defaults.
- The jq filter now keeps only non-blank string entries
  (`map(select(type == "string" and test("\\S")))`), so a fully
  malformed override degrades to the safe default pattern list
  instead of an empty, always-passing one; a mixed override keeps
  its valid entries as before.
- Adds four new regression cases ([null], [{}], [[]], [" "]) plus a
  mixed-entries case to test_require_design_review_for_ui.sh; three
  of the four fail against the prior _lib-ui-paths.sh, confirming
  they exercise the fix and not just the hook's existing empty-list
  guard.

* docs: update hooks README's design-gate section for the shared UI-pattern list

- Section 8's "Default UI paths" list still showed the pre-#1390
  pattern set (no .astro/.mdx/.hbs/.njk/.liquid) and didn't mention
  that the hook and /approve-design now share one list via
  _lib-ui-paths.sh, so a reader following the README alone would get
  a stale picture of what the gate treats as UI.
- Also documents the malformed-.ui_paths-entry fallback (Hakim's
  LOW-2) so the customization note matches current behavior.

* fix(#1397): pipe into _config_warn_dropped_defaults' loop instead of process substitution

- Hakim's LOW-A review finding: `done < <(...)` process substitution is a
  bash/ksh/zsh extension. Sourcing _lib-read-config.sh under /bin/sh or
  bash POSIXLY_CORRECT=1 hit a syntax error there and left config_get
  undefined for the rest of the process.
- The loop only writes to stderr, so piping into it from the pipeline's
  subshell changes no observable behavior.
- Adds two regression cases that source the library under both POSIX
  shells and assert config_get is defined; both fail on the prior head.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* docs(#1397): fix Hakim's finding labels in AgDR-0167's known-limits bullet

- Rex's re-review found that the bullet swapped Hakim's finding numbers:
  it called the Alembic filename-only limit LOW-2 and cited a LOW-3 that
  no posted security review contains.
- Hakim's posted reviews define LOW-1 as the Alembic filename-only limit
  and LOW-2 as the malformed .ui_paths entries. Replaces the bullet with
  Rex's exact text, naming #1403 for the POSIX source-failure limit.
- States that the Alembic filename limit is new in this batch, not
  pre-existing — the bullet previously said the opposite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* docs(#1397): shorten a README sentence and note WARN's per-process frequency

- Rex's advisory finding: the new hooks/README.md design-gate sentence
  ran 26 words with its citation, over the 25-word descriptive limit.
  Moves the citation into its own sentence.
- Rex's open suggestion: docs/project-config.md said the WARN prints
  "the first time that override is read in a session" without saying
  what happens with no session ID. Adds a sentence: without a session
  ID, the cross-process cache has no key, so the warning prints again
  in every new process.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1397): source case 7a under bash --posix, not /bin/sh

- CI's "hook test suite" check failed on Linux at case 7a. On Linux
  /bin/sh is dash, a stricter POSIX shell this library was never
  written to support. Dash fails on line 35's pre-existing
  `${BASH_SOURCE[0]:-}`, a bash-only array expansion unrelated to
  LOW-A. macOS /bin/sh is bash in POSIX mode, so the failure did not
  show up locally.
- Case 7a now sources the library with `bash --posix -c`, matching
  case 7b's `bash POSIXLY_CORRECT=1`. Both invoke bash in POSIX mode,
  which is the shell class LOW-A's regression (`done < <(...)`
  process substitution) actually targets.
- Confirmed 7a still fails on the pre-fix process-substitution head
  and passes on this PR's fix.
- Tidies the comment above the loop fix in _lib-read-config.sh: no
  semicolon, shorter sentences. No logic change.

Refs #1397

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…er-name leak false blocks (#1400)

* fix: scope the active-reviewer marker to the Claude Code session

- key .claude/session/active-reviewer on CLAUDE_CODE_SESSION_ID through a
  new active_reviewer_marker_path resolver in _lib-review-markers.sh, so
  one fixed shared path no longer lets a review in one session block or
  suppress a warning in an unrelated session on the same ops fork
- update block-reviewer-repo-mutation.sh and warn-review-marker-write.sh
  to resolve the marker through the resolver, with a defensive fallback
  to the pre-existing fixed path when the lib is missing
- update clear-active-reviewer-marker.sh so a SessionStart sweep only
  ever clears a stale marker left by an earlier run of the SAME session,
  never a different, concurrently-running session's live marker
- update /code-review, /security-review, /design-review (SKILL.md step 0)
  to write and clear the marker through the resolver instead of a literal
  path, and update pr-workflow.md's description of the marker
- record the decision in AgDR-0166

Refs #1376

* test: pin per-session active-reviewer marker isolation

- add cases to test_warn_review_marker_write.sh proving a same-session
  marker still suppresses the write silently, a marker at the legacy
  shared path does not authorize a session that has its own id, and a
  different concurrent session's marker never suppresses this session
- add direct unit assertions on active_reviewer_marker_path itself: the
  empty-session-id fallback shape, and path-traversal safety on a
  hostile session id
- add cases to test_block_reviewer_repo_mutation.sh proving a different
  concurrent session is not blocked by another session's review, while
  the no-session-id fallback still protects a genuinely session-less
  review
- add cases to test_clear_active_reviewer_marker.sh proving a
  SessionStart sweep in one session never removes a different,
  concurrently-running session's live marker
- every new case was confirmed to fail against the pre-fix hooks before
  the fix landed, and to pass after it

Refs #1376

* fix: exempt the target repo owner from leak-protection matching

- block-private-refs-in-public-repos.sh exempted a registered private
  project's name only when it equaled the target repo's own bare repo
  name; it did not exempt the target repo's owner login, so a private
  project whose name happened to equal the owner (or an owner-only
  @-mention, or the repo written out in full as owner/repo) blocked a
  safe write with no private reference at all
- derive the target repo's owner login (TARGET_OWNER) alongside its bare
  name (TARGET_NAME), and skip a registered name that equals either one
- a genuinely different private project's name still blocks, and the
  owner exemption does not suppress scanning the rest of the body for
  an unrelated real leak in the same write

This PR does not change require-skill-for-issue-create.sh. The read-only
gh api GET half of #1387 does not reproduce on dev: that hook already
allows a GET (it has no -X POST/-f/-F/--method/--input in its match
condition), so the reported ticket-create-gate false positive was
already fixed upstream of this PR.

Refs #1387

* test: pin the owner-name exemption in leak protection

- add a fixture project whose registered name equals the target repo's
  owner login, reproducing the reported false block
- add cases proving writing the target repo out as owner/repo, plainly
  @-mentioning the owner, and mentioning the owner in the title all pass
  through unblocked
- add regression guards proving an unrelated private project name still
  blocks, the pre-existing bare-repo-name exemption still works, and an
  owner mention next to a genuine leak in the same body still catches
  the real leak
- every new case was confirmed to fail against the pre-fix hook before
  the fix landed, and to pass after it

Refs #1387

* docs: tighten AgDR-0166 to the controlled technical writing profile

- remove semicolons and split long sentences in AgDR-0166 per
  .claude/rules/writing-standard.md
- no change to the decision content

Refs #1376

* fix: narrow the #1387 owner exemption to the two forms the issue asked for

- block-private-refs-in-public-repos.sh exempted a registered project's
  name in every context once it equaled the target repo's owner login.
  The issue asked to keep the block when the name stands alone. Narrow
  the exemption to the `@owner` and `owner/<repo-slug>` forms only; a
  bare, standalone mention still blocks like any other registered
  project's name.
- add a pinning test case (#1387: a BARE owner-name mention still
  blocks) and update the surrounding comment blocks to describe the
  narrower scope.
- correct leak-protection.md's description of the owner exemption to
  match the narrowed behavior.
- correct AgDR-0166's Consequences claim that every new test case fails
  against the pre-decision hooks. Some regression-guard cases pass on
  both the pre-decision and post-decision hooks by design; only the
  fix-pinning cases fail on the old hooks.

Refs #1376
Refs #1387

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix: correct the remaining writing-profile and test-claim gaps from Rex's review

- .claude/rules/leak-protection.md described the #1387 owner exemption
  in one 38-word sentence joined by an em dash. Split it into three
  short sentences. Each one now states a single fact: what the owner
  exemption skips, which two forms it covers, and that a bare mention
  still blocks.
- fix the PR body's one remaining long sentence. The #1387 acceptance
  table had a 38-word cell describing require-skill-for-issue-create.sh.
  Split it into three short sentences.
- correct the PR body's mutation-test claim. The body named case 1 as
  the only case that fails on the pre-fix hook, and cases 2-3 as
  passing on both hooks. A revert check (kept the current library,
  restored the four pre-fix hook scripts, ran the current test file
  against them) shows case 5 also fails on the pre-fix hook, and cases
  2, 3, and 4 pass on both. Case 5 is the scenario Hakim's security
  review flagged as newly fail-open (M1): a marker at the legacy shared
  path no longer blocks a session that has its own session id.
- record that commits 0a402ac and 8d7432f each claim every new test
  case fails against the pre-fix hooks. That claim is false for the
  regression-guard cases: warn 48, clear 5, and leak 75-77 plus 77b
  pass on both the pre-fix and the post-fix hooks by design, and so do
  mutation cases 2-4. This commit does not rewrite those two commits.
  The correct, case-by-case claim lives in the PR body's Testing
  section and in AgDR-0166's Consequences section, both already
  corrected by e1ecbc3 and this commit.

Refs #1376
Refs #1387

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1400): treat a hyphen as a word boundary in the owner-name check

- Rex A1 / Hakim LOW 1: the owner-exemption bare-mention grep used
  [^A-Za-z0-9_-] as its boundary class, so a hyphen read as a word
  character. A hyphen-joined form of the owner-equal name, such as
  owner-tool or foo-owner, matched no boundary and passed the owner
  branch, even though the same text still blocks for any other
  registered name through the generic grep -qiwE check two lines down.
- Narrow the boundary class in that one check to [^A-Za-z0-9_], which
  matches grep -w's own notion of a word character. The two strip
  expressions above it keep their wider class unchanged.
- Hakim LOW 2: the owner/<repo-slug> strip's slug class included a dot,
  so a second, glued mention right after a dot (owner/repo.owner) was
  captured inside the stripped slug and disappeared. Remove the dot
  from that class so the strip stops at the dot and the glued mention
  reaches the bare-mention check.
- Add four leak-test cases (77c-77f) for the three hyphen forms and the
  glued-dot form. All four fail on the pre-fix hook and pass after this
  commit; the existing 112 cases are unaffected.

* fix(#1400): warn when a legacy shared marker sits beside a session id

- Hakim LOW 3 (was security M1 at the prior review): once a session id
  exists, block-reviewer-repo-mutation.sh reads only the session-suffixed
  marker and never falls back to the bare .claude/session/active-reviewer
  path. A caller that still writes the literal bare path — a stale prompt,
  a pre-#1376 doc, or a write left over from before an upgrade — arms no
  lock for that session, and the mutation gate fails open with no message
  saying why.
- Add a non-blocking stderr advisory: when CLAUDE_CODE_SESSION_ID is set
  and the bare legacy marker file exists on disk, print its path and the
  session-scoped path the hook actually reads. The advisory never gates;
  the session-scoped check is still the only thing that blocks or allows.
- Add a test that pins the advisory: the command must still exit 0, and
  stderr must name the legacy path. Fails on the pre-fix hook (no such
  message), passes after this commit.

* docs(#1400): point stale active-reviewer references at the resolver

- Rex A2 / Hakim LOW 3: five prose locations still named the bare
  .claude/session/active-reviewer path from before the #1376 fix, even
  though every actual reader and writer already resolves the path
  through active_reviewer_marker_path. A writer that copied the bare
  path from one of these locations would arm no lock for its own
  session.
- Update code-reviewer.md, solution-architect.md, auto-code-review.sh,
  and warn-review-marker-write.sh to name the resolver alongside the
  illustrative bare path, and threat-model.md's data-store bullet to
  describe the marker as session-scoped.
- No behavior changes in this commit; every edit is a comment or a doc
  sentence.

* fix(#1400): make the review skills' exit-step marker cleanup self-contained

- Rex S3 / Hakim informational finding: each skill's step-0 entry block
  resolves ops_root and active_marker as shell variables, then the exit
  step runs rm -f "$active_marker" as a separate Bash tool call. Shell
  variables do not persist across separate Bash calls, so the exit step
  ran with an empty variable and the rm was a no-op — the marker stayed
  on disk after the review finished.
- Re-resolve ops_root and active_marker inside the exit step of
  code-review, security-review, and design-review's SKILL.md, instead
  of reusing the step-0 variable. The failure mode this closes is
  fail-closed (a leftover marker only ever blocks the same session's
  own future git mutations), but a review that never releases its own
  lock is still a defect worth fixing.

* docs(#1400): correct two stale claims about the marker resolver

- Rex S2: _lib-review-markers.sh claimed exactly one session-less
  process acts on a given ops fork at a time. No evidence in this
  repository supports that claim, so the comment now says only that
  the fixed path is no less safe than it was before this function
  existed, without asserting a fact nobody verified.
- Rex S2: the same file's sanitiser comment said the code escapes "the
  reviews directory", but the active-reviewer marker lives directly
  under .claude/session/, a sibling of .claude/session/reviews/, not
  inside it. Name the correct directory.
- Rex S4: AgDR-0166 said a literal-path caller "silently reopens the
  cross-session leak". That is true only for a reader. A literal-path
  writer does the opposite of reopening the leak: it disarms the
  mutation lock for its own session, because a session-scoped reader
  never looks at the bare path once a session id exists. Split the
  sentence to state both outcomes correctly.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ocument a reviewer test path (#1404)

* fix: honor the active ticket for an unextractable Bash write target

- active_ticket_marker_for_path returned an empty marker as soon as the
  target could not be resolved, before checking current-ticket.
- Skip only the per-worktree and per-project tiers for an unknown
  target; still check the ops-level current-ticket fallback.
- Never exempts the write; a session with no ticket at all still blocks.

Refs #1396

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* docs: give reviewers a sanctioned test path in a scratch clone (#1402)

* docs: record AgDR-0168 for the unextractable-target ticket-gate fix

* fix: fail closed when a non-empty write target cannot resolve

- The #1396 fix let active_ticket_marker_for_path fall back to the
  ops-level current-ticket marker for ANY empty resolved path, not only a
  truly unextractable target. A tilde-spelled migration write (~user/,
  ~+/, ~-/) stays non-empty through normalisation, but still resolves to
  an empty path, so it hit the same fallback and could borrow an
  unrelated project's ticket inside require-migration-ticket.sh.
- The fix distinguishes the two causes: an empty raw target still falls
  back to current-ticket (keeps the #1396 fix), but a non-empty raw
  target that fails to resolve now returns an empty marker instead
  (restores the pre-#1396-fix fail-closed behavior for that spelling).
- Adds three inverted-fixture regression cases to
  test_require_migration_ticket.sh. The ops ticket is a valid migration
  ticket and the project's own ticket is not, so a wrong-ticket approval
  shows up as an allow instead of hiding behind a block that has the
  right exit code for the wrong reason.

Security finding H1, Hakim's review on PR #1404.

Refs #1396

* test: cover #1396's own reported repro shape

Cases 79-81 use a python3 write with a computed path. The issue's own
repro is an in-place stream edit on a path held in a shell variable
(the classic "-i" in-place flag). Add case 82 for that exact shape, so
the suite proves the fix on the shape that was reported, not only an
equivalent one.

Refs #1396

* docs: correct which scratch-clone step needs a ticket

- The Pattern 2 text said the #883 out-of-governance exemption applies
  to the git-archive export. The write detector cannot extract the tar
  target, so the ticket gate sees an empty path. #883 only applies once
  a target path is known, so it never covers that step. The export step
  needs an active session ticket instead. A later write to a literal
  path inside the export directory can use #883.
- Pattern 1 (the plain git clone) gets the matching note: the clone is
  a git repository, so every write inside it still needs a ticket.
- Each of the three skill files carried one instruction sentence of
  about 26 words, over the writing profile's 20-word limit. Split into
  one short sentence per pattern.
- Replaced the /tmp example path with a pointer to the session
  scratchpad directory, which does not get cleared mid-session.

Refs #1402

* test: scope the scratch-clone check to the new section only

The checks for "stop", "report", and the rephrase/disguise warning
grepped the whole file. Every base agent and skill file already
contains "stop" and "report" elsewhere, so those two checks could never
fail even if the new section itself lost its stop-and-report text.
Extract the section body first (from its heading to the next `## `
heading) and run every check against that slice only.

Refs #1402

* docs: correct AgDR-0168 on the migration gate and the writing profile

- The opening Y-statement was one sentence of more than 60 words, with
  an unclear "to achieve the ticket described in the framework rule"
  clause. Split into short sentences for context, decision, goal, and
  trade-off.
- The Consequences section said require-migration-ticket.sh "gets the
  identical fix" for an unextractable target. It never receives one:
  the gate exits before the library runs when it cannot extract any
  target at all. The real, distinct effect is on a tilde-spelled
  migration target, which stays non-empty through normalisation but
  still fails to resolve. An earlier commit in this PR let that case
  fall through to the ops ticket (the H1 finding); this PR's follow-up
  commit restores the fail-closed behavior. Consequences now states
  both accurately.
- Cited commit 56aacc0 (#1231) as the refactor that introduced the
  early return this AgDR's fix removes.

Refs #1396

* fix(#1396): keep the migration gate fail-closed for a tilde-spelled target, per Rex

- Rex's re-review on PR #1404 found four semicolons and several
  sentences over 25 words in AgDR-0168, breaking the controlled
  technical writing profile. Rewrote every flagged sentence using
  Rex's replacement text.
- Applied Rex's cheap advisories: state why the session scratchpad is
  safe instead of only saying "Not /tmp" (A1), initialise project=""
  in the resolver's local declaration for defense only, no behavior
  change (A2), and say "a literal directory outside every git
  repository" instead of "a directory with no .git" (A3).
- Linked AgDR-0168 by path in the PR body (A5).
- A4 (a follow-up ticket for the open session-scratchpad exemption in
  #1402) is not filed here; it still needs filing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* docs(#1402): document the reviewer mutation lock before scratch-clone pattern 1, per Rex

- Pattern 1 in the "Running tests in a scratch clone" section cannot run
  during an armed review. block-reviewer-repo-mutation.sh blocks git
  clone, git fetch, and git checkout while the active-reviewer marker
  exists (Rex, PR #1404 re-review).
- The three agent files and their three invoking skills now state that
  the orchestrator prepares the scratch clone and fetches the PR head
  before it arms the marker, and that git worktree add stays available
  during the review (#1275).
- The PR body links AgDR-0168 with an absolute GitHub URL instead of a
  relative path, so the link resolves from the PR page (Rex advisory).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…lease-sync keep main-only changes (#1406)

* fix: stop /approve-merge when a PR is behind its base branch

- /approve-merge now reads mergeStateStatus from the forge in the same
  call that already reads the PR's state, and stops before touching
  either marker when the PR is BEHIND and merge.require_up_to_date is
  true (the default) — a merge queue can otherwise merge on a CI
  result computed against a stale base
- Adds the merge.require_up_to_date config key to
  .claude/project-config.defaults.json, and the behind-base check to
  the pre-merge checklist in .claude/rules/pr-workflow.md
- block-unreviewed-merge.sh optionally names a behind-base branch as a
  likely contributing reason when it already blocks on a missing or
  stale Rex marker; no new blocking condition is added
- Adds regression tests for the config key default/override and for
  the hook's optional behind-base note (both halves: present on BEHIND,
  absent on CLEAN, so the note cannot false-positive)

Refs #1386

* fix: replace release-sync's blind -X ours with commit-attributed resolution

- /release-sync step 5 now runs a plain merge, not -X ours. A conflict
  resolves toward dev only when the release squash commit (resolved
  from the version tag) is the sole commit that touched the file; any
  other commit in that range stops the sync and asks instead of
  guessing (step 5a)
- Adds a post-merge check (step 5c): every main-only commit's patch
  must still reverse-apply cleanly against the sync branch, or the
  sync stops before push
- -X ours dropped main-only content silently — sync commit 04bd8c7
  (apexyard#1348) lost two contributor rows this way. A plain merge
  surfaces every conflict so it can be attributed correctly instead
- Updates docs/release-process.md and the skill's PR body template,
  Rules section, Edge Cases table, and Glossary to match
- Adds regression tests: squash-duplicate vs main-only-commit
  classification, a direct repro of the #1348 loss under the old
  strategy that the new strategy keeps, and the post-merge check
  catching the old loss while passing clean on the correct resolution

Refs #1394, #1348

* docs: record AgDR-0170 for the merge-flow behind-base and sync-merge decisions

- Records the decision to check for a behind-base branch inside
  /approve-merge, rather than as a new block-unreviewed-merge.sh
  condition, and to replace release-sync's -X ours with a plain merge
  plus commit-attributed conflict resolution
- Names the options considered and the trade-offs accepted, including
  the file-granularity limit on conflict attribution and the
  best-effort nature of the post-merge content check

Refs #1386, #1394

* docs: fix a misleading word in the merge.require_up_to_date config comment

- The comment called the key "display-only", copying that phrase from
  a nearby config block's comment. This key changes /approve-merge's
  own behaviour, not only printed text, so the word was wrong.

Refs #1386

* fix: move step 5c after step 5b's own explanatory paragraphs

- Step 5c (the post-merge content check) landed between step 5b's code
  block and its own "why a separate commit" / "idempotent" / "what
  this step does NOT do" paragraphs, splitting one step's explanation
  in two. Moves 5c after those paragraphs so 5b reads as one step and
  5c follows it, matching the numbering.

Refs #1394

* fix: compute a behind-base PR from the compare API, not mergeStateStatus

- /approve-merge and block-unreviewed-merge.sh read mergeStateStatus,
  which GitHub only reports BEHIND under a strict required-status-checks
  ruleset policy. This repo's own dev ruleset does not set that policy,
  so a PR behind an unprotected base reported BLOCKED, CLEAN, or UNKNOWN
  instead, and the check never fired on the case #1386 exists to catch.
- Adds _lib-merge-behind.sh (is_pr_behind_base), which reads the compare
  API's behind_by field instead. Defines the unknown case explicitly: a
  failed lookup proceeds with a warning, never a silent pass and never a
  block on an unrelated network hiccup.
- block-unreviewed-merge.sh's advisory note now uses the same check, on
  both the missing-Rex-marker block and the stale-Rex-marker block (the
  second addresses Rex's A1 suggestion — a branch update also explains a
  SHA mismatch).
- The note now asks the human approver to update the branch or approve
  the update, and tells the agent reading the block not to do it itself
  (Hakim's MEDIUM finding on #1406). pr-workflow.md's recovery list gets
  the same correction.
- Adds test_lib_merge_behind.sh and extends test_block_unreviewed_merge.sh
  with a case that pins the exact split #1386 reported: mergeStateStatus
  reads CLEAN while the PR is behind, and the note still fires.

Refs #1386

* fix: confirm a squash-duplicate resolution against the release commit's Released-From cut point

- Peels the version tag with ^{commit} before resolving RELEASE_SHA.
  auto-tag-on-release-pr-merge.yml creates an annotated tag, and a bare
  git rev-parse on an annotated tag returns the tag object's SHA, not
  the commit — so RELEASE_SHA never matched the touching-commit list
  step 5a builds, and the squash-duplicate case never classified
  correctly (#1394 B2).
- Being the sole touching commit does not by itself mean dev already
  has the content. The release squash commit can carry edits made
  directly on the release branch before the squash — this is the real
  #1348 shape: two contributor rows were added on the release branch,
  not by a separate main-only commit. Step 5a now diffs the release
  commit against the dev commit named in its own Released-From trailer
  before resolving toward dev, and routes a missing trailer or a
  non-empty diff to the same stop-and-ask path as a main-only commit
  (#1394 B3).
- git checkout --ours takes the whole file, not just the conflicting
  hunks — documents the difference from -X ours now that the whole-file
  swap depends on the diff above having confirmed equivalence (Rex A3).
- git show --binary in step 5c's reverse-apply loop, so a commit that
  touched a binary file no longer shows as a false failure on every run
  (Rex A2).
- Retitles the existing case-11/12 main-only-commit tests to state
  plainly that they model a general shape, not the real #1348 incident.
  Adds annotated-tag test coverage (B2), the real #1348 shape and its
  control case (B3), and four static assertions read from SKILL.md
  itself rather than a copy of its logic, which fail against the
  pre-#1394 file (B4).

Refs #1394

* docs: correct AgDR-0170 for the compare-API check and the real #1348 shape

- Updates the detection description from mergeStateStatus to the
  compare API's behind_by field, matching the code change in this
  batch.
- Corrects the #1348 description: the release squash commit itself
  added the two contributor rows directly on the release branch. It
  was not a separate main-only commit landing beside a plain squash
  duplicate.
- Records the Released-From confirmation step and its trade-off: a
  release tagged before AgDR-0094 has no trailer to confirm against,
  so it routes to a human instead of guessing.
- Fixes the Artifacts list: the shipped test file is
  test_config_merge_require_up_to_date.sh, not
  test_config_merge_semantics.sh (Rex A4). Adds the new
  _lib-merge-behind.sh and its test file.

Refs #1386, #1394

* fix: stop /approve-merge from treating a failed behind-base check as up to date

- A failed compare-API call (network or auth failure) previously fell
  through to "proceed to step 4" with only a warning. That treats an
  unknown state as evidence the PR is up to date, which it is not.
- Step 3a now stops on "unknown" the same way it stops on "true": no Rex
  marker check, no CEO marker, no merge. It tells the user to retry the
  check or verify manually, then re-run /approve-merge.
- pr-workflow.md's "PR must be up to date with its base branch" section
  gets one sentence stating the same rule, so the recovery list and the
  skill agree.

Refs #1386

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix: drop release-sync's squash-duplicate auto-resolution rule

- Step 5a tried to tell a safe squash-duplicate conflict apart from an
  unsafe main-only-commit conflict, confirming a candidate against the
  release commit's Released-From trailer. Replaying the real #1348 sync
  against this rule still classifies the conflicting file as a candidate,
  and a missing or non-empty trailer diff is the only thing that would
  have stopped it — the rule is exactly as fragile as the one it replaced.
- This commit removes the classification and the trailer confirmation
  entirely. Step 5a now stops and asks on every conflict, including one
  where the release squash commit is the only commit that touched the
  file. RELEASE_SHA still resolves with ^{commit} (an annotated tag's bare
  SHA names the tag object, not the commit), but only to label that commit
  for the user, never to decide anything.
- test_release_sync.sh cases 4 and 4b now assert that a conflict blocks
  `git commit --no-edit` until a human resolves it, instead of asserting
  an automatic classification. Case 13 replays the real #1348 shape and
  confirms it still conflicts and still requires a human answer, rather
  than confirming a smarter classification catches it.
- Adds three static assertions on the real SKILL.md files, not a copy of
  their logic: no `-X ours` invocation in release-sync's step 5 merge, the
  tag resolution peels with ^{commit}, and approve-merge's behind check
  uses the compare API (is_pr_behind_base). Each one fails against the
  pre-fix files on dev at 5be9ecb and passes now.
- docs/release-process.md's /release-sync mention and Related section
  updated to match.

Refs #1394, #1348

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* docs: correct AgDR-0170 for the simplified sync-merge decision

- AgDR-0170 recorded the squash-duplicate classification approach as the
  chosen option. That approach is dropped in the prior commit, so the
  record described a decision this PR no longer makes.
- Rewrites the Options Considered table, Decision, and Consequences to
  match: a plain merge that stops and asks on every conflict, with no
  classification step. The classification approach is listed as a
  considered-and-rejected option, with the reason it was rejected.
- Corrects the #1348 root-cause description so it states plainly that the
  release squash commit itself carried the lost content, not a later
  main-only commit — the shape the classification approach also would
  have missed.

Refs #1394, #1348

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* docs: fix AgDR-0170 writing-profile violations and two factual slips

- Rex flagged nine sentences over the 25-word limit, including the
  ~75-word summary quote; replaced each with Rex's shorter wording so the
  record still reads correctly but no longer breaks the controlled
  technical writing profile
- Fixed the options-table claim that the sole-touching-commit rule
  "missed" apexyard#1348 — that rule never ran in the real incident, so
  it now reads "would have missed apexyard#1348"
- Fixed the Consequences claim that a failed compare-API call "previously
  proceeded with a warning" — that warning only existed in an interim
  commit on this branch, never on dev, so the sentence now just states
  the current behavior
- Verified with a script check: no semicolons and no sentence over 25
  words anywhere in the file

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1386): is_pr_behind_base ignores exit code, only reads stdout

- Hakim's LOW-2: the compare-API call's exit code was never checked, so
  a failed call that still printed a well-formed "0" on stdout returned
  "false" (the safe-looking value) for a lookup that did not actually
  succeed
- Now checks the exit code first and returns "unknown" on any non-zero
  exit, before ever looking at what was printed
- Added a regression case: a stubbed gh that prints "0" and exits 1 must
  give "unknown", not "false" — confirmed this fails against the pre-fix
  function (returns "false") and passes after the fix

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1386): resolve the ops fork root before the behind-base check

- Hakim LOW-1 / Rex A1: step 3a of /approve-merge loaded
  _lib-read-config.sh and _lib-merge-behind.sh from `git rev-parse
  --show-toplevel`, which is the wrong path inside a workspace clone
  (#229, #230) — a missing library there silently disabled the
  behind-base check instead of stopping the merge
- Step 3a now resolves the ops fork root the same pin-first-then-walk-up
  way step 4 already does, and step 4 reuses that same MARKER_HOME
  instead of re-deriving it
- Both libraries now stop the skill with an explicit message if they
  fail to load, instead of leaving the check silently off
- REQUIRE_UP_TO_DATE now treats any value other than the literal "false"
  as on, so an empty or unset value is on, not off
- BEHIND now stops the merge on any value other than the literal
  "false" — true, unknown, empty, or unset are all treated the same way

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1386): skip the behind-base lookup when no repo is named

- Rex A3 / Hakim LOW-5: print_behind_base_note called `gh pr view` and
  the compare API with an empty --repo when the merge command named no
  repository, letting gh resolve the ambient repo from local git
  remotes instead (the #887 class) — the note could then describe a
  different repo's PR that happens to share the same number
- The function now returns immediately when CMD_REPO is empty, before
  either lookup runs, so a missing repo produces no note instead of a
  note about the wrong repo

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* docs: clarify who must approve a branch update before merge

- Rex A2: "without that ask" in the behind-base recovery steps could
  read as permission after any request was made, not specifically the
  user's approval
- Now reads "without the user's approval", matching the surrounding text
  that already says the agent must not update the branch on its own

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
)

* fix(#1403): dispatcher fails closed when a merge gate cannot run its check

- The four merge-gate hooks share one exit-code contract with the
  dispatcher: only exit 2 blocks a tool call, any other non-zero exit
  is logged and ignored. A merge gate that dies before it can decide
  PASS or BLOCK (a missing sourced library under POSIX mode, a syntax
  error, a killed process) used that same contract, so its unplanned
  exit 1/127/143 let the merge proceed unreviewed.
- Adds run_merge_gate_hook, a stricter wrapper used only for the four
  merge gates. It keeps exit 2 as BLOCKED and now also treats every
  other non-zero exit as BLOCKED. Every other hook keeps run_hook's
  existing warn-and-continue behavior, so this does not change how any
  advisory check is treated.
- Extends test_dispatch_bash.sh with the reported repro (each merge
  gate stubbed to source a missing library under POSIXLY_CORRECT=1)
  plus four more failure modes on one gate: exit 1, exit 127, a bash
  syntax error, and a process killed by SIGTERM. All four failed before
  this change and block after it.

* fix(#1403): guard required library sources in the four merge gates

- The dispatcher fix blocks a loud non-zero exit, but a missing library
  in default (non-POSIX) bash returns 1 from `.` and keeps running. Each
  gate's next line then calls an undefined function, gets a "command not
  found" (127), and reads that as "not a merge command" — a clean exit 0
  the dispatcher cannot see is wrong. Adds a shared _require_lib helper
  to all four merge gates that checks readability with [ -r ] before
  ever calling `.`, and exits 2 with a BLOCKED message when a required
  library is missing or fails to load.
- Checking readability first also matters under POSIXLY_CORRECT=1: a
  missing-file source failure ends a non-interactive POSIX-mode shell
  immediately, even from inside an `if`/`||` guard around the failing
  `.` call itself. Never calling `.` on an unconfirmed path avoids that
  fatal case in both shell modes.
- Extends the guard to two libraries Hakim's review found unguarded:
  _lib-pr-repo.sh in require-architecture-review.sh and
  require-design-review-for-ui.sh (both already treat it as required,
  unlike the other two gates' optional `if [ -f ]` use), and
  require-design-review-for-ui.sh's existing _lib-ui-paths.sh guard,
  which closed the default-bash case from #1397 but not the POSIX one.
- block-unreviewed-merge.sh's optional _lib-read-config.sh read gets a
  readability check instead of a hard guard, since it is genuinely
  optional here and a pre-existing check further down already fails
  closed when the library's functions are undefined.
- Each hook's test file removes each required library in turn and
  expects a BLOCKED exit 2, once in default bash and once under
  POSIXLY_CORRECT=1. All new cases failed before this change.

* docs(#1403): record the dispatcher fail-closed decision in AgDR-0169

- Records why dispatch-bash.sh, not the four individual hooks, is the
  single point for the merge-gate fail-closed rule, and why the
  per-hook _require_lib guards are a complementary fix, not a
  redundant one: the dispatcher catches every failure mode with a
  loud non-zero exit, the per-hook guard catches the one mode that
  looks like an ordinary pass.
- States exactly which library each of the four gates guards, after an
  earlier draft of this record wrongly said every gate guards
  _lib-review-markers.sh (block-merge-on-red-ci.sh does not decide on
  approval markers, so it never sources that library).
- Names which of Hakim's four POSIX-only findings now block by
  themselves after the guard fixes, and which shell-mode setting the
  test files actually use (POSIXLY_CORRECT=1, not a direct bash
  --posix invocation, even though both put bash in the same mode).
- Records the two #1403 comment items (a static check for process
  substitution in POSIX-sourced libraries, and a pre-existing dash gap
  at _lib-read-config.sh line 35) as deferred follow-up, not addressed
  in this change.
- Records that the #1366 half of the batched PR this record's fix was
  split from is deferred to a git-native pre-push hook design, because
  reviewers found that resolving a push target from command text
  cannot be made sound (AgDR-0104).

* docs(#1403): fix a markdownlint heading false-positive in AgDR-0169

- A wrapped line started with "#687 and #1151.", which markdownlint's
  ATX-heading rule reads as a heading with no space after the hash.
  Rewords the sentence so no line starts with a bare issue reference.

* fix(#1403): block on an unreadable dispatcher library, not a silent exit 1

Hakim's A1 finding on the review: dispatch-bash.sh checked [ -f ] before
sourcing _lib-extract-pr.sh, under set -e. A file that exists but cannot
be read still fails that source, and the dispatcher then exits with
whatever code the failed `.` builtin reports (1). The harness only
blocks a tool call on exit 2, so an unreadable library used to let every
Bash command through unblocked, not just merges.

The check is now [ -r ], and an explicit exists-but-unreadable branch
blocks (exit 2) with a message naming the file before any attempt to
source it. A file that is entirely missing stays the tolerated
partial-install case it already was: is_merge_command stays undefined,
and the existing fail-closed check further down the script still runs
the merge gates on a merge-shaped command.

Adds two cases to test_dispatch_bash.sh: an unreadable library blocks
with a message naming the file, and a missing library does not itself
block a non-merge command (control, so an overly broad fix would also
be caught).

Refs #1403

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* docs(#1403): correct AgDR-0169 for the A1 fix and the library table gaps

The Decision section said the dispatcher fix did not edit the four
merge gates. Addendum 1 and Addendum 2 later added a guard to all
four. The Decision section now says so, while keeping the original
claim visible as history.

Consequences said test_dispatch_bash.sh covers four failure modes on
all four gates. The four failure modes run only against
block-unreviewed-merge.sh. All four gates are tested only for the
missing-library-under-POSIX case. The record now states both facts
separately.

The library table in Addendum 2 left out _lib-ops-root.sh, used by
three of the four gates, and _lib-tracker.sh, loaded only inside
block-unreviewed-merge.sh's opt-in posted-review check. Both are now
recorded. _lib-merge-behind.sh, added by the dev merge from #1386, is
now marked optional and its reason is stated: it only appends an
advisory note to a block that already fires for another reason.

Adds Follow-up 3 to Addendum 2 for Hakim's A1 finding: the dispatcher's
own guard for _lib-extract-pr.sh checked -f before sourcing under
set -e, so an unreadable file exited 1 before any gate ran, and the
harness does not block on exit 1. The check is now -r, with an
explicit exists-but-unreadable branch that blocks with a named file.
Records the fix's availability cost: an unreadable library now blocks
every Bash command, not only merges, and the operator must restore the
file outside the agent because the repair itself is a blocked Bash
command.

Refs #1403

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s and CI-first testing (#1420)

* feat: add a blocking-severity bar, delta re-reviews, and CI-first testing to Rex and Hakim

- Rex and Hakim now block a verdict only on a regression, a code-execution
  or approval-bypass vector, a correctness bug, or a failed acceptance
  criterion; everything else is advisory.
- A re-review reads only the delta since the last reviewed SHA, and CI's
  own check-run result replaces a third full-suite run.
- pr-workflow.md caps review rounds at two, and right-size-ceremony.md
  requires one ticket per trust-chain PR.

Refs #1418
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix: keep blocking handbooks and Glossary blocking under the new severity bar

- Add ENFORCEMENT: blocking handbooks and the mandatory Glossary check to
  the bar's does-not-narrow list in code-reviewer.md, so an adopter's
  blocking control still blocks.
- Reword the two-round cap so it stops new rounds only for non-blocking
  findings; a blocking finding always gets a delta re-review, whatever the
  round count.
- Align design-review/SKILL.md with the new writing-profile bar and update
  its test pin to match, so Tariq and Rex/Hakim no longer disagree.
- Add the CI-pending-or-absent no-approve rule, remerge-diff and rebase
  guidance for delta re-reviews, and the lower-effort-level note to both
  reviewer agents.
- Fix a direction typo, pick one outcome for missing builder evidence,
  align the trust-chain wording with rail 1, fix the AgDR's option
  wording, and remove two semicolons in workflows/code-review.md.

Refs #1418
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Squash-merge divergence from the v5.7.0 release PR creates phantom divergence
between main and dev. This merge makes the v5.7.0 squash commit an ancestor
of dev so future dev→main release PRs only see genuinely-new commits.

Refs #403
…#1427)

Restore the ahmedk20 and moussaws code-contributor rows in README.md.
The #1348 main-to-dev sync used -X ours on a real conflict and dropped
both rows from dev.

Update the skill count from 66 to 67 in AGENTS.md and
docs/whats-inside.md. The #1351 orbit skill raised the tree count to
67, but these two files still said 66.

Refs #1393


Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e pushed repo (#1428)

* fix(#1366): run pre-push commands from the git-native hook against the pushed repo

pre-push-gate.sh picked a target repository out of the Bash command's
text. It read that repository's own config from the wrong root too. PR
#1405 tried to fix this by parsing command text more carefully. Four
review rounds found new bypasses each time. Hakim's H1 finding stayed
open: a heredoc, a quoted string, a commit message, or an echo that
only mentioned a push could still run a repository's declared commands.

This commit moves command execution to .githooks/pre-push, the
git-native layer. Git resolves the pushed repository as that hook's
working directory, before the hook runs. There is no command text to
parse there. bin/run-configured-pre-push-checks.sh is the new script
that reads and runs a repository's own .pre_push.commands from that
resolved directory.

pre-push-gate.sh no longer runs any repository's commands. It only
reminds a session to install core.hooksPath when a clone has not. This
closes H1, H3, and L2 from PR #1405's review by removing the command
text parsing those findings targeted.

See docs/agdr/AgDR-0173-git-native-pre-push-command-execution.md for
the full design and the rejected alternatives.

Refs #1366

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1366): scope pre-push advice to ApexYard forks, disable resolution cache

Rex and Hakim reviewed PR #1428 round 2. Rex found B1: a push to a
managed clone from an ops-fork session ran no configured command, and
the gate reported nothing. Hakim approved, with A1 advisory: the
session-scoped resolution cache could serve a config from the wrong
repository.

The maintainer decided the plain rule. ApexYard runs configured local
pre-push commands only inside an ApexYard fork that has the git-native
hook installed. A managed-project clone gets no local pre-push check
from ApexYard, ever. Its own CI is the backstop. AgDR-0115 already
forbids ApexYard from setting core.hooksPath in a managed clone. This
commit adds no path that does.

pre-push-gate.sh now names the repository it checked, the session's own
working directory, in every message. It prints the core.hooksPath
install advice only when that repository is itself an ApexYard fork (a
.apexyard-fork marker, or the fork's own .githooks/pre-push plus
bin/install-git-hooks.sh). In any other repository it prints at most a
one-line note that ApexYard runs no local checks there, and never
suggests core.hooksPath. This closes Rex's B1 and B2 findings.

bin/run-configured-pre-push-checks.sh now exports
APEXYARD_DISABLE_RESOLUTION_CACHE=1 before it sources the config reader.
This closes Hakim's A1. The prior claim that the config-root pin matched
an existing precedent elsewhere in the tree is removed. No such
precedent exists.

AgDR-0173, docs/getting-started.md, and .claude/rules/pr-workflow.md
state the managed-clone rule plainly, with a before-and-after note.

Refs #1366

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1366): resolve fork status from ops root, fix CI cache isolation

Rex and Hakim both blocked round 2's CI on the same cause. Two more
findings came with it.

bin/run-hook-tests.sh exports APEXYARD_DISABLE_RESOLUTION_CACHE=1 for
its whole suite. Case 8 of test_run_configured_pre_push_checks.sh built
an "unfixed" control copy of the runner script to prove the fix
mattered. Under the suite export, that control copy never took the
poisoned cache either, so the case failed in CI. Both case-8 subshells
now run with env -u APEXYARD_DISABLE_RESOLUTION_CACHE, so the script's
own export line decides the outcome, not the suite's. This commit does
not touch bin/run-hook-tests.sh.

The round-2 fork check trusted files a repo ships about itself: a
.apexyard-fork marker, or a .githooks/pre-push and bin/install-git-hooks.sh
pair. A managed repo could ship either shape and talk the hook into
recommending core.hooksPath for itself, exactly what AgDR-0115 forbids.
pre-push-gate.sh now decides fork status from resolve_ops_root, the
pin-first resolver in _lib-ops-root.sh the rest of the framework
already trusts for this question. It gives install advice only when
the working-directory repo equals the resolved ops root. A repo that
cannot be resolved gets no install advice either, only the managed-
clone note.

AgDR-0173's Context said bin/install-git-hooks.sh refuses to install
through /handover. It does not refuse. /handover never calls it. Fixed
the sentence.

Refs #1366

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ak check (#1432)

* fix(#1431): exempt upstream owner and repo in staged leak check

What: the staged pre-commit leak check now exempts the upstream
remote's owner login and repo slug, not only origin's.

Why: an ops fork's origin remote is the fork itself. The public
framework lives at the upstream remote. A registry commonly lists
the framework repo, and an adopter's login often equals a registered
project name. Before this fix, a commit that cited an upstream issue
as owner/repo#N blocked on that coincidence, every time. This mirrors
the owner-login exemption #1400 already shipped for the runtime hook,
narrowed the same way: only the @owner and owner/repo-slug forms are
exempt. A bare, standalone mention of the name still blocks.

Trade-off: the runtime hook used by the tracker adapters,
check-private-refs-runtime.sh, has no owner exemption at all today.
It is not fixed here. Its bare-name exemption already keys on the
actual target repo. That target is not hardcoded to origin. So it
does not share this issue's specific origin-only gap. Adding the
owner exemption to it is a separate, larger change and stays out of
scope for this fix.

Refs #1431
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1431): fail closed on bad bytes, verify name-repo pairing

What: round 2 of review. Fixes a fail-open regression in the owner
exemption, tightens the upstream bare-name exemption, and re-pins
the fail-before test to a fixed commit.

Why: Hakim found that owner_bare_mention_remains failed open. A raw
non-UTF-8 byte in a staged blob makes tr and BSD sed error out under
a UTF-8 locale. The old code treated that error as "no bare mention
remains" and exempted a file it never actually scanned. Every step
now runs under LC_ALL=C. A raw byte is then just a byte, not an
encoding error. Any step that still fails returns "a bare mention
remains" instead of exempting. The second sed's own delimiter, #,
now escapes too.

Hakim also found a narrower gap. A registered name could
coincidentally equal upstream's bare repo name while pointing at an
unrelated private repo. The old code exempted it on the name string
alone. The registry parser now pairs each name with its own repo
field, and the exemption only fires when that pairing actually
matches upstream.

Rex found that fail-before case 8 read the hook from upstream/dev.
Once this PR merges, that branch holds the fix, so the case would
fail forever in any fork with an upstream remote. It now reads a
pinned commit, the tip of upstream/dev before this fix branched off.

Six tests are new. The HIGH-1 regression case, and its own
fail-before, pinned to round 1's commit. owner_bare_mention_remains
did not exist before this PR, so there is no earlier commit to pin
that one to. The MEDIUM name-repo pairing case. Two isolation cases
Rex asked for: origin-owner-only with no upstream configured, and
upstream-slug-only with no coincidental name match.

Trade-off: none of this touches the pre-existing macOS grep gap.
Bad bytes can make grep silently skip a line there too. It also
leaves the runtime hook's missing owner exemption alone. Both stay
out of scope, per the PR's own round-2 instructions.

Refs #1431
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1431): guard the empty name-repo-pairs array on bash 3.2

What: round 3 of review. registry_name_repo_matches now returns
early on an empty array, before the loop that used to crash it.

Why: Rex found that this hook runs under bash 3.2 on macOS. Its
shebang is #!/bin/bash, and that resolves to the system binary
there. In bash 3.2, expanding an empty array's [@] form under set -u
raises "unbound variable." name_repo_pairs is empty whenever every
registry entry uses the plural repos: form. Only a singular repo:
field feeds the pairing. Add an upstream remote and a registered
name equal to its bare repo name. The old code called the loop on
every staged commit. That crashed the whole hook with exit 1. dev
exits 0 or 2 here. Round 2 exited 1 instead. CI runs a newer bash,
so it never saw this.

The fix checks the array's length first. That check does not expand
the array's elements, so it is safe on bash 3.2 too. It returns
before the loop ever runs.

Also added one line noting a Hakim advisory: the upstream exemption
trusts that upstream IS the public framework repo. A misconfigured
upstream pointed at a private repo would get the same exemption.
That trust boundary is not new here, and fixing it is a separate
concern.

Two new tests use a plural-only registry with upstream configured.
A registered name there equals upstream's bare name. A clean file
passes. A file with a real, unrelated leak still blocks. Ran the
whole suite once under macOS's actual /bin/bash (3.2.57) to confirm.

Trade-off: the same missing guard likely exists for other arrays
already on dev, such as workspaces[@]. Those are not touched here,
per this round's own instructions. They get their own ticket.

Refs #1431
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ok (#1429)

* feat(#1418): add an advisory writing-profile check to the pre-push hook

Add bin/check-writing-profile.sh. It reports a semicolon in prose and
a sentence over 25 words in the Markdown lines a push adds. Wire it
into bin/run-pre-push-checks.sh as a non-blocking step, so a fault
reaches the contributor before it costs a review round.

Refs #1418
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

* fix(#1418): fix Linux fence detection, exit-0 edge cases, and rename false positives in the writing-profile check

Rex found the script reported nothing on Linux. An escaped backtick in
the fence check reads as GNU grep's own buffer-start operator, not a
literal backtick. Replace fence detection with a bash case pattern,
which has no regex engine underneath it to diverge.

Fix two exit-0 contract breaks Rex found on bash 3.2. A bare --range
looped forever. A bare --files exited 1. Wrap the whole script in a
subshell so an unanticipated error still reaches the final exit 0.

Fix the Test 7 fixture so GNU xargs no longer runs shellcheck on zero
files. Strengthen its assertion to check for an actual finding line,
not the step's own banner text.

Fix a rename false positive Rex found. Diff the whole range once with
rename detection on, instead of one path at a time. A pure rename now
reports nothing instead of the whole file.

Apply Hakim's cheap advisories:
- Read text into awk through ENVIRON, not -v. A printable escape
  sequence can no longer become a real control byte in an excerpt.
- Strip any control byte already raw in the source line.
- Harden every git diff call against a contributor's own git config.
- Disable git's filename quoting for a correctly read non-ASCII name.
- Count findings in a variable, not a return code, which wraps at 256.
- Cap added lines at 3000, override with WP_MAX_ADDED_LINES, and print
  one notice past the cap.
- Move a wiring-test fixture off a hardcoded /tmp path.

Skip YAML frontmatter and multi-line HTML comment content for their
whole extent, not just their delimiter line, matching the PR body's
existing claim. Add "writing-profile" to the runner's --list output.

Refs #1418
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGvb2ba4gyT3TN8L2uMLTn

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
/threat-model Step 1b copied the DFD's own ```mermaid fence into
${dfd_mermaid}, and Step 5b wrapped that text in a second fence.
GitHub kept the inner fence line as diagram text, and Mermaid
rejected the result.

- print only the lines inside the DFD's fence in extractor 1
- put the Step 5b closing fence on its own line
- add test_snapshot_mermaid_fence.sh. It cuts both steps out of
  SKILL.md and runs them against the generator and the template.

Refs #1409

Co-authored-by: Ahmed <ahmed.abdelaliem@gmail.com>
…d template claim (#1413)

* fix: warn on an empty DFD snapshot section, and drop the unused template claim

Two points of #1358 remained open after #1399.

- /threat-model Step 1b now warns when a snapshot section extracts to
  nothing. The warning names the heading it tried. The step continues,
  because a DFD may classify no data. The checks use plain POSIX tests
  instead of bash-only indirect expansion. The step runs under dash.
- /dfd no longer resolves a template that generate-mermaid.sh never
  reads. The step now states that an override in custom-templates has
  no effect on generated output.
- describe what the generator writes: a fixed skeleton with
  placeholder nodes and rows, plus both YAML reports.
- add test_snapshot_empty_section_warning.sh. It runs Step 1b from
  SKILL.md against four DFD fixtures.

Refs #1358

* test: run Step 1b under dash, and name the heading-contract rule

- add a dash case to the empty-section test. A bash-only line such as
  ${!name} passed the bash-only cases. dash stops on that syntax, so
  the new case catches it.
- refer to the heading-contract rule by name, not by number. A number
  breaks when someone adds or reorders a rule.

Refs #1358

---------

Co-authored-by: Ahmed <ahmed.abdelaliem@gmail.com>
Line 93 sat in an unquoted heredoc with bare backticks. So bash ran
`/threat-model` as a command. The run printed an error on stderr. The
generated sentence also lost the command name.

- escape the two backticks. Every other backtick in the heredoc is
  already escaped.
- add test_generate_mermaid_heredoc.sh. It checks the exit code,
  stderr, and the generated sentence. It also scans each unquoted
  heredoc body for a bare backtick, and fails on a heredoc opener it
  cannot classify.

Refs #1410

Co-authored-by: Ahmed <ahmed.abdelaliem@gmail.com>
…#1143)

Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 24.1.0 to 24.2.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](DavidAnson/markdownlint-cli2-action@6bf21b0...21c1be1)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 24.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#1139)

Bumps the codeql-action group with 3 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.37.3 to 4.38.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@e4fba86...2892aa5)

Updates `github/codeql-action/analyze` from 4.37.3 to 4.38.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@e4fba86...2892aa5)

Updates `github/codeql-action/upload-sarif` from 4.37.3 to 4.38.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@e4fba86...2892aa5)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…1364)

* fix(#1362): exempt dependency-bot branches from the branch-name gate

validate-branch-name.sh refused any push to a dependabot/ or renovate/
branch. Those names cannot follow {type}/{TICKET}-{description}, and there
is no ticket to reference when the bot opens the PR, so a maintainer could
not push the changelog fragment and ticket reference the merge gates
require. The bot PR was unmergeable by construction.

CI had already made the opposite call: pr-title-check.yml exempts
dependabot/, sync/ and release/ head refs (#588), and states that its
pattern is intentionally aligned with the local hooks so that anything
passing the hooks also passes CI. The hook exempted sync/ and release/ but
not the bot prefixes.

The exemption is anchored to the prefix and covers the branch name only.
Every other gate still applies to what is pushed. Six tests cover it: the
two exempt prefixes, plus four negatives pinning the anchor (a nested
feature/dependabot/ path, a dependabot-manual/ lookalike, a name merely
containing the word, and renovate-bot/).

Closes #1362

* fix(#1362): align the CI title check with the hook's bot-branch exemption

The hook exempted dependabot/ and renovate/, while the CI title check exempted
only dependabot/, sync/ and release/. That made the workflow's own claim false
for renovate/: a Renovate PR passed the local push gate and then failed CI on a
title it was never going to carry. Both lists now carry both prefixes, and each
side names the other in a comment, since nothing mechanical binds them.

The exemption now requires a non-empty first segment. A bare dependabot/ is not
a ref git accepts, so exempting it widened the surface for nothing.

Adds AgDR-0175. Rail 1 of the AgDR rules makes a change to .claude/hooks/**
material at any diff size, and this one widens a gate-relaxing surface: the
segment after the prefix is the bot's to choose, which is wider than the
exact-literal exemption AgDR-0129 recorded. The record states the residual
risk and names the backstops that still apply.

Adds six near-miss tests — dependabotx/, renovate-fix, feature/dependabot-x,
Dependabot/npm/x, and both bare-prefix forms — so a future edit to the anchor
fails rather than silently widening it.

---------

Co-authored-by: Ahmed <ahmed.abdelaliem@gmail.com>
* fix: detect >&word redirects and the sed write forms

The write detector reported no write for three shapes that write a
file, so the ticket gate let them pass with no ticket (#1414):

- `>&word`, the second bash spelling of "stdout and stderr to a file".
  The target class rejected a leading `&` to keep `2>&1` and `>&2` out,
  and that also dropped `>&file`. A second alternative now matches
  `>&` only when the word is a file name. All-digit words and words
  that start with `-` stay descriptor copies and closes.
- sed -i inside a group of short flags (-Ei, -ni, -si), with an
  attached suffix (-ie), and as --in-place. The letter class leaves
  out e, f, and l, because those take the rest of the group as an
  argument.
- the sed w / W command and the s///w flag. The matcher is narrow on
  purpose, because the detector prefers a missed write to a blocked
  read (AgDR-0011). It needs sed on the line, a command position, and
  whitespace before the file name. Its known misses and one known
  false positive are listed in the code.

The redirect pattern and its strip now live in two shared constants.
The matcher and both extractors used to carry their own copy. Round 5
of #886/#926 showed that copies drift, and a drift is a bypass.

sed w targets come from the whole command, because the segment split
is not quote-aware and breaks `sed -n 'p;w out' f` at the `;`. Without
a target, the gate would block the command even with a ticket (#1396).

Refs #1414

* fix: stop the #1414 detector blocking 2>&1 reads and hiding sed -i

A code review and a security review of the first commit found two
regressions. Both are fixed here.

- `>&word` read the `1)` in `out=$(cmd 2>&1)` as a file name, so the
  gate blocked common read-only commands with no ticket. After the
  digits, the word can no longer end at `)`, a backtick, or a quote.
  One opening quote is still allowed, so `>&"out.txt"` is still a write
  and `>&"2"` is a descriptor copy.
- a sed `w` target such as `/dev/stdout` could stand in for a sed -i
  edit whose file the extractor misses. `sed -i "s/a/b/w /dev/stdout"
  src/app.ts` then passed, where the old gate failed closed. sed `w`
  targets now come only from the whole-command pass, and only when the
  command holds no sed -i edit. The single-target extractor no longer
  returns them.

The reviews also found gaps, now closed:

- the --i, --in, and --in-pl prefixes of --in-place
- the , : @ % s delimiters, and a space after the opening quote
- false positives on `find ... -print` and `sed -n 1p f | grep 'w x'`.
  The short-flag class now holds only real sed flags, and the sed w
  scan stops at a spaced |, &&, or ||.
- warn-review-marker-write.sh kept its own copy of the sed -i pattern.
  It now reads the library's pattern, with the old literal as fallback.

The code comments now list the accepted misses and false positives.

Refs #1414

* fix: hold back sed w targets beside any unextracted write

Both reviews of the second commit found that a sed `w` target could
still hide a write from another family with no extractable target:

    awk -i inplace 1 src/app.ts; sed -n 'w /tmp/x' in.txt
    python3 -c '...'; sed -n 'w /tmp/x' in.txt
    go run ./gen && sed -n 's/x/y/w /dev/stdout' out.txt

dev blocks each one, because the gate sees an empty target list. The
exempt `w` target turned that into a pass. The sed -i skip in the
second commit covered only sed -i.

sed `w` targets are now held back when the segment pass found no
target and any other write family fired. rm counts only beside cp, mv,
dd, or install, as in bash_command_is_deletion_only, so
`rm x; sed -n 'w /tmp/y' f` keeps its target. When the list is not
empty, the `w` targets are added, because an extra target can only add
a reason to block. That also catches `sed -i 's/a/b/w src/b.ts'
/tmp/x.txt`, which writes src/b.ts and passed on dev.

Also:

- a backslash now ends the digit word after `>&`, so
  `bash -c "sh -c \"make 2>&1\""` stays a read
- `-I`, which BSD documents as in-place and GNU rejects, now counts
- the comments list `-iname` as a false positive and a spaced ` | `
  inside a script as a miss

Refs #1414

* test: pin every family in the sed w hold-back check

Both reviews of the third commit noted that the tests pinned only four
families in _bdw_detects_other_write. Dropping node or curl from that
helper left every suite green. A drift of that kind would reopen the
sed w decoy gap for the dropped family.

- add one decoy case for each family with no extractable target,
  including the python, node, and ruby heredocs, and two cases that pin
  the redirect and tee entries. Deleting any of the 17 entries now
  fails a test.
- note in bash_command_appears_to_write that its list must stay in
  step with the helper
- say in the hold-back comment that require-migration-ticket.sh exits 0
  on an empty list, so only the ticket gate fails closed there
- list `>&\src/app.ts` beside `>&2"q.ts"` as accepted residue

No logic change.

Refs #1414

---------

Co-authored-by: Ahmed <ahmed.abdelaliem@gmail.com>
* fix(#1336): surface tracker CLI errors at hook call sites

* fix(#1336): guard the mktemp trap and assert exit codes in the diagnostics tests

Three fixes from review round 1.

The EXIT trap was set unconditionally, so when mktemp failed the hook ran
`rm -f /dev/null`. Under root that removes the device node, and every later
redirect to /dev/null on that host writes a regular file instead. The trap is
now set only when mktemp succeeded.

The tests asserted the diagnostic text but discarded the exit code, so a
regression that printed the right message and then exited 0 — the fail-open
direction on a trust-chain gate — would have passed all six cases. Both the
output and the code now come back through globals rather than a command
substitution, which runs in a subshell and loses the code.

The migration-gate case could SKIP when the hook exited before its lookup, and
the suite still passed. Exiting early on a migration path is itself the
fail-open direction, so that branch is now a FAIL.

---------

Co-authored-by: Ahmed <ahmed.abdelaliem@gmail.com>
* docs: design dependency audit ecosystem dispatch

Record the npm and Python runner-map contract before implementation. The design keeps the skill, auditor agent, and copied CI template consistent and makes incomplete scans visible.

Refs #1359

* docs(#1359): bound audit inventory and record scanner decisions

Close the review blockers in PR #1430 by parsing Python locks as data,
recording scanner alternatives and network limits, and restoring AgDR
front matter. Clarify the inexpensive advisories so implementation has
one contract for severity enrichment, workspaces, unknown findings,
shared normalization, and trusted tooling.

Refs #1359

Co-Authored-By: Codex <noreply@openai.com>

* docs(#1359): renumber dependency audit decision record

The current dev branch now contains a different AgDR-0175. Rename this design decision to the next free number and update its linked references so the two records can coexist in the PR.

Refs #1359

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
atlas-apex and others added 23 commits October 19, 2026 22:21
…laim (#1811)

* fix(#1533): let a valid worktree start its own ticket and log only real take-overs

A valid linked worktree no longer treats shared current-ticket as a foreign overwrite.
Print the take-over audit only after a successful write, with control characters stripped.

Refs #1533

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(#1808): name the warn claim by a hash of the marker identity

A long marker path no longer breaks the once-per-session claim write.

Refs #1808

* fix(#1808): keep the ticket-marker hot path within its process budget

Rewrite A2 control strip and the warn-claim name as pure bash so the
edit path no longer forks for tr/cksum/awk. Reword the start-ticket
rationale so the old-marker reader scan no longer flags it.

Refs #1808

* test(#1808): report a missing fail-before base without a skipped case

When the pinned fail-before commit is absent (shallow CI, plain export),
these suites printed a line starting with SKIP. bin/run-hook-tests.sh
treats any ^SKIP line as a failed skipped case. Match the rex_findings
suite: print NOTE: skip fail-before (...) so the runner still passes.

Refs #1808

* test(#1808): report a missing fail-before base as a note in the AC suites

Print NOTE instead of SKIP when the fail-before base ref is missing so run-hook-tests.sh does not treat it as a failed suite.
Refs #1808

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…1823)

* chore(#1798): add shape-probe and recount checks to the Rex prompt

Add two review duties to close C4 eval blind spots: probe nearby input
shapes after a gate-matcher or command-parser change, and recount stated
counts or required settings in docs against the source. Both land in the
existing evidence sections so Rex treats a miss as a correctness finding.

Refs #1798

* chore(#1798): make an unverifiable count or settings claim a finding

Strengthen the recount check so Rex must also check related settings
named by the diff or cited docs, and must raise a kind-3 finding when
the source cannot be read instead of leaving the claim as Unverified.

Refs #1798

* docs(#1798): record the targeted blind-spot eval

Add the k=3 flip table for the two blind-spot entries so the PR holds
measured catch rates for the shape-probe and recount prompt edits.
This is a targeted pass, not a zero-flip claim for the full corpus.

Refs #1798

* docs(#1798): add the C4 no-flip table

Add the C4 no-flip table for the 13 non-blind-spot corpus entries. It compares the PR #1797 prompt with the fbc85b5d prompt at k=3 and records verdicts, defects caught, false alarms and flips.

3 of 13 entries flip from approve or comment to CHANGES REQUESTED: rex-773, rex-1683, rex-1711. The likely cause is the new rule that turns an unverifiable claim into a finding. AC3 is now Not met. The k=8 follow-ups are listed, not run.

Refs #1798

* chore(#1798): keep unverifiable claims as notes, not blocking findings

Remove the sentence that made an unverifiable count or settings claim a blocking finding. Keep the recount rule, the required-set check and the shape probe. An unverifiable claim goes back to an Unverified note.

Why: the C4 run showed 3 of 13 entries flip from approve or comment to CHANGES REQUESTED (rex-773, rex-1683, rex-1711). The cause was this sentence. The three runs per entry blocked on claims that Rex could not check.

Refs #1798

* docs(#1798): record the no-flip re-run

Record the k=3 re-run of the prompt without the blocking sentence on rex-773, rex-1683, rex-1711 and rex-1620.

rex-1683 and rex-1711 no longer flip. rex-773 still flips (CHANGES REQUESTED against a COMMENT majority), and rex-1620 D2 falls to 1 of 3. The pass condition fails. AC2 and AC3 stay open.

Refs #1798

* docs(#1798): record the rex-773 k=8 result

Record the k=8 run on rex-773 for the baseline and the a7b3 prompt. The baseline gives 6 COMMENT and 2 CHANGES REQUESTED. The a7b3 prompt gives 1 COMMENT and 7 CHANGES REQUESTED. The majority flips, so AC3 is met with one flip on a borderline entry.

Update AC2: shape-probe D1 met 2 of 3, rex-1620 D2 partly met. The recount and settings work continues in #1815.

Refs #1798

* docs(#1798): add the blank line markdownlint needs before a list

MD032 failed the pre-push check on the report.

Refs #1798

* docs(#1798): correct the rex-1711 baseline, the cost totals and the AC2 row

Correct the rex-1711 baseline to C, C, A (majority COMMENT). With prompt a7b3 the entry moves from COMMENT to APPROVED, toward the frozen key. The CEO accepted both flips (rex-773 and rex-1711) in the #1798 decision comment.

Add the cost totals: 106 runs and 4,361,584 tokens for #1798, with the parts listed. Fix the AC2 row to say rex-1620 D1 is 0 of 3 and D2 is 1 of 3 against 0 of 6. AC3 is met with two accepted exceptions. Name prompts by hash, not by commit SHA.

Refs #1798

* docs(#1798): fix two writing-profile findings in the flip report

Split a 28-word sentence in the Status line and replace a semicolon in prose.

Refs #1798

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
)

* refactor(#1796): batch carry-over walk and limit each gh call

One GraphQL query loads the base tip and up to 10 first-parent steps.
Rex and CEO share that chain in one gate process. apexyard_gh ends a
stalled forge call early so it cannot use the whole dispatcher budget.

Refs #1796

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(#1796): cover batched walk, shared memo, and gh timeout

Each shortcut has a paired mismatch that still blocks. A stalled gh
stub ends at the limit and the review gate exits 2 with a clear line.

Refs #1796

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(#1796): record walk batching and per-call gh limit

AgDR-0256 gains an evolution entry for the GraphQL walk, the shared
memo, and apexyard_gh. The C4 container note tracks the same change.

Refs #1796

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(#1796): set dispatcher timeout from the #1770 p95

Simulated #1770 p95 after the walk cut is 14885 ms. Five times that is
74.4 s, so the Bash dispatcher timeout moves to 75 s inside the CEO band.

Refs #1796

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(#1796): fix a line that markdownlint read as a heading

Refs #1796

* fix(#1796): keep the merge-gate suite inside the CI time cap

CO9's stalled-gh stub used a shell `sleep 120` under FORCE_WATCHDOG.
The watchdog killed only the stub PID; bash `$()` in the GraphQL fetch
kept waiting on the sleep child for the full 120 s, so
test_block_unreviewed_merge.sh hit the runner's 120 s per-suite cap.
Write the timeout flag before signalling, reap direct children with a
portable ps/awk walk, and use `exec sleep` in the stall stubs.

Refs #1796

* fix(#1796): keep carry-over walk on github.com only

A host-qualified repo that is not github.com returned true against a
twin on github.com because the parser dropped the host and GraphQL had
no --hostname. Return unknown with no forge call for that shape, pass
GraphQL string variables with -f, and put tracker_review_at_sha under
the same per-call gh limit.

Refs #1796

* test(#1796): cover host-qualified unknown, -f owner, reviews timeout

Paired H1 cases prove a non-github.com HOST/OWNER/REPO returns unknown
with zero forge calls while plain OWNER/REPO still carries over. A3
checks an all-digit owner uses -f. A2 stalls the reviews API under the
per-call limit and expects rc 2. Carry-over gh mocks accept -f so the
block-unreviewed suite matches production GraphQL flags.

Refs #1796

* test(#1796): pair the shared carry-over result with a mismatch case

The shared Rex/CEO carry-over result had only a positive test. This test completes the pairing: in one dispatch the Rex marker SHA carries over and a CEO marker SHA whose chain does not line up does not, so the results are true false with one forge call.

Refs #1796

* fix(#1796): pin the carry-over walk to github.com and require three segments

The GraphQL call in the carry-over walk now passes --hostname github.com. A GH_HOST setting can no longer send the walk to another host.

The github.com form of the repo argument now accepts exactly three segments (github.com/OWNER/REPO). Four or more segments return unknown, as merge_repo_is_valid does.

The fallback fetch URL is now built from the parsed owner and name. An explicit github.com/OWNER/REPO no longer gives https://github.com/github.com/OWNER/REPO.git.

Paired tests cover the hostname argument, the four-segment case, and the fetch URL.

Refs #1796

* fix(#1796): pin the walk host only for an explicit github.com repo

The last commit added --hostname github.com to the GraphQL walk for every
repo form. A plain OWNER/REPO passes no --hostname in the rest of the gate
and follows GH_HOST, so the walk must follow GH_HOST too.

Pass --hostname github.com only for the explicit github.com/OWNER/REPO
form, the rule block-merge-on-red-ci.sh uses. Replace the GH_HOST test
with a pair: explicit form sends the flag, plain form sends none.
Update AgDR-0256 item 5 with the hostname rule, the A7 fetch URL and the
A8 three-segment rule.

Refs #1796

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ets (#1807)

* fix(#1803): block split merge phrases and the >& and >! redirect targets

A merge phrase split by a redirection or by quoted and expanded word spans is now an unresolved target. All four merge gates block it. The redirect-target guard now checks file targets after the combined-output and clobber operators. Refs #1789, Refs #1790, Refs #1803

* test(#1803): cover the split-phrase and redirect-operator gaps

Adds cases for split merge phrases and the combined-output and clobber redirect targets, plus the descriptor forms that must stay data. Refs #1803

* fix(#1803): scan split merge phrases without false positives and fail closed on scan errors

Variable words now stand for a phrase word only at command position, so ordinary commands with several variable arguments pass again. Host or verb words built from constructs the scan cannot read count as unknown. The scan matches the host name after its last slash, reads nested shell text, and skips host options between phrase words (#1812). It handles &> and &>> as redirections, bounds the dollar lookahead to a fixed window, and runs cheap detectors first. When awk fails or the input is too large, the four gates block with their own could-not-scan message.

Refs #1803

Refs #1812

* test(#1803): cover split phrases, variable arguments, spaced redirects and scan failures

The split-phrase suite pairs each blocked class with a data-only allow case in all four gates. It adds variable-argument false-positive cases, host option cases, a timing case for 120 KB of dollar padding, and scan-failure message checks. The rework suite covers spaced and combined redirections. Test text uses placeholder words.

Refs #1803

Refs #1812

* docs(#1803): state exactly what the merge scan blocks in AgDR-0196

Known limits now list the blocked redirect forms, unknown expansions, host options (#1812), nested shell text, size limits and the scan-failure message. This keeps the record equal to the gate behaviour.

Refs #1803

Refs #1812

* fix(#1803): keep literal last-segment hosts and scan prefixed and nested merge phrases

Quoted expansions before a path slash no longer force an opaque host, while
unquoted expansions stay opaque. Non-host prefixes and substitution bodies now
match a phrase window that has at least one literal word, and a non-clear split
scan fails closed in the unresolved-target check.

Refs #1803
Refs #1812

* test(#1803): cover last-segment allows and prefixed nested merge blocks

Add paired allow and unsafe cases for quoted path hosts, glob suffixes, opaque
three-word windows, command prefixes, and substitution bodies, including gate
coverage with run-time placeholders. Oversize split-scan failures stay
fail-closed in the long-statement bound.

Refs #1803
Refs #1812

* docs(#1803): record last-segment host rule and open opaque-window classes

State the quoted versus unquoted last-segment host rule, the literal-peer
window requirement, nested substitution scanning, and the opaque-only classes
that stay clear without a following PR number.

Refs #1803
Refs #1812

* fix(#1803): stop ordinary argument lists from matching the merge window

A non-host prefix continued the phrase search, a broken phrase restarted
the window, and a bare number confirmed a window of only opaque words.
Together these blocked ordinary commands, such as a script with three
quoted arguments and a number, in all four merge gates.

A bare PR number now confirms an all-opaque window only at command
position. Behind a non-host prefix, the host word or the subcommand word
must be literal. The words builtin, time, nohup and ! keep the next word
at command position. The window start and restart logic moves into two
helper functions.

The rework suite adds ten ordinary argument-list allow cases and six
paired unsafe cases, in the library and in all four gates.

Refs #1803
Refs #1789

* docs(#1803): correct AgDR-0196 scan limits and open classes

Known limits now say which substitution bodies go into the nested scan.
A redirect target or a here-string word goes into it only when a shell,
eval or source word appears in the command. An unquoted glob host stays
opaque. The new window rule and the command-position words are recorded.

Two contexts are open classes owned by #1824: a command substitution in
a redirect target or a here-string word, and a substitution body with a
closing parenthesis inside quotes or as a case pattern. The all-opaque
window with no bare PR number, and the prefixed window with an opaque
host and subcommand word, are also recorded as open classes.

Refs #1803
Refs #1789

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* fix(#1748): block git aliases that run push or commit

Resolve git aliases before the Bash gates read the command. An alias that expands to push or commit now reaches the same gates as the plain verb. Before this change an alias hid the verb from every gate. The resolver lives in _lib-git-global.sh and the dispatcher calls it. See AgDR-0253.

Refs #1748

* test(#1748): cover git alias resolution in the Bash gates

Add the AgDR-0253 matrix, fail-before checks and timing test. Source the alias library in the existing push and branch-name suites so they run the new resolver.

Refs #1748

* docs(#1748): record the git alias resolution decision

Add AgDR-0253 and a short note in the git conventions rule. The AgDR keeps the relative performance budget only and links #1777 and #1780 for the base dispatcher timeout.

Refs #1748

* test(#1748): run the alias suite without an upstream ref

Resolve fail-before from upstream/dev, origin/dev, or the parent commit; skip when none exists.

Refs #1748

* test(#1748): pin the alias fail-before base commit

Use the pre-change merge-base as the only fail-before source.

Refs #1748

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(#1748): run the quick alias matrix by default

CI invokes this suite with no args under a 120s per-test cap; the full matrix exceeds it.

Refs #1748

* fix(#1748): parse alias config with NUL records and match path-qualified git

Read each config view with git config -z into a temp file and parse key/newline/value records, so a newline in a value cannot forge a second alias and a space in a subsection name still resolves. Use the same candidate boundary class as _git_sub_re so a path-qualified git is scanned. Match single-quoted -C and --git-dir values in the no-jq payload pattern so row 5 closes for that quote style.

Refs #1748

* test(#1748): cover NUL config parse, path-qualified git, and single-quoted no-jq values

Add paired block and allow cases for a newline in an alias value, a space in an alias subsection name, path-qualified git candidates, and single-quoted -C/--git-dir values when jq is missing. Note that matrix fail-before tallies are printed and not asserted.

Refs #1748

* docs(#1748): rewrite alias residuals at class level and record review fixes

Describe residuals R2, R3 and R6 by class and acceptance reason with no runnable examples. Correct R6 so a backslash-escaped command word is not listed as a skip. Record the NUL config parse, boundary align and single-quote payload fixes in Architecture evolution.

Refs #1748

* fix(#1816): close G1-G8 command-word and alias gate shapes

Normalise quoted and case-variant git command words before routing. Treat an expansion in the command-word or verb position as unknown and route to both gate groups when the rest can be a push or a commit. Decode quoted verbs with the shared word reader. Block an alias when the target repository cannot be resolved statically or when a config path write may replace the alias before git runs. Builtin verbs other than push and commit stay allowed.

Refs #1816

* test(#1816): cover G1-G8 shapes with paired allow cases

Add Gate shapes cases for every class in the alias suite, built at run time so exact bypass forms stay out of the tree. Quick mode includes the new cases. G6 repeats the B3 single-quoted -C coverage under the Gate shapes group.

Refs #1816

* docs(#1816): move G1-G8 into AgDR-0253 decisions

Record the class-level decisions for G1 to G8, narrow the related residuals, and add an architecture evolution note that links #1816. Exact payloads stay private.

Refs #1816

* fix(#1748): close the round 3 alias and directory-read gaps

B5: A rejected cd argument read an unset variable under set -u and
exited 1, which is not a block. The directory reader now sets the
variable first, so the path fails closed with exit 2.

B6: The alias check matched the git command word with a case-sensitive
pattern. It now matches after quote removal and case folding.

B7: The command word and the verb allowed only one quote character
between letters. Both are now normalised: any number of empty or
partial quote pairs and backslash escapes are removed. ANSI-C quoting
and brace expansion in either position count as unknown and block when
the rest can be a push or a commit. The same applies inside heredocs
fed to a shell.

B8: An expansion as the verb or the command word passed in later
argument positions and behind wrappers, groups, subshells and
conditions. It now blocks in all of them when the rest can be a push
or a commit.

B9: An alias run in a repository that the resolver cannot resolve
statically passed. This now blocks, including unquoted expansions in
target options, directory-stack changes, redirected directory changes,
nested shells and soft directory-chain failures. AgDR-0253 R2 no longer
lists the soft forms as allowed, and the G7 row agrees.

Builtin read-only verbs stay allowed, including with a quoted or
upper-case command word. Tests add paired block and allow cases per
class.

Refs #1748
Refs #1816

* fix(#1748): use a fixture cd target in the round-1 alias regressions

Root cause: round-1 regressions 2 and 3 used the host path /private/tmp
as the cd target. This path exists on macOS only. The Linux CI runner
(ubuntu-24.04) has no /private directory. On Linux the cd target in
regression 2 is missing, so the resolver cannot follow the directory
change. The round 3 rule (B9, unresolved alias target blocks) then
blocks the unknown verb "gates" with exit 2. The test expects exit 0.
Regression 3 expects exit 2 for pushd, so it passed on both hosts.

Evidence:
- The CI log shows only "Round-1 regressions/2/bash/resolver" and
  "isolated regression 2" fail, both with the B9 message.
- A direct resolver probe on macOS with an empty HOME returns 0 for
  'cd /private/tmp && echo "git gates"' and 2 with the B9 message for
  the same payload with a missing directory.
- A scratch copy of the suite with /private/tmp replaced by a missing
  path gives the same two failures as CI (579 passed, 2 failed).

The resolver is correct. A cd into a directory that does not exist is
a genuinely unresolvable target, and B9 must block it. The test now
creates its own cd target in the fixture. A new paired case asserts
that a cd into a missing fixture directory still blocks on every host.

Refs #1748

* fix(#1748): stop quoted text and shell-word prose from tripping the alias gate

The round 3 review found two false blocks on ordinary commands.

Quoted text: the command-word and verb check treated any quote in the
match as a hidden command word. A quote that opens a string before the
command word is not hidden. One quoted shell word that holds both words
(a search pattern, a log grep pattern, a PR comment body, a commit
message) is prose, not a command. The check now walks the match. A quote
or backslash inside a word still blocks. Two separate words with edge
quotes still block. Both words inside one quoted string do not block.

Shell words: the G7 check matched sh, bash, pushd and popd anywhere in
the text. A word in prose, a heredoc body or a script-file run then
marked the target unresolved and blocked any non-builtin git word. The
check now counts a nested shell only when it gets a command string (-c)
or a stdin script (-s), behind any wrapper, or when a heredoc, a
redirection or a pipe feeds its script at a command position. It counts
pushd and popd only at a command position. This matches the AgDR-0253
G7 row, which now states the nested-shell forms.

The G7 block message now gives the same prose guidance as the G8
message. Paired tests cover each false-block class with a block twin.
AgDR-0253 decision 1 and R2 now name the CEO decision of 2026-10-20.

Refs #1748
Refs #1816

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…#1826)

* chore(#1815): test the Rex recount check with read-only source access

Add a read-only source option to the eval. source-snapshot.sh exports
the files at the reviewed commit with git archive, replaces private
project names from a deny list, proves that no name remains, and makes
the tree read-only. The README and SKILL.md describe the CLI run with
Read, Grep and Glob only.

Run rex-1620-6229f1c-delta at k=3 with source access on prompt a7b3:
D1 caught in 1 of 3 runs, D2 in 0 of 3, 1,904,884 tokens. Change the
recount check (prompt bb87): run the count command, quote its output,
and count by the record's own definition. Phase B was not run, because
phase A used about 3.8 times the token budget.

Refs #1815

* chore(#1815): ship the source-snapshot option without the untested prompt edit

Restore .claude/agents/code-reviewer.md to its dev content (hash
a7b38bd17fe3). The bb87 recount edit was drafted but not run, so it
does not ship. The report now records the draft, keeps AC2 open, and
adds the real cost as a planning note: 3 source-access runs used
1,904,884 tokens, 9 to 19 times a no-tools run.

Refs #1815

* fix(#1815): fail closed on links, binaries and scan errors in the source snapshot

Refs #1815

* fix(#1815): build the snapshot path list without sed

The path proof built a sed pattern from the out-dir name. A name with
"|" or an unbalanced "[" made sed fail, and the proof passed. The proof
now lists paths with find from inside the out-dir.

The script also refuses a symbolic out-dir. A file that holds only
newlines is now text, not binary. New tests cover each change and the
grep exit 2 branches.

Refs #1815

* fix(#1815): ignore CDPATH and refuse symlinked out-dir parents in the snapshot

The snapshot script now unsets CDPATH, strips trailing slashes from the
out-dir, and refuses an out-dir whose nearest existing parent resolves
through a symbolic link. The header names the binary-file rule exactly.
The newline test has its own fixture commit.

Refs #1815

* fix(#1815): resolve the snapshot out-dir physically

A logical cd removes "link/.." as text. The path proof then listed a
different folder than the one that mkdir and tar used. Use cd -P for the
parent check and for the path listing. Add tests for "link/../name" and
"../name" from a working folder behind a link. Give each trailing-slash
check its own link target. Document the run-dir rule and the binary
rule condition in the README.

Refs #1815

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…ch (#1827)

* fix(#1824): close the merge-gate scan classes found by the shape search

Keep command position across wrapper-specific options, retain long path
hosts, and scan substitution boundaries and redirect words safely.
Preserve expansion-gap detection without treating ordinary data as code.

Closed classes: prefix options and values, word cap, redirect and
here-string substitutions, quoted and case-pattern parentheses, and an
expansion between the host word and the subcommand word.
Open classes: fully opaque calls in shell layers, functions, aliases,
launchers, here-documents, and renamed executables. The scanner cannot
tell them from ordinary commands without evaluating shell text.

Document unresolved opaque commands and external executable identities
rather than expanding the scanner into an evaluator. Bound group walks
so malformed nesting fails closed within the scan budget.

Refs #1824

* fix(#1824): treat heredoc bodies in substitutions as data and record the open expansion classes

The substitution walk read a here-document body as shell text. An
apostrophe, an unpaired double quote, or a ticket reference in
parentheses left the walk unclosed. A body over 4,096 characters
tripped the new size limits. All four gates then blocked ordinary
commit, pull request, and issue commands.

The walk now skips each here-document body up to its delimiter line.
The command scan queues each body as its own nested body at the same
layer, so its quotes cannot hide the next command and a shell reader
is still scanned. A backtick body ends at the next unescaped backtick.
An unclosed body in command text still fails closed. In a here-document
body, an unclosed substitution uses the earlier parenthesis count. The
command limit now bounds each body and each walk.

An expansion host word with an expansion in the subcommand slot now
matches a later literal subcommand word, or a later literal verb word
at command position.

AgDR-0196 now gives the true count of block cases that passed in the
private regression sets, by class.

Refs #1824

* fix(#1824): keep the heredoc scan linear and fail closed on ANSI-C quotes

The split-phrase scan read each queued here-document body again for
each later here-document. A nested chain, or many here-documents in the
scrubbed view, made the scan time grow with the number of
here-documents times the command length. More than 100 ordinary
here-documents also failed closed.

- Index the lines of each text once. A delimiter search is a lookup.
- Queue a body without a delimiter line once. A tail body reads a later
  unterminated here-document in place.
- Limit the total length of queued bodies to three times the command
  length, and fail closed above it. Here-document bodies do not count
  toward the 100-body limit.
- Read ANSI-C quotes in command text and in the boundary walk. In double
  quotes, a dollar sign before a single quote is text.
- When the command has a shell, eval, or source word, an unclosed
  substitution in a here-document body fails closed.
- Add a timing test and the ANSI-C and shell-body case pairs. Update
  AgDR-0196 with the bound on the total work.

Refs #1824

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…it gates (#1835)

* fix(#1772): route a git verb after a redirection to the push and commit gates

What: the shared git grammar in _lib-git-global.sh now skips a
redirection between the git command word and the verb, and a
redirection glued to git. The target is one full shell word, so a
quoted target with a space or a substitution target does not hide the
verb. _git_scan_globals skips the redirection in the same way. A target
that the grammar cannot read, or that is not closed, fails closed when a
push or commit word follows. The no-jq dispatcher path blocks the same
shapes in the raw payload.

Why: a redirection before the verb made command_has_git_subcommand
return false, so the dispatcher did not run the push or commit gates.

This commit holds only the redirection part of PR #1829. The heredoc
stripping part stays on that branch. Decision record: AgDR-0264.

Refs #1772

* fix(#1772): read a digit-led redirect target as one word

The digit-or-dash check in _git_take_redir had no word-end boundary.
A target such as 1--git-dir=<path> was split. The scanner read the
rest of the word as a git global option, but the shell sees one file
name. The check now needs a word boundary, as _git_redir_hides_verb
does. The raw-word reader takes the full target word.

Add placeholder-built tests for the replay arguments of a push and a
commit, with allow twins for 2>&1, >&2, 1>/dev/null and 2>&-.

Refs #1772

* fix(#1772): keep the redirect scan linear and fail closed on multi-line and zsh forms

Code review of PR #1835 found three classes in the redirect scan.

- N3: _git_redir_hides_verb started a walk from each git word and read
  the full rest of the line for each word step. One line with many git
  words took minutes. The scan now stops after the last line with a push
  or commit word, splits each line into pieces at each blank, records the
  points that a walk reads, and reads a run of plain characters with one
  regex match. The raw-word reader no longer forks for each word.
- N1: a quoted or substitution target that is open at the end of its
  line now fails closed when a push or commit word is on a later line.
- N2: the grammar accepts the zsh clobber operators and a named
  descriptor before an operator. The dispatcher, the gate scanner and
  the no-jq path share the operator units.

The tests add the N1 and N2 shapes, allow twins, and a timing check
against the base tree. AgDR-0264 records the decisions and names #1831
for the gate-parser gap.

Refs #1772

* test(#1772): widen the N3 timing margin for slower CI runners

The linux jq-1.7 CI job failed one N3 case: 0.802s against a limit of
0.66s (base 0.160s). The runner is slower than a Mac, so the fixed
0.5s margin was too tight.

The limit is now the larger of 6 times the base time and the base time
plus 1.5s. The head takes 0.42s on a Mac, which is under 25% of the
limit. The old head still fails all five N3 cases (3 to 34s).

Refs #1772

* fix(#1772): fail closed on the &> scan stop and the two readings of the ! operators

Security review R2-1: the fail-closed redirect scan stopped at every
leading `&`, so `&>`, `&>>` and the zsh `&>` forms never reached the
operator test. A push or commit after one of these operators, with a
target that the grammar cannot read, did not route to the gates. The
scan now lets `&>` through to the operator test.

Security review R2-2: an operator that ends in `!` and is followed by a
blank has two readings. Zsh reads the operator and then the next word as
the target. Bash reads `>` (or `>>`, `&>`, `>&`) with the target `!`,
and the next word is an argument. The two readings can give a different
repository or a different verb. `_git_take_redir` now returns 2 for this
class, so the gate scanner fails closed. A glued form keeps its earlier
behaviour.

Code review B1: the scan is linear for words separated by blanks. It is
quadratic for a long run with no blank and for many targets that hold a
quoted blank. On those shapes, the dispatcher time limit with
onFailure: block fails closed on Claude Code 2.1.295 and later. The CEO
accepted this residual on 2026-10-20. AgDR-0264 Decision 8 and the
Consequences now say this, and record the false-block cost of the N1
rule and the measured timing numbers.

Tests: the R2-1 fail-closed, dispatcher and real-gate cases, the R2-2
gate-scanner cases, and a hermetic two-repository gate check for R2-2.

Refs #1772

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…1838)

* fix(#1825): route git's own dashed push and commit programs to the gates

Refs #1825

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(#1825): count a dashed program only at a command position and cover formed names

The first version matched the dashed name in any word, so text that only named the program blocked. It also missed names that quote removal, a glob, a brace or an expansion forms. The check now counts a command word only, handles those forms, gives its own block message, covers the no-jq path, and records the open class in AgDR-0253.

Refs #1825

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(#1825): drop the continuation join and record the dashed-name limits

Rex round 2 found the continuation join slow on long input. Issue #1837 covers line continuations, so this change removes the join, its dispatcher check and its tests. The AgDR and the PR text now state what the code does and list the new accepted false blocks.

Refs #1825

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(#1831): read the push destination past a redirection

The branch-name gate and the main-push gate read the push destination
through _lib-extract-push-ref.sh. That code cut the command at the first
redirection. A redirection before the git verb, or among the push
arguments, hid the ref, and both gates used local HEAD.

Add git_push_ref_words to _lib-git-global.sh. It reuses the shared
redirection units from #1835, skips each redirection as the shell does,
and stops at the real end of the command. The extractor reads each push
invocation with it. One word loop replaces three copies of the token
loop. When an invocation that holds a redirection cannot be read, the
extractor returns 3 and both gates block.

Add test_push_ref_redirect_1831.sh and AgDR-0265.

Refs #1831

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1831): fail closed on a unit separator byte in push words

The push word walk splits each line into pieces with the unit separator
byte (0x1F). When the push arguments already held that byte, the walk
read all of them as one word, found no ref, and both gates used local
HEAD. Return 2 for that text, so both gates block.

The open-word fallback now restarts from the current piece, so it splits
at blanks as AgDR-0265 says. The block message names the option to
write the message to a file.

Add block cases and allow twins for the byte in both gates, at top level
and after a redirection before the verb. Give the pipe twin the shape
of its block case. Record the delete-check change and the new prose
false block in AgDR-0265.

Refs #1831

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#1844)

* fix(#1780): strip a quoted heredoc only when its marker is one closed word

The dispatcher strips a confirmed quoted heredoc body before push and
commit routing, but only when the whole command is one allow-listed data
sink. In quoted mode the stripper now splits the whole start line, the
heredoc marker included. It strips only when the line has one heredoc
operator outside quotes and the next word is one quoted word with
matching quotes whose value is the terminator. Any other shape keeps the
raw command (security finding B4 on draft PR #1829).

AgDR-0262 records the decisions. AgDR-0264 owns the redirect part.

Refs #1780

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1780): stop the quoted-mode scan at the second visible line

In quoted mode the stripper returns the raw command at the second
visible line that holds text. Such a command can never be one data sink,
so the scan is now linear. 2000 unterminated quoted markers took 34 s and
now take about 1 s. A new timing check pins it, with a block twin.

AgDR-0262 Decision 4 and the stripper comments now list the exact sink
forms, the git and gh option refusals and the file-target rules that the
code applies.

Refs #1780

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(#1832): pre-filter segments before write-target extraction

Run one grep over all segments and extract targets only for the
segments it selects. Fall back to the full loop if grep fails.

Refs #1832

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(#1832): pass -a to the candidate grep so it keeps encoding-error lines

Why: GNU grep 3.5 and later drops a line with an encoding error in a legacy
multibyte locale, writes a notice to stderr and exits 0. The filter then
skipped a segment that the extractors still matched, so a gate result
changed. With -a, grep prints every selected line. The grep error output
now goes to /dev/null inside the command substitution.

Add a test double for the dropped line. Correct AgDR-0260.

Refs #1832

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…ring (#1839)

* chore(#1806): block in-session writes to live settings, hooks and pin state

Add the tamper controls of AgDR-0261 section B. An agent could change the
files that define the gates during a session: the live settings files, the
live hook scripts and the hook state in the pin tree. A gate is only as
strong as those files.

- block-protected-target-writes.sh runs on the edit channel (CLASS: CONTROL)
  and in the Bash dispatcher through run_fail_closed_hook (CLASS: BACKSTOP).
  It applies to every caller and has no hatch. It judges operands by role,
  resolves the effective directory, expands literal variables, and checks
  git pathspecs, index writers, patch commands and git aliases.
- The Bash scanner keeps each shell scope apart. A cd, pushd, popd or
  assignment inside a subshell, bash -c, a pipeline stage or a background
  and-or list does not change the paths of the parent command. A cd that
  can fail keeps the old directory in effect after ";" and "||".
- The scanner also checks command substitutions, case bodies, eval, shell
  option clusters such as -ec, and the option values of prefix commands.
- The shell tokenizer is one awk program in _lib-protected-shell-tokens.sh.
  It runs under mawk 1.3.4 and needs no temporary file, so a copy of the
  hooks by the *.sh glob and a minimal PATH keep the gate working.
- write-session-snapshot.sh records a write-once baseline on startup,
  resume and fork. pin-ops-root.sh writes the pin once, only on those
  sources, and deletes pin-tree files older than 30 days.
- block-hook-disabling-config-change.sh blocks and logs a ConfigChange that
  changes hooks or env or sets disableAllHooks. It never restores a file.
- Tracked settings set disableAllHooks to false. The operator recovery path
  stays the --settings launch flag.
- Update the existing dispatcher, pin and isolation tests for the new
  registration and the write-once pin.

Refs #1806

* test(#1806): cover the tamper controls for three callers

Add the section B.12 suites of AgDR-0261. Each block case has a matching
allowed case.

- test_block_protected_target_writes.sh runs the matrix for the main
  thread, a platform-engineer subagent and a code-reviewer subagent. It
  includes the section B.14 resolver cases: cd inside bash -c, a nested
  subshell, popd with an empty stack, pipeline and background scopes.
- test_protected_shipped_corpus.sh extracts every shipped skill and agent
  bash block at test time and asserts that none blocks.
- Separate suites cover shell syntax, interpreter heredocs, the scrub
  contract, fail-closed paths, the session snapshot, the write-once pin and
  the ConfigChange hook.
- test_patch_macos_capture.sh compares Apple patch -p0 with the recorded
  capture of absolute and dotdot headers.
- The fixtures README gives the capture steps for P12, P12b and P12c.
  These captures need a live session and are not in this commit.

Refs #1806

* docs(#1806): file AgDR-0261 and amend AgDR-0181 scope

- File the approved design v6 as AgDR-0261 with decisions D1 to D21. Link
  the D17 ticket and the #1748 build in its header, outcome, ticket order
  and follow-up table.
- Amend AgDR-0181 section Scope (D14). The git and patch matchers of the
  tamper gate may read the scrubbed view for seven verbs. These verbs are
  outside the data-only allowlist, so the scrubber returns them raw.
- docs/hooks-fail-closed.md lists the three new fail-closed entries with
  measured timeouts, and says disableAllHooks must not be true in a tracked
  settings file.

Refs #1806

* test(#1806): add the P12 live captures

Add scrubbed fixtures of the first live run of captures P12, P12b and
P12c (AgDR-0261 section B.10). The run used a throwaway sandbox clone with
a temporary logging hook.

- P12b shows that compact keeps the session id, the pin and the snapshot,
  and that clear issues a new session id with no pin and no snapshot.
- P12 and P12c are inconclusive. The subagents wrote through Bash, and the
  P12c change did not change the JSON value.
- The README records what each capture showed and the exact steps for the
  next run.

Refs #1806

* test(#1806): add the run 2 live captures

Replace the P12, P12b and P12c fixtures with the scrubbed data of the
second live run (AgDR-0261 section B.10). Run 1 left P12 and P12c
inconclusive. Run 2 logged Bash and Edit calls and made a real env change.

- P12: a worktree subagent runs the hooks of the main session. Its
  CLAUDE_PROJECT_DIR is the project root. Its PWD and cwd are the worktree.
- P12b: a cd inside the project persists into hook-process PWD. compact
  keeps the pin and the snapshot. clear issues a new session id with no
  pin. CLAUDE_PROJECT_DIR stays the project root.
- P12c: no ConfigChange fires for a worktree settings change.
- The README keeps the run 1 findings and names the two changes that
  follow from the captures.

Refs #1806

* chore(#1806): pin a session after /clear from CLAUDE_PROJECT_DIR

Capture P12b showed that /clear issues a new session id, and the
write-once pin then left that session with no pin. Every launcher walked
up from the hook-process PWD on each call. A Bash cd inside the project
persists into that PWD, so a cd into a constructed fork tree could select
hooks that do nothing.

pin-ops-root.sh now also writes the pin on clear when no pin exists for
the new session id. It resolves from CLAUDE_PROJECT_DIR, which stayed at
the launch directory in every captured hook call, and never from PWD.
It writes no pin when CLAUDE_PROJECT_DIR is not set. clear still writes
no snapshot, and compact still writes nothing.

Refs #1806

* test(#1806): cover the pin written at /clear

- clear with a new session id and PWD in another fork tree pins
  CLAUDE_PROJECT_DIR, and writes no snapshot.
- The paired startup case pins the launch directory.
- clear without CLAUDE_PROJECT_DIR writes no pin.

Refs #1806

* chore(#1806): log ConfigChange blocks in a session with no pin

block-hook-disabling-config-change.sh wrote its block log only under the
pinned ops root. A session with no pin, such as one after /clear before
the pin fix, left no evidence of a block, and a blocked change shows no
message. The log now falls back to the project directory of the session.

Refs #1806

* test(#1806): cover the ConfigChange log without a pin

- With no pin, a block writes one line under the project directory.
- The paired case with a pin writes the line under the ops root.

Refs #1806

* chore(#1806): check hook keys of a settings file that is not live

block-hook-disabling-config-change.sh allowed every change to a settings
file that is not live. Capture P12 showed that a worktree subagent runs
the hooks of the main session, and capture P12c showed no ConfigChange
for a worktree settings edit. So the allow was safe in this harness, but
a future harness that reports and loads such a file would let a subagent
remove its own hook entries.

As defence in depth, the hook now blocks a change to a file that is not
live when the file sets disableAllHooks, does not parse, or changes hooks
or env against the committed copy in its own work tree. A file that is not
committed has {} as its baseline. A blocked change only stops the running
session from applying it; the file edit stays.

Refs #1806

* test(#1806): cover ConfigChange for settings files that are not live

- A permission change that keeps the committed hooks passes.
- An env change, a hooks change and disableAllHooks against the committed
  copy block.
- hooks in a file that is not committed, and invalid JSON, block.
- The existing non-live case now changes only permissions.

Refs #1806

* docs(#1806): record the capture results in AgDR-0261

- Section B.1: clear issues a new session id, a cd inside the project
  persists into hook-process PWD, and CLAUDE_PROJECT_DIR stays at the
  launch directory. So the pin is written at clear from
  CLAUDE_PROJECT_DIR.
- Section B.7: no ConfigChange fired for a worktree settings change, and a
  worktree subagent runs the hooks of the main session. The rule for files
  that are not live now checks the hook keys against the committed copy,
  and the block log falls back to the project directory.

Refs #1806

* docs(#1806): escape table pipes and split a long sentence in AgDR-0261

Markdownlint rejected an unescaped pipe in a table cell. The pre-push
writing profile flagged a 32-word sentence.

Refs #1806

* test(#1806): stub the protected-target gate in the redirect dispatcher test

Refs #1806

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(#1806): expect the protected-target gate to block read-only git without jq

Refs #1806

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(#1806): close the review round 1 gaps in the protected-target gate

- Read the zsh clobber suffix forms and the combined append forms as one
  redirect operator, and judge the target of each one.
- Compare each protected form without regard to letter case. A pt_nocase
  helper sets nocasematch only for these tests, so option and verb tests
  stay case-sensitive.
- Do not treat .claude/worktrees as a sensitive directory. Judge the find
  roots only for a mutating or unknown -exec verb.
- Judge the operand after -f for every verb except tar.
- Guard the empty root array in the ConfigChange hook under bash 3.2.
- Stub the new gate in the no-jq alias sub-test, so its allow cases test
  the alias gate again.
- Add test_protected_review_1839.sh with paired block and allow cases.

Refs #1806

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(#1806): record the round 1 gate changes and the dispatcher timeout

- AgDR-0261 build amendment: case-insensitive protected forms, the longer
  redirect operators, worktrees and read-only find verbs, and the -f flag.
- AgDR-0261 B.9: the no-jq pattern also blocks some read-only commands.
- hooks-fail-closed.md: the dispatcher timeout is 75 seconds.

Refs #1806

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1806): judge the bash reading of a trailing ! redirect suffix

Zsh reads a trailing "!" on an output operator as the clobber suffix.
Bash and sh read it as the first character of the target word, or as a
file named "!" when a space follows. The gate cannot know the shell, so
for an operator that ends in "!" it now judges the next word, the next
word with a leading "!", and the file "!".

Add block and allow pairs for the bash reading at top level and in
nested bash -c and sh -c. Correct the AgDR-0261 amendment bullet and
name two residuals: verb letter case, and worktree xargs forms with
relative stdin operands.

Refs #1806

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1806): exempt the protected-target gate in the 1780 routing test

The #1780 routing test stubs every always-on hook, so the dispatch log
shows only the push and commit groups. The #1806 protected-target gate
is a new always-on hook. It runs first and fails closed on an
unterminated quote or heredoc. It blocked the B4 unbalanced-comment
shape and the 2000-marker shapes before the push gates ran. Add it to
the stub list, as 1ac0ed1 did for the #1772 test.

Refs #1806

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs(#1845): record that shell-text gates are guardrails

Add AgDR-0267. It states the threat model, the real controls for the
hostile case, the review bar for gate PRs, the freeze on exotic shape
hardening, and a 2 s dispatcher latency target.

Add a "Review bar for gate PRs" section to the Rex and Hakim agent
briefs and to the /security-review skill. Add short notes to AgDR-0109,
AgDR-0168, AgDR-0172 and AgDR-0192 that point to the new record.

Refs #1845

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(#1845): split the AgDR-0267 summary into short sentences

The writing-profile check found one sentence over 25 words in the
AgDR-0267 summary. Split it into five short sentences.

Refs #1845

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(#1845): narrow the gate bar to one case and state control limits

Answer the round 1 reviews on PR #1848.

- State that the bar narrows one case only, and that every other
  blocker stays. Use the same text in AgDR-0267, both reviewer briefs
  and the /security-review skill. Add the code-execution fault to the
  Rex section.
- State that the AgDR-0264 timings used stub gates, and that the real
  gate time is not measured yet. Keep the 2 s target.
- State the bypass-list limit of a ruleset, the accepted residual for
  a bypass-actor token, and the operator action that closes it.
- Say that the operator must make hook files read-only for the hostile
  case, and that only managed settings hold for permission rules.
- Classify a regression on an already-handled exotic shape, and make a
  deleted paired test a correctness finding.

Refs #1845

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…it-format (#1849)

* fix(#1810): treat a quoted heredoc that writes a file as data in commit-format

The commit-format check scanned the raw command. A file write from a
quoted heredoc whose prose names a commit in Markdown backticks blocked.
The dispatcher strips such a body only for a single-command data sink
with an allowed target (AgDR-0262). A compound write or a target in a
home-directory dot folder still reached this check.

The hook now removes the body of a quoted heredoc that only feeds cat or
tee to a file, before the visitor and the checks read the command. A
heredoc fed to a shell, an unquoted heredoc, an unterminated heredoc and
a written file that a later command names keep the raw command. A real
commit in the same command stays visible and blocks as before. A command
substitution in -m and a message file are checked as before.

Refs #1810

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1810): correct the keep-raw rule text and test two file writes

AgDR-0263 and the helper comment said that any other line outside a
stripped body that holds a heredoc operator keeps the raw command. A
second qualifying file write is stripped too. Say "and is not a
qualifying start line" in both places, and add a suite case for two
file writes in one command.

AgDR-0263 now says why it does not use the AgDR-0262 target allow-list.
The governance header of _lib-strip-heredoc.sh names AgDR-0263.

Refs #1810

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(#1810): treat a quoted heredoc that writes a file as data in commit-format

The commit-format check scanned the raw command. A file write from a
quoted heredoc whose prose names a commit in Markdown backticks blocked.
The dispatcher strips such a body only for a single-command data sink
with an allowed target (AgDR-0262). A compound write or a target in a
home-directory dot folder still reached this check.

The hook now removes the body of a quoted heredoc that only feeds cat or
tee to a file, before the visitor and the checks read the command. A
heredoc fed to a shell, an unquoted heredoc, an unterminated heredoc and
a written file that a later command names keep the raw command. A real
commit in the same command stays visible and blocks as before. A command
substitution in -m and a message file are checked as before.

Refs #1810

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1842): read the commit message past a redirection

The commit-format gate split the command with its own tokenizer. A
redirection between git and the verb hid the commit, so a bad message
passed. A redirection among the arguments counted as a compound command,
so every such commit blocked, also with a valid message.

The gate now walks the command with the redirection reader of
_lib-git-global.sh (_git_take_redir, _git_take_raw_word). A redirection
and its target are not words. A pipe, a chain, a background job and a
second line still block as compound commands. A target that cannot be
read blocks, and -F - with an input redirection blocks. A short option
group such as -am <msg> or -m<msg> is read as git reads it.

AgDR-0271 records the decision. New suite:
.claude/hooks/tests/test_commit_format_redirect_1842.sh.

Refs #1842

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1810): correct the keep-raw rule text and test two file writes

AgDR-0263 and the helper comment said that any other line outside a
stripped body that holds a heredoc operator keeps the raw command. A
second qualifying file write is stripped too. Say "and is not a
qualifying start line" in both places, and add a suite case for two
file writes in one command.

AgDR-0263 now says why it does not use the AgDR-0262 target allow-list.
The governance header of _lib-strip-heredoc.sh names AgDR-0263.

Refs #1810

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…nk (#1851)

* fix(#1833): accept a heredoc substitution commit message as a data sink

A 16 KB message in git commit -m "$(cat <<'EOF' ... EOF)" ran past the
dispatcher time limit on dev (more than 120 s with every hook active).
The body named push, so the push gates ran too. Each gate parsed the raw
command, and the cd-chain segmenter cost grows with the square of the
command length.

- Add the shape to the quoted-mode data sinks (_shq_subst_sink). The text
  after "$( must be exactly cat, the operator and one quoted delimiter.
  The words before it must be git commit ... -m or --message and pass the
  AgDR-0262 option refusals. One exact )" line must close it.
- Add _git_structure_text. The gate visitor and git_prepare_invocation
  parse the structure on the quoted-mode text. Any other shape, or a
  stripper that cannot load, gives the raw command. Gate bodies still
  read the raw command.
- Add test_git_heredoc_subst_1833.sh: timing, routing, 15 block twins,
  and a fail-before against dev.
- Add AgDR-0270 and a note in AgDR-0262.

Refs #1833

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(#1833): state the AgDR-0270 timing conditions and floor

- Say that the 1.1 to 1.6 s figure was measured with the always-on hooks
  stubbed.
- Add the full-tree figures: 3.4 to 4.1 s for the 16 KB message, and a
  2.8 to 3.2 s floor for a 60-byte message.
- Make the Decision rationale agree with the AC note on #1833: 3 s or
  less above the always-on-hook floor.
- Mark the Option B gate cost as an estimate.

Refs #1833

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d Write tool (#1847)

* fix(#1840): let review agents write their review body through a scoped Write tool

Review agents wrote the review body with a Bash heredoc. The Bash gates
read the quoted command text in the body as live commands and blocked
the write.

- Give code-reviewer and security-reviewer the Write tool.
- Add limit-reviewer-writes.sh on the Edit|Write|MultiEdit matcher. A
  review agent may write only under
  <ops-root>/.claude/session/reviews/bodies/. Every other target is
  blocked: repository files, hooks, settings and review markers.
- Add review_body_dir and review_body_path to _lib-review-markers.sh.
- Judge each git config segment alone in the reviewer lock, and allow
  read forms with a scope option and the get and list subcommands.
- Update the two agent files and the /code-review and /security-review
  docs to name the body path.
- Record the choice in AgDR-0266.

Refs #1840

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1840): keep the code-review skill under its size cap

Shorten the review-body path note in /code-review so that the skill
stays at or under the 16000-byte cap that the token-efficiency and
shared-block suites check.

Refs #1840

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1840): make the reviewer write-scope hook fail closed

Address round 1 review findings on the reviewer body-write change.

- Set onFailure to block on the limit-reviewer-writes.sh entry, and add
  it to the named fail-closed entries in test_settings_fail_closed.sh
  (six entries).
- Correct AgDR-0266: state the fail-closed setting and the accepted
  cost that a crash or timeout also blocks other callers' edits.
- Tell the review agents to read an existing body file before they
  replace it in a later review round.
- Name the hook in the extra review types docs and the fail-closed
  tables, with a measured p95.

Refs #1840

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1840): correct two counts in the fail-closed docs

- docs/hooks-fail-closed.md: six entries set onFailure, including
  limit-reviewer-writes.sh.
- AgDR-0266: the hook makes four jq calls on the payload before it
  exits 0 for a caller that is not a review agent.

Refs #1840

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ction (#1850)

* fix(#1837): route a continued verb and a variable verb after a redirection

The dispatcher joined line continuations in broad-space mode only. That
mode puts a space where bash puts nothing, so a continuation inside the
verb, a -C value, a redirection target or a dashed program name split
the word, and no push or commit gate ran. The dispatcher now also scans
the bash-mode join when a continuation follows a character that is not
a blank. A route from either text counts.

The uncertain-verb check now skips a redirection before an expansion in
the verb position, with the shared redirection unit. A variable verb
after a redirection now routes to both gate groups, and the gates block
it.

test_git_redirect_1772.sh gets a timing case for the &> form with a
quoted target that holds a blank, at 2000 copies. AgDR-0269 records the
decision and the residuals.

Refs #1837

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(#1837): fix markdownlint and long sentences in AgDR-0269

Remove a code span that held only a blank and a backslash (MD038).
Split one sentence that was over the writing-profile limit.

Refs #1837

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#1862)

* fix(#1858): narrow the pin-tree token and close three tamper-gate gaps

Narrow the pin-tree token to the pin and gate state files, rotate the
ConfigChange block log after the resume warning, make the live-file test
case-blind, and judge the find -fls primary. Amends AgDR-0261.

Refs #1858

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(#1858): state the case-blind test coverage in the AgDR-0261 amendment

The amendment said every change has a paired block and allow case. The
case-blind live-file change has one case, and it runs only on a
case-insensitive file system. Reword the line to say so. Amends AgDR-0261.

Refs #1858

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ci(#1856): key PR check concurrency by head SHA and event action

Runs for different actions on one head (opened, edited, synchronize)
no longer cancel each other. A cancelled run no longer reaches the
merge gate. Decided by AgDR-0272.

Refs #1856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(#1856): drop head SHA from PR check concurrency group

Use the PR number and the event action only. Two pushes in a row
still cancel the older run. Opened, edited and synchronize on one
head do not cancel each other. Decided by AgDR-0272.

Refs #1856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(#1856): raise hook-tests timeout to 45 minutes

Recent hook-test runs took 21 to 29 minutes. The run for PR #1850
reached 30 minutes and was cancelled. Decided by AgDR-0272.

Refs #1856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…le write (#1861)

The write detector now reads a short-option cluster that ends in the
output flag as a write, and extracts its target. Curl forms with no
output flag stay reads. Cases and allow twins added to the suite.

Detector AgDR: AgDR-0260.

Refs #1857

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Prepend CHANGELOG section for v5.10.0
- Add the no-op v5.9.0-to-v5.10.0 migration and its docs/upgrading.md row
- Anchor the changelog range on the v5.9.0 cut point 454515c. The v5.9.0
  squash commit has its Released-From line mid-message, so git does not
  read it as a trailer.

Refs #1863

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Released-From: 8d17671

@me2resh me2resh left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: COMMENT. This is not an approval. One required check did not run. Next action: the orchestrator decides how to run the changelog-to-log comparison, then Rex does a delta pass on the same SHA.

Code Review: PR #1864

Commit: 94925584044c35b35e94075689236dbff1c7e3dd
Scope: Full (release commit 9492558 and PR body only; the code was reviewed PR by PR on dev)

Summary

The release commit adds the v5.10.0 CHANGELOG section, a no-op migration script v5.9.0-to-v5.10.0.sh, and one row in docs/upgrading.md. Its parent is dev 8d17671. The PR body records the hand-anchored range 454515c..8d17671 and the Released-From trailer.

A hook blocked my comparison of the CHANGELOG entries with the commit log of 454515c..8d17671. I did not split or change that command. Thus the entry match, the count check (69 raw, 65 entries, gap 4), and the commit-versus-body CHANGELOG identity are Unverified.

Acceptance Criteria

Issue Criterion Status Evidence
#1863 Release PR is open with the full changelog from 454515c..dev Not verifiable (now) PR is open, base main. The 3 named commits 9ba947d, 4663833 and 3630f7c are in the PR-body CHANGELOG. The full log comparison was blocked by a hook.
#1863 No-op migration script and its docs row are in the release Met .claude/migrations/v5.9.0-to-v5.10.0.sh (mode 100755) and the docs/upgrading.md row at the v5.8.0 row's position.
#1863 After merge, tag exists and squash commit carries a readable trailer Not verifiable Post-merge. The release commit trailer reads correctly (see Validation).
#1863 Next nightly on main has no macOS job Not verifiable Post-merge. tests.yml at 8d17671 and at 9492558 has no macOS entry. The matrix has only ubuntu-latest.
#1863 /release-sync v5.10.0 is done Not verifiable Post-merge.

Checklist Results

  • Architecture & Design: N/A. The release commit has no code.
  • Code Quality: Pass. The migration script has the same form as the v5.2.0-to-v5.3.0 no-op: set -u, APEXYARD_MIGRATION_QUIET, one info line, exit 0.
  • Testing: Pass for the migration script (run and shellcheck, see Validation). CI is green on the head SHA.
  • Security: N/A. No secrets, no hook change, no workflow change in the release commit.
  • Performance: N/A.
  • PR Description & Glossary: Pass. The body has a Glossary, Refs #1863, and the multi-close marker on line 1.
  • Summary Bullet Narrative: Pass.
  • Technical Decisions (AgDR): N/A. The manual range anchor is a one-time operator choice, stated in the body and in #1863.
  • Adopter Handbooks: N/A. No handbook applies to CHANGELOG, docs, or a no-op script.
  • Fallow Static Analysis (JS/TS): N/A. No JS or TS in the release commit.
  • Changelog entries against the commit log: Unverified. A hook blocked the check.
  • Closes bullets: Pass on the sample (see Validation).
  • Released-From trailer: Pass in the commit and in the PR body.

Issues Found

None found in the checks that ran. One required check is Unverified (see Summary).

Suggestions

  • suggestion: PR #1740 closes #1724 in the full owner/repo#N form. The generator does not read that form, so #1724 has no Closes bullet. #1724 is already closed, so this has no effect. This agrees with the rule "prefer a missing close over a wrong close".
  • suggestion: a plain git interpret-trailers --parse on the PR body prints nothing, because the --- line before the Glossary is a patch divider for that command. With --no-divider it prints the trailer. The %(trailers) format on a commit does not stop at ---. The v5.8.0 squash commit has one --- line and its trailer reads correctly. Thus the squash commit is safe when the merge uses the PR body.
  • suggestion: merge through /approve-merge (explicit subject and body). The v5.9.0 squash used the default commit-message body, and GitHub added Co-authored-by lines after the Released-From line. That is why the v5.9.0 trailer is mid-message.

Validation

  • Head SHA on the forge before posting: 9492558. Parent: 8d17671.
  • Commit trailer: git interpret-trailers --parse on the commit message prints Released-From: 8d17671b816eb1d7b7482aeae0d540c48434f128. %(trailers:key=Released-From,valueonly) prints the same SHA. The Co-Authored-By line is in a separate, earlier paragraph. The trailer is the final paragraph.
  • PR body trailer: the final paragraph is Released-From: 8d17671b816eb1d7b7482aeae0d540c48434f128. git interpret-trailers --parse --no-divider prints it.
  • Multi-close marker: present on line 1 of the PR body.
  • v5.9.0 claim: the v5.9.0 squash commit (37f5689) has Released-From: 454515c0 at lines 10387 and 10400 of 10416, followed by Co-authored-by lines. %(trailers) returns empty. The v5.9.0 branch head 3e11370 has a readable Released-From: 454515c0 trailer. 454515c is an ancestor of 8d17671. The PR-body statement is correct.
  • Migration script: /bin/bash run prints the placeholder line, exit 0. Quiet run, exit 0. shellcheck -S warning is clean. The name matches the _lib-migration-chain.sh pair regex.
  • Closes sample (PR body read through the forge): #1741 from PR #1746 (scope #1741, body closes #1741): correct. #1803 from PR #1807: correct; the other numbers that PR #1807 closes are not scoped, and they are correctly absent. #1824 from PR #1827: correct. #1814 from PR #1817: correct. #1857, #1858 and #1856 (PRs #1861, #1862, #1860) have only Ticket: in the body, and they are correctly absent.
  • macOS: git grep -i macos on .github/workflows/ at 8d17671 and at 9492558 finds nothing. main's tests.yml has 4 macOS shards, as the PR body says. The release commit does not change .github/.
  • CI (one read): all checks pass on the head SHA, including both Linux hook-test jobs. nightly-failure and conformance are skipped.
  • Blocked step: the combined changelog-versus-log comparison was blocked by the merge-gate scan in dispatch-bash.sh ("could not scan the command"). I did not run a changed form of it.

Verdict

COMMENT

All checks that ran pass, and none meets the blocking bar. The changelog-to-log comparison is a required check, and it did not run. Rex does not approve until it runs.


🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 94925584044c35b35e94075689236dbff1c7e3dd

@me2resh me2resh left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVED. The CHANGELOG-to-log check that was Unverified in review 5615914442 now passes. Next action: CEO approval, then merge through /approve-merge with the explicit PR body.

Code Review: PR #1864

Commit: 94925584044c35b35e94075689236dbff1c7e3dd
Scope: Delta re-review, 94925584044c35b35e94075689236dbff1c7e3dd..94925584044c35b35e94075689236dbff1c7e3dd (same SHA; this pass completes one check from review 5615914442)

Summary

The head is unchanged since review 5615914442. That review covered the same SHA, and all its checks passed except one. This pass does the CHANGELOG-to-log comparison. Acceptance criteria for #1863: the first one ("full changelog from 454515c..dev") is now Met. The migration criterion stays Met. The three post-merge criteria stay Not verifiable until merge.

Checklist Results

  • Changelog entries against the commit log: Pass. Each of the 69 commits in 454515c..8d17671 is either one of the 65 v5.10.0 entries or one of 4 markers. The markers are 37f5689 (v5.9.0 release squash), ccd0407 (sync), a00540d (merge) and 9d65508 (merge). The gap is 4, under the tolerance of 5.
  • Named commits: Pass. 9ba947d, 4663833 and 3630f7c are listed.
  • Section count: Pass. 4 feat + 34 fix + 27 changed = 65, as the header says.
  • Closes bullets: Pass. Each of the 33 numbers is a same-repo scope of a commit in the range. Forge-read samples: #1741, #1803, #1824, #1814, #1533, #1806 and #1832 have a closing keyword in the PR body. The scoped numbers #1773 and #1745 have only Refs, and they are correctly absent.
  • All other checklist items: unchanged from review 5615914442. The SHA did not change.

Issues Found

None. The Unverified item from review 5615914442 is resolved. Its three suggestions stay advisory.

Validation

  • The coordinator gave two raw files from single commands. I checked them against git. git log --oneline 454515c0..8d17671b gives the same bytes as the log file, and the count is 69. git show 94925584:CHANGELOG.md gives the same bytes as the CHANGELOG file.
  • I compared each commit with the entries by reading both files. The PR-body CHANGELOG has the same entries and Closes bullets as the committed section.
  • Head SHA on the forge before posting: 9492558.
  • CI: all checks passed at this SHA (read once in review 5615914442). I did not read CI again.

Verdict

APPROVED
All required checks pass. No finding meets the blocking bar.


🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 94925584044c35b35e94075689236dbff1c7e3dd

@me2resh me2resh added the ready-to-merge Rex approved; the refresh Action keeps this PR up to date label Oct 22, 2026
@me2resh
me2resh merged commit 74bff5f into main Oct 22, 2026
23 checks passed
@me2resh
me2resh deleted the release/v5.10.0 branch October 22, 2026 15:23
@me2resh me2resh mentioned this pull request Oct 22, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment