From d9811fc114668c0ea60c3004c6b5159f3573c75a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 13:36:19 +0000 Subject: [PATCH 1/9] grants: a new route keeps a branch-bound protected-path grant Every prompt starts a new route, so binding protected-path grants to the route voided them on each user message and forced a new grant for the same edit again and again. A branch-bound grant is already held to the repository, branch ref, anchor ancestry, exact files and TTL, so it no longer checks the route. Production and external-write grants keep their exact route, HEAD and tree binding. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BhxEnEALZRsc7kaPwG2kiR --- .getzilla/getzilla/state.py | 4 +++- AGENTS.md | 2 +- tests/test_grant_lifecycle.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.getzilla/getzilla/state.py b/.getzilla/getzilla/state.py index e1468e3d..310bae8e 100644 --- a/.getzilla/getzilla/state.py +++ b/.getzilla/getzilla/state.py @@ -501,8 +501,10 @@ def has_valid_approval( if approval.get('scope') != scope: continue branch_bound = scope == 'protected-path' and approval.get('binding') == BRANCH_RESOURCES_BINDING + # A branch-bound grant is already held to the branch, its anchor and the exact files; every + # new prompt starts a new route, so binding the route as well made each message void it. if any(approval.get(key) != value for key, value in bindings.items() - if not (branch_bound and key == 'git_head')): + if not (branch_bound and key in {'git_head', 'route_id'})): continue grant_binding_digest = approval.get('grant_binding_digest') legacy_tree_fingerprint = approval.get('tree_fingerprint') diff --git a/AGENTS.md b/AGENTS.md index b96a068e..3ba45251 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -270,7 +270,7 @@ For each report with probes, list the claims probed, exact commands and concise ## Local delegated grants - `scripts/getzilla_approve.py` does not originate authority. It materializes explicit or standing user consent already present in the working context. -- Every grant names explicit actions and exact resources. Protected-path grants bind repository, route, change, full branch ref, anchor ancestry, unchanged files outside the exact grant set and TTL. Commits within the approved file set retain the grant. A branch change or out-of-scope edit invalidates it. Production/external grants retain exact HEAD/tree binding. Legacy records retain their original validation. +- Every grant names explicit actions and exact resources. Protected-path grants bind repository, change, full branch ref, anchor ancestry, unchanged files outside the exact grant set and TTL; a new route (every prompt starts one) keeps them. Commits within the approved file set retain the grant. A branch change or out-of-scope edit invalidates it. Production/external grants retain exact HEAD/tree binding. Legacy records retain their original validation. - An agent may invoke `getzilla_approve.py` only when the user has explicitly delegated the named operation. The wildcard scope is forbidden. - Under L5 autonomy, the owner's standing consent is that explicit delegation for merge, tag and release of the project's own repository. It applies only with green required checks on the exact head SHA, resolved review threads and a passed independent review. The agent still records an exact action and resource for each operation. - Trust CI security approvals use Ed25519 envelopes generated by `adaptive-trust-ci approval-create` on a human-controlled machine and submitted to the external API. Local grants are never accepted by Trust CI. diff --git a/tests/test_grant_lifecycle.py b/tests/test_grant_lifecycle.py index fc13e4dd..25895aa0 100644 --- a/tests/test_grant_lifecycle.py +++ b/tests/test_grant_lifecycle.py @@ -135,6 +135,24 @@ def test_literal_git_backslash_names_are_outside_exact_resources_in_every_layer( outside.write_text('initial dirty bytes\n') self.assertFalse(valid(root), f'{layer} Git names must retain literal backslashes') + def test_a_new_route_keeps_a_branch_bound_grant_but_not_a_production_grant(self) -> None: + with github_project() as root: + add_approval(root, 'protected-path', 'one file', 15, + actions=['protected-path-write'], resources=[TARGET]) + add_approval(root, 'production', 'push', 15, actions=['git-push-branch']) + set_active_route(root, build_route(root, 'Next prompt about something else', 's1').to_dict()) + self.assertTrue(valid(root)) + self.assertFalse(valid(root, 'production', None)) + + def test_a_branch_change_still_voids_the_grant_after_a_new_route(self) -> None: + from tests.branch_support import git + with github_project() as root: + add_approval(root, 'protected-path', 'one file', 15, + actions=['protected-path-write'], resources=[TARGET]) + set_active_route(root, build_route(root, 'Next prompt', 's1').to_dict()) + git(root, 'checkout', '-q', '-b', 'other-branch') + self.assertFalse(valid(root)) + def test_other_scopes_keep_exact_tree_binding(self) -> None: with github_project() as root: approval = add_approval(root, 'production', 'push', 15, actions=['git-push-branch']) From 6144f93d242dac0266a60c2759d1ea24dae259e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 13:45:15 +0000 Subject: [PATCH 2/9] grants: pin change_id and the scope guard in tests The security review found that change_id is now the only task binding of a branch-bound grant and that no test pinned it, nor the protected-path scope guard. Add a test for each; both mutants are now killed. Update the has_valid_approval docstring. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BhxEnEALZRsc7kaPwG2kiR --- .getzilla/getzilla/state.py | 3 ++- tests/test_grant_lifecycle.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.getzilla/getzilla/state.py b/.getzilla/getzilla/state.py index 310bae8e..1eec1594 100644 --- a/.getzilla/getzilla/state.py +++ b/.getzilla/getzilla/state.py @@ -457,7 +457,8 @@ def has_valid_approval( action: str | None = None, resource: str | None = None, ) -> bool: - """Recheck scope-specific source binding plus repository, route, change and TTL.""" + """Recheck scope-specific source binding plus repository, change and TTL, and the route for + every grant except a branch-bound protected-path grant.""" if scope in {'production', 'external-write'} and action: from .human_gates import gate_block_reason diff --git a/tests/test_grant_lifecycle.py b/tests/test_grant_lifecycle.py index 25895aa0..cc467614 100644 --- a/tests/test_grant_lifecycle.py +++ b/tests/test_grant_lifecycle.py @@ -144,6 +144,24 @@ def test_a_new_route_keeps_a_branch_bound_grant_but_not_a_production_grant(self) self.assertTrue(valid(root)) self.assertFalse(valid(root, 'production', None)) + def test_a_new_change_package_still_voids_a_branch_bound_grant(self) -> None: + from getzilla.state import set_active_change + with github_project() as root: + add_approval(root, 'protected-path', 'one file', 15, + actions=['protected-path-write'], resources=[TARGET]) + self.assertTrue(valid(root)) + set_active_change(root, {'change_id': 'another-change'}) + self.assertFalse(valid(root)) + + def test_a_forged_branch_binding_on_a_production_grant_keeps_the_route_check(self) -> None: + with github_project() as root: + # A complete branch-bound record relabelled as production, from another route. + approval = add_approval(root, 'protected-path', 'one file', 15, + actions=['protected-path-write'], resources=[TARGET]) + approval.update(scope='production', actions=['git-push-branch'], route_id='some-other-route') + approvals_path(root).write_text(json.dumps([approval]), encoding='utf-8') + self.assertFalse(valid(root, 'production', None)) + def test_a_branch_change_still_voids_the_grant_after_a_new_route(self) -> None: from tests.branch_support import git with github_project() as root: From bd8dcace878cf3773d9849bada11f93800483deb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 14:14:39 +0000 Subject: [PATCH 3/9] grants: bind branch-bound grants to the session, not the route Skipping the route broke session isolation: a grant issued in one session also served another session in the same worktree (tests/test_control_plane_sessions failed on CI). A protected-path grant now records its session and checks that instead of the route, so later prompts of the same session keep it and another session never gets it. Without a known session on both sides the exact route is still required. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BhxEnEALZRsc7kaPwG2kiR --- .getzilla/getzilla/state.py | 16 ++++++++++------ AGENTS.md | 2 +- tests/test_grant_lifecycle.py | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.getzilla/getzilla/state.py b/.getzilla/getzilla/state.py index 1eec1594..f885be3f 100644 --- a/.getzilla/getzilla/state.py +++ b/.getzilla/getzilla/state.py @@ -430,7 +430,8 @@ def add_approval( 'route_id': route.get('route_id'), 'change_id': _active_change_id(root), 'git_head': head, - **({'binding': BRANCH_RESOURCES_BINDING, 'git_branch': branch, 'anchor_commit': head} + **({'binding': BRANCH_RESOURCES_BINDING, 'git_branch': branch, 'anchor_commit': head, + 'session_id': route.get('session_id')} if normalized_scope == 'protected-path' else {}), 'grant_binding_digest': tree_fingerprint( root, normalized_resources if normalized_scope == 'protected-path' else (), @@ -457,8 +458,8 @@ def has_valid_approval( action: str | None = None, resource: str | None = None, ) -> bool: - """Recheck scope-specific source binding plus repository, change and TTL, and the route for - every grant except a branch-bound protected-path grant.""" + """Recheck scope-specific source binding plus repository, change and TTL, and the route; a + branch-bound protected-path grant checks its session instead of the route when both are known.""" if scope in {'production', 'external-write'} and action: from .human_gates import gate_block_reason @@ -502,10 +503,13 @@ def has_valid_approval( if approval.get('scope') != scope: continue branch_bound = scope == 'protected-path' and approval.get('binding') == BRANCH_RESOURCES_BINDING - # A branch-bound grant is already held to the branch, its anchor and the exact files; every - # new prompt starts a new route, so binding the route as well made each message void it. + # Every new prompt starts a new route, so a branch-bound grant follows its session instead: + # later prompts of the same session keep it, another session never gets it. Without a known + # session on both sides the exact route is still required. + session = route.get('session_id') + session_bound = branch_bound and bool(session) and approval.get('session_id') == session if any(approval.get(key) != value for key, value in bindings.items() - if not (branch_bound and key in {'git_head', 'route_id'})): + if not (branch_bound and key == 'git_head') and not (session_bound and key == 'route_id')): continue grant_binding_digest = approval.get('grant_binding_digest') legacy_tree_fingerprint = approval.get('tree_fingerprint') diff --git a/AGENTS.md b/AGENTS.md index 3ba45251..f09f5041 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -270,7 +270,7 @@ For each report with probes, list the claims probed, exact commands and concise ## Local delegated grants - `scripts/getzilla_approve.py` does not originate authority. It materializes explicit or standing user consent already present in the working context. -- Every grant names explicit actions and exact resources. Protected-path grants bind repository, change, full branch ref, anchor ancestry, unchanged files outside the exact grant set and TTL; a new route (every prompt starts one) keeps them. Commits within the approved file set retain the grant. A branch change or out-of-scope edit invalidates it. Production/external grants retain exact HEAD/tree binding. Legacy records retain their original validation. +- Every grant names explicit actions and exact resources. Protected-path grants bind repository, session, change, full branch ref, anchor ancestry, unchanged files outside the exact grant set and TTL; a new route of the same session (every prompt starts one) keeps them, and another session never gets them. Without a known session they bind the exact route. Commits within the approved file set retain the grant. A branch change or out-of-scope edit invalidates it. Production/external grants retain exact HEAD/tree binding. Legacy records retain their original validation. - An agent may invoke `getzilla_approve.py` only when the user has explicitly delegated the named operation. The wildcard scope is forbidden. - Under L5 autonomy, the owner's standing consent is that explicit delegation for merge, tag and release of the project's own repository. It applies only with green required checks on the exact head SHA, resolved review threads and a passed independent review. The agent still records an exact action and resource for each operation. - Trust CI security approvals use Ed25519 envelopes generated by `adaptive-trust-ci approval-create` on a human-controlled machine and submitted to the external API. Local grants are never accepted by Trust CI. diff --git a/tests/test_grant_lifecycle.py b/tests/test_grant_lifecycle.py index cc467614..d532de2f 100644 --- a/tests/test_grant_lifecycle.py +++ b/tests/test_grant_lifecycle.py @@ -144,6 +144,23 @@ def test_a_new_route_keeps_a_branch_bound_grant_but_not_a_production_grant(self) self.assertTrue(valid(root)) self.assertFalse(valid(root, 'production', None)) + def test_a_route_of_another_session_voids_the_grant(self) -> None: + with github_project() as root: + add_approval(root, 'protected-path', 'one file', 15, + actions=['protected-path-write'], resources=[TARGET]) + set_active_route(root, build_route(root, 'Another session prompt', 's2').to_dict()) + self.assertFalse(valid(root)) + + def test_without_a_known_session_the_exact_route_is_still_required(self) -> None: + with github_project() as root: + approval = add_approval(root, 'protected-path', 'one file', 15, + actions=['protected-path-write'], resources=[TARGET]) + approval.pop('session_id') + approvals_path(root).write_text(json.dumps([approval]), encoding='utf-8') + self.assertTrue(valid(root)) + set_active_route(root, build_route(root, 'Next prompt', 's1').to_dict()) + self.assertFalse(valid(root)) + def test_a_new_change_package_still_voids_a_branch_bound_grant(self) -> None: from getzilla.state import set_active_change with github_project() as root: From c6fa112c65201bcbf21ad76997e31769845b6708 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 14:25:56 +0000 Subject: [PATCH 4/9] Bind grant sessions to the caller, not the route A session without its own route pointer reads the shared route of another session, and 'manual' is not a session. The check now uses the caller's bound session and skips the route only when caller, route and grant name the same session. getzilla_approve.py binds CLAUDE_CODE_SESSION_ID. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BhxEnEALZRsc7kaPwG2kiR --- .getzilla/getzilla/state.py | 14 ++++++----- AGENTS.md | 2 +- scripts/getzilla_approve.py | 5 +++- tests/test_grant_lifecycle.py | 46 +++++++++++++++++++++++++++++++++-- 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/.getzilla/getzilla/state.py b/.getzilla/getzilla/state.py index f885be3f..8e62eb09 100644 --- a/.getzilla/getzilla/state.py +++ b/.getzilla/getzilla/state.py @@ -431,7 +431,7 @@ def add_approval( 'change_id': _active_change_id(root), 'git_head': head, **({'binding': BRANCH_RESOURCES_BINDING, 'git_branch': branch, 'anchor_commit': head, - 'session_id': route.get('session_id')} + 'session_id': _SESSION.get()} if normalized_scope == 'protected-path' else {}), 'grant_binding_digest': tree_fingerprint( root, normalized_resources if normalized_scope == 'protected-path' else (), @@ -459,7 +459,8 @@ def has_valid_approval( resource: str | None = None, ) -> bool: """Recheck scope-specific source binding plus repository, change and TTL, and the route; a - branch-bound protected-path grant checks its session instead of the route when both are known.""" + branch-bound protected-path grant checks the caller's bound session instead of the route when the + route and the grant name that same session.""" if scope in {'production', 'external-write'} and action: from .human_gates import gate_block_reason @@ -504,10 +505,11 @@ def has_valid_approval( continue branch_bound = scope == 'protected-path' and approval.get('binding') == BRANCH_RESOURCES_BINDING # Every new prompt starts a new route, so a branch-bound grant follows its session instead: - # later prompts of the same session keep it, another session never gets it. Without a known - # session on both sides the exact route is still required. - session = route.get('session_id') - session_bound = branch_bound and bool(session) and approval.get('session_id') == session + # later prompts of the same session keep it, another session never gets it. The session is the + # caller's bound one; unless caller, route and grant all name it the exact route is required. + session = _SESSION.get() + session_bound = (branch_bound and bool(session) and route.get('session_id') == session + and approval.get('session_id') == session) if any(approval.get(key) != value for key, value in bindings.items() if not (branch_bound and key == 'git_head') and not (session_bound and key == 'route_id')): continue diff --git a/AGENTS.md b/AGENTS.md index f09f5041..21dfe37e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -270,7 +270,7 @@ For each report with probes, list the claims probed, exact commands and concise ## Local delegated grants - `scripts/getzilla_approve.py` does not originate authority. It materializes explicit or standing user consent already present in the working context. -- Every grant names explicit actions and exact resources. Protected-path grants bind repository, session, change, full branch ref, anchor ancestry, unchanged files outside the exact grant set and TTL; a new route of the same session (every prompt starts one) keeps them, and another session never gets them. Without a known session they bind the exact route. Commits within the approved file set retain the grant. A branch change or out-of-scope edit invalidates it. Production/external grants retain exact HEAD/tree binding. Legacy records retain their original validation. +- Every grant names explicit actions and exact resources. Protected-path grants bind repository, session, change, full branch ref, anchor ancestry, unchanged files outside the exact grant set and TTL; a new route of the same session (every prompt starts one) keeps them, and another session never gets them. The session is the caller's own (hook payload, or `CLAUDE_CODE_SESSION_ID` for `getzilla_approve.py`); unless the caller, its route and the grant name the same session, they bind the exact route. Commits within the approved file set retain the grant. A branch change or out-of-scope edit invalidates it. Production/external grants retain exact HEAD/tree binding. Legacy records retain their original validation. - An agent may invoke `getzilla_approve.py` only when the user has explicitly delegated the named operation. The wildcard scope is forbidden. - Under L5 autonomy, the owner's standing consent is that explicit delegation for merge, tag and release of the project's own repository. It applies only with green required checks on the exact head SHA, resolved review threads and a passed independent review. The agent still records an exact action and resource for each operation. - Trust CI security approvals use Ed25519 envelopes generated by `adaptive-trust-ci approval-create` on a human-controlled machine and submitted to the external API. Local grants are never accepted by Trust CI. diff --git a/scripts/getzilla_approve.py b/scripts/getzilla_approve.py index 75e1d92c..02163cca 100644 --- a/scripts/getzilla_approve.py +++ b/scripts/getzilla_approve.py @@ -2,13 +2,14 @@ import argparse import json +import os import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / '.getzilla')) -from getzilla.state import add_approval +from getzilla.state import add_approval, bind_session from getzilla.util import find_root RELEASE_ACTIONS = ['git-push-branch', 'git-push-tag', 'github-release'] @@ -68,6 +69,8 @@ if not actions: parser.error('at least one --action or --profile is required') +# A grant follows the caller's session across its later routes; an unknown session keeps the exact route. +bind_session(os.environ.get('CLAUDE_CODE_SESSION_ID') or os.environ.get('GETZILLA_SESSION_ID')) try: result = add_approval( find_root(), diff --git a/tests/test_grant_lifecycle.py b/tests/test_grant_lifecycle.py index d532de2f..252f26eb 100644 --- a/tests/test_grant_lifecycle.py +++ b/tests/test_grant_lifecycle.py @@ -14,7 +14,7 @@ sys.path.insert(0, str(ROOT / '.getzilla')) from getzilla.router import build_route -from getzilla.state import add_approval, approvals_path, has_valid_approval, set_active_route +from getzilla.state import add_approval, approvals_path, bind_session, has_valid_approval, set_active_route from tests._support import project_copy TARGET = 'tests/test_cpu_capacity.py' @@ -26,7 +26,11 @@ def github_project() -> Iterator[Path]: subprocess.run(['git', 'remote', 'add', 'origin', 'git@github.com:Dimkox/Getzilla.git'], cwd=root, check=True) set_active_route(root, build_route(root, 'Fix startup CPU regression test', 's1').to_dict()) - yield root + bind_session('s1') + try: + yield root + finally: + bind_session(None) def valid(root: Path, scope: str = 'protected-path', resource: str | None = TARGET) -> bool: @@ -149,8 +153,46 @@ def test_a_route_of_another_session_voids_the_grant(self) -> None: add_approval(root, 'protected-path', 'one file', 15, actions=['protected-path-write'], resources=[TARGET]) set_active_route(root, build_route(root, 'Another session prompt', 's2').to_dict()) + self.assertTrue(valid(root), 's1 still reads its own route') + bind_session('s2') self.assertFalse(valid(root)) + def test_another_caller_session_cannot_use_the_shared_route_of_the_grant_session(self) -> None: + with github_project() as root: + add_approval(root, 'protected-path', 'one file', 15, + actions=['protected-path-write'], resources=[TARGET]) + set_active_route(root, build_route(root, 'Next prompt', 's1').to_dict()) + bind_session('s2') # no route pointer of its own + self.assertFalse(valid(root)) + # Its own grant made over the shared route of s1 does not follow s1's next route either. + add_approval(root, 'protected-path', 'one file', 15, + actions=['protected-path-write'], resources=[TARGET]) + self.assertTrue(valid(root)) + set_active_route(root, build_route(root, 'Third prompt', 's1').to_dict()) + self.assertFalse(valid(root)) + + def test_a_manual_or_unbound_caller_needs_the_exact_route(self) -> None: + for caller in ('manual', None): + with self.subTest(caller=caller), github_project() as root: + bind_session(caller) + approval = add_approval(root, 'protected-path', 'one file', 15, + actions=['protected-path-write'], resources=[TARGET]) + self.assertIsNone(approval['session_id']) + self.assertTrue(valid(root)) + set_active_route(root, build_route(root, 'Next prompt', 'manual').to_dict()) + self.assertFalse(valid(root)) + + def test_the_cli_records_the_caller_session(self) -> None: + with github_project() as root: + env = {**os.environ, 'CLAUDE_CODE_SESSION_ID': 's1'} + env.pop('GETZILLA_SESSION_ID', None) + subprocess.run([sys.executable, str(ROOT / 'scripts' / 'getzilla_approve.py'), 'protected-path', + '--reason', 'one file', '--action', 'protected-path-write', '--resource', TARGET], + cwd=root, env=env, check=True, capture_output=True) + self.assertEqual(json.loads(approvals_path(root).read_text(encoding='utf-8'))[-1]['session_id'], 's1') + set_active_route(root, build_route(root, 'Next prompt', 's1').to_dict()) + self.assertTrue(valid(root)) + def test_without_a_known_session_the_exact_route_is_still_required(self) -> None: with github_project() as root: approval = add_approval(root, 'protected-path', 'one file', 15, From 4f455817a979da73ea826094b5e8020053ae4318 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:55:28 +0000 Subject: [PATCH 5/9] docs: bring QUICKSTART, START_HERE, REFERENCE, CHANGELOG and state up to date - QUICKSTART: launcher, runtime evidence without report commits, route reconcile, review snapshots, light lane, and a disk cleanup section. - START_HERE and REFERENCE: current main a45fbdd, correct v2.3.0 link, continuation at issue #134; REFERENCE lists all getzilla commands. - PROJECT_STATE: current_continuation moves to issue #134; the finished PR #79 contour is kept as historical_pr79_continuation. - CHANGELOG: 2.3.1 lists the work merged since 2026-10-21 (#121-#179). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BhxEnEALZRsc7kaPwG2kiR --- CHANGELOG.md | 10 +++++ PROJECT_STATE.json | 16 ++++++-- QUICKSTART.md | 32 +++++++++++++-- START_HERE.md | 8 ++-- docs/REFERENCE.md | 79 ++++++++++++++++++++++++++----------- tests/test_project_state.py | 10 +++-- 6 files changed, 116 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ea3380..ff3f3911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,16 @@ - a new v2 signed-result contract accepts optional argv; v1 remains unchanged; upgrade verifiers and closed-schema consumers before the separate Trust CI producer rollout; - the project-authored ISO/IEC 42001 clause map is informational and never gates. - Lean install: Bitrix and SEO-landing skills come only with their domain (`extra:bitrix`, `extra:seo-landing`, `--extras`), cursor rules belong to `agent:cursor`, and a fresh install puts no hook shims in the repository root (332 → 283 entries on a plain Python repository). +- Install (#128, #129): the plan shows the real footprint, the engine ships as one archive on fresh installs, and Bitrix agents and Grok briefs travel with their component. +- `getzilla update` (#132, #135): one command updates Getzilla, writes the new files into the project on a new `getzilla-update-` branch and updates third-party tools; `--accept-local` keeps a changed kept file. +- Delivery cost (#121, #142, #144): a light lane for small low-risk diffs, scoped grants, runtime evidence instead of report commits, and review plans from the exact diff. +- Router (#125, #139): risk and complexity come from the task, and pasted material no longer sets high risk, incident or the production gate. +- `getzilla duplicates` (#171): source-only duplication analysis with optional jscpd. +- `getzilla report` shows route observation gaps (#173). +- Disk cleanup, issue #168 (#174–#178): `getzilla cleanup inventory` (read-only, with repository copies), `cleanup worktrees [--remove]` (removes only worktrees with no unique work), `cleanup snapshot ` / `--remove ` for review copies; test runs keep child temporary files in their own run folder. +- Grants (#179): a branch-bound protected-path grant follows the caller's session across its later routes; another session, a `manual` caller or an unknown session still needs the exact route. Production and external-write grants keep their exact route, HEAD and tree binding. +- Fixes: factory controller pidfd check (#137), CAS lock retry on a transient macOS ENOENT (#133), clearer ambiguous-root hook denials (#131), `pypdf` 6.21.2 (#130), and `setuptools` 84.1.0 in the factory, delivery and Trust CI builds (#173). +- Docs: README lists every `getzilla` command; QUICKSTART describes runtime evidence and disk cleanup; the handoff points at the current continuation. ## Observed v2.3.0 publication — 2026-10-21 diff --git a/PROJECT_STATE.json b/PROJECT_STATE.json index 8f8d4ee7..ef5a39fd 100644 --- a/PROJECT_STATE.json +++ b/PROJECT_STATE.json @@ -2,8 +2,8 @@ "schema_version": 2, "repository": "Dimkox/Getzilla", "default_branch": "main", - "observed_at": "2026-10-11T02:13:17+00:00", - "observed_main_sha": "540c12f93e798c2f483455a5086ab0018ac3216e", + "observed_at": "2026-10-24T15:55:00+00:00", + "observed_main_sha": "a45fbdd90e4f98e05c2f45c43afb28a63b3e448c", "product_version": "2.3.1", "latest_published_release": "v2.3.0", "source_installers": { @@ -23,6 +23,15 @@ "engineering_contract": "AGENTS.md", "roadmap": "DARK_FACTORY_ROADMAP.md", "current_continuation": { + "issue": 134, + "branch": "ccr-315b4684-r3j424", + "target_version": "2.3.1", + "source_base": "a45fbdd90e4f98e05c2f45c43afb28a63b3e448c", + "status": "source_candidate_low_batch_and_release_pending", + "delivered_since_published_release": "PRs #121-#179 on main, including issue #168 disk cleanup (#174-#178) and session-bound protected-path grants (#179).", + "next_action": "Fix the Low findings collected in issue #134 in small PRs, then prepare the 2.3.1 release. Bind independent review to the exact reviewed commit, save reports outside Git, run one final PR verifier and require public Actions linux/windows on the exact head. Under conditional L5, record exact merge/tag/release grants only after green exact-head checks, resolved threads and passed independent review. Keep v2.3.0 immutable. Native harness executes local tasks; do not start a daemon or enable providers. Production installation/task acceptance still needs an exact owner target and scoped authority." + }, + "historical_pr79_continuation": { "issue": 72, "route_id": "43786cadd549", "branch": "fix/codex-cmd-hooks-20261011", @@ -36,7 +45,8 @@ "route_id": "65799e00561a", "branch": "release/2.2.2-20261011", "record_scope": "Preserved uncommitted release contour reused in PR79; not an additional delivered branch." - } + }, + "record_scope": "Finished contour: PR #79 merged; kept as dated provenance, not an instruction." }, "historical_cleanup_continuation": { "pull_request": 241, diff --git a/QUICKSTART.md b/QUICKSTART.md index 1166155a..3a135b63 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -31,6 +31,8 @@ At the end of every run, first install or re-run, the installer offers to update With the installer done, continue at step 3. The manual steps below are the same path by hand. +Every `python3 scripts/...` command below also runs through the launcher, for example `getzilla verify --mode pr` or `getzilla install-into --plan ` (the installer links it to `~/.local/bin/getzilla`; on Windows use `bin\getzilla.cmd`). The [command table in README.md](README.md#commands) lists all commands. + 0. Check tools (minimum or newer; doctor offers a fallback install if something is missing): ```bash python3 scripts/getzilla_doctor.py --offer-install @@ -127,18 +129,24 @@ python3 scripts/getzilla_architecture.py fitness --base <40-char-sha> --head <40 python3 scripts/getzilla_status.py ``` + `getzilla change start` writes the change package to `.getzilla/runtime/changes//`. This is ignored runtime state, not Git content; older tracked packages under `engineering/changes/` are history. + 5. Complete delivery checks once: - Run bounded controls on a clean committed candidate. Independent route-selected reviewers inspect that exact commit. Save their reports as regular non-executable Markdown under the active change evidence directory. Commit and freeze the report-containing tree. + Commit the candidate. Bind the route to the committed diff, run a bounded control, then let the independent route-selected reviewers inspect that exact commit. A reviewer that needs its own copy makes one with `getzilla cleanup snapshot ` and removes it with `getzilla cleanup snapshot --remove ` when the report is returned. Save the reports outside Git, in the runtime package evidence folder or `.getzilla/runtime/reviews//`. No report commit or freeze commit is needed. ```bash + python3 scripts/getzilla_route.py --reconcile --json + python3 scripts/getzilla_verify.py --mode fast --no-record --test tests.test_quality_gates --budget 180 python3 scripts/getzilla_verify.py --mode pr - python3 scripts/getzilla_review.py code_review --status pass --report engineering/changes//evidence/code-review.md --reviewed-commit + python3 scripts/getzilla_review.py code_review --status pass --report .getzilla/runtime/reviews//code-review.md --reviewed-commit python3 scripts/getzilla_status.py ``` Repeat the receipt command for every selected review kind. A source change requires new independent reviews and a new final gate. Local receipts are preflight evidence. Public repositories use pinned Actions checks; private ones use the external Trust CI gate. Require green exact-head checks before merge. + Small low-risk changes (at most 50 changed code lines, no control files) take the light lane: one independent review and focused checks, no change package. Full CI still runs. + 6. Follow `/getzilla-delivery` and `/release-readiness`. Conditional AGENTS L5 covers merge, tag and release of the project's own repository after required checks, independent review and resolved threads. Record exact action/resource grants. Production writes, deployment and external writes need separate scoped authority. `getzilla_deploy.py` prepares commands. Explicit owner overrides of local workflow rules must name the rule, scope, reason, deferred checks and follow-up validation. Keep the result UNVERIFIED until deferred checks run; do not create fake passing evidence or bypass external checks. @@ -157,6 +165,22 @@ Persist the exact derived `workflow/task-graph.json` / `workflow/convergence-rep 7. Trust project hooks in the TUI: `/hooks-trust` +## Keep the disk clean + +Getzilla never deletes work on its own. These commands show what it keeps and remove only what holds no unique work: + +```bash +getzilla cleanup inventory # what is on disk, by class, with repository copies; deletes nothing +getzilla cleanup worktrees # every worktree and the reasons to keep it (dry run) +getzilla cleanup worktrees --remove # removes only worktrees with no changes, no unique files and no unpushed commits +getzilla cleanup snapshot --remove # removes one review copy made by `cleanup snapshot ` +getzilla branches report --repo . --policy --output # old branches; deletes nothing +``` + +`getzilla branches apply` deletes branches only from a reviewed plan, with `--plan`, `--plan-id` and `--allow-delete`. See [docs/branch-lifecycle.md](docs/branch-lifecycle.md). + +`inventory` lists repository copies under `.getzilla/runtime/` (old review copies, for example) with their sizes. It keeps them until you decide: delete one only after you have checked it holds nothing you need. + ## Try the local browser demo From this checkout, start the Python-only dashboard with no install and no frontend build: @@ -352,6 +376,6 @@ curl -sSfL https://github.com/sigstore/cosign/releases/download/v2.4.3/cosign-li sudo install -m 0755 /tmp/cosign /usr/local/bin/cosign ``` -Freeze tracked workflow state at `reviewing` before independent review. Do not transition tracked state to `ready` after freeze: that changes reviewed source and stales the reports. Fresh final receipts, zero status gaps and exact-head external PR results record completion. The old prepare-only command printer's ready prerequisite grants no authority and does not restrict separately authorized exact L5 operations. A full lifecycle-state redesign is a later task. +New workflow state is ignored runtime data and may move to `ready` without changing reviewed source. Fresh final receipts, zero status gaps and exact-head external PR results record completion. No report-only or freeze commit is needed. The old prepare-only command printer's ready prerequisite grants no authority and does not restrict separately authorized exact L5 operations. -Saved report-only deltas use exactly `code-review.md`, `test-review.md`, `bitrix-review.md`, `security-review.md`, `data-review.md` or `release-review.md` directly under the active change evidence directory. Analysis, README/index, checkpoint and arbitrary custom Markdown files are source changes, not saved review reports. +Legacy tracked report-only deltas use exactly `code-review.md`, `test-review.md`, `bitrix-review.md`, `security-review.md`, `data-review.md` or `release-review.md` directly under the active change evidence directory. Analysis, README/index, checkpoint and arbitrary custom Markdown files are source changes, not saved review reports. diff --git a/START_HERE.md b/START_HERE.md index 401c7191..474d913a 100644 --- a/START_HERE.md +++ b/START_HERE.md @@ -2,7 +2,7 @@ > **Origin.** Getzilla is the renamed successor of the [predecessor project](https://github.com/Dimkox/adaptive-grok-build-pro/tree/c4e506f3d3a45e000f5b9f9121f698c1d16814e3). This repository was started on 2026-10-05 from that project's final PR #242 head (identical to its merged main commit `f97966c`) `c4e506f3d3a45e000f5b9f9121f698c1d16814e3` without its git history. Pull-request numbers, releases, tags and checks mentioned in historical sections below belong to the predecessor repository. Renamed: the core package and stack (`.getzilla/getzilla`), `scripts/getzilla_*.py`, `getzilla_factory`, `getzilla_delivery` and the `GETZILLA_*` test variables. Kept on purpose: the Grok Build CLI contract (`.grok/`), Grok provider settings, the deployed Trust CI identity (`adaptive-trust-ci/verified`, App `adaptive-trust-ci`) and L5 runtime paths (`adaptive-l5`). Installs made before the rename are upgraded through `scripts/install_into.py --plan`, which reports the pre-rename stack files for retirement. -Current source version is `2.3.1`, an unpublished release candidate with the merged verifier child-test scope fix. Getzilla [v2.3.0](https://github.com/Dimkox/Getzilla/releases/tag/v2.2.1) was published 2026-10-09T03:47:35Z at `05f85f12b72b3a897c45c24e172fac8317120b7c`, tree `ea0e67c678c90764196dadce09910954d6be0371`, with ZIP and checksum assets. Continue `fix/codex-cmd-hooks-20261011`, route `43786cadd549`, from `PROJECT_STATE.json.current_continuation` and the [2.3.1 source package](engineering/changes/20261012-task-43786c/brief.md). Run bounded controls, independent exact-commit reviews, save reports outside Git and one final PR verifier. Require public Actions linux/windows on the exact head. Under conditional L5, record exact merge/tag/release grants after green exact-head checks, resolved threads and independent review. Production target/task acceptance still needs an exact owner target and scoped authority. The native harness is the executor. No daemon or provider activation is needed for the supported local task. Production target/task acceptance remains unproved. Predecessor release and M8 records below are historical; published artifacts stay immutable. +Current source version is `2.3.1`, an unpublished release candidate: everything merged to `main` since v2.3.0, up to PR #179. Getzilla [v2.3.0](https://github.com/Dimkox/Getzilla/releases/tag/v2.3.0) was published 2026-10-21T11:01:37Z at `2bb417431f1e4b9494e4af71ebf27adb1d04d898`, tree `81076df2cd7db8713f7178ead66ebaa2728f861e`, with ZIP and checksum assets. Continue from `PROJECT_STATE.json.current_continuation`: the open Low batch in issue #134, then the 2.3.1 release. The [command table in README.md](README.md#commands) lists every `getzilla` command. Run bounded controls, independent exact-commit reviews, save reports outside Git and one final PR verifier. Require public Actions linux/windows on the exact head. Under conditional L5, record exact merge/tag/release grants after green exact-head checks, resolved threads and independent review. Production target/task acceptance still needs an exact owner target and scoped authority. The native harness is the executor. No daemon or provider activation is needed for the supported local task. Production target/task acceptance remains unproved. Predecessor release and M8 records below are historical; published artifacts stay immutable. This file is the zero-context entrypoint for any new agent, human, Codex/Grok/Claude session, or clean clone of this repository. Do not depend on chat history to continue the project. @@ -26,7 +26,7 @@ Record exact base/head, dirty paths, profile/reason, changed-path digest and all ## Current project state -Getzilla main observed at `540c12f93e798c2f483455a5086ab0018ac3216e` on 2026-10-11; current source candidate is 2.3.1. Public Actions linux/windows are the required exact-head checks. Historical private Trust CI identity `adaptive-trust-ci/verified@06ecf1c875bc`, App ID ``, is not fresh verification. +Getzilla main observed at `a45fbdd90e4f98e05c2f45c43afb28a63b3e448c` on 2026-10-24 (PR #179 merge); current source candidate is 2.3.1. Public Actions linux/windows are the required exact-head checks. Historical private Trust CI identity `adaptive-trust-ci/verified@06ecf1c875bc`, App ID ``, is not fresh verification. Historical core observation (pre-publication snapshot **2026-10-03**): historical `main` was `3f41be92161fef451a2dfa7451eb458ce8f022b3`, and the core comparison base was `63799f8760d3a55028d83ab5ff0116ececf8f7d1`; accepted packaging identity was unknown in that dated record. The known published identity is now bound separately by `historical_v2_1_1_published_release`: PR #238 and target `97a7581238022356b2de8d193a9bd8363fc92dc3`. Prior `2.1.0` custody was built twice from source `e5856acfd4bc7a186f40a740b54ec86459462db5` / tree `0dfa04f3ec3ea9c7a04c723e9d127603fc72999b`; fetch refs before assuming remote state. @@ -38,11 +38,11 @@ Historical core observation (pre-publication snapshot **2026-10-03**): historica - **Bounded owner M8:** one accepted Liqvera case qualifies; accounting and human readiness are complete by explicit owner confirmation, with unknown numeric totals `null`. `python3 scripts/getzilla_m8.py activate` persists per-clone ignored activation; `admit --action local_test` consumes it on every use; `revoke` demotes to L0. Initial L1 permits local reads/tests, ceiling L2 grants no external authority. See [commands and boundaries](docs/REFERENCE.md#bounded-local-m8). - **Next unmet outcomes:** empirical executable M7 durable current lookup/exact-profile evidence, general M9 signed environment/recovery qualification, and factory-runtime site publication remain separate. They do not require another first completed pilot or thirty owner cases. - **Merge authority:** App-owned `adaptive-trust-ci/verified@06ecf1c875bc`, GitHub App ID ``, on the exact up-to-date PR head. The deployed Trust CI service is separate from L5; repository tests and runtime success do not replace it. -- **Active delivery:** follow `PROJECT_STATE.json.current_continuation` for the 2.3.1 source candidate and [release source package](engineering/changes/20261012-task-43786c/brief.md). Collect independent exact-head reviews, save reports outside Git, run one final verifier and require public Actions linux/windows on the exact head. Published Getzilla v2.2.1 stays immutable. Conditional AGENTS L5 covers exact-grant merge/tag/release after checks; production writes remain separately scoped. +- **Active delivery:** follow `PROJECT_STATE.json.current_continuation` for the 2.3.1 source candidate. Issue #168 (disk cleanup) is delivered through PRs #174–#178; PR #179 makes protected-path grants follow their session. Collect independent exact-head reviews, save reports outside Git, run one final verifier and require public Actions linux/windows on the exact head. Published Getzilla v2.3.0 stays immutable. Conditional AGENTS L5 covers exact-grant merge/tag/release after checks; production writes remain separately scoped. Historical planning links: the [core package](engineering/changes/20261002-assemble-2-1-1-factory-source-with-heartbeat-and-ffb3d8/brief.md) and [release-wide ledger](engineering/changes/20261002-assemble-2-1-1-factory-source-with-heartbeat-and-ffb3d8/release-scope-ledger.md) retain the dated conditional design, not current delivery instructions. F/G successors remain unaccepted; the accepted Liqvera product satisfies the existing-product prerequisite. PR #173 and PR #12 remain historical delivered work; the optional SEO side project was delivered through PR #19. -Machine-readable handoff: [`PROJECT_STATE.json`](PROJECT_STATE.json). `fresh_clone.continuation_record` selects `current_continuation`; `active_delivery`, `current_unreleased_change` and `active_source_delivery` are labelled historical, so their old next-action fields are not instructions. `published_release` binds immutable Getzilla `v2.2.1`; `historical_v2_1_1_published_release` binds predecessor `v2.1.1`; the prior `v2.0.19` record is preserved verbatim in `prior_published_releases`. `local_candidate` is explicitly historical pre-publication provenance. `historical_v2_1_0_artifact_custody` preserves all three original custody/delivery records exactly. Historical source, delivery and runtime records remain dated evidence and do not imply operational activation. +Machine-readable handoff: [`PROJECT_STATE.json`](PROJECT_STATE.json). `fresh_clone.continuation_record` selects `current_continuation`; the finished PR #79 contour is kept in `historical_pr79_continuation`; `active_delivery`, `current_unreleased_change` and `active_source_delivery` are labelled historical, so their old next-action fields are not instructions. `published_release` binds immutable Getzilla `v2.3.0`; `historical_getzilla_v2_2_1_published_release` keeps `v2.2.1`; `historical_v2_1_1_published_release` binds predecessor `v2.1.1`; the prior `v2.0.19` record is preserved verbatim in `prior_published_releases`. `local_candidate` is explicitly historical pre-publication provenance. `historical_v2_1_0_artifact_custody` preserves all three original custody/delivery records exactly. Historical source, delivery and runtime records remain dated evidence and do not imply operational activation. The earlier [dated backlog](engineering/changes/20260921-research-open-backlog-map-each-item-to-its-sourc-d54d3a/issue-queue.json) remains historical; the latest recorded counts and scope live in `PROJECT_STATE.json` (its nested issue inventory is dated separately). The [accepted CI ingress operation](engineering/changes/20260921-fix-production-trust-ci-webhook-ingress-and-rest-68274/evidence/operational-acceptance.md) restored actual GitHub intake; [PR #173 delivery](engineering/changes/20260921-fix-the-combined-source-delivery-of-issue165-int-cf2384/evidence/pr173-delivery.json) records the later external result. Separate #158 source has focused and complete Trust CI suite evidence but still needs current-main integration, the full route gate, reviews and eventual separately authorized live acceptance. diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 3c5d309f..53937ff3 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -93,7 +93,7 @@ Known weak seams this line is closing: - A timeout or interrupted verifier is incomplete, not a pass. - A restarted attempt cannot reuse an old model/config/profile identity without requalification. -The price is deliberate. For a small personal patch this can feel too heavy. For an autonomous agent touching someone else's repository, the route, change package, fitness checks, receipts, external holdout and human merge gate are the safety boundary. The published release is `v2.1.1`. Liqvera is a completed owner-accepted factory-built product; empirical autonomy qualification and production authority remain separate. +The price is deliberate. For a small personal patch this can feel too heavy. For an autonomous agent touching someone else's repository, the route, change package, fitness checks, receipts, external holdout and human merge gate are the safety boundary. The latest published release is `v2.3.0`. Liqvera is a completed owner-accepted factory-built product; empirical autonomy qualification and production authority remain separate. ## Advanced startup baseline @@ -105,11 +105,11 @@ The observed **controller + 12 child-agent slots**, routing cap `max_parallel_an ## Current state -Identity: **2.3.1 source candidate**, including the verifier child-test scope fix. Latest published [v2.3.0](https://github.com/Dimkox/Getzilla/releases/tag/v2.3.0) is immutable: published 2026-10-21T11:01:37Z, annotated tag `815aeabc5d215d65e72651692a6457920de9fa71`, commit `2bb417431f1e4b9494e4af71ebf27adb1d04d898`, tree `81076df2cd7db8713f7178ead66ebaa2728f861e`, with ZIP and checksum assets. This publication does not establish installation, provider activation or production acceptance. +Identity: **2.3.1 source candidate**: everything merged to `main` since v2.3.0, up to PR #179. Latest published [v2.3.0](https://github.com/Dimkox/Getzilla/releases/tag/v2.3.0) is immutable: published 2026-10-21T11:01:37Z, annotated tag `815aeabc5d215d65e72651692a6457920de9fa71`, commit `2bb417431f1e4b9494e4af71ebf27adb1d04d898`, tree `81076df2cd7db8713f7178ead66ebaa2728f861e`, with ZIP and checksum assets. This publication does not establish installation, provider activation or production acceptance. -Observed main on 2026-10-12: `540c12f93e798c2f483455a5086ab0018ac3216e`. This is the release contour comparison base. PR #79 contains the command-policy and routing repairs plus this release candidate. It remains unmerged; current source checks must qualify this exact candidate. +Observed main on 2026-10-24: `a45fbdd90e4f98e05c2f45c43afb28a63b3e448c`, the PR #179 merge. Since v2.3.0, `main` gained `getzilla update` (#132, #135), the light lane for small changes (#121, #142), exact-diff review plans (#144), disk cleanup for issue #168 (#174–#178) and session-bound protected-path grants (#179). Continue from `PROJECT_STATE.json.current_continuation`: the Low batch in issue #134, then the 2.3.1 release. -Continue `fix/codex-cmd-hooks-20261011`, route `43786cadd549`, through the [2.2.2 source package](../engineering/changes/20261012-task-43786c/brief.md) and `PROJECT_STATE.json.current_continuation`. This record is historical. Today, reports and receipts live in ignored runtime storage under `.getzilla/runtime` and in portable evidence exports, not in committed Markdown. PASS receipts require `--reviewed-commit `; consumers recheck the reviewed tree and ancestry. Public Actions linux/windows provide the required exact-head checks. The historical private Trust CI identity is `adaptive-trust-ci/verified@06ecf1c875bc`, App ID ``; this record is not fresh verification. Conditional AGENTS L5 covers exact-grant merge/tag/release after checks, independent review and resolved threads; production mutation, deploy, external writes and human security approvals remain separate. +The finished PR #79 contour (`fix/codex-cmd-hooks-20261011`, route `43786cadd549`, [source package](../engineering/changes/20261012-task-43786c/brief.md)) is kept in `historical_pr79_continuation`. Today, reports and receipts live in ignored runtime storage under `.getzilla/runtime` and in portable evidence exports, not in committed Markdown. PASS receipts require `--reviewed-commit `; consumers recheck the reviewed tree and ancestry. Public Actions linux/windows provide the required exact-head checks. The historical private Trust CI identity is `adaptive-trust-ci/verified@06ecf1c875bc`, App ID ``; this record is not fresh verification. Conditional AGENTS L5 covers exact-grant merge/tag/release after checks, independent review and resolved threads; production mutation, deploy, external writes and human security approvals remain separate. The native harness executes local tasks. Do not add a daemon or enable providers to complete this source repair. The [historical M8 package](../engineering/changes/20261004-task-b25860/brief.md) and predecessor release records remain dated provenance. @@ -222,7 +222,7 @@ v2.1.1 опубликован 2026-10-03 из PR #238; ZIP и контрольн python3 scripts/getzilla_status.py ``` - `getzilla_route.py` создаёт маршрут, а `getzilla_change.py start` — пакет новой задачи; если пакет уже существует, повторный `start` пропустите. Создание маршрута/пакета пишет локальное состояние .getzilla/runtime/active-route.json и engineering/changes//. Не перезаписывайте уже активную задачу новым маршрутом. Используйте `/getzilla-delivery`: только выбранные allowed_agents, один write_agent на изолированную ветку/worktree, проверки и независимые reviewers. Если route.human_gates содержит scope_and_design_approval, человек сначала утверждает конкретный scope/design; `getzilla_gate.py decide` лишь фиксирует уже принятое решение, не выдаёт внешнее разрешение. Production/external actions требуют собственного точного делегирования и применимых внешних approvals. + `getzilla_route.py` создаёт маршрут, а `getzilla_change.py start` — пакет новой задачи; если пакет уже существует, повторный `start` пропустите. Создание маршрута/пакета пишет локальное состояние .getzilla/runtime/active-route.json и .getzilla/runtime/changes// (старые пакеты в engineering/changes/ — история). Не перезаписывайте уже активную задачу новым маршрутом. Используйте `/getzilla-delivery`: только выбранные allowed_agents, один write_agent на изолированную ветку/worktree, проверки и независимые reviewers. Если route.human_gates содержит scope_and_design_approval, человек сначала утверждает конкретный scope/design; `getzilla_gate.py decide` лишь фиксирует уже принятое решение, не выдаёт внешнее разрешение. Production/external actions требуют собственного точного делегирования и применимых внешних approvals. 5. Для уже реально созданного дочернего агента heartbeat/watchdog дают наблюдаемость. Hook SubagentStart регистрирует его автоматически; при отсутствии hook: @@ -253,11 +253,11 @@ v2.1.1 опубликован 2026-10-03 из PR #238; ZIP и контрольн PR/release по умолчанию останавливают последующие проверки при первом отказе, неизвестном статусе или недопустимом skip. Отчёт сохраняет реальный результат и явно помечает незапущенные проверки; source-stability и финальный QG остаются обязательными. `--keep-going` собирает диагностику, сохраняя архитектурный preflight и правила admission. Именованный fast smoke требует чистый committed HEAD, `--no-record` и явные unittest-имена; бюджет subprocess — 1–180 секунд (по умолчанию 180), после таймаута добавляется ограниченная очистка процесса и проверка идентичности. Он не создаёт и не заменяет receipt. Полный успешный PR gate может занять больше 180 секунд; smoke или таймаут не заменяют его. - После CPU-снимка и маршрутизации зафиксируйте trusted base/HEAD и staged/unstaged/untracked inventory. Именованные controls выше — observation, без receipt и scope admission. Затем все независимые route-selected reviewers проверяют кандидат; полные отчёты сохраняются в engineering/changes//evidence/. Закоммитьте отчёты и заморозьте дерево. Единственный финальный qualifying local gate — `python3 scripts/getzilla_verify.py --mode pr`: его fail-closed selector выбирает scope по действительному base..HEAD и dirty inventory до тяжёлых checks. Зафиксируйте profile/reason, проверенные пути и skips; skipped не означает passed. Этот gate пишет локальный receipt в .getzilla/runtime/receipts// и может идти параллельно с внешним App-owned exact-head Trust CI после точного delegated UNVERIFIED branch transport. Полный local gate до review не требуется. Status показывает evidence_gaps, package_incomplete и agent_diagnostics; изменение дерева делает прежние receipts устаревшими. + После CPU-снимка и маршрутизации зафиксируйте trusted base/HEAD и staged/unstaged/untracked inventory. Именованные controls выше — observation, без receipt и scope admission. Затем все независимые route-selected reviewers проверяют кандидат; полные отчёты сохраняются вне Git: в папке evidence пакета `.getzilla/runtime/changes//` или в `.getzilla/runtime/reviews//`. Коммит отчётов и заморозка дерева не нужны. Единственный финальный qualifying local gate — `python3 scripts/getzilla_verify.py --mode pr`: его fail-closed selector выбирает scope по действительному base..HEAD и dirty inventory до тяжёлых checks. Зафиксируйте profile/reason, проверенные пути и skips; skipped не означает passed. Этот gate пишет локальный receipt в .getzilla/runtime/receipts// и может идти параллельно с внешним App-owned exact-head Trust CI после точного delegated UNVERIFIED branch transport. Полный local gate до review не требуется. Status показывает evidence_gaps, package_incomplete и agent_diagnostics; изменение дерева делает прежние receipts устаревшими. 7. Доставка — отдельная ветка и PR. Условное AGENTS L5 разрешает merge/tag/release своего репозитория после зелёных exact-head checks, независимого review и закрытых threads, с точными action/resource grants. Прямой push в main запрещён. Public Getzilla требует Actions linux/windows; private consumer требует настроенный App-owned Trust CI и применимые подписанные approvals. Локальный PASS не заменяет внешнюю проверку. Production, deploy и external writes требуют отдельного точного разрешения. - При отказе verifier изучите конкретный failed check, соберите связанные замечания в один ремонт тому же writer и повторите затронутые короткие controls. После независимого review сохраните полные отчёты, закоммитьте и заморозьте кандидат; финальные локальная проверка и внешний exact-head check могут идти параллельно после явно делегированного UNVERIFIED branch transport. Identical merged tree без новых product changes — no-op, без нового полного прогона. При отказе установщика сохраните указанный stage для ручной инспекции, если сообщено `manual cleanup required`; не удаляйте неизвестную цель. При неоднозначном runtime/provider результате сохраните состояние и audit, выполните документированное reconcile, не повторяйте публикацию/inference вслепую. Для реального factory control plane и L5 нужны отдельные операторские конфигурация, зависимости, authority и rollout/recovery runbooks; установка локального стека сама их не активирует и не выдаёт live URL. + При отказе verifier изучите конкретный failed check, соберите связанные замечания в один ремонт тому же writer и повторите затронутые короткие controls. После независимого review сохраните полные отчёты вне Git, не меняя проверенный коммит; финальные локальная проверка и внешний exact-head check могут идти параллельно после явно делегированного UNVERIFIED branch transport. Identical merged tree без новых product changes — no-op, без нового полного прогона. При отказе установщика сохраните указанный stage для ручной инспекции, если сообщено `manual cleanup required`; не удаляйте неизвестную цель. При неоднозначном runtime/provider результате сохраните состояние и audit, выполните документированное reconcile, не повторяйте публикацию/inference вслепую. Для реального factory control plane и L5 нужны отдельные операторские конфигурация, зависимости, authority и rollout/recovery runbooks; установка локального стека сама их не активирует и не выдаёт live URL. ## Verification scope selection @@ -323,7 +323,7 @@ For a fresh clone, bootstrap state comes from `START_HERE.md` / `PROJECT_STATE.j - [`.grok/skills/`](../.grok/skills/) - [`.grok/agents/`](../.grok/agents/) - [`.grok/hooks/`](../.grok/hooks/) -- [Hook compatibility template](../.getzilla/templates/hook_root_shim.py) — the installer generates nine root aliases in consumer projects; canonical source hooks live in `.grok/hooks/`. +- [Hook compatibility template](../.getzilla/templates/hook_root_shim.py) — root aliases for older consumer installs only; a fresh install keeps every hook entry script in `.grok/hooks/`, the canonical source. - [`scripts/getzilla_route.py`](../scripts/getzilla_route.py) - [`scripts/getzilla_change.py`](../scripts/getzilla_change.py) - [`scripts/getzilla_agent.py`](../scripts/getzilla_agent.py) — local heartbeat, progress, bounded watchdog and acknowledged same-task recovery; [operator runbook](../engineering/runbooks/local-agent-watchdog.md) @@ -525,22 +525,53 @@ Local loop: measured route → change → bounded committed-HEAD observations Declared route `human_gates` are flags: their decisions are recorded in the active change package and their open state is shown (gate status and the `open_human_gates` entry in the change history), but they never block a change transition or an action. The one exception is `paid_run_approval`: the first live paid run stays blocked until an approved decision names its exact resource, and a missing or changed declaration of that gate fails closed. Inspect gate status with `python3 scripts/getzilla_gate.py status`; record only an explicit operator decision with `python3 scripts/getzilla_gate.py decide --reason "..." --actor "..."` (production decisions also require `--action`; approve the migration plan with `--action migration-plan`; external-write decisions require exact `--action external-write --resource ...`). Decisions are bound to the active route, change, gate and scope digest. They are mutable local workflow evidence, not authenticated human identity, delegated grants, Trust CI signed approvals or merge authority. Production and external-write operations still require their exact delegated grants. -| Script | Role | -|--------|------| -| `scripts/getzilla_route.py` | Classify / show route | -| `scripts/getzilla_change.py` | Start durable local change package | -| `scripts/getzilla_governance.py` | Validate/summarize target-owned governance, check read-only projections, and emit an exact clean-state handoff | -| `scripts/getzilla_status.py` | Local runtime status | -| `scripts/getzilla_gate.py` | Inspect or record route-bound local human-gate decisions | -| `scripts/getzilla_verify.py` | Local verification preflight (unittest, Ruff, Bandit, known vulnerabilities, measured coverage in `pr`/`release`) | -| `scripts/getzilla_vulns.py` | Known vulnerabilities in pinned dependencies from OSV (`--online`, `--db DIR`, `--download-db DIR`) | -| `scripts/getzilla_review.py` | Record local review receipt | -| `scripts/getzilla_approve.py` | Delegated local action/resource grant bound to repository, route, change, exact HEAD and tree fingerprint; not accepted by Trust CI | -| `scripts/getzilla_deploy.py` | Prepare-only delivery commands | -| `scripts/getzilla_doctor.py` | Local health check | -| `scripts/getzilla_demo.py` | Start the loopback-only read-only product tour using bundled sample and checkout-derived evidence | -| `scripts/install_into.py` | Plan an existing repository read-only or atomically materialize an absent new target | -| `adaptive-trust-ci` | External API, worker, migration, signed approvals, holdout verification, attestation verification and app-bound branch protection | +Every command runs as `getzilla ` through `bin/getzilla` (`bin\getzilla.cmd` on Windows), or as `python3 scripts/getzilla_cli.py `. `getzilla --help` shows its options. The [README command table](../README.md#commands) groups them by task. + +| Command | Script | Role | +|--------|------|------| +| `doctor` | `scripts/getzilla_doctor.py` | Local health check; `--offer-install` offers missing tools | +| `setup-agent` | `scripts/getzilla_setup_agent.py` | Coding agent and model provider setup; `--forget-key` removes the stored key | +| `install-into` | `scripts/install_into.py` | Plan an existing repository read-only, or atomically materialize an absent new target | +| `adopt` | `scripts/adopt_existing.py` | Adopt a folder without Git into a new local workspace (`plan`, `apply`, `status`, `resume`, `abort`) | +| `components` | `scripts/getzilla_components.py` | Show and add optional components (`agent:`, `extra:bitrix`, `extra:seo-landing`, `compliance:eu-ai-act`, `factory`) | +| `baseline` | `scripts/getzilla_baseline.py` | Mandatory pinned baseline: `status`, `apply` | +| `harness` | `scripts/getzilla_harness.py` | Check, or with `--write` regenerate, the generated agent harness copies | +| `update` | `scripts/getzilla_upgrade.py` | Update Getzilla, write the new files into the project on a `getzilla-update-` branch, then update third-party tools (`--no-self`, `--no-tools`, `--accept-local`) | +| — | `scripts/getzilla_update.py` | Third-party tools only (agent CLIs, Superpowers, BMAD, Spec Kit, vibevm, CVE database, OpenGrep); `--status` shows the last results | +| `cpu` | `scripts/getzilla_startup_cpu.py` | Startup step zero: measure and record usable CPUs (`--reserve`, `--attach`) | +| `par` | `scripts/getzilla_par.py` | Run independent commands in parallel on the measured CPUs (`-j`, `--each`) | +| `route` | `scripts/getzilla_route.py` | Classify, show, reconcile, complete or cancel the task route; `--add-reviewer` adds a reviewer | +| `status` | `scripts/getzilla_status.py` | Local runtime status: route, change package, evidence gaps, agents | +| `change` | `scripts/getzilla_change.py` | Start, transition or show the change package | +| `spec` | `scripts/getzilla_spec.py` | Validate and summarize typed change specifications | +| `agent` | `scripts/getzilla_agent.py` | Agent heartbeat, progress, bounded watchdog and acknowledged same-task recovery | +| `test` | `scripts/getzilla_cli.py` | Run the test suite with pytest on the measured CPUs | +| `verify` | `scripts/getzilla_verify.py` | Local verification preflight (unittest, Ruff, Bandit, known vulnerabilities, measured coverage in `pr`/`release`) | +| `review` | `scripts/getzilla_review.py` | Record a local review receipt bound to `--reviewed-commit` | +| `gate` | `scripts/getzilla_gate.py` | Inspect or record route-bound local human-gate decisions | +| `vulns` | `scripts/getzilla_vulns.py` | Known vulnerabilities in pinned dependencies from OSV (`--online`, `--db DIR`, `--download-db DIR`) | +| `duplicates` | `scripts/getzilla_duplicates.py` | Read-only source duplication analysis; uses jscpd when it is installed | +| `graph` | `scripts/getzilla_graph.py` | Read-only Python code graph: `build`, `status`, `impact`, `export` | +| `architecture` | `scripts/getzilla_architecture.py` | Validate, diff and draw the architecture model | +| `governance` | `scripts/getzilla_governance.py` | Validate/summarize target-owned governance, check read-only projections, and emit an exact clean-state handoff | +| `artifacts` | `scripts/getzilla_artifacts.py` | Import, compile and converge Spec Kit, BMAD and Superpowers artifacts | +| `risk-terms` | `scripts/getzilla_risk_terms.py` | Risk vocabulary language packs: `check`, `languages`, `scaffold` | +| `compliance` | `scripts/getzilla_compliance.py` | Scaffold an EU AI Act adapter for a registered AI system (optional layer) | +| — | `scripts/getzilla_audit_bundle.py` | EU AI Act release audit bundle (optional layer) | +| `approve` | `scripts/getzilla_approve.py` | Delegated local grant for exact actions and resources. Protected-path grants bind repository, caller session, change, branch, anchor and file set; other scopes bind route, exact HEAD and tree. Not accepted by Trust CI | +| `protected-write` | `scripts/getzilla_protected_write.py` | Apply an approved protected-path write from a manifest (`--dry-run` first) | +| `ci` | `scripts/getzilla_ci.py` | Select the CI gate by visibility; `--write` renders the pinned public workflow; `--protect-plan` shows branch protection | +| `trust-ci` | `scripts/getzilla_trust_ci.py` | Trust CI host: `init`, `status`, `github-app`, `trust-key` | +| `deploy` | `scripts/getzilla_deploy.py` | Prepare-only delivery commands | +| — | `scripts/getzilla_evidence.py` | Export bounded local workflow evidence, without approvals or secrets, with `--output ` | +| `cleanup` | `scripts/getzilla_cleanup.py` | `inventory` (read-only disk report with repository copies), `worktrees [--remove]` (guarded removal), `snapshot ` / `snapshot --remove ` (review copies) | +| `branches` | `scripts/branch_lifecycle.py` | Branch retention `report`; `apply` deletes only from a reviewed plan with `--allow-delete` | +| `report` | `scripts/getzilla_report.py` | Per-route overhead, observation gaps and defects caught before merge | +| `history` | `scripts/getzilla_history.py` | Offline historical evidence accounting | +| — | `scripts/getzilla_m8.py` | Bounded local M8 owner policy: `status`, `activate`, `admit --action `, `revoke` | +| `demo` | `scripts/getzilla_demo.py` | Start the loopback-only read-only product tour using bundled sample and checkout-derived evidence | +| — | `scripts/package_stack.py` | Build the release ZIP and its checksum sidecar | +| — | `adaptive-trust-ci` | External API, worker, migration, signed approvals, holdout verification, attestation verification and app-bound branch protection | ## Reviewed verification dispositions diff --git a/tests/test_project_state.py b/tests/test_project_state.py index f35f537c..67b81d05 100644 --- a/tests/test_project_state.py +++ b/tests/test_project_state.py @@ -245,13 +245,15 @@ def test_getzilla_current_identity_and_release_continuation(self) -> None: self.assertIsNone(prior['artifact']) self.assertFalse(published['operational_activation']) continuation = state['current_continuation'] - self.assertEqual(continuation['issue'], 72) - self.assertEqual(continuation['branch'], 'fix/codex-cmd-hooks-20261011') - self.assertEqual(continuation['route_id'], '43786cadd549') - self.assertTrue((ROOT / continuation['change_package'] / 'brief.md').is_file()) + self.assertEqual(continuation['issue'], 134) + self.assertEqual(continuation['source_base'], state['observed_main_sha']) self.assertEqual(continuation['target_version'], state['product_version']) self.assertIn('one final PR verifier', continuation['next_action']) self.assertIn('Native harness', continuation['next_action']) + self.assertIn('Keep v2.3.0 immutable', continuation['next_action']) + finished = state['historical_pr79_continuation'] + self.assertEqual((finished['issue'], finished['pull_request'], finished['route_id']), (72, 79, '43786cadd549')) + self.assertTrue((ROOT / finished['change_package'] / 'brief.md').is_file()) self.assertIn('scripts/getzilla_m8.py', state['operational_qualification']['m8_scope']) def test_release_handoff_keeps_history_and_production_authority_separate(self) -> None: From f9d1c1cc18efb6877832f91de68bd88141bf1871 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:14:24 +0000 Subject: [PATCH 6/9] docs(readme): list every getzilla command and the current source state A Commands section lists all launcher commands by task, with their main options. The source line names what 2.3.1 adds since v2.3.0, and the file map says where review copies live and how they are removed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BhxEnEALZRsc7kaPwG2kiR --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a6e8ba7a..e1da40a5 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ # Getzilla -Current source: **2.3.1 source candidate**, including the verifier child-test scope fix. Latest published: [v2.3.0](https://github.com/Dimkox/Getzilla/releases/tag/v2.3.0), source `2bb417431f1e4b9494e4af71ebf27adb1d04d898`, with [ZIP](https://github.com/Dimkox/Getzilla/releases/download/v2.3.0/getzilla-2.3.0.zip) and [checksum](https://github.com/Dimkox/Getzilla/releases/download/v2.3.0/getzilla-2.3.0.zip.sha256) assets. Publication does not prove production readiness. Continue through [START_HERE.md](START_HERE.md). +Current source: **2.3.1 source candidate** (unpublished): all work merged to `main` since v2.3.0, up to PR #179. It adds `getzilla update`, disk cleanup (`getzilla cleanup`), session-bound grants and the light lane for small changes. Latest published: [v2.3.0](https://github.com/Dimkox/Getzilla/releases/tag/v2.3.0), source `2bb417431f1e4b9494e4af71ebf27adb1d04d898`, with [ZIP](https://github.com/Dimkox/Getzilla/releases/download/v2.3.0/getzilla-2.3.0.zip) and [checksum](https://github.com/Dimkox/Getzilla/releases/download/v2.3.0/getzilla-2.3.0.zip.sha256) assets. Publication does not prove production readiness. Continue through [START_HERE.md](START_HERE.md). Getzilla lets an AI write code for your project and checks that code before it is allowed to stay. @@ -96,7 +96,7 @@ echo "aa10c167d02aa8633f0d0d4478b9c0ad0fac3122ef293f633f1075f218889807 getzilla The installer is verified before it runs: the commands download it from the `v2.3.0` release tag, compare its SHA-256 with the digest published here (the test suite binds this digest to the installer bytes at the published tag), run it only when the digest matches (the check and the run are one command, so a failed check stops it), and install Getzilla from the same tag. Never pipe the installer straight into `bash` or `iex`. If the check fails, do not run the file: re-download it and report the mismatch. -To choose without being asked, set `GETZILLA_AGENT` (`qwen`, `codex`, `claude`, `gemini`, `copilot`, `grok`), `GETZILLA_PROVIDER` (`openrouter`, `native`) and `OPENROUTER_API_KEY` first. To switch agents later, run `python3 scripts/getzilla_setup_agent.py --agent codex` from the Getzilla folder. +To choose without being asked, set `GETZILLA_AGENT` (`qwen`, `codex`, `claude`, `gemini`, `copilot`, `grok`), `GETZILLA_PROVIDER` (`openrouter`, `native`) and `OPENROUTER_API_KEY` first. To switch agents later, run `getzilla setup-agent --agent codex` (or `python3 scripts/getzilla_setup_agent.py --agent codex` from the Getzilla folder). To bring third-party tools (agent CLIs, Superpowers, BMAD, Spec Kit, vibevm, the CVE database) to their latest versions and install the pinned, SHA-256-verified OpenGrep release, run `python3 scripts/getzilla_update.py`; the installer offers this at the end of every run. @@ -118,6 +118,52 @@ python3 scripts/install_into.py --plan /absolute/path/to/your/repo Then follow [QUICKSTART.md](QUICKSTART.md). +## Commands + +All commands go through one launcher, `getzilla `. The Linux and macOS installer links it to `~/.local/bin/getzilla`; add `~/.local/bin` to `PATH` if the shell does not find it. On Windows, use `bin\getzilla.cmd`. Without the launcher, run `python3 scripts/getzilla_cli.py ` from the Getzilla folder. `getzilla --help` lists the commands, and `getzilla --help` shows the options of one command. + +| Area | Command | What it does | +| --- | --- | --- | +| Set up | `getzilla doctor [--offer-install]` | Checks this machine and the repository, and offers to install missing tools | +| | `getzilla setup-agent --agent ` | Sets the coding agent (`qwen`, `codex`, `claude`, `gemini`, `copilot`, `grok`) and where its models come from | +| | `getzilla install-into --plan ` | Shows, read-only, what Getzilla would add to a project; `--materialize-new ` creates a new project | +| | `getzilla adopt plan\|apply\|status\|resume\|abort` | Adopts a folder without Git into a new local workspace | +| | `getzilla components status`, `components add ` | Shows and adds optional parts, for example `agent:cursor`, `extra:bitrix`, `compliance:eu-ai-act`, `factory` | +| | `getzilla baseline status\|apply` | Shows or applies the mandatory pinned baseline | +| | `getzilla harness [--write]` | Checks or regenerates the agent files in `.qwen/`, `.claude/`, `.codex/`, `.gemini/`, `.github/` and `.cursor/` | +| Update | `getzilla update [--no-self] [--no-tools] [--accept-local]` | Updates Getzilla, writes the new files into the project on a new branch, then updates third-party tools | +| A task | `getzilla cpu` | Measures and records the usable CPUs (startup step zero) | +| | `getzilla route ""` | Creates the task route; `--show` prints it, `--reconcile` binds it to the committed diff, `--add-reviewer ` adds a reviewer | +| | `getzilla status` | Shows the route, the change package, open evidence gaps and agents | +| | `getzilla change start\|transition\|show` | Manages the change package of the task | +| | `getzilla spec validate\|summary\|coverage\|map\|generate` | Checks typed change specifications | +| | `getzilla agent start\|heartbeat\|progress\|watchdog\|watch\|...` | Records agent heartbeats and checkpoints, and finds stuck agents | +| | `getzilla par -j -- "cmd1" "cmd2"` | Runs independent commands in parallel on the measured CPUs | +| Check | `getzilla test` | Runs the test suite on the measured CPUs | +| | `getzilla verify --mode fast\|pr\|release` | Runs the checks the route selects and records a receipt | +| | `getzilla review --status pass\|fail --report --reviewed-commit ` | Records an independent review | +| | `getzilla gate status`, `gate decide approved\|rejected` | Shows or records a human-gate decision | +| | `getzilla vulns [--online]` | Checks pinned dependencies against known vulnerabilities (OSV) | +| | `getzilla duplicates` | Finds duplicated source code (read-only; jscpd is optional) | +| | `getzilla graph build\|status\|impact\|export --root ` | Builds a Python code graph and shows the impact of a change (read-only) | +| | `getzilla architecture validate\|summary\|drift\|diagram\|diff\|fitness` | Checks the architecture model and draws its diagrams | +| | `getzilla governance validate\|summary\|handoff\|project\|check-projections` | Checks governance rules and evidence | +| | `getzilla artifacts import\|compile\|converge\|validate\|export` | Reads Spec Kit, BMAD and Superpowers files as advisory input | +| | `getzilla risk-terms check\|languages\|scaffold` | Checks the risk vocabulary language packs | +| | `getzilla compliance scaffold` | Creates an EU AI Act adapter for a registered AI system (optional layer) | +| Deliver | `getzilla approve --action --resource ` | Records a delegated local grant for one exact operation; no wildcard | +| | `getzilla protected-write --manifest ` | Applies an approved write to a protected path | +| | `getzilla ci --plan\|--write` | Selects the CI gate by repository visibility and writes the pinned public workflow | +| | `getzilla trust-ci init\|status\|github-app\|trust-key` | Sets up and checks a self-hosted Trust CI host | +| | `getzilla deploy` | Prints, never runs, the publish commands a person runs | +| Keep clean | `getzilla cleanup inventory` | Lists what Getzilla keeps on disk, with sizes and repository copies; deletes nothing | +| | `getzilla cleanup worktrees [--remove]` | Lists worktrees and the reasons to keep them; `--remove` removes only those with no unique work | +| | `getzilla cleanup snapshot `, `snapshot --remove ` | Makes a review copy of one commit and removes it after the review | +| | `getzilla branches report\|apply` | Reports old branches; `apply` deletes only from a reviewed plan ([branch lifecycle](docs/branch-lifecycle.md)) | +| Look back | `getzilla report [--since all]` | Shows per-route overhead and the defects caught before merge | +| | `getzilla history snapshot` | Offline accounting of historical evidence | +| | `getzilla demo --open` | Opens the read-only local demo in your browser | + ## What is inside - `scripts/`: the commands you run, such as `getzilla_route.py` and `getzilla_verify.py` @@ -128,6 +174,7 @@ Then follow [QUICKSTART.md](QUICKSTART.md). - `.getzilla/runtime/`: ignored task state, plans, analyses and reviews; `engineering/changes/` retains historical packages. Change packages are created on demand at `.getzilla/runtime/changes//`. - Small low-risk diffs of at most 50 code lines use one review without a package or report-only commits. Full CI remains required. - `scripts/getzilla_evidence.py`: portable local evidence archive; CI keeps its own actual run artifacts for 30 days. +- Disk use: review copies go to the runtime `snapshots` folder (created on first use) and are removed with `getzilla cleanup snapshot --remove`; test runs keep their temporary files inside their own run folder. `getzilla cleanup inventory` shows what is left. - `architecture/`: a map of the system ([system.yaml](architecture/system.yaml), [rules.yaml](architecture/rules.yaml), [diagrams](architecture/generated/)) - `trust-ci/`: the optional outside checker, run as its own service - `factory/`, `delivery/`, `pilot/`: bigger automation pieces, switched off by default From 5c6d7878a2cb1ca16b2f210660a78e5d6231b389 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:18:35 +0000 Subject: [PATCH 7/9] docs: fix command wording found in review of #180 history takes a snapshot file, approve needs --reason, --add-reviewer works with --reconcile, getzilla test falls back to serial unittest without pytest-xdist, report overhead is per profile, and the state record counts the merged PRs from #114. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BhxEnEALZRsc7kaPwG2kiR --- PROJECT_STATE.json | 2 +- README.md | 10 +++++----- docs/REFERENCE.md | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/PROJECT_STATE.json b/PROJECT_STATE.json index ef5a39fd..8808c922 100644 --- a/PROJECT_STATE.json +++ b/PROJECT_STATE.json @@ -28,7 +28,7 @@ "target_version": "2.3.1", "source_base": "a45fbdd90e4f98e05c2f45c43afb28a63b3e448c", "status": "source_candidate_low_batch_and_release_pending", - "delivered_since_published_release": "PRs #121-#179 on main, including issue #168 disk cleanup (#174-#178) and session-bound protected-path grants (#179).", + "delivered_since_published_release": "PRs #114-#179 on main, including issue #168 disk cleanup (#174-#178) and session-bound protected-path grants (#179).", "next_action": "Fix the Low findings collected in issue #134 in small PRs, then prepare the 2.3.1 release. Bind independent review to the exact reviewed commit, save reports outside Git, run one final PR verifier and require public Actions linux/windows on the exact head. Under conditional L5, record exact merge/tag/release grants only after green exact-head checks, resolved threads and passed independent review. Keep v2.3.0 immutable. Native harness executes local tasks; do not start a daemon or enable providers. Production installation/task acceptance still needs an exact owner target and scoped authority." }, "historical_pr79_continuation": { diff --git a/README.md b/README.md index e1da40a5..f85a5f4d 100644 --- a/README.md +++ b/README.md @@ -133,13 +133,13 @@ All commands go through one launcher, `getzilla `. The Linux and macOS | | `getzilla harness [--write]` | Checks or regenerates the agent files in `.qwen/`, `.claude/`, `.codex/`, `.gemini/`, `.github/` and `.cursor/` | | Update | `getzilla update [--no-self] [--no-tools] [--accept-local]` | Updates Getzilla, writes the new files into the project on a new branch, then updates third-party tools | | A task | `getzilla cpu` | Measures and records the usable CPUs (startup step zero) | -| | `getzilla route ""` | Creates the task route; `--show` prints it, `--reconcile` binds it to the committed diff, `--add-reviewer ` adds a reviewer | +| | `getzilla route ""` | Creates the task route; `--show` prints it, `--reconcile` binds it to the committed diff, `--reconcile --add-reviewer ` keeps an extra reviewer | | | `getzilla status` | Shows the route, the change package, open evidence gaps and agents | | | `getzilla change start\|transition\|show` | Manages the change package of the task | | | `getzilla spec validate\|summary\|coverage\|map\|generate` | Checks typed change specifications | | | `getzilla agent start\|heartbeat\|progress\|watchdog\|watch\|...` | Records agent heartbeats and checkpoints, and finds stuck agents | | | `getzilla par -j -- "cmd1" "cmd2"` | Runs independent commands in parallel on the measured CPUs | -| Check | `getzilla test` | Runs the test suite on the measured CPUs | +| Check | `getzilla test` | Runs the test suite with pytest-xdist on the measured CPUs, otherwise serial unittest | | | `getzilla verify --mode fast\|pr\|release` | Runs the checks the route selects and records a receipt | | | `getzilla review --status pass\|fail --report --reviewed-commit ` | Records an independent review | | | `getzilla gate status`, `gate decide approved\|rejected` | Shows or records a human-gate decision | @@ -151,7 +151,7 @@ All commands go through one launcher, `getzilla `. The Linux and macOS | | `getzilla artifacts import\|compile\|converge\|validate\|export` | Reads Spec Kit, BMAD and Superpowers files as advisory input | | | `getzilla risk-terms check\|languages\|scaffold` | Checks the risk vocabulary language packs | | | `getzilla compliance scaffold` | Creates an EU AI Act adapter for a registered AI system (optional layer) | -| Deliver | `getzilla approve --action --resource ` | Records a delegated local grant for one exact operation; no wildcard | +| Deliver | `getzilla approve --reason "" --action --resource ` | Records a delegated local grant for one exact operation; no wildcard | | | `getzilla protected-write --manifest ` | Applies an approved write to a protected path | | | `getzilla ci --plan\|--write` | Selects the CI gate by repository visibility and writes the pinned public workflow | | | `getzilla trust-ci init\|status\|github-app\|trust-key` | Sets up and checks a self-hosted Trust CI host | @@ -160,8 +160,8 @@ All commands go through one launcher, `getzilla `. The Linux and macOS | | `getzilla cleanup worktrees [--remove]` | Lists worktrees and the reasons to keep them; `--remove` removes only those with no unique work | | | `getzilla cleanup snapshot `, `snapshot --remove ` | Makes a review copy of one commit and removes it after the review | | | `getzilla branches report\|apply` | Reports old branches; `apply` deletes only from a reviewed plan ([branch lifecycle](docs/branch-lifecycle.md)) | -| Look back | `getzilla report [--since all]` | Shows per-route overhead and the defects caught before merge | -| | `getzilla history snapshot` | Offline accounting of historical evidence | +| Look back | `getzilla report [--since all]` | Shows per-profile overhead and the defects caught before merge | +| | `getzilla history ` | Offline accounting of historical evidence from a snapshot file | | | `getzilla demo --open` | Opens the read-only local demo in your browser | ## What is inside diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 53937ff3..b9828dc0 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -540,12 +540,12 @@ Every command runs as `getzilla ` through `bin/getzilla` (`bin\getzilla | — | `scripts/getzilla_update.py` | Third-party tools only (agent CLIs, Superpowers, BMAD, Spec Kit, vibevm, CVE database, OpenGrep); `--status` shows the last results | | `cpu` | `scripts/getzilla_startup_cpu.py` | Startup step zero: measure and record usable CPUs (`--reserve`, `--attach`) | | `par` | `scripts/getzilla_par.py` | Run independent commands in parallel on the measured CPUs (`-j`, `--each`) | -| `route` | `scripts/getzilla_route.py` | Classify, show, reconcile, complete or cancel the task route; `--add-reviewer` adds a reviewer | +| `route` | `scripts/getzilla_route.py` | Classify, show, reconcile, complete or cancel the task route; `--reconcile --add-reviewer ` keeps an extra reviewer | | `status` | `scripts/getzilla_status.py` | Local runtime status: route, change package, evidence gaps, agents | | `change` | `scripts/getzilla_change.py` | Start, transition or show the change package | | `spec` | `scripts/getzilla_spec.py` | Validate and summarize typed change specifications | | `agent` | `scripts/getzilla_agent.py` | Agent heartbeat, progress, bounded watchdog and acknowledged same-task recovery | -| `test` | `scripts/getzilla_cli.py` | Run the test suite with pytest on the measured CPUs | +| `test` | `scripts/getzilla_cli.py` | Run the test suite with pytest-xdist on the measured CPUs, otherwise serial unittest | | `verify` | `scripts/getzilla_verify.py` | Local verification preflight (unittest, Ruff, Bandit, known vulnerabilities, measured coverage in `pr`/`release`) | | `review` | `scripts/getzilla_review.py` | Record a local review receipt bound to `--reviewed-commit` | | `gate` | `scripts/getzilla_gate.py` | Inspect or record route-bound local human-gate decisions | @@ -566,8 +566,8 @@ Every command runs as `getzilla ` through `bin/getzilla` (`bin\getzilla | — | `scripts/getzilla_evidence.py` | Export bounded local workflow evidence, without approvals or secrets, with `--output ` | | `cleanup` | `scripts/getzilla_cleanup.py` | `inventory` (read-only disk report with repository copies), `worktrees [--remove]` (guarded removal), `snapshot ` / `snapshot --remove ` (review copies) | | `branches` | `scripts/branch_lifecycle.py` | Branch retention `report`; `apply` deletes only from a reviewed plan with `--allow-delete` | -| `report` | `scripts/getzilla_report.py` | Per-route overhead, observation gaps and defects caught before merge | -| `history` | `scripts/getzilla_history.py` | Offline historical evidence accounting | +| `report` | `scripts/getzilla_report.py` | Per-profile overhead, observation gaps and defects caught before merge | +| `history` | `scripts/getzilla_history.py` | Offline historical evidence accounting from a snapshot file (`getzilla history `) | | — | `scripts/getzilla_m8.py` | Bounded local M8 owner policy: `status`, `activate`, `admit --action `, `revoke` | | `demo` | `scripts/getzilla_demo.py` | Start the loopback-only read-only product tour using bundled sample and checkout-derived evidence | | — | `scripts/package_stack.py` | Build the release ZIP and its checksum sidecar | From 6d8b0b7374031c1769b2bd9318eaf82f840a9635 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 18:20:10 +0000 Subject: [PATCH 8/9] cleanup: close the #134 Low findings in inventory, snapshots and worktree checks - Inventory: a linked .getzilla or runtime folder is not read; one entry budget covers the copy search and all copy sizes; an unreadable folder, a linked copy root or a cut size makes the report incomplete. - Review snapshots: the parent-folder refusal has its own message, and the module says a snapshot holds committed content only. - Tests pin the guards that mutants survived before: annotated tag peel, marker removal after a failed worktree add, the main-worktree base, the marker schema, the 0700 mode, unregistered folders, the refs/worktree check, a failed reflog read, the 2 s margins. - getzilla status answers --help; a test runs --help for every command. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BhxEnEALZRsc7kaPwG2kiR --- .getzilla/getzilla/resource_inventory.py | 48 ++++++++++++----- .getzilla/getzilla/review_snapshot.py | 7 ++- scripts/getzilla_status.py | 6 +++ tests/test_getzilla_cli.py | 7 +++ tests/test_resource_inventory.py | 65 +++++++++++++++++++++++- tests/test_review_snapshot.py | 61 ++++++++++++++++++++++ tests/test_worktree_safety.py | 27 +++++++++- 7 files changed, 204 insertions(+), 17 deletions(-) diff --git a/.getzilla/getzilla/resource_inventory.py b/.getzilla/getzilla/resource_inventory.py index 0a4b6789..44762958 100644 --- a/.getzilla/getzilla/resource_inventory.py +++ b/.getzilla/getzilla/resource_inventory.py @@ -3,6 +3,7 @@ Each `.getzilla/runtime` entry gets one class: `protected` (state, approvals, receipts, evidence, worktrees), `ephemeral` (output Getzilla can recreate) or `unknown`. Unknown is never disposable. A `..` part, a linked `.getzilla` or runtime folder, or a symlink below it makes the entry unknown. +The inventory does not read a linked `.getzilla` or runtime folder at all and reports itself incomplete. Nothing in this module deletes. `disposable()` is a path check, not a lock: a deleting caller must re-check each part through held directory descriptors (`O_NOFOLLOW`, `dir_fd`) and must skip entries a live run still owns, such as `par/-` while that pid runs. @@ -103,13 +104,27 @@ def measure(path: Path, budget: int = MAX_ENTRIES) -> dict[str, Any]: return {'bytes': size, 'entries': entries, 'complete': complete} -def repository_copies(root: Path, depth: int = MAX_COPY_DEPTH) -> list[dict[str, Any]]: - """Repository copies under the runtime folders agents write snapshots to. Links are not followed, - and a copy is not searched for nested copies. The copies stay `unknown`: nothing proves them disposable.""" +def _linked_runtime(root: Path) -> bool: + """A linked `.getzilla` or runtime folder points the inventory at another tree: it is not read.""" + return os.path.islink(root / '.getzilla') or os.path.islink(root / RUNTIME) + + +def repository_copies(root: Path, depth: int = MAX_COPY_DEPTH, + budget: int = MAX_ENTRIES) -> tuple[list[dict[str, Any]], bool]: + """Repository copies under the runtime folders agents write snapshots to, and whether the search + and every size are complete. One entry budget covers the search and all sizes. Links are not + followed, and a copy is not searched for nested copies. The copies stay `unknown`: nothing proves + them disposable.""" runtime = root / RUNTIME found: list[dict[str, Any]] = [] + if _linked_runtime(root): + return found, False + complete = True for top in COPY_ROOTS: - if os.path.islink(runtime) or os.path.islink(runtime / top) or not (runtime / top).is_dir(): + if os.path.islink(runtime / top): + complete = False + continue + if not (runtime / top).is_dir(): continue pending = [(runtime / top, 0)] while pending: @@ -117,15 +132,23 @@ def repository_copies(root: Path, depth: int = MAX_COPY_DEPTH) -> list[dict[str, try: with os.scandir(folder) as listing: entries = list(listing) + names = {entry.name for entry in entries} + children = [Path(entry.path) for entry in entries if entry.is_dir(follow_symlinks=False)] except OSError: + complete = False continue - if COPY_MARKERS <= {entry.name for entry in entries}: - found.append({'path': folder.relative_to(root).as_posix(), **measure(folder)}) + budget -= len(entries) + if budget <= 0: + return sorted(found, key=lambda item: item['path']), False + if COPY_MARKERS <= names: + size = measure(folder, budget) + budget -= size['entries'] + complete = complete and size['complete'] + found.append({'path': folder.relative_to(root).as_posix(), **size}) continue if level < depth: - pending.extend((Path(entry.path), level + 1) for entry in entries - if entry.is_dir(follow_symlinks=False)) - return sorted(found, key=lambda item: item['path']) + pending.extend((child, level + 1) for child in children) + return sorted(found, key=lambda item: item['path']), complete def _disk(path: Path) -> dict[str, int] | None: @@ -141,8 +164,9 @@ def inventory(root: Path, temp_dir: Path | None = None) -> dict[str, Any]: runtime = root / RUNTIME temp = Path(temp_dir or tempfile.gettempdir()) resources: list[dict[str, Any]] = [] + linked = _linked_runtime(root) try: - names = sorted(os.listdir(runtime)) + names = [] if linked else sorted(os.listdir(runtime)) except OSError: names = [] for name in names: @@ -159,9 +183,9 @@ def inventory(root: Path, temp_dir: Path | None = None) -> dict[str, Any]: group = totals.setdefault(item['class'], {'resources': 0, 'bytes': 0}) group['resources'] += 1 group['bytes'] += item['bytes'] - copies = repository_copies(root) + copies, copies_complete = repository_copies(root) return {'schema_version': 'getzilla-resource-inventory-v1', 'runtime': str(runtime), 'temp': str(temp), 'resources': resources, 'totals': totals, 'repository_copies': copies, - 'complete': all(item['complete'] for item in resources), + 'complete': copies_complete and all(item['complete'] for item in resources), 'disk': {'runtime': _disk(runtime if runtime.exists() else root), 'temp': _disk(temp)}, 'deleted': 0} diff --git a/.getzilla/getzilla/review_snapshot.py b/.getzilla/getzilla/review_snapshot.py index adcd27a4..cb69d35b 100644 --- a/.getzilla/getzilla/review_snapshot.py +++ b/.getzilla/getzilla/review_snapshot.py @@ -4,7 +4,8 @@ detached Git worktree under the main worktree's `.getzilla/runtime/snapshots` (mode 0700), so it shares the object store instead of copying it, and writes an owner marker next to it. `remove` deletes it again once the report is returned. Edits inside a snapshot are reviewer probes and are -discarded on purpose; reports never live in a snapshot. +discarded on purpose; reports never live in a snapshot. A snapshot holds committed content only: +commit the candidate before review, because staged, unstaged and untracked changes are not copied. `remove` acts only on a direct child of that folder (no symlink on the way) whose marker names the commit the worktree is still detached at. Anything else is kept. @@ -68,8 +69,10 @@ def remove(root: Path, path: str) -> dict[str, Any]: target = Path(os.path.abspath(path)) if target.parent != Path(os.path.abspath(base)) or target.name.endswith('.json'): return {'path': path, 'result': 'kept', 'reason': 'not a direct child of .getzilla/runtime/snapshots'} - if target.is_symlink() or Path(os.path.realpath(target)).parent != Path(os.path.realpath(base)): + if target.is_symlink(): return {'path': path, 'result': 'kept', 'reason': 'the snapshot path is a symlink'} # remove would follow it + if Path(os.path.realpath(target)).parent != Path(os.path.realpath(base)): # a Windows junction, for example + return {'path': path, 'result': 'kept', 'reason': 'the snapshot path resolves outside the snapshots folder'} marker = _marker(base, target.name) entry = next((item for item in list_worktrees(root) if os.path.abspath(item['path']) == str(target)), None) diff --git a/scripts/getzilla_status.py b/scripts/getzilla_status.py index a00db01f..8ca40f41 100644 --- a/scripts/getzilla_status.py +++ b/scripts/getzilla_status.py @@ -8,9 +8,15 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / '.getzilla')) +import argparse import json import os +argparse.ArgumentParser( + prog='getzilla status', + description='Print the active route, change package, agents, human gates and evidence gaps as JSON.', +).parse_args() + from getzilla.package_status import collect_worktree, inspect_package, receipt_inputs_unavailable from getzilla.receipts import validate_evidence from getzilla.human_gates import ARTIFACT_NOTICE, gate_statuses diff --git a/tests/test_getzilla_cli.py b/tests/test_getzilla_cli.py index 92483967..596a88ee 100644 --- a/tests/test_getzilla_cli.py +++ b/tests/test_getzilla_cli.py @@ -66,6 +66,13 @@ def test_arguments_pass_through_to_the_script(self) -> None: self.assertEqual(proc.returncode, 0, proc.stderr) self.assertIn('scaffold', proc.stdout) + def test_every_command_answers_help_with_usage(self) -> None: + for name in cli.COMMANDS: + with self.subTest(command=name): + proc = run(name, '--help') + self.assertEqual(proc.returncode, 0, proc.stderr) + self.assertIn('usage:', proc.stdout.lower()) + def test_project_scripts_take_precedence_inside_a_project(self) -> None: with tempfile.TemporaryDirectory() as tmp: project = Path(tmp) / 'project' diff --git a/tests/test_resource_inventory.py b/tests/test_resource_inventory.py index c73862a7..66edb359 100644 --- a/tests/test_resource_inventory.py +++ b/tests/test_resource_inventory.py @@ -7,11 +7,14 @@ import tempfile import unittest from pathlib import Path +from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / '.getzilla')) -from getzilla.resource_inventory import EPHEMERAL, PROTECTED, classify, disposable, inventory, measure # noqa: E402 +from getzilla.resource_inventory import ( # noqa: E402 + EPHEMERAL, PROTECTED, classify, disposable, inventory, measure, repository_copies, +) def snapshot(base: Path) -> dict[str, bytes]: @@ -95,6 +98,16 @@ def test_unreadable_path_parts_fail_closed(self) -> None: (self.runtime / 'par' / 'file').write_text('not a directory') self.assertEqual(classify(self.root, self.runtime / 'par' / 'file' / 'child'), 'unknown') + def test_a_missing_runtime_folder_fails_closed(self) -> None: + fresh = self.base / 'fresh' + fresh.mkdir() + self.assertEqual(classify(fresh, fresh / '.getzilla' / 'runtime' / 'par' / 'run-1'), 'unknown') + (fresh / '.getzilla').mkdir() + self.assertEqual(classify(fresh, fresh / '.getzilla' / 'runtime' / 'par' / 'run-1'), 'unknown') + + def test_a_dot_dot_part_alone_is_unknown(self) -> None: + self.assertEqual(classify(self.root, self.runtime / 'par' / '..' / 'par' / 'run-1'), 'unknown') + def test_measure_does_not_follow_links_and_discloses_the_budget(self) -> None: big = self.base / 'big' big.mkdir() @@ -148,6 +161,56 @@ def test_repository_copies_are_found_counted_once_and_never_followed_through_lin self.assertTrue(all(item['bytes'] > 0 for item in report['repository_copies'])) self.assertFalse(any(disposable(self.root, self.root / path) for path in paths)) + def test_a_linked_getzilla_runtime_or_copy_folder_is_not_read_and_is_incomplete(self) -> None: + self.make_copy(self.runtime / 'scratch' / 'review-1') + linked = self.base / 'linked' + linked.mkdir() + self.link(linked / '.getzilla', self.root / '.getzilla') + report = inventory(linked, self.temp) + runtime_entries = [item for item in report['resources'] if item['location'] == 'runtime'] + self.assertEqual((runtime_entries, report['repository_copies'], report['complete']), ([], [], False)) + runtime_link = self.base / 'runtime-link' + (runtime_link / '.getzilla').mkdir(parents=True) + self.link(runtime_link / '.getzilla' / 'runtime', self.runtime) + self.assertEqual(repository_copies(runtime_link), ([], False)) + outside = self.make_copy(self.base / 'outside-scratch' / 'copy') + self.link(self.runtime / 'archive', outside.parent) + copies, complete = repository_copies(self.root) + self.assertEqual([item['path'] for item in copies], ['.getzilla/runtime/scratch/review-1']) + self.assertFalse(complete) + self.assertFalse(inventory(self.root, self.temp)['complete']) + + def test_one_entry_budget_covers_the_search_and_every_copy_size(self) -> None: + self.make_copy(self.runtime / 'scratch' / 'review-1') + self.make_copy(self.runtime / 'scratch' / 'review-2') + copies, complete = repository_copies(self.root) + self.assertEqual((len(copies), complete), (2, True)) + copies, complete = repository_copies(self.root, budget=8) + self.assertFalse(complete) + self.assertTrue(all(item['entries'] <= 8 for item in copies)) + self.assertFalse(repository_copies(self.root, budget=1)[1]) + + def test_the_budget_left_after_the_search_bounds_the_copy_size(self) -> None: + big = self.make_copy(self.runtime / 'scratch' / 'big') + for index in range(10): + (big / 'scripts' / f'file-{index}.py').write_text('x\n') + self.assertEqual(repository_copies(self.root, budget=100)[1], True) + copies, complete = repository_copies(self.root, budget=6) # the search needs 4 entries, the size 15 + self.assertEqual(([item['path'] for item in copies], complete), (['.getzilla/runtime/scratch/big'], False)) + self.assertFalse(copies[0]['complete']) + + def test_an_unreadable_copy_folder_marks_the_report_incomplete(self) -> None: + (self.runtime / 'scratch' / 'locked').mkdir() + real_scandir = os.scandir + + def denied(path): + if Path(path).name == 'locked': + raise PermissionError('denied') + return real_scandir(path) + + with patch('getzilla.resource_inventory.os.scandir', side_effect=denied): + self.assertEqual(repository_copies(self.root), ([], False)) + def test_cli_lists_repository_copies(self) -> None: (self.root / '.git').mkdir() self.make_copy(self.runtime / 'scratch' / 'review-9') diff --git a/tests/test_review_snapshot.py b/tests/test_review_snapshot.py index 502836a7..33c585c8 100644 --- a/tests/test_review_snapshot.py +++ b/tests/test_review_snapshot.py @@ -128,7 +128,68 @@ def test_an_unknown_commit_leaves_no_marker(self) -> None: review_snapshot.create(self.root, 'no-such-commit') self.assertFalse(self.snapshots.exists() and any(self.snapshots.iterdir())) + def test_an_option_shaped_revision_is_not_read_as_an_option(self) -> None: + for revision in ('--all', '--git-dir', '-q'): + with self.subTest(revision=revision), self.assertRaises(WorktreeError): + review_snapshot.create(self.root, revision) + self.assertFalse(self.snapshots.exists() and any(self.snapshots.iterdir())) + + def test_an_annotated_tag_is_peeled_to_its_commit(self) -> None: + git(self.root, 'tag', '-a', '-m', 'release', 'v1', self.first) + path = review_snapshot.create(self.root, 'v1') + marker = json.loads((self.snapshots / f'{path.name}.json').read_text()) + self.assertEqual(marker['commit'], self.first) + self.assertEqual(review_snapshot.remove(self.root, str(path))['result'], 'removed') + + def test_a_failed_worktree_add_leaves_no_marker(self) -> None: + real_git = review_snapshot._git + + def failing(root, *args): + if args[:2] == ('worktree', 'add'): + raise WorktreeError('worktree add failed') + return real_git(root, *args) + + with patch('getzilla.review_snapshot._git', side_effect=failing), self.assertRaises(WorktreeError): + review_snapshot.create(self.root, self.first) + self.assertEqual(list(self.snapshots.iterdir()), []) + + def test_a_snapshot_made_from_a_task_worktree_lives_under_the_main_worktree(self) -> None: + task = self.root / '.getzilla' / 'runtime' / 'worktrees' / 'task' + git(self.root, 'worktree', 'add', '-q', '-b', 'task', str(task)) + path = review_snapshot.create(task, self.first) + self.assertEqual(path.parent, self.snapshots) + self.assertFalse((task / '.getzilla' / 'runtime' / 'snapshots').exists()) + self.assertEqual(review_snapshot.remove(task, str(path))['result'], 'removed') + + def test_a_marker_with_another_schema_is_not_an_owner_marker(self) -> None: + path = review_snapshot.create(self.root, self.first) + marker = self.snapshots / f'{path.name}.json' + marker.write_text(json.dumps({'schema_version': 'other', 'commit': self.first})) + result = review_snapshot.remove(self.root, str(path)) + self.assertEqual((result['result'], result['reason']), ('kept', 'no owner marker')) + self.assertTrue(path.is_dir()) + + @unittest.skipIf(os.name == 'nt', 'POSIX modes') + def test_an_existing_snapshots_folder_is_made_private(self) -> None: + self.snapshots.mkdir(parents=True, mode=0o755) + os.chmod(self.snapshots, 0o755) + review_snapshot.create(self.root, self.first) + self.assertEqual(stat.S_IMODE(os.stat(self.snapshots).st_mode), 0o700) + + def test_a_marker_next_to_an_unregistered_folder_is_kept(self) -> None: + self.snapshots.mkdir(parents=True) + plain = self.snapshots / 'plain' + plain.mkdir() + (plain / 'work.txt').write_text('unique') + marker = self.snapshots / 'plain.json' + marker.write_text(json.dumps({'schema_version': review_snapshot.SCHEMA, 'commit': self.first})) + result = review_snapshot.remove(self.root, str(plain)) + self.assertEqual((result['result'], result['reason']), ('kept', 'not a registered snapshot worktree')) + self.assertEqual((plain / 'work.txt').read_text(), 'unique') + self.assertTrue(marker.is_file()) + def test_cli_creates_and_removes_a_snapshot(self) -> None: + (self.root / '.getzilla').mkdir(exist_ok=True) # find_root must stop here, not at an outer project cli = [sys.executable, str(ROOT / 'scripts' / 'getzilla_cleanup.py'), 'snapshot'] created = json.loads(subprocess.run([*cli, self.second, '--json'], cwd=self.root, capture_output=True, text=True, check=True).stdout) diff --git a/tests/test_worktree_safety.py b/tests/test_worktree_safety.py index 2de0bb9c..5350fc38 100644 --- a/tests/test_worktree_safety.py +++ b/tests/test_worktree_safety.py @@ -5,6 +5,7 @@ import subprocess import sys import tempfile +import time import unittest from pathlib import Path from unittest.mock import patch @@ -167,6 +168,8 @@ def test_commits_only_a_worktree_ref_or_the_head_reflog_reaches_keep_it(self) -> commit(kept_ref, 'app.py', 'value = 5\n', 'only in refs/worktree') git(kept_ref, 'update-ref', 'refs/worktree/keep', 'HEAD') git(kept_ref, 'checkout', '-q', '--detach', 'origin/main') + # Only refs/worktree reaches that commit now; the reflog worktree is added after the expiry. + git(kept_ref, 'reflog', 'expire', '--expire=now', '--all') reflog = self.add('reflog', '--detach') commit(reflog, 'app.py', 'value = 6\n', 'only in the reflog') git(reflog, 'checkout', '-q', '--detach', 'origin/main') @@ -175,6 +178,21 @@ def test_commits_only_a_worktree_ref_or_the_head_reflog_reaches_keep_it(self) -> self.assertIn('commits not on any remote-tracking ref', report['reasons'], path) self.assertEqual(remove(self.root, str(path))['result'], 'kept') + def test_a_failed_head_reflog_read_keeps_the_worktree(self) -> None: + from getzilla import worktree_safety + path = self.add('reflog-error', '-b', 'reflog-error') + real_git = worktree_safety._git + + def failing(worktree, *args): + if args[:2] == ('reflog', 'show'): + raise worktree_safety.WorktreeError('reflog unreadable') + return real_git(worktree, *args) + + with patch('getzilla.worktree_safety._git', side_effect=failing): + report = self.report(path) + self.assertFalse(report['removable']) + self.assertIn('check failed: reflog unreadable', report['reasons']) + def test_an_operation_in_progress_keeps_the_worktree(self) -> None: path = self.add('merging', '-b', 'merging') git_dir = Path(git(path, 'rev-parse', '--absolute-git-dir')) @@ -266,8 +284,8 @@ def denied(target, *args, **kwargs): hidden.add(str(os.getpid())) # this test process started before the worktree below older = worktree_safety._started_at(Path('/proc') / str(os.getpid())) self.assertIsNotNone(older) - self.assertFalse(worktree_safety._in_use(path, older + 60)) - self.assertFalse(worktree_safety._in_use(path, older + 10), 'a 2 s margin, not more') + self.assertIs(worktree_safety._in_use(path, older + 60), False) + self.assertIs(worktree_safety._in_use(path, older + 10), False, 'a 2 s margin, not more') with patch('getzilla.worktree_safety._started_at', return_value=None): self.assertIsNone(worktree_safety._in_use(path, older + 60), 'an unknown start fails closed') @@ -295,6 +313,11 @@ def test_the_creation_time_survives_move_and_repair_and_a_clock_step(self) -> No os.utime(git_dir / 'commondir', (future, future)) os.utime(git_dir / 'gitdir', (future, future)) self.assertIsNone(worktree_safety._created_at(path)) + for ahead, trusted in ((5, False), (1, True)): # a 2 s tolerance for clock ticks, not more + stamp = time.time() + ahead + os.utime(git_dir / 'commondir', (stamp, stamp)) + os.utime(git_dir / 'gitdir', (stamp, stamp)) + self.assertEqual(worktree_safety._created_at(path) is not None, trusted, ahead) def test_a_worktree_a_process_works_in_is_kept(self) -> None: path = self.add('busy', '-b', 'busy') From 536817b94aae69c30d39af025ff2dd55db863e70 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 18:24:07 +0000 Subject: [PATCH 9/9] cleanup: count each copy entry once and accept a search that uses exactly the budget Review of #181: the copy folder listing was counted by the search and again by its size, and a search that used the full budget reported itself incomplete. Tests pin both boundaries. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BhxEnEALZRsc7kaPwG2kiR --- .getzilla/getzilla/resource_inventory.py | 8 ++++---- tests/test_resource_inventory.py | 13 +++++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.getzilla/getzilla/resource_inventory.py b/.getzilla/getzilla/resource_inventory.py index 44762958..816518c3 100644 --- a/.getzilla/getzilla/resource_inventory.py +++ b/.getzilla/getzilla/resource_inventory.py @@ -137,15 +137,15 @@ def repository_copies(root: Path, depth: int = MAX_COPY_DEPTH, except OSError: complete = False continue - budget -= len(entries) - if budget <= 0: - return sorted(found, key=lambda item: item['path']), False - if COPY_MARKERS <= names: + if COPY_MARKERS <= names: # its own entries are counted once, by its size size = measure(folder, budget) budget -= size['entries'] complete = complete and size['complete'] found.append({'path': folder.relative_to(root).as_posix(), **size}) continue + budget -= len(entries) + if budget < 0: + return sorted(found, key=lambda item: item['path']), False if level < depth: pending.extend((child, level + 1) for child in children) return sorted(found, key=lambda item: item['path']), complete diff --git a/tests/test_resource_inventory.py b/tests/test_resource_inventory.py index 66edb359..e7232dc4 100644 --- a/tests/test_resource_inventory.py +++ b/tests/test_resource_inventory.py @@ -190,12 +190,21 @@ def test_one_entry_budget_covers_the_search_and_every_copy_size(self) -> None: self.assertTrue(all(item['entries'] <= 8 for item in copies)) self.assertFalse(repository_copies(self.root, budget=1)[1]) + def test_a_search_that_uses_exactly_the_budget_is_complete(self) -> None: + (self.runtime / 'scratch' / 'empty').mkdir() + self.assertEqual(repository_copies(self.root, budget=1), ([], True)) + self.assertEqual(repository_copies(self.root, budget=0), ([], False)) + def test_the_budget_left_after_the_search_bounds_the_copy_size(self) -> None: big = self.make_copy(self.runtime / 'scratch' / 'big') for index in range(10): (big / 'scripts' / f'file-{index}.py').write_text('x\n') - self.assertEqual(repository_copies(self.root, budget=100)[1], True) - copies, complete = repository_copies(self.root, budget=6) # the search needs 4 entries, the size 15 + copies, complete = repository_copies(self.root, budget=100) + self.assertTrue(complete) + # The scratch listing has 1 entry; the copy counts its own 14 entries once, through its size. + self.assertEqual(copies[0]['entries'], 14) + self.assertTrue(repository_copies(self.root, budget=15)[1], 'the exact budget is enough') + copies, complete = repository_copies(self.root, budget=14) self.assertEqual(([item['path'] for item in copies], complete), (['.getzilla/runtime/scratch/big'], False)) self.assertFalse(copies[0]['complete'])