Skip to content

feat(hardened): Agent SDK CLI Wizard and TAC commands - #404

Merged
POWERFULMOVES merged 1 commit into
PMOVES.AI-Edition-Hardenedfrom
feat/hardened-agent-sdk-cli
Jan 3, 2026
Merged

feat(hardened): Agent SDK CLI Wizard and TAC commands#404
POWERFULMOVES merged 1 commit into
PMOVES.AI-Edition-Hardenedfrom
feat/hardened-agent-sdk-cli

Conversation

@POWERFULMOVES

Copy link
Copy Markdown
Owner

Summary

  • PMOVES Agent SDK CLI Wizard
  • Agent SDK TAC commands
  • Crush rebranded to PMOVES

Changes

  • ec5120e: Add PMOVES Agent SDK CLI Wizard & Rebrand Crush
  • c1fe4bf: TAC command integration

Test Plan

  • CLI wizard functions correctly
  • TAC commands work as expected

…365)

* feat(cli): rebrand Crush CLI to PMOVES CLI

Update user-facing branding from "Crush CLI" to "PMOVES CLI" while
maintaining backward compatibility with existing Crush infrastructure.

Changes:
- Update crush_app help text: "Crush CLI integration" → "PMOVES CLI integration"
- Update crush_configurator.py docstring to emphasize PMOVES deployment
- Update command help texts for setup/status/preview commands
- Update user-facing documentation in .claude/commands/crush/

Rationale:
The "Crush" name originated as an internal codename but the production
CLI should reflect the PMOVES brand for consistency with the broader
PMOVES.AI ecosystem. The underlying "crush" command name and file
paths are preserved for backward compatibility.

Modified Files:
- pmoves/tools/mini_cli.py
- pmoves/tools/crush_configurator.py
- .claude/commands/crush/setup.md
- .claude/commands/crush/status.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* feat(cli): add PMOVES Agent SDK commands to mini CLI

Implement agent-sdk sub-commands for creating and managing PMOVES Agent
instances with full ecosystem access via interactive CLI wizard.

Features Implemented:
- `pmoves agent-sdk create` - Interactive wizard for agent creation
  - 5 agent roles: researcher, code-reviewer, media-processor, knowledge-manager, general
  - Role-based tool and subagent configuration
  - Automatic NATS, TensorZero, and Hi-RAG connection
  - Unique agent ID generation with timestamps
  - Beautiful formatted output with configuration summary

- `pmoves agent-sdk run` - Execute tasks with existing agents
  - Task execution with streaming output
  - Model override support
  - Session resumption capability

- `pmoves agent-sdk list` - List agent instances
  - Status filtering
  - Configurable limit (placeholder for SessionManager integration)

- `pmoves agent-sdk status` - Check agent status
  - NATS heartbeat monitoring
  - Active agent information (placeholder for SessionManager)

Technical Details:
- Integrated with PMOVES-BoTZ Agent SDK
- Async/await pattern for agent lifecycle management
- Interactive role selection with graceful Ctrl+C handling
- Comprehensive error handling for missing dependencies
- Auto-discovery of PMOVES-BoTZ submodule

Usage Examples:
```bash
# Interactive agent creation
pmoves agent-sdk create

# Pre-select role
pmoves agent-sdk create --role researcher

# Execute task
pmoves agent-sdk run pmoves-researcher-1735123456 "Analyze architecture"

# List agents
pmoves agent-sdk list --status active --limit 50
```

Related Documentation:
- .claude/commands/agent-sdk/create.md
- .claude/commands/agent-sdk/run.md
- .claude/commands/agent-sdk/resume.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* docs(agent-sdk): update CLI documentation for run and resume commands

Update user-facing documentation for agent-sdk CLI commands to reflect
the new PMOVES CLI integration pattern.

Changes:
- `.claude/commands/agent-sdk/run.md`
  - Updated from skill-based to CLI command documentation
  - Added usage examples with `pmoves agent-sdk run`
  - Documented arguments and options
  - Added troubleshooting section

- `.claude/commands/agent-sdk/resume.md`
  - Updated from skill-based to CLI command documentation
  - Added session management workflow
  - Documented session states and storage backends
  - Added troubleshooting section

Documentation Pattern:
All agent-sdk command documentation now follows a consistent pattern:
- Usage section with use cases
- Implementation section with CLI examples
- Arguments and options tables
- What It Does checklist
- Related commands section
- Notes and troubleshooting

This aligns with the create.md documentation updated in the previous
implementation phase.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* fix(agent-sdk): address all PR #365 review comments

Fix all 14 issues from comprehensive PR review across error handling,
documentation, and code quality improvements.

Critical Fixes (4):
- Make NATS connection mandatory with ConnectionError on failure
- Add two-layer error handling to task execution
- Replace generic Exception catches with specific error types
- Exit with code 1 on all failure paths

