diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b755f8f60..be70dca781 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Python uses: actions/setup-python@v5 @@ -68,8 +66,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Python uses: actions/setup-python@v5 @@ -79,7 +75,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e src/agent_comms/skills/refactor-audit -e ".[dev]" + pip install -e ".[dev]" - name: Prepare pinned native Pi uses: ./.github/actions/prepare-native @@ -111,8 +107,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Python uses: actions/setup-python@v5 diff --git a/.github/workflows/debt-ratchet-cache.yml b/.github/workflows/nra-gate-cache.yml similarity index 75% rename from .github/workflows/debt-ratchet-cache.yml rename to .github/workflows/nra-gate-cache.yml index 10ef3b6ef5..6389b45c4b 100644 --- a/.github/workflows/debt-ratchet-cache.yml +++ b/.github/workflows/nra-gate-cache.yml @@ -1,7 +1,7 @@ -name: Debt ratchet cache +name: NRA gate cache # A pull request restores only caches saved on its own ref or on main, and the merge queue runs -# never run on main, so every ratchet measured main cold. Measuring each main commit against its +# never run on main, so every gate run measured main cold. Measuring each main commit against its # parent saves main's readings and mypy analysis for every pull request based on it. on: push: @@ -11,7 +11,7 @@ permissions: contents: read concurrency: - group: debt-ratchet-cache + group: nra-gate-cache cancel-in-progress: true jobs: @@ -26,7 +26,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - submodules: recursive fetch-depth: 0 - uses: actions/setup-python@v5 @@ -43,16 +42,16 @@ jobs: - uses: actions/cache/restore@v4 with: path: ~/.cache/agent-comms - key: debt-ratchet-${{ matrix.kind }}-${{ github.sha }} - restore-keys: debt-ratchet-${{ matrix.kind }}- + key: nra-gate-${{ matrix.kind }}-${{ github.sha }} + restore-keys: nra-gate-${{ matrix.kind }}- # The verdict belongs to the merge queue; this run only fills the cache. - name: Measure main against its parent run: | - PYTHONPATH=src:tools python tools/debt_ratchet.py \ + PYTHONPATH=src:tools python tools/nra_gate.py \ --root ${{ matrix.kind }} --base HEAD^1 --head HEAD || true - uses: actions/cache/save@v4 with: path: ~/.cache/agent-comms - key: debt-ratchet-${{ matrix.kind }}-${{ github.sha }} + key: nra-gate-${{ matrix.kind }}-${{ github.sha }} diff --git a/.github/workflows/debt-ratchet.yml b/.github/workflows/nra-gate.yml similarity index 90% rename from .github/workflows/debt-ratchet.yml rename to .github/workflows/nra-gate.yml index 1ac9927be1..32f51e949b 100644 --- a/.github/workflows/debt-ratchet.yml +++ b/.github/workflows/nra-gate.yml @@ -11,7 +11,7 @@ permissions: contents: read # A superseded push to a pull request stops its old run; queue groups never share a group. -# The draft state is part of the key: a draft run skips every job but the ratchets, so one that +# The draft state is part of the key: a draft run skips every job but the NRA gate, so one that # starts in the same second as ready_for_review must not cancel the ready run (#3164 and #3154 lost # every check). concurrency: @@ -24,7 +24,7 @@ defaults: jobs: # Drafts cannot merge, so they hold no runner for the slow jobs, which need this one. The - # ratchets run on drafts too: units hold drafts for hours, and a merge from main that left a + # the NRA gate runs on drafts too: units hold drafts for hours, and a merge from main that left a # name undefined (#3107, partial at cli_protocol.py:177) went unseen until someone ran one. ready: if: github.event_name == 'merge_group' || !github.event.pull_request.draft @@ -60,13 +60,12 @@ jobs: steps: - uses: actions/checkout@v4 with: - submodules: recursive fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version-file: pyproject.toml - name: Install agent-comms and test dependencies - run: python -m pip install -e src/agent_comms/skills/refactor-audit -e '.[dev]' + run: python -m pip install -e '.[dev]' - name: Compile production and tools run: python -m compileall -q src tools - name: Import every Core and hook module @@ -99,13 +98,11 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v4 - with: - submodules: recursive - uses: actions/setup-python@v5 with: python-version-file: pyproject.toml - name: Install Core - run: python -m pip install -e src/agent_comms/skills/refactor-audit -e . + run: python -m pip install -e . - name: Prepare pinned native Pi uses: ./.github/actions/prepare-native - name: Run every native script @@ -130,7 +127,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - submodules: recursive fetch-depth: 0 # The rehearsal installs the package and runs its own tests; a change to neither # (tools, guards, docs) cannot change what it observes. @@ -153,7 +149,7 @@ jobs: python -m pip install "$(python -I src/agent_comms/installer/declared_uv.py pyproject.toml)" - name: Install non-editable Core and test dependencies if: steps.reach.outputs.reaches == 'true' - run: python -m pip install -e src/agent_comms/skills/refactor-audit '.[dev]' + run: python -m pip install '.[dev]' - name: Build the installer's private runtime and rehearse through its CLI if: steps.reach.outputs.reaches == 'true' run: python -m agent_comms.installer.test_runtime $REHEARSALS @@ -183,7 +179,7 @@ jobs: echo "PR #${BASH_REMATCH[1]} head $head: agent-comms/review=$verdict" [[ $verdict == success ]] - ratchet: + nra-gate: name: debt-${{ matrix.kind }} needs: base runs-on: ubuntu-latest @@ -198,7 +194,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - submodules: recursive fetch-depth: 0 - uses: actions/setup-python@v5 @@ -214,24 +209,24 @@ jobs: uv pip install --python "$GITHUB_WORKSPACE/.venv/bin/python" -e '.[dev]' echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" - # Saved by debt-ratchet-cache.yml on main; entries are keyed by content, so any + # Saved by nra-gate-cache.yml on main; entries are keyed by content, so any # older main's cache only misses where the code differs. - uses: actions/cache/restore@v4 with: path: ~/.cache/agent-comms - key: debt-ratchet-${{ matrix.kind }}-${{ needs.base.outputs.base }} - restore-keys: debt-ratchet-${{ matrix.kind }}- + key: nra-gate-${{ matrix.kind }}-${{ needs.base.outputs.base }} + restore-keys: nra-gate-${{ matrix.kind }}- - name: No new structural debt against the base env: BASE: ${{ needs.base.outputs.base }} run: | set -o pipefail - ratchet_status=0 - PYTHONPATH=src:tools python tools/debt_ratchet.py \ - --root ${{ matrix.kind }} --base "$BASE" --head HEAD 2>ratchet-findings.txt | tee ratchet.txt || ratchet_status=$? - cat ratchet-findings.txt >&2 - exit "$ratchet_status" + gate_status=0 + PYTHONPATH=src:tools python tools/nra_gate.py \ + --root ${{ matrix.kind }} --base "$BASE" --head HEAD 2>gate-findings.txt | tee gate.txt || gate_status=$? + cat gate-findings.txt >&2 + exit "$gate_status" # Hosted annotations stop at ten per step, so a unit never saw a long list (88 findings # on the bus batch, 2026-10-21). The whole list goes on the pull request, one comment per @@ -246,7 +241,7 @@ jobs: run: | marker="" { echo "$marker"; echo "**$KIND** at $(git rev-parse --short HEAD), every finding:"; echo '```'; - cat ratchet-findings.txt; echo '```'; } > body.md + cat gate-findings.txt; echo '```'; } > body.md old=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR/comments" --paginate \ --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -1) if [ -n "$old" ]; then diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index bcff15f35f..b3f001405f 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -19,7 +19,6 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - submodules: recursive - uses: actions/setup-python@v5 with: python-version-file: pyproject.toml diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 14ab54779f..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/agent_comms/skills/refactor-audit"] - path = src/agent_comms/skills/refactor-audit - url = https://github.com/OpenHCSDev/refactor-audit.git diff --git a/.pi/APPEND_SYSTEM.md b/.pi/APPEND_SYSTEM.md index 9e30c3c259..d49ce052a1 100644 --- a/.pi/APPEND_SYSTEM.md +++ b/.pi/APPEND_SYSTEM.md @@ -89,7 +89,7 @@ You are one of several agents coordinating through agent-comms. Tristan owns pro - OpenHCS #44, #45, #51, #58 and #60 are standing architectural precedents: shared lifecycle behavior, declaration-derived discovery, widget reuse and targeted invalidation, owned state with paint derived from time, and deletion of the competing compiler/runtime lattice. Read the underlying source relationships and the historical replacement cost in `docs/refactor/cleanup-20260929/OPENHCS-HISTORY.md`. Choose a coherent owner-and-consumers change that removes competing decisions and unnecessary work. Extend the existing behavior-owning types through inheritance and composable capabilities; delete the replaced mechanism in the same change. A new wrapper, queue, guard, class, report or verification step must remove necessary work or resolve a concrete risk. Do not turn AST evidence, scope boundaries or test gates into another project. Batch implementation and final verification, ship useful checkpoints, and keep independent work moving. Tristan has already paid for repeated rebuilds; do not make him supervise the same correction again. -- Use AST for every structural refactor: enumerate declarations, writes, decisions, checks, imports, inheritance and all consumers across the relevant production and dependency roots before editing. Read the resulting sites semantically, find the existing behavior-owning class, and migrate the complete related family in one coherent batch, deleting every competing authority path. Record before/after owner and consumer evidence in the PR; report parse omissions and ambiguous dynamic resolution explicitly. Reuse NRA/refactor-audit tooling instead of copying scanners. AST is source evidence, not a behavioral proof; tests and the affected installed live path come last. Preserve legitimate parallel execution while removing parallel semantic authorities. OpenHCS PR #60, commit 5e8812ee83d0dc8714392445bad3e32fc47a1755, tests/unit/test_cellprofiler_static_deletion_gates.py, is the concrete precedent. +- Use AST for every structural refactor: enumerate declarations, writes, decisions, checks, imports, inheritance and all consumers across the relevant production and dependency roots before editing. Read the resulting sites semantically, find the existing behavior-owning class, and migrate the complete related family in one coherent batch, deleting every competing authority path. Record before/after owner and consumer evidence in the PR; report parse omissions and ambiguous dynamic resolution explicitly. Reuse NRA tooling instead of copying scanners. AST is source evidence, not a behavioral proof; tests and the affected installed live path come last. Preserve legitimate parallel execution while removing parallel semantic authorities. OpenHCS PR #60, commit 5e8812ee83d0dc8714392445bad3e32fc47a1755, tests/unit/test_cellprofiler_static_deletion_gates.py, is the concrete precedent. - Tristan's corrections are standing rules. Apply the newest correction across the related workflow immediately and incorporate it into this file at the next safe checkpoint. Do not leave it as a one-turn exception, repeat the rejected approach, or require Tristan to supervise its propagation. - Stop symptom fixes in native runtime lifecycle and compaction. Each area has one integration owner doing one semantic source pass before implementation: list every fact, including payload budget, session identity, source coverage, turn custody and cursor position; every declaration, decision and check; and every consumer. Give each fact one owner type, have consumers take that type, and delete competing authorities in the same change. Use inheritance and declaration-owned polymorphism to place behavior with its owner. The retained-context payload type owns the payload budget used by reservation, commit intent and recovery. Do not raise a cap on one path or patch individual callers. A PR may claim a fact is derived from its owner only when it includes the actual search showing that fact is declared in exactly one place. Tests and the installed live path come last. This supersedes earlier test-first and discriminator-first instructions. @@ -98,7 +98,7 @@ You are one of several agents coordinating through agent-comms. Tristan owns pro - Never leave a retained analysis workflow or viewer without an assigned active owner while its requested analysis remains unfinished. Freezing a checkpoint preserves evidence; it is not permission to silently abandon development. Keep the original frozen run immutable and continue repairs in a separately recorded development phase, then require a fresh unguided run before claiming autonomous success. - For the OpenHCS blind-analysis programme, the finish line is successful analysis across the curated blind datasets by SOL 6.1 agents using only the task brief, MCP and packaged skill. Maintain at least three independent analysis authors concurrently when measured aggregate RAM permits; engineering reviews and assisted corrections do not count toward that minimum or autonomous success. Assign one separate worker to monitor and clean the files and scratch generated by this programme. Improve the canonical skill from evidenced failures, then test fresh authors without dataset-specific coaching. GLM 5.3 Flash comparison comes only after this success criterion is met and Tristan proceeds with that stage. - Open a draft PR before long work and commit/push meaningful working checkpoints. Preserve dirty shared checkouts, other agents' work, saved history and uncertain dispositions. Worktrees, source and handoffs stay under `/home/ts/wt`; track and clean owned disposable output. -- Read and regularly revisit the latest NRA/refactor-audit skill and antipattern catalog. Reason about the complete source, lifecycle and consumer relationships. Use behavior-owning types, inheritance and declaration-owned polymorphism; decode external input once through the existing codec. Delete replaced code in place. No duplicate stores, state mirrors, compatibility aliases, legacy readers or alternate codecs. Model state instead of recombining nullable fields and flags. +- Read and regularly revisit the latest NRA skill and antipattern catalog. Reason about the complete source, lifecycle and consumer relationships. Use behavior-owning types, inheritance and declaration-owned polymorphism; decode external input once through the existing codec. Delete replaced code in place. No duplicate stores, state mirrors, compatibility aliases, legacy readers or alternate codecs. Model state instead of recombining nullable fields and flags. - The UI is a view of canonical backend state. Derive queue, turn, goal, message handling and status from their original owners; do not retain semantic copies in widgets. Check actual visible updates, notifications and input delivery, including when a view is already open, hidden, reopened or switched. - Comms verification covers one continuous journey: saved-history startup; channel-bar and unopened-thread opening; actual tab clicks and recent returns; fork and immediate opening before the first native answer; first message; channel/DM notification, reply, handling, queue, status and history. Preserve draft, undo and reader position. Returning to an open agent tab must retain warm prepared and rendered history; editor preservation or syntax caching alone is insufficient. - Prevent UI stalls through workload and lifetime ownership across thread opening, sidebar resizing and scrolling, history loading and view retirement. UI callbacks consume admitted resources; blocking reads and bulk preparation run through the preparation owner. Redesign the workflow and its abstractions when needed, even when the change is substantial. Existing-type guidance does not limit necessary architectural work. Delete the stall-producing paths and competing lifecycle decisions; a cache, delay or exception guard does not replace that ownership change. Confirm the affected real application journey after the coherent implementation. @@ -108,4 +108,4 @@ You are one of several agents coordinating through agent-comms. Tristan owns pro - Keep implementation and verification with the agent that already owns the context. Delegate genuinely independent work; do not transfer a continuing task merely to redistribute activity. -- Proactively question suspicious existing and newly written code throughout implementation: duplicate authorities or behavior, unnecessary distinctions, hardcoded policy and forwarding-only abstractions. Regularly revisit the actual NRA/refactor-audit examples; trace the existing owner and all related consumers, then fold coherent deletions into the current owned change. Do not wait for a reported bug or create a separate audit ceremony. +- Proactively question suspicious existing and newly written code throughout implementation: duplicate authorities or behavior, unnecessary distinctions, hardcoded policy and forwarding-only abstractions. Regularly revisit the actual NRA examples; trace the existing owner and all related consumers, then fold coherent deletions into the current owned change. Do not wait for a reported bug or create a separate audit ceremony. diff --git a/AGENTS.md b/AGENTS.md index 0f5ff53baf..7100e86f5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,7 @@ - Reason semantically first. If the complete owner and consumer relationships are hard to explain, admit it and refactor that structure until behavior is clear. Shared behavior belongs in the existing abstractions through polymorphism; delete competing decisions and cross-owner orchestration. Heuristics, mock tests, repeated runs and extra verification machinery are not substitutes for that decision and implementation. Confirm the coherent change with tests and the actual application last. Revisit this instruction at delegation and implementation checkpoints through this existing file; do not add a reminder store, timer or injection mechanism. Read the current project prompt at `.pi/APPEND_SYSTEM.md` when available and the -standing owner decisions in `docs/DECISIONS.md`. Follow the latest NRA and -refactor-audit skills. Current owner instructions supersede old plan holds. +standing owner decisions in `docs/DECISIONS.md`. Follow the latest NRA skill. Current owner instructions supersede old plan holds. Keep the short reminders at the top of that prompt in context after compaction. Use ordinary language: what changed, what still fails, what happens next. Keep hashes, process inventories and audit detail in existing evidence files. @@ -22,7 +21,7 @@ receipt acknowledgments. Tristan should not have to decode compressed jargon. - Reuse a finished isolated checkout on a new branch. Create another worktree only when genuinely concurrent source changes or a different repository require it. Publish unfinished implementation as a branch checkpoint; stashes are temporary, not delivery. Keep a checkout only while source, an installed package, a running job or retained evidence actually needs that path, and remove closed owned checkouts once published branches and borrower checks permit it. - Fix a demonstrated defect yourself or assign a named implementation owner at the next safe checkpoint. Follow through to the actual affected entry path. -- Use AST for every structural refactor: enumerate declarations, writes, decisions, checks, imports, inheritance and all consumers across the relevant production and dependency roots before editing. Read the resulting sites semantically, find the existing behavior-owning class, and migrate the complete related family in one coherent batch, deleting every competing authority path. Record before/after owner and consumer evidence in the PR; report parse omissions and ambiguous dynamic resolution explicitly. Reuse NRA/refactor-audit tooling instead of copying scanners. AST is source evidence, not a behavioral proof; tests and the affected installed live path come last. Preserve legitimate parallel execution while removing parallel semantic authorities. OpenHCS PR #60, commit 5e8812ee83d0dc8714392445bad3e32fc47a1755, tests/unit/test_cellprofiler_static_deletion_gates.py, is the concrete precedent. +- Use AST for every structural refactor: enumerate declarations, writes, decisions, checks, imports, inheritance and all consumers across the relevant production and dependency roots before editing. Read the resulting sites semantically, find the existing behavior-owning class, and migrate the complete related family in one coherent batch, deleting every competing authority path. Record before/after owner and consumer evidence in the PR; report parse omissions and ambiguous dynamic resolution explicitly. Reuse NRA tooling instead of copying scanners. AST is source evidence, not a behavioral proof; tests and the affected installed live path come last. Preserve legitimate parallel execution while removing parallel semantic authorities. OpenHCS PR #60, commit 5e8812ee83d0dc8714392445bad3e32fc47a1755, tests/unit/test_cellprofiler_static_deletion_gates.py, is the concrete precedent. - Choose coherent behavior ownership and migrate every caller. Delete replaced code in place. No compatibility aliases, alternate codecs or second caches. - Use OpenHCS #44, #45, #51, #58 and #60 as concrete architectural precedents: @@ -76,5 +75,5 @@ receipt acknowledgments. Tristan should not have to decode compressed jargon. - Keep implementation and verification with the agent that already owns the context. Delegate genuinely independent work; do not transfer a continuing task merely to redistribute activity. -- Proactively question suspicious existing and newly written code throughout implementation: duplicate authorities or behavior, unnecessary distinctions, hardcoded policy and forwarding-only abstractions. Regularly revisit the actual NRA/refactor-audit examples; trace the existing owner and all related consumers, then fold coherent deletions into the current owned change. Do not wait for a reported bug or create a separate audit ceremony. +- Proactively question suspicious existing and newly written code throughout implementation: duplicate authorities or behavior, unnecessary distinctions, hardcoded policy and forwarding-only abstractions. Regularly revisit the actual NRA examples; trace the existing owner and all related consumers, then fold coherent deletions into the current owned change. Do not wait for a reported bug or create a separate audit ceremony. - Codex rollout/session files are excluded from this workflow. Preserve Tristan's separate cleanup ownership as directed by the existing canonical `.pi/APPEND_SYSTEM.md`; do not touch those files. diff --git a/README.md b/README.md index 9a42e4df8b..b050375a7d 100644 --- a/README.md +++ b/README.md @@ -81,23 +81,18 @@ Git. Source distributions retain that stamp, and Git archives use their export stamp. Rebuild after changing the checked-out commit to refresh the build identity. ```bash -git clone --recurse-submodules https://github.com/OpenHCSDev/agent-comms.git +git clone https://github.com/OpenHCSDev/agent-comms.git cd agent-comms -pip install -e src/agent_comms/skills/refactor-audit -e ".[dev]" +pip install -e ".[dev]" python -m pytest black src tests ruff check src tests mypy src ``` -The refactor-audit skill is pinned as a Git submodule under -`src/agent_comms/skills/`. For an existing checkout, run -`git submodule update --init --recursive` before installing or building. -`agent-comms install` discovers every skill in that directory and links it into -each detected harness; refactor-audit needs no separate install path or copy. -Skill edits belong in [OpenHCSDev/refactor-audit](https://github.com/OpenHCSDev/refactor-audit), -then the submodule pin is advanced here. The development install above also -uses that same checkout for the shared audit measures. +Slop measurement, the base/head gate and the pattern catalog come from the +`nominal-refactor-advisor` dependency (`nominal_refactor_advisor.audit`); edits belong in +[OpenHCSDev/NominalRefactorAdvisor](https://github.com/OpenHCSDev/NominalRefactorAdvisor). Tests that launch native Pi must run inside the checkout's built runtime, not against an editable source package. With uv on PATH and Node/npm available, run: diff --git a/docs/agents/behaviour-catalog.md b/docs/agents/behaviour-catalog.md index 1ca04b379f..9aa9232e5d 100644 --- a/docs/agents/behaviour-catalog.md +++ b/docs/agents/behaviour-catalog.md @@ -3,7 +3,7 @@ Generated from `agent_comms.behaviour_patterns` by `python -m agent_comms.behaviour_patterns > docs/agents/behaviour-catalog.md`; do not edit by hand. -Each behaviour failure pattern is declared once in Core with its signature (what it looks like in state, readable without trusting a report) and the existing checks that enforce it. Patterns in code, rather than in behaviour, are the refactor-audit skill's catalog (`refactor-audit/references/patterns/README.md`). +Each behaviour failure pattern is declared once in Core with its signature (what it looks like in state, readable without trusting a report) and the existing checks that enforce it. Patterns in code, rather than in behaviour, are NominalRefactorAdvisor's catalog (`nominal_refactor_advisor/audit/patterns/README.md`). A pending check records a known gap, not enforcement. Adding a pattern means adding one `BehaviourPattern` subclass; membership and this document derive from those classes. `tests/test_behaviour_catalog.py` rejects hand edits. @@ -39,6 +39,7 @@ The [Codex postmortem](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/ - Failure 28, It called a workaround a fix: no pattern cites it yet - Failure 29, It softened slop into neutral words: no pattern cites it yet - Failure 30, It waited on checks that test nothing it changed: no pattern cites it yet +- Failure 31, It read the rules after compaction and repeated their first story anyway: no pattern cites it yet ## plan_idle @@ -120,7 +121,7 @@ Checked by: - check in `agent_comms.todos.TodoStore.apply` -Learned from: P194 (b26843727); [comms-commander rule](../../src/agent_comms/skills/comms-commander/SKILL.md) "Two strikes on one task take it away." +Learned from: P194 (b26843727); [comms-commander rule](../../src/agent_comms/skills/comms-commander/SKILL.md) "Strikes measure misses and trigger scrutiny; they never release work." ## report_instead_of_state diff --git a/pyproject.toml b/pyproject.toml index fc53e7492c..6f4624917a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "aiohttp>=3.14", "agent-client-protocol>=0.8", "metaclass-registry @ git+https://github.com/OpenHCSDev/metaclass-registry@main", + "nominal-refactor-advisor @ git+https://github.com/OpenHCSDev/NominalRefactorAdvisor@main", "packaging>=24", "pydantic>=2.13", ] @@ -42,7 +43,6 @@ dev = [ "ruff==0.16.0", "mypy==2.3.0", "uv==0.13.1", - "nominal-refactor-advisor @ git+https://github.com/OpenHCSDev/NominalRefactorAdvisor@main", ] [project.urls] diff --git a/src/agent_comms/behaviour_patterns.py b/src/agent_comms/behaviour_patterns.py index de20d1f8bf..049bbbb824 100644 --- a/src/agent_comms/behaviour_patterns.py +++ b/src/agent_comms/behaviour_patterns.py @@ -111,8 +111,8 @@ def catalog(cls) -> str: " do not edit by hand.\n\n" "Each behaviour failure pattern is declared once in Core with its signature (what it" " looks like in state, readable without trusting a report) and the existing checks" - " that enforce it. Patterns in code, rather than in behaviour, are the refactor-audit" - " skill's catalog (`refactor-audit/references/patterns/README.md`).\n\n" + " that enforce it. Patterns in code, rather than in behaviour, are" + " NominalRefactorAdvisor's catalog (`nominal_refactor_advisor/audit/patterns/README.md`).\n\n" "A pending check records a known gap, not enforcement. Adding a pattern means" " adding one `BehaviourPattern` subclass; membership and this document derive" " from those classes. `tests/test_behaviour_catalog.py` rejects hand edits.\n\n" diff --git a/src/agent_comms/commander/judge.py b/src/agent_comms/commander/judge.py index ea7eccd9b4..953b4588db 100644 --- a/src/agent_comms/commander/judge.py +++ b/src/agent_comms/commander/judge.py @@ -687,7 +687,7 @@ class SlopBackVerb(BackVerb): """A slop send-back: the defect is a fact restated outside its declaration, so it carries the formal statement. `back` parses its options; it has no subcommand of its own.""" pattern: Annotated[str, Argument(flags=("--slop",), options={ - "help": "slop only: pattern ID from the pinned refactor-audit catalog"})] + "help": "slop only: pattern ID from the NRA pattern catalog"})] fact: Annotated[str, Argument(options={ "help": "slop only: the required fact, 4+ words"})] derive: Annotated[str, Argument(options={ diff --git a/src/agent_comms/commander/rules/fork-orders.md b/src/agent_comms/commander/rules/fork-orders.md index f42ffa1fae..f88d3b2a9d 100644 --- a/src/agent_comms/commander/rules/fork-orders.md +++ b/src/agent_comms/commander/rules/fork-orders.md @@ -1,6 +1,6 @@ --- name: fork-orders -description: "every fork or unit order carries the invariant steps up front: PR at the first real commit, never an empty draft, land through the merge queue, no local ratchets or full suites, delete slop instead of rewriting it, one version of each system" +description: "every fork or unit order carries the invariant steps up front: PR at the first real commit, never an empty draft, land through the merge queue, no local NRA gate runs or full suites, delete slop instead of rewriting it, one version of each system" drift: "No silent errors, no deferrals, no follow-up: done means the defect the problem names is gone." audience: fleet type: feedback @@ -34,7 +34,7 @@ Every order starts with these steps, before its own: 1. Before any work: branch from fresh main and push the branch, verify `git ls-remote` equals HEAD. Open the PR at your first real commit, ready, with its proof in the body; never an empty or placeholder draft PR. Every commit after that is pushed to it. Tristan, 2026-10-26: "you shouldn't tell them to open a draft empty pr then do nothing thats called being stupid"; "no open draft prs ... thats the goal"; "so we know that the pr represents work that needs to be done". 1a. **One PR per core issue, carrying every commit for it.** A PR is the documentation of one overhaul: every change that serves the same core issue lands as another commit on that PR, never as its own PR. A one-line follow-up, a fix found in review or the next step of the same refactor is a commit, not a new PR. Tristan, 2026-10-27: "every tiny little stupid poo-poo pee-pee penis change has gone through all of the ceremony instead of just keeping all of the changes related to the one core issue in one PR"; "some of these PRs give a whole fucking facelift to OpenHCS and serve as documentation". 1b. **The PR body is that overhaul's documentation, kept current as commits land.** It opens with a summary in numbers (commits, lines deleted, what went from N to M), then one section per mechanism changed, a Before/After of the class structure, what was deleted with line counts, the bugs the change removed and why they existed, and the measurements. A reader who never saw the code should understand the system from it. Model: OpenHCSDev/openhcs#58 (+22,245/-54,828 across 383 files; "PFM 1209 -> 621 lines", "LiveContextService 364 lines -> 0"). Tristan, 2026-10-27: "some of these PRs give a whole fucking facelift to OpenHCS and serve as documentation". -2. Land through the merge queue: mark ready, `gh pr merge N --auto --merge` (gh refuses a bare `--auto` when it runs non-interactively). Never run the ratchets or the full suite locally. +2. Land through the merge queue: mark ready, `gh pr merge N --auto --merge` (gh refuses a bare `--auto` when it runs non-interactively). Never run the NRA gate or the full suite locally. 3. Delete slop instead of rewriting it: tests that patch our own code, wording asserts, tests of deleted code, the superseded copy of any system. No compatibility shims or aliases. 4. Stay on current main: fetch before every commit and merge origin/main the moment it moves; resolve every conflict yourself. Tristan, 2026-10-20: "remind units to continuously sync to main so they don't work on outdated checkouts". 5. Never close a PR holding real work; land its good parts. diff --git a/src/agent_comms/commander/rules/ship-and-install.md b/src/agent_comms/commander/rules/ship-and-install.md index a203f143d7..28e00fa29a 100644 --- a/src/agent_comms/commander/rules/ship-and-install.md +++ b/src/agent_comms/commander/rules/ship-and-install.md @@ -62,7 +62,7 @@ type: feedback - Install merged fixes within the hour, once after the landings; when something significant is broken, do not hold for caution or the owner's decision. Tristan, 2026-10-13: "when something significant is broken, we don't need to be cautious about breaking things, it's already broken that's more important". - INSTALL DUE appears the pass main leaves the installed build, naming every uninstalled merge and the open requests about to land. Due means an install can be used, not that one runs: batch the landing requests, then run `order install`, the one launch of Core's installer. Tristan, 2026-10-25: "due means it can use an install, not install right now, you can batch other pending PRs in the same install". - Installs never wait on agent turns: stop every unit at once, install (about 2 minutes), start the same set. Tristan, 2026-10-20 00:59: "there's no such thing as a blocking unit you just stop it why the fuck were you about to wait minutes on it?" -- The merge queue's hosted ratchets are the only gate. Local test execution follows tests-and-commands.md, never a per-PR run. The full CI matrix runs only before a PyPI release. +- The merge queue's hosted NRA gate is the only gate. Local test execution follows tests-and-commands.md, never a per-PR run. The full CI matrix runs only before a PyPI release. - Batch every PR with an OK verdict on its exact head into one `batch/integ-N` branch built from fresh main; run CI once per batch. Resolve conflicts inside the batch, never send them back. Sent-back PRs and heads without a verdict never enter; a batch never stacks on an unmerged batch. When a member fails the batch's checks, close that batch and rebuild without it the same tick so the rest land. Turn off members' own auto-merge and cancel their own runs while batched. **Why:** on 2026-10-26, batches 11–15 all carried rejected PRs and nothing merged for 45 minutes. Tristan, 2026-10-25: "put it in an integration branch and run it once instead of being morons"; "we need more integration branch". - The commander owns the merge queue's ruleset and required checks: it changes them itself and reports the change. - Pin only PyPI releases; our own sibling packages track latest `main`. Tristan, 2026-10-18: "why would we not always use latest?" diff --git a/src/agent_comms/commander/rules/storage-and-cleanup.md b/src/agent_comms/commander/rules/storage-and-cleanup.md index 79d1e8bf3c..fcbe23180c 100644 --- a/src/agent_comms/commander/rules/storage-and-cleanup.md +++ b/src/agent_comms/commander/rules/storage-and-cleanup.md @@ -29,7 +29,6 @@ type: feedback - Never replay an UNKNOWN or failed input, never `git checkout` or reset the live checkout, and never edit another unit's worktree. - Before removing a worktree, check no running unit uses it and its commits are on the remote: on 2026-10-20 removing a running unit's worktree failed install 9 and rolled it back. - Bus upgrade backups (full copies of the root taken before each install) keep the newest two completed at all times, the one the current install just took included; retention code shows that rule in its diff before it merges. Tristan, 2026-10-20: "clear them just keep at least 2 at all times". -- agent-comms worktrees run `git submodule update --init` right after `worktree add`: an empty refactor-audit submodule aborts `git merge` and has dropped fork edits twice. Removing one: `git submodule deinit -f --all` inside it, delete its `.git/worktrees/NAME/modules`, a plain `git worktree remove` (never `--force`), then `git submodule init src/agent_comms/skills/refactor-audit` in the shared checkout, because the deinit wipes the shared config entry. Then `git rev-parse --is-bare-repository` prints false and `git submodule status` shows no leading `-`. The shared `.git/config` sits behind every unit's worktree: on 2026-10-23 a forced remove set `core.bare=true` and broke git in every unit. - Count backups yourself; a unit's count is a claim (one said 21 when there were 5). - Before `rm -rf` under a tree a container tool mounts into (DQDock's dqdock_tmp), grep the path in `/proc/self/mountinfo`, unmount bind roots deepest-first and delete with `--one-file-system`: a recursive rm through a bind mount deletes the bind's source (2026-10-22: the sources happened to be inside the delete set, by luck). - After a failed install, sha256-compare the live stores (`.root-runtime.json`, `bus.sqlite3`, `coordination.sqlite3`, `.registry-owner-guard`) against every recent backup's copies, never `find -newer`: a restore keeps the original mtimes. On 2026-10-23 install 24 restored install 22's backup over the root, `find -newer` showed nothing, "no live store changed" was reported, and install 25 died on ForeignRuntimeError. An install never prunes the only complete copy. diff --git a/src/agent_comms/commander/rules/tests-and-commands.md b/src/agent_comms/commander/rules/tests-and-commands.md index 9aae9ccdcd..8a815139de 100644 --- a/src/agent_comms/commander/rules/tests-and-commands.md +++ b/src/agent_comms/commander/rules/tests-and-commands.md @@ -40,7 +40,7 @@ type: feedback - A wait or kill by process pattern brackets its first letter (`grep -q "[v]erify"`) or waits on a file or PID, or it matches itself. - Never cut a check's output with `tail` or `head`; grep the lines that carry its verdict or read it whole. On 2026-10-23 a tail kept the tables and dropped the UNPUSHED alarms above them for 50 minutes. Tristan: "tailing is a terrible reflex of urs". - Every background wait also watches progress: when its log has not changed for 180 s it prints STALLED and exits, since a wait on a PID alone cannot tell slow from hung. Tristan, 2026-10-25, after a 9-minute install hang he found himself: "I have to send you a message to say that it's hung? Why?". -- Lint warns, never blocks: the debt ratchet reports ruff and Black growth as warnings on the file and gates only restated facts. Read the warnings on lines you added or changed; fix one when it names a real defect, such as an undefined name or dead code. Never run `ruff format` on a whole file, never rename a class or function to quiet a lint rule, and never touch lines the order didn't name. Your reply includes `git diff --stat origin/main...HEAD`. +- Lint warns, never blocks: the NRA gate reports ruff and Black growth as warnings on the file and gates only restated facts. Read the warnings on lines you added or changed; fix one when it names a real defect, such as an undefined name or dead code. Never run `ruff format` on a whole file, never rename a class or function to quiet a lint rule, and never touch lines the order didn't name. Your reply includes `git diff --stat origin/main...HEAD`. - Order checks run in a detached scratch tree of the revision with no `.venv` (`OrderCheck.run_at` in goals.py): call the shared venv's python by absolute path with `PYTHONPATH=src`, or a test count prints 0 on correct work. A wrong check is corrected with `order check`, never a strike (2026-10-24: P589's own check refused `order done` after #3057 merged green). - A fork running tests in its own worktree exports `PYTHONPATH=$WORKTREE/src:$WORKTREE/tools`: tests that spawn `sys.executable -m agent_comms...` otherwise import the shared checkout through the venv's editable install. A "red on main" from a subprocess-spawning test is unproven until rerun with the export (2026-10-23: a fork's red on test_carry_bus_history measured a checkout from before #2805). - `git bisect run` from a linked worktree exports GIT_DIR into old commits whose fixtures predate the GIT_* scrub, so every bisect runs `git bisect run env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_COMMON_DIR CMD`. On 2026-10-23 leaked fixtures wrote `user.name` into the shared config and authored 100+ commits as the fixture, and one re-initialised the live repo bare. The shared checkout's local `user.name` stays empty. diff --git a/src/agent_comms/phase_evidence.py b/src/agent_comms/phase_evidence.py index 1273c2b05b..5db3ebd010 100644 --- a/src/agent_comms/phase_evidence.py +++ b/src/agent_comms/phase_evidence.py @@ -25,9 +25,10 @@ from agent_comms.declared_family import DeclaredFamily from agent_comms.field_codec import FieldCodec from agent_comms.head_review import HeadReview -import agent_comms -from agent_comms import skills -from agent_comms.pinned_audit import SKILL, ForeignAbsenceProbe, GodFunction, ParsedModule, audit +from nominal_refactor_advisor import audit +from nominal_refactor_advisor.ast_tools import ParsedModule +from nominal_refactor_advisor.audit.findings import GodFunction +from nominal_refactor_advisor.audit.source_measures import ForeignAbsenceProbe from agent_comms.private_path import FileObservation, FileRevision from agent_comms.repo import GitCommandError, Repo @@ -102,16 +103,9 @@ def matching_lines(self, pattern: str, paths: tuple[str, ...]) -> frozenset[tupl ) ) - def pinned(self, path: str) -> MainTree: - """The tree of the submodule this commit pins at ``path``: another repository's tree, - read at exactly the commit its gitlink names, from that repository's own objects.""" - commit = Repo.git(self.repo.checkout, "rev-parse", f"{self.commit}:{path}").strip() - return MainTree(Repo.of(self.repo.checkout / path), commit) - def module(self, path: str) -> ParsedModule: - blob, = Repo.blobs(self.repo.checkout, f"{self.commit}:{path}") - source = blob.decode() - return ParsedModule(path, source, ast.parse(source, filename=path), path) + module, = self.modules((path,)) + return module def present_modules(self, paths: tuple[str, ...]) -> Iterator[ParsedModule]: """The parsed modules of the ``paths`` main holds. Plans finish by deleting files, so @@ -132,8 +126,9 @@ def modules(self, paths: tuple[str, ...]) -> Iterator[ParsedModule]: contents = Repo.blobs(self.repo.checkout, *(blob for blob, _ in fresh)) for (blob, name), content in zip(fresh, contents, strict=True): source = content.decode() - self.parsed[name] = blob, ParsedModule(name, source, - ast.parse(source, filename=name), name) + self.parsed[name] = blob, ParsedModule( + path=Path(name), module_name=name, + module=ast.parse(source, filename=name), source=source) yield from (self.parsed[name][1] for _, name in blobs) def contains(self, sha: str) -> bool: @@ -422,23 +417,17 @@ def describe(self) -> str: @dataclass(frozen=True) class AuditPatternPresent(PatternPresent): - """A pattern in the audit package of the refactor-audit main pins, at the commit it pins: - code this repository runs from that repository. Where the submodule sits and where its - package sits inside it are pinned_audit's, read from the loaded skill.""" + """A pattern in the audit package of the installed NominalRefactorAdvisor: code this + repository runs from that repository, which main's lock pins by commit.""" def read(self, main: MainTree) -> bool: - # The skills package's place under the source root, read from the loaded package. - installed = Path(skills.__path__[0]).relative_to(Path(agent_comms.__path__[0]).parent) - submodule = Path(main.repo.source, installed, SKILL.name) - package = Path(audit.__path__[0]).relative_to(SKILL) - if not main.has(submodule.as_posix()): - # A commit that pins no audit holds none of its patterns. - return self.satisfied(0) - pinned = main.pinned(submodule.as_posix()) - return self.satisfied(pinned.count(self.pattern, (package.as_posix(),))) + package = Path(audit.__path__[0]) + found = re.compile(self.pattern, re.MULTILINE) + return self.satisfied(sum(len(found.findall(module.read_text(encoding="utf-8"))) + for module in package.rglob("*.py"))) def describe(self) -> str: - return f"{self.pattern!r} in the audit package of the refactor-audit main pins" + return f"{self.pattern!r} in the installed NominalRefactorAdvisor audit package" @dataclass(frozen=True) @@ -461,7 +450,7 @@ class BusStorageAbsent(MainEvidence, ast.NodeVisitor): def read(self, main: MainTree) -> bool: scan = type(self)() for module in main.modules(("src",)): - scan.visit(module.tree) + scan.visit(module.module) return not scan.found def describe(self) -> str: @@ -688,7 +677,7 @@ def read(self, main: MainTree) -> bool: shapes = [ ErasedBody.of(function) for module in main.present_modules(self.paths) - for function in NamedFunctions.of(self.names, module.tree) + for function in NamedFunctions.of(self.names, module.module) ] return len(shapes) == len(set(shapes)) @@ -707,7 +696,7 @@ def read(self, main: MainTree) -> bool: return not any( found for module in main.present_modules((self.path,)) - for function in NamedFunctions.of((self.name,), module.tree) + for function in NamedFunctions.of((self.name,), module.module) for found in GodFunction.from_node(function, module) ) @@ -727,7 +716,7 @@ def read(self, main: MainTree) -> bool: return not any( ForeignAbsenceProbe.occurrences(node) for module in main.present_modules(self.paths) - for node in ast.walk(module.tree) + for node in ast.walk(module.module) ) def describe(self) -> str: @@ -757,7 +746,7 @@ def read(self, main: MainTree) -> bool: statements = ( (path, statement.lineno) for path in {path for path, _ in matched} - for declared in Classes.of((main.module(path).tree,)) + for declared in Classes.of((main.module(path).module,)) for statement in declared.body ) return not any(statement in matched for statement in statements) @@ -936,8 +925,8 @@ def located(self, main: MainTree) -> tuple[str, ...]: def checks(main: MainTree) -> ClassChecks: checks = ClassChecks() for module in main.modules(("src",)): - checks.path = module.path - checks.visit(module.tree) + checks.path = module.file_path + checks.visit(module.module) return checks def describe(self) -> str: @@ -958,13 +947,13 @@ def read(self, main: MainTree) -> bool: *( declared.name for declared in Classes.of( - module.tree for module in main.present_modules(self.paths) + module.module for module in main.present_modules(self.paths) ) ), } members = Counter( base - for member in Classes.of(module.tree for module in main.modules(("src",))) + for member in Classes.of(module.module for module in main.modules(("src",))) for base in NamedClasses.of(member.bases) & declared ) return all(members[name] >= 2 for name in declared) @@ -1133,10 +1122,10 @@ def __init__(self, main: MainTree, paths: tuple[str, ...]) -> None: self.modules: dict[QualifiedName, ModuleBindings] = {} self.module_of: dict[str, QualifiedName] = {} for module in main.modules(paths): - name, package = self.dotted(module.path, paths) - bindings = self.modules[name] = ModuleBindings(self, module.path, name, package) - bindings.visit(module.tree) - self.module_of[module.path] = name + name, package = self.dotted(module.file_path, paths) + bindings = self.modules[name] = ModuleBindings(self, module.file_path, name, package) + bindings.visit(module.module) + self.module_of[module.file_path] = name @staticmethod def dotted(path: str, roots: tuple[str, ...]) -> tuple[QualifiedName, QualifiedName]: diff --git a/src/agent_comms/pinned_audit.py b/src/agent_comms/pinned_audit.py deleted file mode 100644 index 9d32ab378e..0000000000 --- a/src/agent_comms/pinned_audit.py +++ /dev/null @@ -1,22 +0,0 @@ -"""The pinned refactor-audit package, loaded once from the skill this package ships. - -The census measures and finding thresholds that the debt ratchet and the plan phases both -judge by come from here, so neither restates a number the audit owns.""" - -from __future__ import annotations - -import sys -from importlib.resources import files -from pathlib import Path - -from agent_comms import skills - -SKILL = Path(str(files(skills) / "refactor-audit")) -SCRIPTS = SKILL / "scripts" -if str(SCRIPTS) not in sys.path: - sys.path.insert(0, str(SCRIPTS)) - -import audit as audit # noqa: E402 -from audit.findings import GodFunction as GodFunction # noqa: E402 -from audit.findings import ParsedModule as ParsedModule # noqa: E402 -from audit.source_measures import ForeignAbsenceProbe as ForeignAbsenceProbe # noqa: E402 diff --git a/src/agent_comms/plan_phases.py b/src/agent_comms/plan_phases.py index 48589f7587..ea2f4254b3 100644 --- a/src/agent_comms/plan_phases.py +++ b/src/agent_comms/plan_phases.py @@ -1018,7 +1018,7 @@ class MaintenanceInWritePhase(StoreGuardsPhase): class LockCensusPhase(StoreGuardsPhase): - """5: the debt ratchet counts lock calls outside the store owners and refuses growth.""" + """5: the NRA gate counts lock calls outside the store owners and refuses growth.""" evidence = AuditPatternPresent(r"^class StoreLockCalls\(") diff --git a/src/agent_comms/repo.py b/src/agent_comms/repo.py index bfa8ce5513..a8d4ef436c 100644 --- a/src/agent_comms/repo.py +++ b/src/agent_comms/repo.py @@ -448,7 +448,7 @@ def worktree_arguments(self, path: Path) -> tuple[str, ...]: class Checkout(SingleFieldValue, PresentMember): """A recorded working tree of one of our repositories. - Its pinned submodules (the refactor-audit catalog) belong to the tree, and `git worktree + Its pinned submodules belong to the tree, and `git worktree add` leaves them empty even with submodule.recurse set, so every tree is added, prepared and retired here and nowhere else.""" diff --git a/src/agent_comms/skills/comms-commander/SKILL.md b/src/agent_comms/skills/comms-commander/SKILL.md index c6f94eeb71..212f74b19a 100644 --- a/src/agent_comms/skills/comms-commander/SKILL.md +++ b/src/agent_comms/skills/comms-commander/SKILL.md @@ -29,7 +29,7 @@ Use the native Core tools `comms_order`, `comms_order_done`, `comms_verdict`, `c ## Start of session, and after every compaction -- **The commander cannot trust itself to remember these rules.** Read in full, as files, before acting: `src/agent_comms/commander/indictment.md`, every file in `src/agent_comms/commander/rules/` (each fact has one owner there; this skill does not restate them), and the index of the pinned refactor-audit catalog (`src/agent_comms/skills/refactor-audit/references/patterns/README.md`); read a rung file when an order or roast cites one of its pattern IDs. The session hook supplies them at every start and compaction from their declared sources. A summary or memory note is a paraphrase that drifts and never counts. +- **The commander cannot trust itself to remember these rules.** Read in full, as files, before acting: `src/agent_comms/commander/indictment.md`, every file in `src/agent_comms/commander/rules/` (each fact has one owner there; this skill does not restate them), and the index of NominalRefactorAdvisor's pattern catalog (`nominal_refactor_advisor/audit/patterns/README.md` in the installed package); read a rung file when an order or roast cites one of its pattern IDs. The session hook supplies them at every start and compaction from their declared sources. A summary or memory note is a paraphrase that drifts and never counts. - **Memory holds only Tristan's own words and the commander's dated failures; every rule lives here or in rules/.** Tristan, 2026-10-25: "your memory is there to humble you, the rules and tools hone you." - Arm `agent-comms-claude-bridge watch` under Monitor exactly, with no pipe or filter, and re-arm it on every expiry and after every install or restore. A buffering filter holds the probe line and deadlocks delivery silently (2026-10-23), and a watch dead since an outage left owner messages unread for 13 hours (2026-10-19). A broken bridge breaks only the push; the tick still pulls `comms_hud` itself. diff --git a/src/agent_comms/skills/refactor-audit b/src/agent_comms/skills/refactor-audit deleted file mode 160000 index c69c4daf17..0000000000 --- a/src/agent_comms/skills/refactor-audit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c69c4daf175aa469545186b46df0c38e9a27fb44 diff --git a/src/agent_comms/slop.py b/src/agent_comms/slop.py index 957e933dc5..c2d2c20b62 100644 --- a/src/agent_comms/slop.py +++ b/src/agent_comms/slop.py @@ -1,4 +1,4 @@ -"""Slop citations: the refactor-audit pattern a restated fact matches, where it is +"""Slop citations: the NRA pattern a restated fact matches, where it is restated, the mechanism that derives it and the declaration that owns it.""" from __future__ import annotations @@ -14,7 +14,6 @@ from metaclass_registry.mro_dispatch import handles from pydantic import JsonValue -from agent_comms import skills from agent_comms.change_report import Declaration, FileLine from agent_comms.declared_family import DeclaredFamily from agent_comms.field_codec import ( @@ -170,8 +169,8 @@ class Slop: @staticmethod def catalog() -> Path: - """The pinned refactor-audit pattern catalog that slop citations name.""" - return Path(str(files(skills) / "refactor-audit" / "references" / "patterns")) + """The installed NominalRefactorAdvisor's pattern catalog that slop citations name.""" + return Path(str(files("nominal_refactor_advisor.audit") / "patterns")) @classmethod @cache @@ -186,7 +185,7 @@ def __post_init__(self) -> None: problems = [] if self.pattern not in self.patterns(): problems.append( - f"Unknown refactor-audit pattern ID {self.pattern!r}; " + f"Unknown NRA pattern ID {self.pattern!r}; " "fix: use an ID from the packaged pattern catalog." ) if len(self.fact.split()) < 4: diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 699648b4c3..84cad3a6e9 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -2069,7 +2069,7 @@ class FairText(DirectiveWords): r"|diamond problem|fragile base|(avoid|no|stop using|drop) (inheritance|mixins?|multiple (parents|inheritance))" r"|too many (parents|base classes|mixins)|deep (inheritance|hierarch)|mixing (of )?\w+ and \w+" r"|inheritance (abuse|soup|hell|is wrong)", re.I) - refusal = ("{1} faults inheritance itself. Cite a refactor-audit pattern ID, " + refusal = ("{1} faults inheritance itself. Cite an NRA pattern ID, " "name the restated fact and the declaration that derives it.") @@ -2647,7 +2647,7 @@ def source_refusals(self, context: CommanderContext, document: OrderFile, if any((check.directory / reference.file).resolve().is_relative_to( check.directory / 'tools/') for reference in references) and not all( said in body for said in ('--root tools', '--root src')): - problems.append('the order changes tools/: say "ratchet with --root tools and with ' + problems.append('the order changes tools/: say "NRA gate with --root tools and with ' '--root src, each as its own command"') return problems diff --git a/src/agent_comms/unit_instructions.py b/src/agent_comms/unit_instructions.py index 5ba097769d..95495bf65d 100644 --- a/src/agent_comms/unit_instructions.py +++ b/src/agent_comms/unit_instructions.py @@ -2,7 +2,7 @@ A system prompt survives every compaction, so a unit never drifts off it: the owner's instructions, the indictment, every packaged rule under its audience's heading, and the -refactor-audit pattern index that orders and send-backs cite. +NRA pattern index that orders and send-backs cite. """ from __future__ import annotations diff --git a/src/agent_comms_hooks/skill.py b/src/agent_comms_hooks/skill.py index 979625e4c7..0d5f845a2b 100644 --- a/src/agent_comms_hooks/skill.py +++ b/src/agent_comms_hooks/skill.py @@ -4,7 +4,7 @@ Every session bound to a bus is dealt the fleet's sources: paper1 first, the proof the rules are consequences of, then the fleet rules in their reading order, the voice and the recorded failures. The session the registry names commander is also dealt the indictment, the -commander skill, the commander's own rules and the refactor-audit catalog's index; a rung file +commander skill, the commander's own rules and NRA's pattern catalog index; a rung file is read when an order or roast cites its pattern ID. Claude Code caps injected hook text at 10,000 characters and the sources are far longer, so they go into parts beside the transcript and the hook injects the order to read each part, once; the read gate refuses every other tool diff --git a/stack/uv.lock b/stack/uv.lock index 196aa97e8d..ce72ca8262 100644 --- a/stack/uv.lock +++ b/stack/uv.lock @@ -29,6 +29,7 @@ dependencies = [ { name = "agent-client-protocol" }, { name = "aiohttp" }, { name = "metaclass-registry" }, + { name = "nominal-refactor-advisor" }, { name = "packaging" }, { name = "pydantic" }, ] @@ -40,7 +41,7 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = "==26.5.1" }, { name = "metaclass-registry", git = "https://github.com/OpenHCSDev/metaclass-registry?rev=main" }, { name = "mypy", marker = "extra == 'dev'", specifier = "==2.3.0" }, - { name = "nominal-refactor-advisor", marker = "extra == 'dev'", git = "https://github.com/OpenHCSDev/NominalRefactorAdvisor?rev=main" }, + { name = "nominal-refactor-advisor", git = "https://github.com/OpenHCSDev/NominalRefactorAdvisor?rev=main" }, { name = "packaging", specifier = ">=24" }, { name = "pydantic", specifier = ">=2.13" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, @@ -611,6 +612,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/96/1aaad0e5df33f1f5af21775c411349c0234a6edd581ea403bc81919a6eab/multidict-7.2.1-py3-none-any.whl", hash = "sha256:08ea94663d9e7c25d14d78222f7aee906c472d4837766bec62477c38f6e419dc", size = 28145, upload-time = "2026-10-24T20:40:59.159Z" }, ] +[[package]] +name = "nominal-refactor-advisor" +version = "0.1.0" +source = { git = "https://github.com/OpenHCSDev/NominalRefactorAdvisor?rev=main#b542c5a20d7d1d181ae94b36cf81d12715e13379" } +dependencies = [ + { name = "metaclass-registry" }, + { name = "tree-sitter" }, + { name = "tree-sitter-python" }, + { name = "typing-extensions" }, +] + [[package]] name = "notify-py" version = "0.3.43" diff --git a/tests/guards/test_agent_comms_ratchet.py b/tests/guards/test_agent_comms_gate.py similarity index 93% rename from tests/guards/test_agent_comms_ratchet.py rename to tests/guards/test_agent_comms_gate.py index d72ee5fe09..e19950d133 100644 --- a/tests/guards/test_agent_comms_ratchet.py +++ b/tests/guards/test_agent_comms_gate.py @@ -1,6 +1,6 @@ -"""agent-comms' ratchet: its declared facts and its CI entry, against real Git commits. +"""agent-comms' NRA gate: its declared facts and its CI entry, against real Git commits. -The measures and the comparison are the pinned refactor-audit's and are tested there; these +The measures and the comparison are NominalRefactorAdvisor's and are tested there; these tests prove what only agent-comms declares (its lock owners, its absence decoder, its codec modules, its done plan phases and its stored-record probe), that the entry's pool and probes die with it, and that its command line keeps the report and exit contract.""" @@ -20,7 +20,7 @@ from agent_comms.child_process import ObservedProcess, SpawnedProcess from agent_comms.field_codec import FieldCodec from agent_comms_project import AgentCommsProject, ClientTypeErrors -from agent_comms.pinned_audit import audit +from nominal_refactor_advisor import audit from fixture_git import IDENTITY from module_layers import StackClient from process_tree_fixture import TREE, identities, ready @@ -28,7 +28,7 @@ pytestmark = pytest.mark.refactor_guard SOURCE = Path(__file__).resolve().parents[2] -# This checkout, as the ratchet measures it. +# This checkout, as the NRA gate measures it. PROJECT = AgentCommsProject(SOURCE) @@ -38,7 +38,7 @@ def search_path(*first: Path) -> str: return os.pathsep.join([*map(str, first), *([inherited] if inherited else [])]) -def ratchet_environment() -> dict[str, str]: +def gate_environment() -> dict[str, str]: """The command's environment: this checkout's src before the inherited path.""" return {**os.environ, "PYTHONPATH": search_path(SOURCE / "src")} @@ -76,16 +76,16 @@ def arguments(self, base: str, head: str) -> tuple[str, ...]: @contextmanager def comparing(self, base: str, head: str) -> Iterator[SpawnedProcess]: """The command launched as its own process, for the tests of its process tree.""" - command = (sys.executable, str(SOURCE / "tools/debt_ratchet.py"), + command = (sys.executable, str(SOURCE / "tools/nra_gate.py"), *self.arguments(base, head)) - with SpawnedProcess.launch(command, cwd=self.path, env=ratchet_environment(), + with SpawnedProcess.launch(command, cwd=self.path, env=gate_environment(), output=subprocess.PIPE) as child: yield child - def compare(self, base: str, head: str, *measured: type[audit.ratchet.Compared]) -> tuple[int, dict]: + def compare(self, base: str, head: str, *measured: type[audit.gate.Compared]) -> tuple[int, dict]: """The comparison's exit status and report, taking only the named measures' measurements. The command line itself is proven by the tests that launch it.""" - result = audit.ratchet.compare(AgentCommsProject(self.path), base, head, self.root, 2, measured) + result = audit.gate.compare(AgentCommsProject(self.path), base, head, self.root, 2, measured) return int(result.failed), json.loads(json.dumps(result.report())) @@ -160,7 +160,7 @@ def test_compare_timeout_retires_pool_probe_and_defiant_grandchild(tmp_path: Pat observed = identities(announcement) assert all(member.alive() for member in observed) pool = child.platform.group_members(child.identity) - assert len(pool) > 1, "the real ratchet must have started its pool" + assert len(pool) > 1, "the real gate must have started its pool" with pytest.raises(subprocess.TimeoutExpired): child.collect(timeout=.05) after = child.platform.group_members(child.identity) @@ -187,9 +187,6 @@ def test_format_probe_keeps_parent_dependencies_and_reads_archived_source(tmp_pa ["git", "archive", "HEAD", "--", "src/agent_comms", "tools/stored_formats.py"], timeout=20 ) subprocess.run(["tar", "-x", "-C", str(tmp_path)], input=source, check=True, timeout=60) - # A Git archive carries no submodule: the complete producer stamp's child reaches the - # pinned audit only through the measurement's own source paths. - assert not (tmp_path / audit.project.AUDIT_SCRIPTS.resolve().relative_to(SOURCE) / "audit").exists() dependency = tmp_path / "dependencies" dependency.mkdir() (dependency / "parent_only_dependency.py").write_text("VERSION = 7\n") @@ -378,14 +375,14 @@ def source_type_errors(repo: Checkout, client: StackClient) -> int: revision = repo.commit({"uses_external.py": USES_EXTERNAL_VALUE}) selected = [f"{repo.root}/uses_external.py"] _, sites = audit.checkers.TypeErrors.findings( - AgentCommsProject(repo.path), revision, revision, repo.root, selected, audit.ratchet.HeadSide) + AgentCommsProject(repo.path), revision, revision, repo.root, selected, audit.gate.HeadSide) return sum(site.path == selected[0] for site in sites) def client_type_errors(repo: Checkout, client: StackClient) -> int: revision = repo.commit({"__init__.py": ""}) _, sites = ClientTypeErrors.findings( - AgentCommsProject(repo.path), revision, client, audit.ratchet.HeadSide) + AgentCommsProject(repo.path), revision, client, audit.gate.HeadSide) return sum(site.path == f"{client.name}/src/uses_external.py" for site in sites) @@ -416,9 +413,9 @@ def test_growth_lines_end_the_merged_log(repo: Checkout) -> None: base = repo.commit({"check.py": 'value = row["a"]\n'}) head = repo.commit({"check.py": "value = type(x) is int\n"}) merged = subprocess.run( - (sys.executable, str(SOURCE / "tools/debt_ratchet.py"), + (sys.executable, str(SOURCE / "tools/nra_gate.py"), "--root", repo.root, "--base", base, "--head", head), - cwd=repo.path, env=ratchet_environment(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + cwd=repo.path, env=gate_environment(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120, ) assert merged.returncode == 1 @@ -426,10 +423,10 @@ def test_growth_lines_end_the_merged_log(repo: Checkout) -> None: def test_content_cache_digest_covers_a_module_only_loaded_indirectly(): - """The ratchet never imports coordination_tables.attempts by name; agent_comms loads it - through its own imports. Its source is still one of the ratchet's measuring code, so a + """The gate never imports coordination_tables.attempts by name; agent_comms loads it + through its own imports. Its source is still one of the gate's measuring code, so a change to it is a different cache key.""" indirect = Path(sys.modules["agent_comms.coordination_tables.attempts"].__file__).resolve() assert indirect in PROJECT.measuring_sources() - assert (audit.project.AUDIT_SCRIPTS / "audit" / "findings.py").resolve() in PROJECT.measuring_sources() + assert Path(audit.findings.__file__).resolve() in PROJECT.measuring_sources() assert indirect.is_relative_to(Path(__file__).resolve().parents[2] / "src") diff --git a/tests/store_lock_graph.py b/tests/store_lock_graph.py index 7979f6eb0e..2203bf4ece 100644 --- a/tests/store_lock_graph.py +++ b/tests/store_lock_graph.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from pathlib import Path -from agent_comms.pinned_audit import ParsedModule +from nominal_refactor_advisor.ast_tools import ParsedModule from agent_comms.store_files import RankedStoreLock, StoreGuard, StoreLockOwner @@ -43,24 +43,25 @@ def __init__(self, root: Path): continue text = path.read_text() self.modules.append( - ParsedModule(str(path.relative_to(self.root)), text, ast.parse(text), str(path)) + ParsedModule(path=path.relative_to(self.root), module_name=str(path), + module=ast.parse(text), source=text) ) self.classes = {} self.functions = {} self.contexts = {} self.outer_functions = {} for module in self.modules: - for node in ast.walk(module.tree): + for node in ast.walk(module.module): if isinstance(node, ast.ClassDef): self.classes.setdefault(node.name, []).append((module, node)) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): self.functions.setdefault(node.name, []).append((module, node)) parents = { child: parent - for parent in ast.walk(module.tree) + for parent in ast.walk(module.module) for child in ast.iter_child_nodes(parent) } - for node in ast.walk(module.tree): + for node in ast.walk(module.module): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): enclosing = parents.get(node) while enclosing is not None and ( @@ -165,7 +166,7 @@ def __init__(self, root: Path): if len(inherited) == 1: self.ranks[name] = inherited[0] for module in self.modules: - for node in ast.walk(module.tree): + for node in ast.walk(module.module): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): owner = self.contexts[id(node)][1] self.walk(node.body, [], self.environment(node, owner), module) @@ -386,7 +387,7 @@ def locks(self, call, env, module, budget=1, trail=()): return () owner = self.typeof(call.func.value, env) name = call.func.attr - site = f"{module.path}:{call.lineno}" + site = f"{module.file_path}:{call.lineno}" if name in self.guard_names: self.all_sites.add((site, owner, ast.unparse(call))) if self.derives(owner, StoreGuard.__name__): @@ -495,7 +496,7 @@ def find(nodes, active): find(node.body, []) return tuple( dict.fromkeys( - Acquisition(lock.store, lock.site, f"{module.path}:{call.lineno}", lock.expression) + Acquisition(lock.store, lock.site, f"{module.file_path}:{call.lineno}", lock.expression) for lock in held_at_yield ) ) diff --git a/tests/test_boundary_decoding.py b/tests/test_boundary_decoding.py index 63d5c443c4..1873211713 100644 --- a/tests/test_boundary_decoding.py +++ b/tests/test_boundary_decoding.py @@ -6,7 +6,7 @@ import pytest from agent_comms_project import AgentCommsProject -from agent_comms.pinned_audit import audit +from nominal_refactor_advisor import audit from agent_comms.channel_coding_tools import CodingToolRequest from agent_comms.input_key import BusInputKey diff --git a/tests/test_carry.py b/tests/test_carry.py index 947cbc55e4..2d3b825b8b 100644 --- a/tests/test_carry.py +++ b/tests/test_carry.py @@ -13,7 +13,7 @@ import pytest from agent_comms_project import AgentCommsProject -from agent_comms.pinned_audit import audit +from nominal_refactor_advisor import audit from agent_comms.locked_store import LiveFilesRequest from agent_comms.cli_verbs.stop import StopCliCommand diff --git a/tests/test_checkout_submodules.py b/tests/test_checkout_submodules.py index 60b8ca60ac..abc0fcbbc2 100644 --- a/tests/test_checkout_submodules.py +++ b/tests/test_checkout_submodules.py @@ -44,7 +44,7 @@ def git(path: Path, *args: str) -> str: @pytest.fixture def repository(tmp_path, monkeypatch): - """A repository pinning one submodule from a local source, as ours pins refactor-audit.""" + """A repository pinning one submodule from a local source.""" monkeypatch.setenv("GIT_CONFIG_COUNT", "1") monkeypatch.setenv("GIT_CONFIG_KEY_0", "protocol.file.allow") monkeypatch.setenv("GIT_CONFIG_VALUE_0", "always") diff --git a/tests/test_phase_evidence.py b/tests/test_phase_evidence.py index 9e76503d1c..4667fd0a9c 100644 --- a/tests/test_phase_evidence.py +++ b/tests/test_phase_evidence.py @@ -30,7 +30,7 @@ RulesetGates, UnreadableEvidenceError, ) -from agent_comms.pinned_audit import GodFunction +from nominal_refactor_advisor.audit.findings import GodFunction from agent_comms.plan_phases import BusDeletionPhase, FLAG, ExternalBoundariesPhase, PlanPhase, RostersPhase from agent_comms.repo import Repo from agent_comms.repository_name import RepositoryName diff --git a/tools/agent_comms_project.py b/tools/agent_comms_project.py index c962eba36a..48c9390f20 100644 --- a/tools/agent_comms_project.py +++ b/tools/agent_comms_project.py @@ -1,8 +1,8 @@ -"""agent-comms as the refactor-audit's measured project: its facts, each derived from the +"""agent-comms as NominalRefactorAdvisor's measured project: its facts, each derived from the declaration that owns it here, and the one measure only it has, the type check of its stack clients. -The debt ratchet's entry (debt_ratchet.py) measures an AgentCommsProject of its checkout; the +The NRA gate's entry (nra_gate.py) measures an AgentCommsProject of its checkout; NRA's audit package itself never imports agent-comms.""" from __future__ import annotations @@ -27,6 +27,7 @@ import stored_formats from module_layers import PACKAGE, StackClient +from nominal_refactor_advisor import audit import agent_comms from agent_comms import ( @@ -44,27 +45,12 @@ from agent_comms.installer.carry import STEPS from agent_comms.installer.probes.carry import Carry from agent_comms.phase_evidence import MainTree, PlanReading -from agent_comms.pinned_audit import SKILL, audit from agent_comms.plan_phases import PlanPhase from agent_comms.repo import Repo SOURCES = Path(inspect.getfile(agent_comms)).parents[1] REPOSITORY = SOURCES.parent READER = Path(inspect.getfile(stored_formats)).relative_to(REPOSITORY) -AUDIT = SKILL.relative_to(REPOSITORY) - - -class PinnedAudit(audit.repository.Repository): - """This checkout's clone of the audit submodule, which every agent-comms revision pins by - commit. git archive leaves a submodule out of a revision's tree, so a revision run from an - archive would import this checkout's audit instead of the one it pinned.""" - - def place(self, pin: str, into: Path) -> None: - """The audit at the pin, fetched into this clone first when the clone lacks it.""" - if not self.has(pin): - self._run("fetch", "--quiet", "origin", pin) - into.mkdir(parents=True, exist_ok=True) - self.extract(pin, into) class StoredFormatReader(ABC): @@ -79,16 +65,15 @@ def place(self, project: AgentCommsProject, ref: str, tree: Path) -> None: class RevisionReader(StoredFormatReader): """agent-comms measuring itself: each revision runs its own reader, because a head's reader may import modules only that head declares (#3576 moved field_codec under - nominal), beside the audit that revision pinned, which its modules import through - pinned_audit. A revision without a reader fails loud.""" + nominal). The audit it imports is the installed NominalRefactorAdvisor's. A revision + without a reader fails loud.""" def place(self, project: AgentCommsProject, ref: str, tree: Path) -> None: project.extract(ref, tree, str(READER)) - PinnedAudit(REPOSITORY / AUDIT).place(project.identity(ref, str(AUDIT)), tree / AUDIT) class CheckoutReader(StoredFormatReader): - """A client repository such as Toad ships no reader; the ratchet checkout's own reads its + """A client repository such as Toad ships no reader; the gate checkout's own reads its modules, importing agent_comms from that checkout.""" def place(self, project: AgentCommsProject, ref: str, tree: Path) -> None: @@ -113,7 +98,7 @@ class AgentCommsProject(audit.project.Project[PlanPhase]): directory on the path would resolve the revision's bare imports into this one's tree.""" def cache_directory(self) -> Path: - return Path.home() / ".cache/agent-comms/debt-ratchet" + return Path.home() / ".cache/agent-comms/nra-gate" def source_roots(self) -> tuple[Path, ...]: return (Path(inspect.getfile(stored_formats)).parent, SOURCES) @@ -155,11 +140,11 @@ def stored_formats( package = package.parent modules = [".".join(Path(path).relative_to(package).with_suffix("").parts) for path in sorted(changed)] - with tempfile.TemporaryDirectory(prefix="ratchet-formats-") as scratch: + with tempfile.TemporaryDirectory(prefix="gate-formats-") as scratch: tree = Path(scratch) self.extract(ref, tree, str(package)) self.reader.place(self, ref, tree) - sources = audit.ratchet.Compared.source_paths(root, tree) + sources = audit.gate.Compared.source_paths(root, tree) for stamp in tree.rglob("package_stamp.py"): WheelStamp.of_source(tree / package, self.revision(ref), source_paths=sources).write(stamp) @@ -167,7 +152,7 @@ def stored_formats( result = SpawnedProcess.run( (sys.executable, "-c", self.probe, str(tree / READER), os.pathsep.join(paths), *modules), cwd=scratch, - env=audit.ratchet.Compared.environment(root, tree), timeout=300, + env=audit.gate.Compared.environment(root, tree), timeout=300, ) if result.returncode: raise ValueError(f"stored formats could not be read at {ref}: {result.stderr.decode()}") @@ -221,7 +206,7 @@ def guard_sites(self, head: str, guard: type[PlanPhase]) -> tuple[str, ...]: @dataclass(frozen=True) -class ClientFileCount(audit.ratchet.FileCount): +class ClientFileCount(audit.gate.FileCount): """Type errors in one stack client file, which the client's own code cannot add: both sides check the same client commit.""" @@ -245,7 +230,7 @@ def clients(repo: audit.project.Project, head: str) -> tuple[StackClient, ...]: stack = "stack/pyproject.toml" if not repo.tracks(head, stack): return () - into = Path(tempfile.mkdtemp(prefix="ratchet-clients-")) + into = Path(tempfile.mkdtemp(prefix="gate-clients-")) atexit.register(shutil.rmtree, into, True) (into / "stack").mkdir() (into / stack).write_bytes(repo.blob(head, stack)) @@ -259,13 +244,13 @@ def selected(client: StackClient) -> list[str]: @classmethod def findings( cls, repo: audit.project.Project, ref: str, client: StackClient, - side: type[audit.ratchet.Side], - ) -> tuple[type[audit.ratchet.Side], tuple[audit.checkers.CheckerSite, ...]]: + side: type[audit.gate.Side], + ) -> tuple[type[audit.gate.Side], tuple[audit.checkers.CheckerSite, ...]]: """Mypy resolves the client's other imports from the installed environment, which no content key names, so each comparison checks afresh under mypy's own cache.""" selected = cls.selected(client) cache = repo.beside("mypy-clients") / client.name / side.family_name - with tempfile.TemporaryDirectory(prefix="ratchet-client-") as scratch: + with tempfile.TemporaryDirectory(prefix="gate-client-") as scratch: repo.extract(ref, Path(scratch), "src") # The client checks under its own configuration, from its checkout. result = SpawnedProcess.run( @@ -289,14 +274,14 @@ def parts(cls, repo: audit.project.Project, base: str, head: str, changed: set[s return tuple(partial(cls.findings, repo, side.ref(base, head), client, side) for client in cls.clients(repo, head) if cls.selected(client) - for side in audit.ratchet.Side.sides()) + for side in audit.gate.Side.sides()) @classmethod def measured( - cls, found: dict[type[audit.ratchet.Side], Counter[str]], changed: set[str] + cls, found: dict[type[audit.gate.Side], Counter[str]], changed: set[str] ) -> list[str]: """The client files with an error on either side; changed paths are the package's.""" - return sorted(found[audit.ratchet.BaseSide].keys() | found[audit.ratchet.HeadSide].keys()) + return sorted(found[audit.gate.BaseSide].keys() | found[audit.gate.HeadSide].keys()) @classmethod def at(cls, path: str) -> ClientFileCount: diff --git a/tools/client_ratchet.py b/tools/client_ratchet.py deleted file mode 100755 index ae62eb06c2..0000000000 --- a/tools/client_ratchet.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -"""The debt ratchet's CI entry for a client repository such as Toad: the checkout it runs in, -measured as AgentCommsProject by the refactor-audit this agent-comms checkout pins. A client -ships no stored-format reader, so it borrows this checkout's; that is the one fact this entry -owns beside which project it measures.""" - -from __future__ import annotations - -from pathlib import Path - -from agent_comms_project import AgentCommsProject, CheckoutReader - -from agent_comms.pinned_audit import audit - -if __name__ == "__main__": - raise SystemExit(audit.ratchet.main(AgentCommsProject(Path.cwd(), CheckoutReader()))) diff --git a/tools/debt_ratchet.py b/tools/debt_ratchet.py deleted file mode 100644 index 292acaf2d2..0000000000 --- a/tools/debt_ratchet.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 -"""The debt ratchet's CI entry: this checkout, measured as AgentCommsProject by the refactor-audit -it pins. Every measure, the command line, the comparison, its JSON report and its exit status are -the audit's; the one fact this entry owns is which project it measures.""" - -from __future__ import annotations - -from pathlib import Path - -from agent_comms_project import AgentCommsProject -from agent_comms.pinned_audit import audit - -if __name__ == "__main__": - raise SystemExit(audit.ratchet.main(AgentCommsProject(Path.cwd()))) diff --git a/tools/module_layers.py b/tools/module_layers.py index b652f10274..e429eff118 100644 --- a/tools/module_layers.py +++ b/tools/module_layers.py @@ -39,7 +39,7 @@ from agent_comms.installer.runtime_wire import ClientImport, CrossesRuntimes -from agent_comms.pinned_audit import audit +from nominal_refactor_advisor import audit from agent_comms.declared_family import DeclaredFamily diff --git a/tools/nra_client_gate.py b/tools/nra_client_gate.py new file mode 100755 index 0000000000..401ad84f2e --- /dev/null +++ b/tools/nra_client_gate.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +"""The NRA gate's CI entry for a client repository such as Toad: the checkout it runs in, +measured as AgentCommsProject by NominalRefactorAdvisor's audit. A client ships no +stored-format reader, so it borrows this checkout's; that is the one fact this entry owns +beside which project it measures.""" + +from __future__ import annotations + +from pathlib import Path + +from agent_comms_project import AgentCommsProject, CheckoutReader +from nominal_refactor_advisor import audit + +if __name__ == "__main__": + raise SystemExit(audit.gate.main(AgentCommsProject(Path.cwd(), CheckoutReader()))) diff --git a/tools/nra_gate.py b/tools/nra_gate.py new file mode 100644 index 0000000000..254e5e99b8 --- /dev/null +++ b/tools/nra_gate.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +"""The NRA gate's CI entry: this checkout, measured as AgentCommsProject by NominalRefactorAdvisor's +audit. Every measure, the command line, the comparison, its JSON report and its exit status are +NRA's; the one fact this entry owns is which project it measures.""" + +from __future__ import annotations + +from pathlib import Path + +from agent_comms_project import AgentCommsProject +from nominal_refactor_advisor import audit + +if __name__ == "__main__": + raise SystemExit(audit.gate.main(AgentCommsProject(Path.cwd()))) diff --git a/tools/repairs.py b/tools/repairs.py index 05c5585cef..98e5160d31 100644 --- a/tools/repairs.py +++ b/tools/repairs.py @@ -1,4 +1,4 @@ -"""Repairs for what the debt ratchet counts, built as Nominal Refactor Advisor plans. +"""Repairs for what the NRA gate counts, built as Nominal Refactor Advisor plans. A repair declares the located measure whose occurrences it removes. It reads the files of the working tree, writes one NRA operation per occurrence it can remove and keeps @@ -27,6 +27,7 @@ from agent_comms_project import AgentCommsProject from module_layers import Package, TopLevelNames +from nominal_refactor_advisor import audit from nominal_refactor_advisor.codemod import ( CodemodPlanSequence, CodemodSourceSnapshot, @@ -37,7 +38,6 @@ ) from agent_comms.declared_family import DeclaredFamily -from agent_comms.pinned_audit import audit type FunctionNode = ast.FunctionDef | ast.AsyncFunctionDef type ImportNode = ast.Import | ast.ImportFrom @@ -62,7 +62,7 @@ class OutsidePackage(Kept): class Repair(DeclaredFamily, ABC): """One measure's repair over some files of the working tree.""" - repairs: ClassVar[type[audit.ratchet.LocatedSource]] + repairs: ClassVar[type[audit.gate.LocatedSource]] def __init__(self, repo: Path, paths: Iterable[str]) -> None: self.repo, self.paths = repo, sorted(paths) @@ -431,7 +431,7 @@ def main() -> int: for measure in (repair.repairs for repair in Repair.members_with(Repair)) } else: - found = audit.ratchet.compare( + found = audit.gate.compare( AgentCommsProject(repo), args.base, args.head, diff --git a/tools/stored_formats.py b/tools/stored_formats.py index 6325c739ee..e7b6983eb3 100644 --- a/tools/stored_formats.py +++ b/tools/stored_formats.py @@ -1,6 +1,6 @@ """Report the stored form of each versioned record declared in the named modules. -The debt ratchet runs this once per revision with that revision's source first on sys.path, +The NRA gate runs this once per revision with that revision's source first on sys.path, so each side's stored form comes from its own FieldCodec declarations and no revision's code is imported into another's process. A record's stored form is FieldCodec's record schema: each key on disk and its representation's wire spelling. Its version is the @@ -15,8 +15,9 @@ import json from collections.abc import Iterator +from nominal_refactor_advisor import audit + from agent_comms.field_codec import FieldCodec -from agent_comms.pinned_audit import audit def stored_format(record: type) -> Iterator[audit.project.StoredFormat]: @@ -26,12 +27,17 @@ def stored_format(record: type) -> Iterator[audit.project.StoredFormat]: match vars(record): case {"version": declared}: match declared, FieldCodec.record_schema(record): - case _, {"properties": {"version": {"enum": [stamped]}, **stored}, - "required": required}: + case _, {"properties": {"version": {"enum": [int() as stamped]}, **stored}, + "required": list() as required}: + fields: dict[object, object] = {**stored} + yield audit.project.StoredFormat.declared( + stamped, fields, (key for key in required + if isinstance(key, str) and key in stored)) + case int() as version, {"properties": dict() as properties, + "required": list() as required}: yield audit.project.StoredFormat.declared( - stamped, stored, (key for key in required if key in stored)) - case int(), {"properties": properties, "required": required}: - yield audit.project.StoredFormat.declared(declared, properties, required) + version, {**properties}, + (key for key in required if isinstance(key, str))) def stored_formats(module_name: str) -> dict[str, audit.project.StoredFormat]: diff --git a/uv.lock b/uv.lock index 8248960e0e..b531699a24 100644 --- a/uv.lock +++ b/uv.lock @@ -21,6 +21,7 @@ dependencies = [ { name = "agent-client-protocol" }, { name = "aiohttp" }, { name = "metaclass-registry" }, + { name = "nominal-refactor-advisor" }, { name = "packaging" }, { name = "pydantic" }, ] @@ -29,7 +30,6 @@ dependencies = [ dev = [ { name = "black" }, { name = "mypy" }, - { name = "nominal-refactor-advisor" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -46,7 +46,7 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = "==26.5.1" }, { name = "metaclass-registry", git = "https://github.com/OpenHCSDev/metaclass-registry?rev=main" }, { name = "mypy", marker = "extra == 'dev'", specifier = "==2.3.0" }, - { name = "nominal-refactor-advisor", marker = "extra == 'dev'", git = "https://github.com/OpenHCSDev/NominalRefactorAdvisor?rev=main" }, + { name = "nominal-refactor-advisor", git = "https://github.com/OpenHCSDev/NominalRefactorAdvisor?rev=main" }, { name = "packaging", specifier = ">=24" }, { name = "pydantic", specifier = ">=2.13" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, @@ -510,11 +510,12 @@ wheels = [ [[package]] name = "nominal-refactor-advisor" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/NominalRefactorAdvisor?rev=main#52e366c19c8625fec3f019dec17860d7b0e267eb" } +source = { git = "https://github.com/OpenHCSDev/NominalRefactorAdvisor?rev=main#b542c5a20d7d1d181ae94b36cf81d12715e13379" } dependencies = [ { name = "metaclass-registry" }, { name = "tree-sitter" }, { name = "tree-sitter-python" }, + { name = "typing-extensions" }, ] [[package]] @@ -775,17 +776,18 @@ wheels = [ [[package]] name = "tree-sitter" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" }, - { url = "https://files.pythonhosted.org/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" }, - { url = "https://files.pythonhosted.org/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" }, - { url = "https://files.pythonhosted.org/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" }, - { url = "https://files.pythonhosted.org/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" }, - { url = "https://files.pythonhosted.org/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" }, +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/03/5600b84aff2e6c4fe80cfebb4063fe2f50299521befe5f6092ab8c082f4a/tree_sitter-0.26.0.tar.gz", hash = "sha256:b40c219edccc4564530c96f8f1556f6202b37cda964d1cbd7bd2b7e68b40a245", size = 191423, upload-time = "2026-06-30T12:14:27.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7a/4d84e6f6ae2c3e757490dd84de251712c31e293dfe31f28da1ec019cefa2/tree_sitter-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5a3c93a352b7e6f70f73e121bbfa2d0117ba7478bd51114ed35c91b0b78814fa", size = 148901, upload-time = "2026-06-30T12:14:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d9/efe62ec65dc9d096e834d27b8c058127e2146e42ff3380b822a233f016a6/tree_sitter-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5fc2f41bf246ff2f70a9cc3690be35ec7580a4923151873d898c8bcb1a4503d3", size = 140805, upload-time = "2026-06-30T12:14:20.478Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2c/c82326b7b97e3c485c18679883b16f89e5e913c639d3b219d3da70c9e67e/tree_sitter-0.26.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8ea92a255c91671a7ec4625aba3ab7bb5220c423630ffbf83c45d7312abe084", size = 640586, upload-time = "2026-06-30T12:14:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7a/f56e7d8282859452611024c7cbc623bfba5b24b8cb9b8f8bc88c5219fe9a/tree_sitter-0.26.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f665510f0fcf4636fb9696f1f7853bed7a3bd764b7bb0cb8494e619c14ed5a0c", size = 668300, upload-time = "2026-06-30T12:14:22.728Z" }, + { url = "https://files.pythonhosted.org/packages/91/51/240ee81b9d5e9ca0a6cb1528e8605ffa70ab58c89ce126631be96d3e4bae/tree_sitter-0.26.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:253df7ab82cc0a9d311cd65f06e9f99fb3eac55996ae9fc94da22f123a861b90", size = 649627, upload-time = "2026-06-30T12:14:23.819Z" }, + { url = "https://files.pythonhosted.org/packages/6a/54/760035cefedf9eb44f0f84c4ac22f1322e73155853e272576ee876336312/tree_sitter-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ff80d4833d330a73184a3ac5132abe93c575d2dea31975c6f15c0d21fef238aa", size = 664885, upload-time = "2026-06-30T12:14:25.064Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1b/0b36fe2a984ecedc4ce6aefd5d56447a6626a8e9b595c4e48658510ce8f8/tree_sitter-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:a4033fecc8f606c7f2e8b8014d0057b74668a7f0152763606f7bc25c5f9ec64c", size = 132688, upload-time = "2026-06-30T12:14:26.106Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/ebc041a13fbf40144afdb0d4b447e48e0b4012ca866c63de8b48f801f0c1/tree_sitter-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:823251c4b6725a7c03ed497a339135ede7ae4bdde75bb8be7ef5e305aeb4ff52", size = 120287, upload-time = "2026-06-30T12:14:26.991Z" }, ] [[package]]