Documentation (5):
- Correct NATS event subjects (remove non-existent events)
- Add prerequisites sections to all agent-sdk docs
- Fix example code placeholders with runnable examples
- Update model IDs (remove date suffixes)
- Document storage backends and timeouts

Improvements (5):
- Add Google-style docstrings to key functions (≥80% coverage)
- Enhance Crush configurator docstrings
- Improve list/status placeholders with NATS monitoring guidance
- Fix context manager usage pattern
- Add comprehensive timeout documentation

All syntax checks pass. Docstring coverage ≥80%.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

---------

Co-authored-by: Codex Agent <codex-agent@example.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jan 1, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@POWERFULMOVES
POWERFULMOVES merged commit 805b5f2 into PMOVES.AI-Edition-Hardened Jan 3, 2026
1 check passed
@POWERFULMOVES
POWERFULMOVES deleted the feat/hardened-agent-sdk-cli branch January 3, 2026 15:33
POWERFULMOVES added a commit that referenced this pull request Jan 18, 2026
…365) (#404)

* feat(cli): rebrand Crush CLI to PMOVES CLI

Update user-facing branding from "Crush CLI" to "PMOVES CLI" while
maintaining backward compatibility with existing Crush infrastructure.

Changes:
- Update crush_app help text: "Crush CLI integration" → "PMOVES CLI integration"
- Update crush_configurator.py docstring to emphasize PMOVES deployment
- Update command help texts for setup/status/preview commands
- Update user-facing documentation in .claude/commands/crush/

Rationale:
The "Crush" name originated as an internal codename but the production
CLI should reflect the PMOVES brand for consistency with the broader
PMOVES.AI ecosystem. The underlying "crush" command name and file
paths are preserved for backward compatibility.

Modified Files:
- pmoves/tools/mini_cli.py
- pmoves/tools/crush_configurator.py
- .claude/commands/crush/setup.md
- .claude/commands/crush/status.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)



* feat(cli): add PMOVES Agent SDK commands to mini CLI

Implement agent-sdk sub-commands for creating and managing PMOVES Agent
instances with full ecosystem access via interactive CLI wizard.

Features Implemented:
- `pmoves agent-sdk create` - Interactive wizard for agent creation
  - 5 agent roles: researcher, code-reviewer, media-processor, knowledge-manager, general
  - Role-based tool and subagent configuration
  - Automatic NATS, TensorZero, and Hi-RAG connection
  - Unique agent ID generation with timestamps
  - Beautiful formatted output with configuration summary

- `pmoves agent-sdk run` - Execute tasks with existing agents
  - Task execution with streaming output
  - Model override support
  - Session resumption capability

- `pmoves agent-sdk list` - List agent instances
  - Status filtering
  - Configurable limit (placeholder for SessionManager integration)

- `pmoves agent-sdk status` - Check agent status
  - NATS heartbeat monitoring
  - Active agent information (placeholder for SessionManager)

Technical Details:
- Integrated with PMOVES-BoTZ Agent SDK
- Async/await pattern for agent lifecycle management
- Interactive role selection with graceful Ctrl+C handling
- Comprehensive error handling for missing dependencies
- Auto-discovery of PMOVES-BoTZ submodule

Usage Examples:
```bash
# Interactive agent creation
pmoves agent-sdk create

# Pre-select role
pmoves agent-sdk create --role researcher

# Execute task
pmoves agent-sdk run pmoves-researcher-1735123456 "Analyze architecture"

# List agents
pmoves agent-sdk list --status active --limit 50
```

Related Documentation:
- .claude/commands/agent-sdk/create.md
- .claude/commands/agent-sdk/run.md
- .claude/commands/agent-sdk/resume.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)



* docs(agent-sdk): update CLI documentation for run and resume commands

Update user-facing documentation for agent-sdk CLI commands to reflect
the new PMOVES CLI integration pattern.

Changes:
- `.claude/commands/agent-sdk/run.md`
  - Updated from skill-based to CLI command documentation
  - Added usage examples with `pmoves agent-sdk run`
  - Documented arguments and options
  - Added troubleshooting section

- `.claude/commands/agent-sdk/resume.md`
  - Updated from skill-based to CLI command documentation
  - Added session management workflow
  - Documented session states and storage backends
  - Added troubleshooting section

Documentation Pattern:
All agent-sdk command documentation now follows a consistent pattern:
- Usage section with use cases
- Implementation section with CLI examples
- Arguments and options tables
- What It Does checklist
- Related commands section
- Notes and troubleshooting

This aligns with the create.md documentation updated in the previous
implementation phase.

🤖 Generated with [Claude Code](https://claude.com/claude-code)



* fix(agent-sdk): address all PR #365 review comments

Fix all 14 issues from comprehensive PR review across error handling,
documentation, and code quality improvements.

Critical Fixes (4):
- Make NATS connection mandatory with ConnectionError on failure
- Add two-layer error handling to task execution
- Replace generic Exception catches with specific error types
- Exit with code 1 on all failure paths

Documentation (5):
- Correct NATS event subjects (remove non-existent events)
- Add prerequisites sections to all agent-sdk docs
- Fix example code placeholders with runnable examples
- Update model IDs (remove date suffixes)
- Document storage backends and timeouts

Improvements (5):
- Add Google-style docstrings to key functions (≥80% coverage)
- Enhance Crush configurator docstrings
- Improve list/status placeholders with NATS monitoring guidance
- Fix context manager usage pattern
- Add comprehensive timeout documentation

All syntax checks pass. Docstring coverage ≥80%.

🤖 Generated with [Claude Code](https://claude.com/claude-code)



---------

Co-authored-by: Codex Agent <codex-agent@example.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Aug 15, 2026
* feat(tools): pmoves standard branch protection spec + tool

The PMOVES standard branch-protection tool. Single source of truth for
how a PMOVES org repo's main branch is protected. The tool reads a
canonical JSON spec (pmoves_standard.json) and either audits a repo
against the spec, applies the spec to a repo, or drift-checks the
whole org.

What this slice lands:

- pmoves/configs/branch_protection/pmoves_standard.json - the
  canonical spec. 2 profiles (monorepo + fork) + per_repo_overrides
  for the 3 PMOVES repos in the org. The shape mirrors the GitHub
  REST API 1:1, so a profile maps to actual API calls without
  intermediate transformation.

- pmoves/tools/branch_protection.py - the tool. Pure-stdlib Python
  (urllib.request + json), no new deps. Three public functions:
    audit(repo, profile)        - diff actual state vs spec
    apply(repo, profile, dry_run=True) - apply the spec; dry-run by default
    drift_check(org)            - audit every repo in the org's overrides
  8 dataclasses for structured results (AuditResult, DriftItem,
  ApplyResult, DriftReport, etc.) so the orchestrator can consume
  the output. CLI surface: `python -m pmoves.tools.branch_protection
  {audit,apply,drift-check}` with structured JSON output.

Why this is the next slice after the harness v0:

The 3-PR review pass (PMOVES.AI #2477 + PMOVES-hermes-agent #4 +
PMOVES-pinokio #1) shipped the CGP bootstrap contract. The contract
ties 3 repos together, but the SECURITY POSTURE is wildly
asymmetric: PMOVES.AI is heavily protected (4 required status
checks, reviews with code owner enforcement, linear history,
signatures, 3 rulesets), but the 2 forks have NO protection at
all. The Hermes PR #4 was admin-merged only because there's no
required gate to be met - that's a bug-as-feature, not a
designed protection.

This tool makes the asymmetry visible (drift-check) and
correctable (apply). The Mavis cron can call drift-check daily
and publish on pmoves.branch_protection.drift.v1 (the NATS
subject lands in a follow-up slice).

Design notes (codified in the docstring + tests):

- The tool shells out to `gh api` instead of using urllib directly
  for HTTP. Reason: the PMOVES GitHub App token + the operator's
  PAT both flow through gh's auth, and wrapping gh gives the
  operator free auth-state inspection via `gh auth status`.
  Tradeoff: the tool requires `gh` installed and authenticated.
  Documented in BRANCH_PROTECTION_BASELINE.md (follow-up docs).

- dry-run is the default. The tool never issues a PUT/POST without
  `--no-dry-run`. The dry-run output is a JSON list of would-be
  API calls; the operator reviews the list before issuing live
  apply. The 2 forks in the spec will be applied manually after
  this PR merges (slice 2, separate PRs per repo).

- The per_repo_overrides section is the per-repo customization
  point. New forks add themselves here; the spec's strict shape
  (additionalProperties: false on the profile keys) catches
  typos before the tool hits the network.

- The diff function separates block (must-fix) from warn
  (advisory) severity. required_status_checks + required_review
  count are block; dismiss_stale_reviews + require_code_owner
  are warn. A compliant repo has zero block-level drift;
  warn-level drift is logged but doesn't fail the audit.

Three-body: delivery=Mavis (this PR), control=DARKXSIDE (operator
reviews the spec + the drift report, then runs apply manually for
each repo), memory=this trail + the spec + the LEARNINGS file.
CHIT trail unsigned-local (no CHIT_PASSPHRASE loaded in this Mavis
session).

* test(tools): 44 tests for branch_protection across 8 groups

Eight test groups cover the spec loader, the diff logic, the
apply body builder, audit + apply + drift-check end-to-end (with
mocked `gh api`), the CLI surface, and 2 error paths (gh missing
+ gh 404 for unprotected branch).

Test groups (each is a unittest.TestCase class):

- A. SpecLoaderTests (5) - load + resolve_repo_profile default +
  with override + unknown repo + unknown profile
- B. DiffLogicTests (14) - 4 status-check scenarios, 3 review-policy
  scenarios, 4 boolean-field scenarios (including the nested
  {"enabled": bool} shape the real API returns), 3 rulesets
  scenarios
- C. ApplyBodyTests (4) - classic body has all 9 required keys,
  preserves values, ruleset body has 6 required keys, preserves
  rules
- D. AuditTests (6) - compliant repo, no-protection-is-block,
  missing-check-is-block, extra-check-is-warn, explicit profile,
  unknown-repo-raises
- E. ApplyTests (5) - dry-run-no-calls, live-calls, skips-existing-
  ruleset, includes-per-repo-override-checks, unknown-repo-raises
- F. DriftCheckTests (3) - one-report-per-repo, only-org-repos,
  surfaces-audit-error-as-synthetic-drift
- G. CLITests (4) - audit-exits-0-when-compliant, audit-exits-1-
  when-drift, drift-check-exits-2-when-any-drift, apply-default-
  is-dry-run (the dry-run does ONE read for existing rulesets
  so the output can accurately report skip-vs-create; no PUT or
  POST is issued; the test asserts both)
- H. ErrorPathTests (2) - gh-missing-raises, gh-404-for-unprotected-
  returns-none (the real GitHub API returns 404 + "Branch not
  protected" stderr for an unprotected branch; the tool
  recognizes this and returns None, not an error)

All 44 tests pass. The mocked subprocess calls use a lambda
side_effect keyed on (method, path) so the test surface is
explicit and the failure mode is "unmocked gh call" rather than
a silent no-op.

Test design notes (codified in the test docstring):

- The mocks use the SAME shape as the real GitHub API
  response: required_status_checks.checks is a list of
  {"context": "name"} objects; required_linear_history etc are
  nested as {"enabled": bool}; rulesets is a list of dicts with
  name + id + rules + bypass_actors. The diff logic was updated
  to handle both the spec's bare-boolean shape and the API's
  {"enabled": bool} shape, so a real audit + a unit test give
  the same answer.

- The drift-check test (F3) ensures that an audit error (e.g.,
  gh subprocess failure) doesn't crash the whole drift report -
  the erroring repo appears with a synthetic DriftItem so the
  operator can see which repos failed and why. This is the
  pattern the Mavis cron relies on.

Three-body: delivery=Mavis (this), control=DARKXSIDE (operator
can run the tests locally with `python -m unittest
pmoves.tools.tests.test_branch_protection` before applying the
spec to the 2 forks), memory=this trail. CHIT trail unsigned-local.

* docs(agnote): branch protection v0 CLAIM row + 2-fork apply record

Records the Slice 2 fan-out: the PMOVES standard branch protection
tool landed (PR #2490) + both unprotected forks now have
the fork profile applied.

Real-run evidence (this commit's author):
- python -m pmoves.tools.branch_protection apply --repo
  POWERFULMOVES/PMOVES-pinokio --no-dry-run → created classic
  protection (CodeRabbit required, 1 reviewer, linear history,
  conversation resolution) + [main] ruleset (id=20589542)
- python -m pmoves.tools.branch_protection apply --repo
  POWERFULMOVES/PMOVES-hermes-agent --no-dry-run → created
  classic protection (9 required status checks, 1 reviewer,
  linear history, conversation resolution) + [main] ruleset
  (id=20589548)
- python -m pmoves.tools.branch_protection drift-check --org
  POWERFULMOVES (post-apply) → both repos compliant, zero drift

What's NOT in this slice (intentional follow-up):
- PMOVES.AI migration to rulesets-only (Option A approved by
  operator; needs a separate migration script because the
  current tool's `apply` doesn't support "delete classic +
  consolidate rulesets"). The bypass_actor list from the
  [main] ruleset (RepositoryRole id=5, Integration id=1144995,
  Integration id=1236702) must be re-registered in the new
  ruleset.
- NATS subject pmoves.branch_protection.drift.v1 (Slice 3)
- Mavis cron that calls drift-check daily (Slice 3)
- BRANCH_PROTECTION_BASELINE.md + pair-review skill update
  (Slice 4)

Three-body: delivery=Mavis, control=DARKXSIDE, memory=this
trail + PR #2490. CHIT trail unsigned-local.

* feat(tools): PMOVES.AI branch-protection migration (Option A)

The one-off migration script that consolidates PMOVES.AI's
classic + 3-ruleset layered state into a single ruleset
([ main ]) with the status check + review requirements +
copilot_code_review + the 3 bypass_actors preserved.

What this slice lands:

- pmoves/tools/branch_protection_migrate_pmai.py - the
  migration script. Pure-stdlib (no new deps), uses the existing
  branch_protection.py helpers (the same `gh api` wrapper, the
  same spec loader, the same dataclass patterns). 2 public
  functions:
    plan()         - reads the current state, computes the new
                      [ main ] ruleset body, returns a MigrationPlan
    apply(plan, dry_run=True) - issues DELETE classic + PUT
                      [ main ] ruleset; dry-run is the default

- pmoves/tools/tests/test_branch_protection_migrate_pmai.py -
  15 tests across 4 groups (compute_main_ruleset,
  capture_state, plan, apply). All 59 tests pass across
  both the tool + the migration.

- pmoves/configs/branch_protection/pmoves_standard.json -
  added `submodule-gitlink-gate` to the monorepo profile's
  required status checks. The actual state has 5 required
  checks; the original spec had 4. This aligns the spec
  with reality.

The migration is destructive (DELETE classic + PUT ruleset
in a different shape), so dry-run is the default. The
operator reviews the call sequence + the captured state
before --no-dry-run is issued.

Design notes (codified in the LEARNINGS file):

- The list endpoint /rulesets returns a SUMMARY without
  bypass_actors. The migration re-fetches the per-ruleset
  body to get the full bypass_actors list. Without this
  re-fetch, the migration would silently drop the operator's
  preauthorized --admin bypass. Captured in LEARNINGS lesson 1.

- The pull_request rule in a ruleset uses different field
  names than the classic required_pull_request_reviews
  block. The migration explicitly maps the spec's
  required_pull_request_reviews keys to the ruleset
  pull_request parameters. Captured in LEARNINGS lesson 2.

- The spec's monorepo profile hard-codes RepositoryRole id=5
  as the default bypass_actor. The migration OVERRIDES this
  with the captured bypass_actors from the existing ruleset
  (3 actors: RepositoryRole id=5, Integration id=1144995,
  Integration id=1236702). The spec is the source of truth
  for new repos; the migration preserves the operator's
  actual escape hatch for this repo. Captured in LEARNINGS
  lessons 5 + 6.

- The migration is a one-off. After it runs, the canonical
  branch_protection.py apply tool keeps the [ main ]
  ruleset in sync with the spec. The migration script is
  archived in the tool's directory; the spec + the tool are
  the source of truth going forward.

Migration call sequence (dry-run, current state):

1. DELETE /repos/POWERFULMOVES/PMOVES.AI/branches/main/protection
2. PUT /repos/POWERFULMOVES/PMOVES.AI/rulesets/10887588 with:
   - name: [ main ]
   - rules: deletion, non_fast_forward, pull_request (1 reviewer
     + code owner + dismiss stale + review thread resolution),
     copilot_code_review, required_status_checks (5 checks),
   - bypass_actors: 3 (preserved)

Three-body: delivery=Mavis, control=DARKXSIDE (operator
reviews the dry-run output before --no-dry-run), memory=this
trail + the LEARNINGS file + the BRANCH_PROTECTION_BASELINE.md
doc. CHIT trail unsigned-local.

* docs(operations+learnings): branch protection baseline + 5-class LEARNINGS

Two companion docs for the branch protection fan-out.

- pmoves/docs/operations/BRANCH_PROTECTION_BASELINE.md - the
  human-readable version of pmoves_standard.json. Covers:
    - Why a baseline (the 3-PR review pass surfaced the
      asymmetric protection state; this doc is the fix)
    - The 2 profiles (monorepo + fork) with field-level
      rationale + the GitHub doc citation for each
    - Current state per repo (PMOVES.AI: not yet migrated;
      PMOVES-hermes-agent: applied; PMOVES-pinokio: applied)
    - How to apply / audit / drift-check (with the exact
      `python -m pmoves.tools.branch_protection` invocations)
    - How to add a new repo or a new profile
    - The PMOVES.AI migration plan (Option A, the next apply)
    - Wire-up to the harness (load_bootstrap CGP, Mavis cron,
      orchestrator dispatch)
    - 5 references to the official GitHub docs (rulesets,
      protected branches, troubleshooting, MergeStateStatus
      enum, the LEARNINGS file)

- pmoves/tools/LEARNINGS/branch-protection-v0_LEARNINGS.md -
  the 5-class taxonomy + 4-bucket learning signal per the
  pr-trim convention. Populated with 13 already-fixed, 5
  out-of-scope, 0 pre-existing observations. The "Pattern
  update" section adds 6 new lessons to the pmoves-pair-review
  skill's step 7:
    1. The list endpoint /rulesets returns a SUMMARY without
       bypass_actors. Re-fetch the per-ruleset body when
       bypass_actors is needed.
    2. The pull_request rule in a ruleset uses different field
       names than the classic required_pull_request_reviews
       block. Map explicitly.
    3. additionalProperties: false is the right default for
       required objects, but bypass_actors and status_checks
       should stay open (extending pair-review lesson 3 to
       nested arrays).
    4. UNSTABLE = mergeable + bypass_actors re-fetch = mandatory.
       Both are silent-corruption traps.
    5. The spec is the source of truth for fresh repos, but
       the migration captures the existing bypass_actors to
       preserve the operator's escape hatch.
    6. Migrate the operator's preauthorized bypass list
       explicitly; don't rely on the spec's defaults.

Three-body: delivery=Mavis, control=DARKXSIDE, memory=this
trail + the spec + the migration script. CHIT trail
unsigned-local.

* refactor(tools): collapse branch_protection to ruleset-only per operator ratification

Per the operator's 2026-08-10 ratification (PR #2490 review id 4893614185) grounded
in GitHub's "About rulesets" docs: classic branch protection is NOT deprecated,
rulesets LAYER with it ("the most restrictive version of the rule applies"),
and "start using rulesets without overriding any of your existing protection rules"
is the intended adoption path. This collapses the original migration script
(N1/N2/N3 delete) + the classic-PUT body builder (P1-A/P1-C/N8 delete) into
the new ownership split:

  - .github/workflows/branch-protection-sync.yml owns CLASSIC protection
  - pmoves/tools/branch_protection.py owns RULESETS only
  - The two writers layer additively (most-restrictive-wins)
  - Additive adoption is monotonic - the tool can only make a branch stricter

What changes:
  - pmoves/tools/branch_protection.py: dropped _build_classic_body +
    _diff_required_status_checks + _diff_review_policy + _diff_boolean_field;
    added SpecValidator (validates at load, per P1-B); added resolve_branch()
    (per_repo_overrides -> .gitmodules -> spec default -> "main", per N4);
    deep-diff _ruleset_matches() (rules + conditions + bypass_actors, per N6);
    apply() now creates missing AND updates existing rulesets; _gh_api has
    GH_TIMEOUT_SECONDS=30
  - pmoves/configs/branch_protection/pmoves_standard.json: upgraded to v2
    (pmoves.rulesets/v2); profiles only have rulesets: []; monorepo profile
    carries 8 ruleset rules; fork profile has required_approving_review_count=0
    (matches workflow default, per N5); per_repo_overrides includes PMOVES.AI +
    PMOVES-hermes-agent + PMOVES-pinokio + PMOVES-nats-server (the new fork)
  - pmoves/docs/operations/BRANCH_PROTECTION_BASELINE.md: rewritten with the
    ownership split documented at the top
  - pmoves/tools/LEARNINGS/branch-protection-v0_LEARNINGS.md: 5-class taxonomy
    updated (15 already-fixed / 6 owner / 5 out-of-scope / 4 pre-existing);
    2 new pair-review lessons (#7 merge-by-type ruleset overrides; #8
    ~DEFAULT_BRANCH sentinel in conditions.ref_name.include); ratification
    documented (5 of 6 P1s collapse to deletions)

Bugs caught and fixed during the refactor:
  - spec had require_linear_history typo (correct: required_linear_history)
  - VALID_RULESET_RULE_TYPES was missing required_conversation_resolution
  - _ruleset_matches() was running the conditions.ref_name.include comparison
    after stripping the sentinel (should have skipped it entirely)
  - resolve_repo_profile() was REPLACING the rules array in ruleset_overrides
    (should have MERGED by type)

CHIT trail unsigned-local. Three-body: delivery=Mavis, control=DARKXSIDE,
memory=this commit + the spec + the LEARNINGS file.

* test(tools): rewrite 55 tests for ruleset-only branch_protection

The 44 old tests targeted the old classic+ruleset tool shape
(_build_classic_body, _diff_required_status_checks, _diff_review_policy,
_diff_boolean_field). Replaced with 55 tests across 9 groups for the
post-ratification ruleset-only API:

  A. SpecValidatorTests (13 tests)
     - spec shape, rule type validation, target/enforcement validation,
       override->profile cross-check, multi-error collection, load_spec
       path + skip-validation flag, validator set completeness
       (required_conversation_resolution, required_linear_history)

  B. ResolveRepoProfileTests (7 tests)
     - default + ruleset_override merge, unknown repo/profile raise,
       no-input-mutation, MERGE-BY-TYPE semantics (B6, B7)

  C. ResolveBranchTests (4 tests)
     - override wins, .gitmodules lookup matches workflow logic,
       no-override-no-gitmodules -> main, slug extraction for PMOVES.AI

  D. RulesetDiffTests (7 tests)
     - compliant, missing rule, extra rule, drifted parameters,
       drifted bypass_actors, drifted enforcement, ~DEFAULT_BRANCH
       sentinel handling (D7)

  E. AuditTests (5 tests)
     - compliant repo, no rulesets drift, explicit profile, unknown
       repo raise, per-ruleset re-fetch for bypass_actors (lesson #1)

  F. ApplyTests (7 tests)
     - dry-run creates, live creates, update existing with drift,
       skip in-sync, per_repo ruleset_overrides, unknown repo raise,
       strip ~DEFAULT_BRANCH sentinel

  G. DriftCheckTests (3 tests)
     - one report per repo, org filter, audit error surface

  H. CLITests (4 tests)
     - exit 0 compliant, exit 1 drift, exit 2 any-repo drift,
       default dry-run

  I. GHErrorPathTests (4 tests)
     - gh missing, gh timeout, gh nonzero stderr, "Branch not protected"
       returns None (404 is expected state)

Two new pair-review lessons (B6/B7 merge-by-type + D7 ~DEFAULT_BRANCH
sentinel) are codified in the LEARNINGS file.

CHIT trail unsigned-local.

* docs(agnote): Mavis::BRANCH-PROTECTION-V0-RATIFICATION-REFACTOR trail row

Records the 2026-08-10 refactor of the branch_protection tool to
ruleset-only per the operator's ratification (PR #2490 review id
4893614185). Captures the 5-of-6 P1s collapse to deletions, the
ownership split (tool = rulesets, workflow = classic), the spec v2
upgrade, the 4 additional bugs caught and fixed during the refactor
(require_linear_history typo, missing rule type, ~DEFAULT_BRANCH
sentinel handling, merge-by-type ruleset overrides), and the 55-test
rewrite.

CHIT trail unsigned-local. Three-body: delivery=Mavis, control=DARKXSIDE,
memory=this trail.

* refactor(tools): actually delete branch_protection_migrate_pmai.py

The 2026-08-10 ratification said the migration script goes away: classic
branch protection is not deprecated, rulesets layer with it, and "the most
restrictive version of the rule applies" — so there is nothing to migrate
away from and no reason to DELETE classic protection before a replacement
exists.

The refactor commit b0fbf68 rewrote branch_protection.py to ruleset-only
but left the script and its 15 tests on disk, while the PR comment reported
them as deleted. Verified against the tree: both files were still tracked at
78157b7. This makes the reported state the real state.

That closes the four findings that only existed because of the script:
  N1  DELETE fires before any replacement (with a test asserting it should)
  N2  signed commits + linear history silently dropped by the migration
  N3  captured_required_status_checks captured, printed, never used
  #20 migration test docstrings

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

* fix(tools): make the resolved branch load-bearing in the ruleset writer

Five defects, all in the path between "the spec says which branch" and
"the ruleset GitHub actually stores". Verified against the live org, not
just the diff.

1. ~DEFAULT_BRANCH was stripped, never substituted (CRITICAL)
   _build_ruleset_body removed the sentinel from conditions.ref_name.include
   and put nothing back, so a created ruleset carried an EMPTY include list
   and matched no ref. It now takes the resolved branch and writes
   refs/heads/<branch>. The old test asserted only assertNotIn(sentinel),
   which an empty list satisfies — that is how it shipped.

2. The diff SKIPPED the include comparison whenever the spec used the
   sentinel, so a ruleset pinned to the wrong branch reported compliant.
   _ruleset_matches now takes the branch and resolves the sentinel on the
   EXPECTED side only. A live ~DEFAULT_BRANCH stays unresolved on purpose:
   it means "whatever GitHub currently calls default", which is not the
   branch we mean.

   Live evidence for why both matter: PMOVES-hermes-agent's default branch
   is main, but the monorepo consumes PMOVES.AI-Edition-Hardened. The
   ruleset applied in Slice 2 targets ~DEFAULT_BRANCH -> main. The branch
   that actually ships has no ruleset, and audit called it compliant. It
   now reports drift. This is N4 in production, not in theory.

3. .gitmodules lookup used `slug in section`, a substring match. The slug
   PMOVES-nats matched submodule "PMOVES-nats-server" and would write that
   repo's branch. Now matches the exact section name or the url basename.

4. resolve_branch step 3 looped over EVERY profile and returned the first
   branch it found, so one profile declaring a branch would leak it onto
   every repo without an override. It now takes the resolved profile name
   and reads only that profile.

5. apply crashed on `created.get` when a POST returned an empty body
   (_gh_api returns None). The write had already happened, so the repo was
   left changed with no entry in `applied`. Now records it with a fallback id.

Also: rule parameters were compared by strict equality, but GitHub echoes
back its own defaults (required_reviewers, allowed_merge_methods) that the
spec never declares — every audit reported permanent drift and every apply
re-PUT a correct ruleset. Comparison is now a subset over spec-declared keys
only, which is what makes drift_check trustworthy enough to run on a cron.

Tests: 64 pass (was 55). New: C5-C7 (exact + url-basename match, profile
scoping), D8-D11 (include drift on a non-default branch, live sentinel not
silently matched, API-defaulted params not drift, declared mismatch still
reported), F7-F9 (substitution, no spec mutation, empty POST body).
Fixed A11, which patched read_text but not exists() and so passed on the
"spec not found" error without ever reaching validation; and I2-I4, which
depended on a real gh binary being on PATH.

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

* docs(branch-protection): correct the baseline + LEARNINGS against the tree

- NATS catalog link was ../nats-subjects.md, which resolves to
  pmoves/docs/nats-subjects.md — a file that does not exist. Repointed at
  the canonical .claude/context/nats-subjects.md. (A prior comment marked
  this fixed; it was not.)
- require_linear_history -> required_linear_history everywhere. The v2
  contract and the GitHub rule type both use the required_ prefix, and an
  operator copying the table into pmoves_standard.json would fail validation.
- Lesson count 6 -> 8, and "5 of 6 P1s" -> "6 of 6" (the section lists six:
  N1, N2, N3, P1-A, P1-C, N8).
- Rewrote lesson 8. It codified "skip the include check when the spec uses
  the sentinel" as the right behavior; that was the bug. Replaced with the
  general form: a sentinel a builder strips but never substitutes is a
  silent no-op — resolve it, and assert on what replaced it rather than on
  its absence.
- Lesson 5 notes that the migration script it was learned on is gone.
- Documented the release gate on --no-dry-run: claim -> work (dry-run,
  confirm the resolved branch) -> sign -> release, with post-apply evidence.
  If signing is unavailable, the release stays pending.

Two corrections that came out of reading the workflow rather than the diff:

- The doc said the monorepo profile "layers on top of whatever classic
  protection branch-protection-sync.yml writes". It does not.
  The workflow derives its scope from .gitmodules and PMOVES.AI is not a
  submodule of itself, so on the monorepo there is no second writer and
  required_approving_review_count: 1 is the only review gate in play. That
  also means the N5 layering deadlock cannot apply to this profile — the
  fork profile already resolves it at 0.
  Flagged for the operator instead: apply --no-dry-run on PMOVES.AI would
  newly enforce required_signatures and required_linear_history on main.
  Both are real behavior changes to the merge flow, so they are called out
  as decisions rather than defaults.
- Added the 2026-08-10 audit finding: the Slice 2 rulesets on
  PMOVES-hermes-agent and PMOVES-pinokio target ~DEFAULT_BRANCH, so the
  hardened gitlink branch is ungated. Remediation is a re-apply behind the
  release gate.

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

* docs(agnote): 4090-CLAUDE::PR2490-TRIM-16-THREADS trail row

Appended as a correction rather than an edit to the preceding row: that
row recorded the migration script as deleted and the ~DEFAULT_BRANCH
include-skip as correct behavior, and both were wrong against the tree.
The historical row stays as written; this one records what was actually
found and what changed.

Also records the two decisions left to the operator (the PMOVES.AI
--no-dry-run, and the re-apply that remediates the wrong-branch rulesets
on the two Slice 2 forks) rather than taking them.

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

* docs(learnings): lesson 9 — a test that can only assert absence cannot say no

Promotes the root cause of lesson 8 to its own entry, at the team lead's
request, because the fix is a habit and the failure mode is silent.

The guard on the sentinel substitution was
`assertNotIn("~DEFAULT_BRANCH", includes)`, which passes on an empty list.
It therefore held green across exactly the two states it existed to
distinguish: sentinel correctly replaced by a real ref, and sentinel
deleted with nothing put back. A test that could only report success,
guarding a ruleset whose empty include list matched no ref while `apply`
printed "applied".

The tell is structural rather than domain-specific, so the lesson is
written to generalize: an assertion whose predicate is satisfied by the
empty/null/absent case is not a gate. assertNotIn, assertNotEqual,
assertFalse, "no error raised", an empty `grep -v`, `rc == 0` on a command
that no-ops when misconfigured — each admits a degenerate state alongside
the intended one. When a transform removes something, assert on what
replaced it.

With three checks in cost order: ask what the empty case does; mutate the
implementation to the degenerate state and confirm the test goes red (a
guard that survives its own sabotage was never a guard); and for tools
that write to an external system, verify against live state once. Here a
single `gh api ... --jq .default_branch` collapsed the whole question.

Lesson count 8 -> 9 in both this file and the baseline doc.

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

---------

Co-authored-by: Mavis <Mavis@pmoves.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant